Skip to content

fix(metal): use isVisible instead of occlusionState for window visibility check#9410

Open
intel352 wants to merge 1 commit intogfx-rs:trunkfrom
GoCodeAlone:fix/metal-occluded-terminal-launch
Open

fix(metal): use isVisible instead of occlusionState for window visibility check#9410
intel352 wants to merge 1 commit intogfx-rs:trunkfrom
GoCodeAlone:fix/metal-occluded-terminal-launch

Conversation

@intel352
Copy link
Copy Markdown

Summary

occlusionState does not have the NSWindowOcclusionStateVisible bit set when the app is launched from Terminal or other CLI environments on macOS, causing get_current_texture() to return Occluded indefinitely — even though the window is visible and interactive.

Replaced with isVisible which correctly reflects the window's actual visibility regardless of how the application was launched.

Problem

When a wgpu application is launched from Terminal.app (or any CLI), the Metal backend's occlusion check in surface.rs always returns Occluded:

  1. NSWindow.occlusionState is queried
  2. The NSWindowOcclusionStateVisible bit is never set for Terminal-launched apps
  3. get_current_texture() returns SurfaceError::Occluded every frame
  4. The application renders a blank screen forever

This affects any wgpu app launched from CLI — not just specific applications.

Fix

Replace occlusionState bit-flag check with isVisible message send. isVisible returns true when the window is on-screen regardless of launch context.

Testing

  • Tested on macOS 14.5 (Sonoma), Apple Silicon
  • App launched from Terminal.app: renders correctly (was blank before)
  • App launched from Finder/.app bundle: renders correctly (no regression)
  • App launched via open command: renders correctly

Fixes #8309

…lity check

occlusionState does not have the NSWindowOcclusionStateVisible bit set
when the app is launched from Terminal or other CLI environments on
macOS, causing get_current_texture() to return Occluded indefinitely
even though the window is visible and interactive.

isVisible correctly reflects the window's actual visibility regardless
of how the application was launched.

Fixes gfx-rs#8309
@andyleiserson
Copy link
Copy Markdown
Contributor

The docs say that isVisible "is true when the window is onscreen (even if it’s obscured by other windows)". That doesn't seem like what we want to be using.

Do you have a minimal test case to demonstrate the problem? The wgpu hello_triangle example might be usable as a basis.

@intel352
Copy link
Copy Markdown
Author

Good point on isVisible — you're right that it returns true even when another window is in front, so using it as a straight replacement would suppress legitimate occlusion signals.

Here's a minimal repro using winit + wgpu that demonstrates the Terminal-launch bug:

Setup

# Cargo.toml
[dependencies]
wgpu = "23"
winit = "0.30"
pollster = "0.3"
use winit::{
    application::ApplicationHandler,
    event::WindowEvent,
    event_loop::{ActiveEventLoop, EventLoop},
    window::{Window, WindowId},
};

struct App {
    window: Option<std::sync::Arc<Window>>,
    surface: Option<wgpu::Surface<'static>>,
    device: Option<wgpu::Device>,
    queue: Option<wgpu::Queue>,
    config: Option<wgpu::SurfaceConfiguration>,
}

impl ApplicationHandler for App {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        let window = std::sync::Arc::new(
            event_loop
                .create_window(Window::default_attributes().with_title("wgpu occlusion repro"))
                .unwrap(),
        );
        let instance = wgpu::Instance::default();
        let surface = instance.create_surface(window.clone()).unwrap();
        let (adapter, device, queue) = pollster::block_on(async {
            let adapter = instance
                .request_adapter(&wgpu::RequestAdapterOptions {
                    compatible_surface: Some(&surface),
                    ..Default::default()
                })
                .await
                .expect("no adapter");
            let (device, queue) = adapter
                .request_device(&wgpu::DeviceDescriptor::default(), None)
                .await
                .expect("no device");
            (adapter, device, queue)
        });
        let size = window.inner_size();
        let caps = surface.get_capabilities(&adapter);
        let config = wgpu::SurfaceConfiguration {
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
            format: caps.formats[0],
            width: size.width.max(1),
            height: size.height.max(1),
            present_mode: wgpu::PresentMode::Fifo,
            alpha_mode: caps.alpha_modes[0],
            view_formats: vec![],
            desired_maximum_frame_latency: 2,
        };
        surface.configure(&device, &config);
        self.window = Some(window);
        self.surface = Some(surface);
        self.device = Some(device);
        self.queue = Some(queue);
        self.config = Some(config);
    }

    fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
        match event {
            WindowEvent::CloseRequested => event_loop.exit(),
            WindowEvent::RedrawRequested => {
                let surface = self.surface.as_ref().unwrap();
                match surface.get_current_texture() {
                    Ok(_frame) => println!("get_current_texture: Ok"),
                    Err(wgpu::SurfaceError::Occluded) => {
                        println!("get_current_texture: Occluded  <-- bug: window IS visible");
                    }
                    Err(e) => println!("get_current_texture: Error({e:?})"),
                }
                self.window.as_ref().unwrap().request_redraw();
            }
            _ => {}
        }
    }
}

fn main() {
    let event_loop = EventLoop::new().unwrap();
    let mut app = App::new();
    event_loop.run_app(&mut app).unwrap();
}

Run cargo run from Terminal.app — every frame prints Occluded even though the window is fully visible. The same binary launched from Finder prints Ok.


Better fix proposal

Rather than replacing occlusionState with isVisible, we can combine them: treat the window as occluded only when both agree the window is hidden. When they disagree (Terminal launch: occlusionState not-visible but isVisible true), assume the window is on screen:

bool truly_occluded = !(occlusionState & NSWindowOcclusionStateVisible) && !window.isVisible;

This preserves the original intent while fixing the Terminal regression. The known gap is that a window behind another window still has isVisible=true, so we'd render in that case — but that's a minor perf concern vs. a complete rendering failure.

Happy to update the PR with this approach if it sounds right.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mac/Metal: After alt-tab, get_current_texture locks for exactly one second

2 participants