Skip to content

Commit 57d7331

Browse files
authored
fix(windows): cache compiled WebGL programs; WebGPU canvases no longer freeze on resize (#184)
* fix(windows): keep compiled WebGL programs across contexts and launches ANGLE only reuses linked programs when the app installs EGL_ANDROID_blob_cache callbacks; its own cache size defaults to 0. Without them every new context recompiled every shader through the HLSL compiler, which blocked large scenes for about a second on each visit. Programs are now kept in memory and under %LOCALAPPDATA%\<exe>\canvas\program-cache (CANVAS_PROGRAM_CACHE_DIR overrides it; empty keeps it in memory only). * fix(windows): WebGPU canvases no longer freeze the app when resized Configuring the surface from the layout pass waited for the GPU without a limit, while the last presented frame waited on the compositor, which waits on the UI thread. The resize is now applied from the next getCurrentTexture, once that frame is done, drawing offscreen until then. Resizing gets a fresh swapchain, since ResizeBuffers fails while the page still holds a texture of the old one ("window is in use"). getCurrentTexture falls back to the offscreen texture instead of returning null. * chore(windows): rebuild canvasnative.node * fix(webgl): don't wait for a program's link inside linkProgram linkProgram queried ACTIVE_UNIFORM_BLOCKS straight after glLinkProgram, which waits for the link to finish, so ANGLE could never link in the background. The count is now queried (and cached) the first time uniformBlockBinding needs it. * feat(webgl): KHR_parallel_shader_compile on Windows ANGLE compiles and links on worker threads once a context enables it, and COMPLETION_STATUS_KHR lets the page poll without waiting (three.js's compileAsync, for example). * chore(windows): rebuild canvasnative.node * feat(webgl): KHR_parallel_shader_compile on Android Exposed when the driver reports GL_KHR_parallel_shader_compile. The GL default for MAX_SHADER_COMPILER_THREADS_KHR already lets the driver use its own maximum, so nothing else has to be set. COMPLETION_STATUS_KHR reads back as a boolean. * chore(ios,android): rebuild CanvasNative.xcframework and canvas-release.aar * chore: 3.0.0-beta.5
1 parent d30d7ab commit 57d7331

150 files changed

Lines changed: 15902 additions & 15511 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎crates/canvas-c/src/webgl/gl.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pub enum WebGLExtensionType {
4848
WebGLExtensionTypeWEBGL_draw_buffers,
4949
WebGLExtensionTypeOES_fbo_render_mipmap,
5050
WebGLExtensionTypeNone,
51+
WebGLExtensionTypeKHR_parallel_shader_compile,
5152
}
5253

5354
impl Into<WebGLExtensionType> for canvas_webgl::prelude::WebGLExtensionType {
@@ -68,6 +69,9 @@ impl Into<WebGLExtensionType> for canvas_webgl::prelude::WebGLExtensionType {
6869
canvas_webgl::prelude::WebGLExtensionType::EXT_shader_texture_lod => {
6970
WebGLExtensionType::WebGLExtensionTypeEXT_shader_texture_lod
7071
}
72+
canvas_webgl::prelude::WebGLExtensionType::KHR_parallel_shader_compile => {
73+
WebGLExtensionType::WebGLExtensionTypeKHR_parallel_shader_compile
74+
}
7175
canvas_webgl::prelude::WebGLExtensionType::EXT_texture_filter_anisotropic => {
7276
WebGLExtensionType::WebGLExtensionTypeEXT_texture_filter_anisotropic
7377
}

‎crates/canvas-c/src/webgpu/gpu_canvas_context.rs‎

Lines changed: 142 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ pub struct SurfaceData {
3434
pub(crate) error_sink: ErrorSink,
3535
pub texture_data: TextureData,
3636
pub(crate) previous_configuration: wgt::SurfaceConfiguration<Vec<wgt::TextureFormat>>,
37+
/// Not applied yet; frames go offscreen until it is.
38+
pub(crate) pending_configuration: Option<wgt::SurfaceConfiguration<Vec<wgt::TextureFormat>>>,
39+
pending_failed: bool,
40+
/// The last presented frame's submission, until the GPU finishes it.
41+
presented_submission: Option<wgpu_core::SubmissionIndex>,
3742
}
3843

3944
#[derive(Copy, Clone, Debug)]
@@ -91,6 +96,97 @@ fn discard_current_texture(context: &CanvasGPUCanvasContext, operation: &'static
9196
}
9297
}
9398

99+
/// Configuring waits for the GPU, and the last presented frame can wait on the compositor, which
100+
/// waits on this (the UI) thread: so only once that frame is done. Never blocks.
101+
fn presented_frame_done(surface_data: &mut SurfaceData) -> bool {
102+
let Some(index) = surface_data.presented_submission else {
103+
return true;
104+
};
105+
let done = surface_data
106+
.device
107+
.device
108+
.poll(wgt::PollType::Wait {
109+
submission_index: Some(index),
110+
timeout: Some(std::time::Duration::ZERO),
111+
})
112+
.is_ok();
113+
if done {
114+
surface_data.presented_submission = None;
115+
}
116+
done
117+
}
118+
119+
/// A resize gets a new swapchain: `ResizeBuffers` fails while the page still holds a texture of the
120+
/// old one, until it is garbage collected.
121+
fn configure_surface(
122+
surface: &Arc<wgpu_core::instance::Surface>,
123+
device: &Arc<wgpu_core::device::Device>,
124+
previous: Option<&wgt::SurfaceConfiguration<Vec<wgt::TextureFormat>>>,
125+
config: &wgt::SurfaceConfiguration<Vec<wgt::TextureFormat>>,
126+
) -> Option<wgpu_core::present::ConfigureSurfaceError> {
127+
#[cfg(all(target_os = "windows", feature = "d3d"))]
128+
if let Some(previous) = previous {
129+
let mut resized = previous.clone();
130+
resized.width = config.width;
131+
resized.height = config.height;
132+
if &resized == config {
133+
let raw = surface
134+
.raw(wgt::Backend::Dx12)
135+
.and_then(|raw| raw.as_any().downcast_ref::<wgpu_hal::dx12::Surface>());
136+
if let (Some(raw), Some(hal_device)) = (raw, unsafe { Arc::clone(device).as_hal::<wgpu_hal::api::Dx12>() }) {
137+
unsafe { wgpu_hal::Surface::unconfigure(raw, &*hal_device) };
138+
}
139+
}
140+
}
141+
#[cfg(not(all(target_os = "windows", feature = "d3d")))]
142+
let _ = previous;
143+
surface.configure(device, config)
144+
}
145+
146+
/// `false` while pending: not safe to configure yet, or it failed (retried, reported once).
147+
fn apply_pending_configuration(
148+
context: &CanvasGPUCanvasContext,
149+
surface: &Arc<wgpu_core::instance::Surface>,
150+
operation: &'static str,
151+
) -> bool {
152+
let mut data = context.data.lock();
153+
let Some(surface_data) = data.as_mut() else {
154+
return true;
155+
};
156+
if surface_data.pending_configuration.is_none() {
157+
return true;
158+
}
159+
if !presented_frame_done(surface_data) {
160+
return false;
161+
}
162+
let Some(config) = surface_data.pending_configuration.take() else {
163+
return true;
164+
};
165+
match configure_surface(
166+
surface,
167+
&surface_data.device.device,
168+
Some(&surface_data.previous_configuration),
169+
&config,
170+
) {
171+
None => {
172+
surface_data.previous_configuration = config;
173+
surface_data.pending_failed = false;
174+
context
175+
.has_surface_presented
176+
.store(false, std::sync::atomic::Ordering::SeqCst);
177+
true
178+
}
179+
Some(cause) => {
180+
if !surface_data.pending_failed {
181+
surface_data.pending_failed = true;
182+
handle_error_fatal(cause, operation);
183+
}
184+
surface_data.pending_configuration = Some(config);
185+
false
186+
}
187+
}
188+
}
189+
94190
fn copy_capabilities(capabilities: &SurfaceCapabilities) -> SurfaceCapabilities {
95191
SurfaceCapabilities {
96192
formats: capabilities.formats.clone(),
@@ -960,7 +1056,10 @@ pub unsafe extern "C" fn canvas_native_webgpu_context_resize_swap_chain_panel(
9601056
};
9611057
surface_data.texture_data.size.width = width;
9621058
surface_data.texture_data.size.height = height;
963-
let mut new_config = surface_data.previous_configuration.clone();
1059+
let mut new_config = surface_data
1060+
.pending_configuration
1061+
.take()
1062+
.unwrap_or_else(|| surface_data.previous_configuration.clone());
9641063
new_config.width = width;
9651064
new_config.height = height;
9661065

@@ -992,17 +1091,7 @@ pub unsafe extern "C" fn canvas_native_webgpu_context_resize_swap_chain_panel(
9921091
data: texture_data,
9931092
});
9941093

995-
let error = surface
996-
.as_ref()
997-
.and_then(|surface| surface.configure(&surface_data.device.device, &new_config));
998-
if let Some(cause) = error {
999-
handle_error_fatal(cause, "canvas_native_webgpu_context_resize_swap_chain_panel");
1000-
} else {
1001-
context
1002-
.has_surface_presented
1003-
.store(false, std::sync::atomic::Ordering::SeqCst);
1004-
surface_data.previous_configuration = new_config;
1005-
}
1094+
surface_data.pending_configuration = Some(new_config);
10061095
}
10071096

10081097
#[cfg(any(target_os = "ios", target_os = "visionos", target_os = "tvos"))]
@@ -1660,9 +1749,23 @@ pub unsafe extern "C" fn canvas_native_webgpu_context_configure(
16601749
});
16611750
}
16621751

1663-
let error = surface_id
1664-
.as_ref()
1665-
.and_then(|surface| surface.configure(&device_id, &config));
1752+
let (presented_submission, previous) = context
1753+
.data
1754+
.lock()
1755+
.as_mut()
1756+
.filter(|data| Arc::ptr_eq(&data.device.device, &device_id))
1757+
.map(|data| {
1758+
let busy = !presented_frame_done(data);
1759+
(data.presented_submission.filter(|_| busy), Some(data.previous_configuration.clone()))
1760+
})
1761+
.unwrap_or_default();
1762+
let error = match (presented_submission, surface_id.as_ref()) {
1763+
(None, Some(surface)) => {
1764+
discard_current_texture(&context, "canvas_native_webgpu_context_configure");
1765+
configure_surface(surface, &device_id, previous.as_ref(), &config)
1766+
}
1767+
_ => None,
1768+
};
16661769
if let Some(cause) = error {
16671770
handle_error_fatal(cause, "canvas_native_webgpu_context_configure");
16681771
let mut lock = context.data.lock();
@@ -1688,6 +1791,9 @@ pub unsafe extern "C" fn canvas_native_webgpu_context_configure(
16881791
mip_level_count: 1,
16891792
sample_count: 1,
16901793
},
1794+
pending_configuration: presented_submission.map(|_| config.clone()),
1795+
pending_failed: false,
1796+
presented_submission,
16911797
previous_configuration: config,
16921798
});
16931799
{
@@ -1766,14 +1872,26 @@ pub extern "C" fn canvas_native_webgpu_context_get_current_texture(
17661872
return offscreen_current_texture(context);
17671873
};
17681874

1875+
if !apply_pending_configuration(context, surface_id, "canvas_native_webgpu_context_get_current_texture") {
1876+
return offscreen_current_texture(context);
1877+
}
1878+
17691879
let result = surface_id.get_current_texture();
17701880

17711881
match result {
17721882
Ok(texture) => {
17731883
match texture.status {
17741884
SurfaceStatus::Good | SurfaceStatus::Suboptimal | SurfaceStatus::Outdated => {}
1775-
_ => {
1776-
return std::ptr::null_mut();
1885+
// No image this frame: draw offscreen rather than hand the page nothing.
1886+
status => {
1887+
if matches!(status, SurfaceStatus::Lost) {
1888+
if let Some(data) = context.data.lock().as_mut() {
1889+
if data.pending_configuration.is_none() {
1890+
data.pending_configuration = Some(data.previous_configuration.clone());
1891+
}
1892+
}
1893+
}
1894+
return offscreen_current_texture(context);
17771895
}
17781896
}
17791897

@@ -1834,7 +1952,7 @@ pub extern "C" fn canvas_native_webgpu_context_get_current_texture(
18341952
handle_error_fatal(cause,
18351953
"canvas_native_webgpu_context_get_current_texture",
18361954
);
1837-
std::ptr::null_mut()
1955+
offscreen_current_texture(context)
18381956
}
18391957
}
18401958
}
@@ -1855,8 +1973,8 @@ pub unsafe extern "C" fn canvas_native_webgpu_context_present_surface(
18551973

18561974
{
18571975
let lock = context.read_back_texture.lock();
1858-
let surface_lock = context.data.lock();
1859-
if let (Some(dst_texture), Some(data)) = (lock.as_ref(), surface_lock.as_ref()) {
1976+
let mut surface_lock = context.data.lock();
1977+
if let (Some(dst_texture), Some(data)) = (lock.as_ref(), surface_lock.as_mut()) {
18601978
unsafe {
18611979
let texture_extent = wgt::Extent3d {
18621980
width: dst_texture.data.size.width,
@@ -1901,7 +2019,10 @@ pub unsafe extern "C" fn canvas_native_webgpu_context_present_surface(
19012019

19022020
let id = encoder.finish(&desc);
19032021

1904-
data.device.queue.queue.id.submit(&[id]);
2022+
let index = data.device.queue.queue.id.submit(&[id]);
2023+
if texture.surface_id.is_some() {
2024+
data.presented_submission = Some(index);
2025+
}
19052026
}
19062027
}
19072028
}

‎crates/canvas-core/src/gpu/gl/egl.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,10 @@ fn load() -> Option<Egl> {
161161
extensions,
162162
_gles: gles,
163163
};
164+
if egl.has_extension("EGL_ANDROID_blob_cache") {
165+
let set_funcs = egl.instance.get_proc_address("eglSetBlobCacheFuncsANDROID");
166+
super::program_cache::install(set_funcs.map_or(std::ptr::null(), |f| f as *const c_void), display.as_ptr());
167+
}
164168
#[cfg(target_os = "windows")]
165169
protect_immediate_context(&egl);
166170
Some(egl)

‎crates/canvas-core/src/gpu/gl/mod.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ pub use android::*;
1818
#[cfg(target_os = "windows")]
1919
mod egl;
2020

21+
#[cfg(target_os = "windows")]
22+
mod program_cache;
23+
2124
#[cfg(target_os = "windows")]
2225
pub use egl::*;
2326

0 commit comments

Comments
 (0)