Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
553854b
fix(calendar): keep sixth-week dates in their own month grid
Moonbot-Tech Nov 6, 2026
ad6f4d9
fix(runtime): preserve mid-draw invalidation and let unchanged contro…
Moonbot-Tech Nov 6, 2026
9c7e968
fix(render): retain presentation debt until the native backend accept…
Moonbot-Tech Nov 6, 2026
1e5aad2
fix(render): submit retained canvas bakes before growing scene buffers
Moonbot-Tech Nov 6, 2026
8417cfd
fix(wayland): keep timer polls out of the compositor callback chain
Moonbot-Tech Nov 6, 2026
562cc1e
fix(x11): deliver setup-buffered events before the first frame
Moonbot-Tech Nov 6, 2026
4e736de
feat(diagnostics): count actual view renders at the common dispatch p…
Moonbot-Tech Nov 6, 2026
e586a83
docs(canvas): state the Cargo identity required by raw wgpu handles
Moonbot-Tech Nov 6, 2026
2028d56
fix(wgpu): preserve the first validation error in a failed frame
Moonbot-Tech Nov 6, 2026
dc523bc
feat(diagnostics): exercise the existing shared GPU recovery path
Moonbot-Tech Nov 6, 2026
971f222
fix(input): allow context menus inside popovers and across windows
Moonbot-Tech Nov 6, 2026
795f3e5
fix(dropdown): dismiss the owning popover before opening a dialog
Moonbot-Tech Nov 6, 2026
e2ce1e9
chore(components): record the reviewed input and overlay donor drift
Moonbot-Tech Nov 6, 2026
d0b4bc6
fix(input): honor explicit height for single-line fields
Moonbot-Tech Nov 6, 2026
0e1c690
fix(text-area): expose the public size argument through MoonUI
Moonbot-Tech Nov 6, 2026
967afcd
fix(window): share user-close policy with client-side frame controls
Moonbot-Tech Nov 6, 2026
b3b3349
test(gpu): expose native frame delivery to downstream canvas tests
Moonbot-Tech Nov 6, 2026
7657997
fix(dock): synchronize side-root activity when visibility changes
Moonbot-Tech Nov 6, 2026
aca1811
feat(theme): add the optional Obsidian facet skin
Moonbot-Tech Nov 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions crates/moon-gpui-linux/src/linux/wayland/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@ impl WaylandWindow {
WaylandWindow::gpu_canvas_refresh_interval(&state)
};

window.frame();
window.poll_frame();

let now = std::time::Instant::now();
while instant < now {
Expand Down Expand Up @@ -646,9 +646,19 @@ impl WaylandWindowStatePtr {
!state.children.is_empty()
}

/// Start the compositor callback chain on first configure, then replace each completed
/// callback once. Timer polls must not extend this chain: every request survives until Done.
pub fn frame(&self) {
let mut state = self.state.borrow_mut();
let state = self.state.borrow();
state.surface.frame(&state.globals.qh, state.surface.id());
drop(state);
self.poll_frame();
}

/// Poll GPUI and its canvases without registering another Wayland callback. The fallback
/// timer keeps Skip-to-Present transitions live even while the compositor withholds Done.
fn poll_frame(&self) {
let mut state = self.state.borrow_mut();
state.resize_throttle = false;
let force_render = state.force_render_after_recovery;
state.force_render_after_recovery = false;
Expand Down Expand Up @@ -1469,7 +1479,7 @@ impl PlatformWindow for WaylandWindow {
self.0.callbacks.borrow_mut().button_layout_changed = Some(callback);
}

fn draw(&self, scene: &Scene) {
fn draw(&self, scene: &Scene) -> bool {
let mut state = self.borrow_mut();

if state.renderer.device_lost() {
Expand All @@ -1491,14 +1501,15 @@ impl PlatformWindow for WaylandWindow {
}

state.force_render_after_recovery = true;
return;
return false;
}

state.renderer_presented = state.renderer.draw(scene);

if state.renderer.needs_redraw() {
state.force_render_after_recovery = true;
}
state.renderer_presented
}

fn set_gpu_canvas_active(&self, active: bool) {
Expand Down Expand Up @@ -1605,6 +1616,12 @@ impl PlatformWindow for WaylandWindow {
self.borrow().renderer.gpu_specs().into()
}

/// Invalidate the shared device; the next native draw runs normal recovery.
fn request_gpu_device_reset(&self) -> bool {
self.borrow().renderer.request_device_reset();
true
}

fn play_system_bell(&self) {
let state = self.borrow();
let surface = if state.surface_state.toplevel().is_some() {
Expand Down
8 changes: 8 additions & 0 deletions crates/moon-gpui-linux/src/linux/x11/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1577,6 +1577,7 @@ impl LinuxClient for X11Client {
gpui::scap_screen_capture::scap_screen_sources(&self.0.borrow().common.foreground_executor)
}

/// Open a native window and deliver events buffered by its synchronous X11 setup.
fn open_window(
&self,
handle: AnyWindowHandle,
Expand Down Expand Up @@ -1656,6 +1657,13 @@ impl LinuxClient for X11Client {
};

state.windows.insert(x_window, window_ref);
// Synchronous setup can buffer X11 events without leaving the socket readable.
// Drain after GPUI installs callbacks and maps the window, so the first frame
// does not depend on unrelated input. An idle alone does not wake calloop's poll.
state.loop_handle.insert_idle(move |client| {
client.process_x11_events(&xcb_connection).log_err();
});
state.common.signal.wakeup();
Ok(Box::new(window))
}

Expand Down
13 changes: 10 additions & 3 deletions crates/moon-gpui-linux/src/linux/x11/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1689,7 +1689,7 @@ impl PlatformWindow for X11Window {
self.0.callbacks.borrow_mut().button_layout_changed = Some(callback);
}

fn draw(&self, scene: &Scene) {
fn draw(&self, scene: &Scene) -> bool {
let mut inner = self.0.state.borrow_mut();

if inner.renderer.device_lost() {
Expand All @@ -1709,14 +1709,15 @@ impl PlatformWindow for X11Window {
}

inner.force_render_after_recovery = true;
return;
return false;
}

inner.renderer.draw(scene);
let accepted = inner.renderer.draw(scene);

if inner.renderer.needs_redraw() {
inner.force_render_after_recovery = true;
}
accepted
}

fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
Expand Down Expand Up @@ -1912,6 +1913,12 @@ impl PlatformWindow for X11Window {
self.0.state.borrow().renderer.gpu_specs().into()
}

/// Invalidate the shared device; the next native draw runs normal recovery.
fn request_gpu_device_reset(&self) -> bool {
self.0.state.borrow().renderer.request_device_reset();
true
}

fn play_system_bell(&self) {
// Volume 0% means don't increase or decrease from system volume
let _ = self.0.xcb.bell(0);
Expand Down
27 changes: 20 additions & 7 deletions crates/moon-gpui-macos/src/metal_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,14 +551,15 @@ impl MetalRenderer {
// nothing to do
}

pub fn draw(&mut self, scene: &Scene) {
/// Return whether a drawable was acquired and its frame submitted for presentation.
pub fn draw(&mut self, scene: &Scene) -> bool {
let layer = match &self.layer {
Some(l) => l.clone(),
None => {
log::error!(
"draw() called on headless renderer - use render_scene_to_image() instead"
);
return;
return false;
}
};
let viewport_size = layer.drawable_size();
Expand All @@ -573,7 +574,7 @@ impl MetalRenderer {
"failed to retrieve next drawable, drawable size: {:?}",
viewport_size
);
return;
return false;
};

loop {
Expand Down Expand Up @@ -605,7 +606,7 @@ impl MetalRenderer {
command_buffer.present_drawable(drawable);
command_buffer.commit();
}
return;
return true;
}
Err(err) => {
log::error!(
Expand All @@ -626,6 +627,7 @@ impl MetalRenderer {
}
}
}
false
}

/// Renders the scene to a texture and returns the pixel data as an RGBA image.
Expand Down Expand Up @@ -1060,6 +1062,7 @@ impl MetalRenderer {
self.draw_primitives_to_texture(scene, instance_buffer, drawable.texture(), viewport_size)
}

/// Record the scene, preserving canvas bakes when an instance-buffer retry is required.
fn draw_primitives_to_texture(
&mut self,
scene: &Scene,
Expand Down Expand Up @@ -1105,6 +1108,8 @@ impl MetalRenderer {
&mut instance_offset,
);
if !ok {
self.sync_instance_buffer(instance_buffer, instance_offset);
command_buffer.commit();
anyhow::bail!(
"gpu canvas text too large: {} mono, {} poly",
scene.gpu_canvas_text_under_scene.monochrome_sprites.len(),
Expand Down Expand Up @@ -1211,6 +1216,8 @@ impl MetalRenderer {
};
if !ok {
command_encoder.end_encoding();
self.sync_instance_buffer(instance_buffer, instance_offset);
command_buffer.commit();
anyhow::bail!(
"scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
scene.paths.len(),
Expand Down Expand Up @@ -1238,22 +1245,28 @@ impl MetalRenderer {
&mut instance_offset,
);
if !ok {
self.sync_instance_buffer(instance_buffer, instance_offset);
command_buffer.commit();
anyhow::bail!(
"gpu canvas text too large: {} mono, {} poly",
scene.gpu_canvas_text_over_scene.monochrome_sprites.len(),
scene.gpu_canvas_text_over_scene.polychrome_sprites.len(),
);
}

self.sync_instance_buffer(instance_buffer, instance_offset);

Ok(command_buffer.to_owned())
}

/// Publish CPU instance writes before submitting either a complete scene or a failed retry.
fn sync_instance_buffer(&self, instance_buffer: &InstanceBuffer, instance_offset: usize) {
if !self.is_unified_memory {
// Sync the instance buffer to the GPU
instance_buffer.metal_buffer.did_modify_range(NSRange {
location: 0,
length: instance_offset as NSUInteger,
});
}

Ok(command_buffer.to_owned())
}

fn draw_paths_to_intermediate(
Expand Down
4 changes: 2 additions & 2 deletions crates/moon-gpui-macos/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1792,9 +1792,9 @@ impl PlatformWindow for MacWindow {
self.0.as_ref().lock().toggle_tab_bar_callback = Some(callback);
}

fn draw(&self, scene: &gpui::Scene) {
fn draw(&self, scene: &gpui::Scene) -> bool {
let mut this = self.0.lock();
this.renderer.draw(scene);
this.renderer.draw(scene)
}

fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
Expand Down
4 changes: 2 additions & 2 deletions crates/moon-gpui-web/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,7 @@ impl PlatformWindow for WebWindow {
self.inner.callbacks.borrow_mut().appearance_changed = Some(callback);
}

fn draw(&self, scene: &Scene) {
fn draw(&self, scene: &Scene) -> bool {
if let Some((width, height)) = self.inner.pending_physical_size.take() {
if self.inner.canvas.width() != width || self.inner.canvas.height() != height {
self.inner.canvas.set_width(width);
Expand All @@ -681,7 +681,7 @@ impl PlatformWindow for WebWindow {
drop(state);
}

self.inner.state.borrow_mut().renderer.draw(scene);
self.inner.state.borrow_mut().renderer.draw(scene)
}

fn completed_frame(&self) {
Expand Down
38 changes: 31 additions & 7 deletions crates/moon-gpui-wgpu/src/wgpu_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ use std::ptr::NonNull;
use std::rc::Rc;
use std::sync::{Arc, Mutex, Weak};

#[cfg(all(test, any(windows, target_os = "linux")))]
mod tests;

#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct GlobalParams {
Expand Down Expand Up @@ -514,7 +517,8 @@ impl WgpuRenderer {
let last_error_clone = Arc::clone(&last_error);
device.on_uncaptured_error(Arc::new(move |error| {
let mut guard = last_error_clone.lock().unwrap();
*guard = Some(error.to_string());
// Preserve the cause: later submissions may only report that its pipeline is invalid.
guard.get_or_insert_with(|| error.to_string());
}));

let resources = WgpuResources {
Expand Down Expand Up @@ -1283,6 +1287,8 @@ impl WgpuRenderer {
self.draw_gpu_canvas_text(text_frame, instance_offset, pass)
}

/// Submit scene commands, retaining canvas preparation when instance growth retries it.
/// Returns false when the surface cannot accept this frame.
pub fn draw(&mut self, scene: &Scene) -> bool {
// Bail out early if the surface has been unconfigured (e.g. during
// Android background/rotation transitions). Attempting to acquire
Expand Down Expand Up @@ -1461,7 +1467,9 @@ impl WgpuRenderer {
}
}
if overflow {
drop(encoder);
// Canvas prepare already committed its CPU state. Keep those GPU commands;
// the retry clears the partial surface before the only present below.
self.resources().queue.submit([encoder.finish()]);
if self.instance_buffer_capacity >= self.max_buffer_size {
log::error!(
"instance buffer size grew too large: {}",
Expand Down Expand Up @@ -1581,7 +1589,7 @@ impl WgpuRenderer {
}

if overflow {
drop(encoder);
self.resources().queue.submit([encoder.finish()]);
if self.instance_buffer_capacity >= self.max_buffer_size {
log::error!(
"instance buffer size grew too large: {}",
Expand Down Expand Up @@ -1625,7 +1633,7 @@ impl WgpuRenderer {
}

if overflow {
drop(encoder);
self.resources().queue.submit([encoder.finish()]);
if self.instance_buffer_capacity >= self.max_buffer_size {
log::error!(
"instance buffer size grew too large: {}",
Expand Down Expand Up @@ -2394,6 +2402,17 @@ impl WgpuRenderer {
self.device_lost.load(std::sync::atomic::Ordering::SeqCst)
}

/// Destroy the shared device and explicitly request recovery for diagnostics.
/// Ordinary device destruction intentionally does not set the lost callback's flag.
pub fn request_device_reset(&self) {
if !self
.device_lost
.swap(true, std::sync::atomic::Ordering::SeqCst)
{
self.resources().device.destroy();
}
}

/// Returns true if a redraw is needed because GPU state was cleared.
/// Calling this method clears the flag.
pub fn needs_redraw(&mut self) -> bool {
Expand Down Expand Up @@ -2435,16 +2454,21 @@ impl WgpuRenderer {
*gpu_context.borrow_mut() = None;

// Wait briefly for the GPU driver to stabilize, then try to
// recreate the context without software renderers. If this fails
// recreate the context. A software session must be able to recover on
// its existing adapter class; a hardware session must not silently
// fall back to software. If this fails
// the caller should request another frame and retry — the real GPU
// may need more time to come back (e.g. after suspend/resume).
std::thread::sleep(std::time::Duration::from_millis(350));

let instance = WgpuContext::instance(Box::new(window.clone()));
let surface =
create_surface(&instance, display_handle.as_raw(), window_handle.as_raw())?;
let new_context =
WgpuContext::new_rejecting_software(instance, &surface, self.compositor_gpu)?;
let new_context = if self.adapter_info.device_type == wgpu::DeviceType::Cpu {
WgpuContext::new(instance, &surface, self.compositor_gpu)?
} else {
WgpuContext::new_rejecting_software(instance, &surface, self.compositor_gpu)?
};
*gpu_context.borrow_mut() = Some(new_context);
surface
} else {
Expand Down
Loading
Loading