Skip to main content

radiant_rs/backends/
wgpu.rs

1use crate::prelude::*;
2use crate::core::Mat4Trait;
3use wgpu;
4use winit;
5use std::sync::OnceLock;
6
7// Newtype that asserts Send+Sync for the event loop, which is safe because we always
8// build it with with_any_thread(true) on Linux (the only non-macOS platform we target).
9#[cfg(not(target_os = "macos"))]
10struct SendableEventLoop(winit::event_loop::EventLoop<()>);
11#[cfg(not(target_os = "macos"))]
12unsafe impl Send for SendableEventLoop {}
13#[cfg(not(target_os = "macos"))]
14unsafe impl Sync for SendableEventLoop {}
15
16// One event loop per process; created on first Display::new, reused by subsequent ones.
17#[cfg(not(target_os = "macos"))]
18static GLOBAL_EVENT_LOOP: OnceLock<Mutex<SendableEventLoop>> = OnceLock::new();
19
20// Per-window event queues. Filled during pump_app_events; drained by each Display::poll_events.
21#[cfg(not(target_os = "macos"))]
22static WINDOW_EVENTS: OnceLock<Mutex<HashMap<winit::window::WindowId, Vec<crate::core::Event>>>> = OnceLock::new();
23
24/// GPU resources that can be shared across displays that use the same context.
25pub(crate) struct SharedGpu {
26    pub(crate) instance: Arc<wgpu::Instance>,
27    pub(crate) adapter: Arc<wgpu::Adapter>,
28    pub(crate) device: Arc<wgpu::Device>,
29    pub(crate) queue: Arc<wgpu::Queue>,
30}
31
32const MAX_BUFFERS: usize = 10;
33
34use crate::core::Vertex;
35
36const VERTEX_STRIDE: u64 = std::mem::size_of::<Vertex>() as u64;
37
38// Byte offset of texture_uv within a repr(C) Vertex struct.
39// Computed from field sizes: position(8) + offset(8) + rotation(4) + color(16) + bucket_id(4) + texture_id(4) = 44
40const TEXTURE_UV_OFFSET: u64 = 44;
41
42// --------------
43// Public interface
44// --------------
45
46pub mod public {}
47
48// --------------
49// Error
50// --------------
51
52#[derive(Debug)]
53pub enum Error {
54    OsError(String),
55    Incompatible(String),
56    NotAvailable(String),
57    WindowCreation(String),
58    WgpuError(String),
59    Unknown,
60}
61
62impl From<wgpu::CreateSurfaceError> for crate::core::Error {
63    fn from(error: wgpu::CreateSurfaceError) -> crate::core::Error {
64        crate::core::Error::BackendError(Error::WindowCreation(format!("Surface creation failed: {:?}", error)))
65    }
66}
67
68// --------------
69// Blend mode pipeline cache key
70// --------------
71
72#[derive(Hash, PartialEq, Eq, Clone, Copy)]
73struct BlendStateKey(u8, u8, u8, u8, u8, u8);
74
75// --------------
76// Blit resources (texture → frame copy, REPLACE blend, cached in Display)
77// --------------
78
79struct BlitResources {
80    pipeline: Arc<wgpu::RenderPipeline>,
81    bind_group_layout: Arc<wgpu::BindGroupLayout>,
82    nearest_sampler: Arc<wgpu::Sampler>,
83    linear_sampler: Arc<wgpu::Sampler>,
84}
85
86impl BlitResources {
87    fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Arc<Self> {
88        let vert = device.create_shader_module(wgpu::ShaderModuleDescriptor {
89            label: Some("Blit Vert"),
90            source: wgpu::ShaderSource::Wgsl(include_str!("../shader/texture.wgsl").into()),
91        });
92        let frag = device.create_shader_module(wgpu::ShaderModuleDescriptor {
93            label: Some("Blit Frag"),
94            source: wgpu::ShaderSource::Wgsl(include_str!("../shader/texture_frag.wgsl").into()),
95        });
96        let bind_group_layout = Arc::new(device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
97            label: Some("Blit BGL"),
98            entries: &[
99                Context::bgl_uniform(0, wgpu::ShaderStages::VERTEX),
100                Context::bgl_texture_2d(1, wgpu::ShaderStages::FRAGMENT),
101                Context::bgl_sampler(2, wgpu::ShaderStages::FRAGMENT),
102            ],
103        }));
104        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
105            label: Some("Blit Layout"),
106            bind_group_layouts: &[Some(&*bind_group_layout)],
107            immediate_size: 0,
108        });
109        let pipeline = Arc::new(Context::build_texture_pipeline(
110            device, &vert, &frag, &layout, format, wgpu::BlendState::REPLACE,
111        ));
112        let nearest_sampler = Arc::new(device.create_sampler(&wgpu::SamplerDescriptor {
113            label: Some("Blit Nearest"),
114            mag_filter: wgpu::FilterMode::Nearest,
115            min_filter: wgpu::FilterMode::Nearest,
116            address_mode_u: wgpu::AddressMode::ClampToEdge,
117            address_mode_v: wgpu::AddressMode::ClampToEdge,
118            address_mode_w: wgpu::AddressMode::ClampToEdge,
119            ..Default::default()
120        }));
121        let linear_sampler = Arc::new(device.create_sampler(&wgpu::SamplerDescriptor {
122            label: Some("Blit Linear"),
123            mag_filter: wgpu::FilterMode::Linear,
124            min_filter: wgpu::FilterMode::Linear,
125            address_mode_u: wgpu::AddressMode::ClampToEdge,
126            address_mode_v: wgpu::AddressMode::ClampToEdge,
127            address_mode_w: wgpu::AddressMode::ClampToEdge,
128            ..Default::default()
129        }));
130        Arc::new(BlitResources { pipeline, bind_group_layout, nearest_sampler, linear_sampler })
131    }
132}
133
134impl From<&wgpu::BlendState> for BlendStateKey {
135    fn from(bs: &wgpu::BlendState) -> Self {
136        BlendStateKey(
137            bs.color.src_factor as u8,
138            bs.color.dst_factor as u8,
139            bs.color.operation as u8,
140            bs.alpha.src_factor as u8,
141            bs.alpha.dst_factor as u8,
142            bs.alpha.operation as u8,
143        )
144    }
145}
146
147// --------------
148// Event handling (pump_app_events, not available on macOS)
149// --------------
150
151#[cfg(not(target_os = "macos"))]
152// Collects events for all windows during a single pump_app_events call.
153struct EventCollector {
154    window_events: Vec<(winit::window::WindowId, crate::core::Event)>,
155    // MouseDelta is a device event with no window — delivered to all registered windows.
156    mouse_delta: Option<(i32, i32)>,
157}
158
159#[cfg(not(target_os = "macos"))]
160impl winit::application::ApplicationHandler for EventCollector {
161    fn resumed(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
162
163    fn window_event(
164        &mut self,
165        _: &winit::event_loop::ActiveEventLoop,
166        window_id: winit::window::WindowId,
167        event: winit::event::WindowEvent,
168    ) {
169        use winit::event::{WindowEvent, ElementState};
170        use winit::keyboard::PhysicalKey;
171        use crate::core::Event;
172        let ev = match event {
173            WindowEvent::CloseRequested => Some(Event::Close),
174            WindowEvent::Focused(focused) => Some(if focused { Event::Focus } else { Event::Blur }),
175            WindowEvent::CursorMoved { position, .. } => {
176                Some(Event::MousePosition(position.x as i32, position.y as i32))
177            }
178            WindowEvent::MouseInput { state, button, .. } => {
179                mouse_button_to_id(button)
180                    .map(|id| Event::MouseInput(id, state == ElementState::Pressed))
181            }
182            WindowEvent::KeyboardInput {
183                event: winit::event::KeyEvent {
184                    physical_key: PhysicalKey::Code(code), state, ..
185                }, ..
186            } => {
187                keycode_to_id(code)
188                    .map(|id| Event::KeyboardInput(id, state == ElementState::Pressed))
189            }
190            _ => None,
191        };
192        if let Some(ev) = ev {
193            self.window_events.push((window_id, ev));
194        }
195    }
196
197    fn device_event(
198        &mut self,
199        _: &winit::event_loop::ActiveEventLoop,
200        _: winit::event::DeviceId,
201        event: winit::event::DeviceEvent,
202    ) {
203        if let winit::event::DeviceEvent::MouseMotion { delta } = event {
204            self.mouse_delta = Some((delta.0 as i32, delta.1 as i32));
205        }
206    }
207}
208
209#[cfg(not(target_os = "macos"))]
210fn mouse_button_to_id(button: winit::event::MouseButton) -> Option<usize> {
211    let id = match button {
212        winit::event::MouseButton::Left    => 0,
213        winit::event::MouseButton::Right   => 1,
214        winit::event::MouseButton::Middle  => 2,
215        winit::event::MouseButton::Back    => 3,
216        winit::event::MouseButton::Forward => 4,
217        winit::event::MouseButton::Other(n) => {
218            let id = n as usize + 5;
219            if id < crate::core::NUM_BUTTONS { id } else { return None; }
220        }
221    };
222    if id < crate::core::NUM_BUTTONS { Some(id) } else { None }
223}
224
225#[cfg(not(target_os = "macos"))]
226fn keycode_to_id(code: winit::keyboard::KeyCode) -> Option<usize> {
227    use winit::keyboard::KeyCode;
228    use crate::core::InputId;
229    let id = match code {
230        // Digits row
231        KeyCode::Digit1        => InputId::Key1 as usize,
232        KeyCode::Digit2        => InputId::Key2 as usize,
233        KeyCode::Digit3        => InputId::Key3 as usize,
234        KeyCode::Digit4        => InputId::Key4 as usize,
235        KeyCode::Digit5        => InputId::Key5 as usize,
236        KeyCode::Digit6        => InputId::Key6 as usize,
237        KeyCode::Digit7        => InputId::Key7 as usize,
238        KeyCode::Digit8        => InputId::Key8 as usize,
239        KeyCode::Digit9        => InputId::Key9 as usize,
240        KeyCode::Digit0        => InputId::Key0 as usize,
241        // Letters
242        KeyCode::KeyA          => InputId::A as usize,
243        KeyCode::KeyB          => InputId::B as usize,
244        KeyCode::KeyC          => InputId::C as usize,
245        KeyCode::KeyD          => InputId::D as usize,
246        KeyCode::KeyE          => InputId::E as usize,
247        KeyCode::KeyF          => InputId::F as usize,
248        KeyCode::KeyG          => InputId::G as usize,
249        KeyCode::KeyH          => InputId::H as usize,
250        KeyCode::KeyI          => InputId::I as usize,
251        KeyCode::KeyJ          => InputId::J as usize,
252        KeyCode::KeyK          => InputId::K as usize,
253        KeyCode::KeyL          => InputId::L as usize,
254        KeyCode::KeyM          => InputId::M as usize,
255        KeyCode::KeyN          => InputId::N as usize,
256        KeyCode::KeyO          => InputId::O as usize,
257        KeyCode::KeyP          => InputId::P as usize,
258        KeyCode::KeyQ          => InputId::Q as usize,
259        KeyCode::KeyR          => InputId::R as usize,
260        KeyCode::KeyS          => InputId::S as usize,
261        KeyCode::KeyT          => InputId::T as usize,
262        KeyCode::KeyU          => InputId::U as usize,
263        KeyCode::KeyV          => InputId::V as usize,
264        KeyCode::KeyW          => InputId::W as usize,
265        KeyCode::KeyX          => InputId::X as usize,
266        KeyCode::KeyY          => InputId::Y as usize,
267        KeyCode::KeyZ          => InputId::Z as usize,
268        // Control keys
269        KeyCode::Escape        => InputId::Escape as usize,
270        KeyCode::F1            => InputId::F1 as usize,
271        KeyCode::F2            => InputId::F2 as usize,
272        KeyCode::F3            => InputId::F3 as usize,
273        KeyCode::F4            => InputId::F4 as usize,
274        KeyCode::F5            => InputId::F5 as usize,
275        KeyCode::F6            => InputId::F6 as usize,
276        KeyCode::F7            => InputId::F7 as usize,
277        KeyCode::F8            => InputId::F8 as usize,
278        KeyCode::F9            => InputId::F9 as usize,
279        KeyCode::F10           => InputId::F10 as usize,
280        KeyCode::F11           => InputId::F11 as usize,
281        KeyCode::F12           => InputId::F12 as usize,
282        KeyCode::F13           => InputId::F13 as usize,
283        KeyCode::F14           => InputId::F14 as usize,
284        KeyCode::F15           => InputId::F15 as usize,
285        KeyCode::PrintScreen   => InputId::Snapshot as usize,
286        KeyCode::ScrollLock    => InputId::Scroll as usize,
287        KeyCode::Pause         => InputId::Pause as usize,
288        KeyCode::Insert        => InputId::Insert as usize,
289        KeyCode::Home          => InputId::Home as usize,
290        KeyCode::Delete        => InputId::Delete as usize,
291        KeyCode::End           => InputId::End as usize,
292        KeyCode::PageDown      => InputId::PageDown as usize,
293        KeyCode::PageUp        => InputId::PageUp as usize,
294        KeyCode::ArrowLeft     => InputId::CursorLeft as usize,
295        KeyCode::ArrowUp       => InputId::CursorUp as usize,
296        KeyCode::ArrowRight    => InputId::CursorRight as usize,
297        KeyCode::ArrowDown     => InputId::CursorDown as usize,
298        KeyCode::Backspace     => InputId::Backspace as usize,
299        KeyCode::Enter         => InputId::Return as usize,
300        KeyCode::Space         => InputId::Space as usize,
301        // Numpad
302        KeyCode::NumLock       => InputId::Numlock as usize,
303        KeyCode::Numpad0       => InputId::Numpad0 as usize,
304        KeyCode::Numpad1       => InputId::Numpad1 as usize,
305        KeyCode::Numpad2       => InputId::Numpad2 as usize,
306        KeyCode::Numpad3       => InputId::Numpad3 as usize,
307        KeyCode::Numpad4       => InputId::Numpad4 as usize,
308        KeyCode::Numpad5       => InputId::Numpad5 as usize,
309        KeyCode::Numpad6       => InputId::Numpad6 as usize,
310        KeyCode::Numpad7       => InputId::Numpad7 as usize,
311        KeyCode::Numpad8       => InputId::Numpad8 as usize,
312        KeyCode::Numpad9       => InputId::Numpad9 as usize,
313        KeyCode::NumpadAdd     => InputId::Add as usize,
314        KeyCode::NumpadDecimal => InputId::Decimal as usize,
315        KeyCode::NumpadDivide  => InputId::Divide as usize,
316        KeyCode::NumpadMultiply=> InputId::Multiply as usize,
317        KeyCode::NumpadSubtract=> InputId::Subtract as usize,
318        KeyCode::NumpadEnter   => InputId::NumpadEnter as usize,
319        KeyCode::NumpadEqual   => InputId::NumpadEquals as usize,
320        KeyCode::NumpadComma   => InputId::NumpadComma as usize,
321        // Punctuation & symbols
322        KeyCode::Quote         => InputId::Apostrophe as usize,
323        KeyCode::Backslash     => InputId::Backslash as usize,
324        KeyCode::CapsLock      => InputId::Capital as usize,
325        KeyCode::Comma         => InputId::Comma as usize,
326        KeyCode::Convert       => InputId::Convert as usize,
327        KeyCode::Equal         => InputId::Equals as usize,
328        KeyCode::Backquote     => InputId::Grave as usize,
329        KeyCode::KanaMode      => InputId::Kana as usize,
330        KeyCode::AltLeft       => InputId::LAlt as usize,
331        KeyCode::BracketLeft   => InputId::LBracket as usize,
332        KeyCode::ControlLeft   => InputId::LControl as usize,
333        KeyCode::ShiftLeft     => InputId::LShift as usize,
334        KeyCode::SuperLeft     => InputId::LWin as usize,
335        KeyCode::LaunchMail    => InputId::Mail as usize,
336        KeyCode::MediaSelect   => InputId::MediaSelect as usize,
337        KeyCode::MediaStop     => InputId::MediaStop as usize,
338        KeyCode::Minus         => InputId::Minus as usize,
339        KeyCode::AudioVolumeMute    => InputId::Mute as usize,
340        KeyCode::MediaTrackNext     => InputId::NextTrack as usize,
341        KeyCode::NonConvert         => InputId::NoConvert as usize,
342        KeyCode::IntlBackslash      => InputId::OEM102 as usize,
343        KeyCode::Period             => InputId::Period as usize,
344        KeyCode::MediaPlayPause     => InputId::PlayPause as usize,
345        KeyCode::Power              => InputId::Power as usize,
346        KeyCode::MediaTrackPrevious => InputId::PrevTrack as usize,
347        KeyCode::AltRight           => InputId::RAlt as usize,
348        KeyCode::BracketRight       => InputId::RBracket as usize,
349        KeyCode::ControlRight       => InputId::RControl as usize,
350        KeyCode::ShiftRight         => InputId::RShift as usize,
351        KeyCode::SuperRight         => InputId::RWin as usize,
352        KeyCode::Semicolon          => InputId::Semicolon as usize,
353        KeyCode::Slash              => InputId::Slash as usize,
354        KeyCode::Sleep              => InputId::Sleep as usize,
355        KeyCode::Tab                => InputId::Tab as usize,
356        KeyCode::AudioVolumeDown    => InputId::VolumeDown as usize,
357        KeyCode::AudioVolumeUp      => InputId::VolumeUp as usize,
358        KeyCode::WakeUp             => InputId::Wake as usize,
359        KeyCode::BrowserBack        => InputId::WebBack as usize,
360        KeyCode::BrowserFavorites   => InputId::WebFavorites as usize,
361        KeyCode::BrowserForward     => InputId::WebForward as usize,
362        KeyCode::BrowserHome        => InputId::WebHome as usize,
363        KeyCode::BrowserRefresh     => InputId::WebRefresh as usize,
364        KeyCode::BrowserSearch      => InputId::WebSearch as usize,
365        KeyCode::BrowserStop        => InputId::WebStop as usize,
366        KeyCode::IntlYen            => InputId::Yen as usize,
367        KeyCode::ContextMenu        => InputId::Apps as usize,
368        _ => return None,
369    };
370    Some(id)
371}
372
373// --------------
374// Display
375// --------------
376
377#[derive(Clone)]
378pub struct Display {
379    inner: Arc<DisplayInner>,
380}
381
382struct DisplayInner {
383    window: Arc<winit::window::Window>,
384    instance: Arc<wgpu::Instance>,
385    adapter: Arc<wgpu::Adapter>,
386    surface: wgpu::Surface<'static>,
387    device: Arc<wgpu::Device>,
388    queue: Arc<wgpu::Queue>,
389    config: Mutex<wgpu::SurfaceConfiguration>,
390    blit_resources: Arc<BlitResources>,
391}
392
393impl Display {
394    pub fn new(descriptor: crate::core::DisplayBuilder, shared: Option<SharedGpu>) -> crate::core::Result<Display> {
395        pollster::block_on(Self::create_async(descriptor, shared))
396    }
397
398    async fn create_async(descriptor: crate::core::DisplayBuilder, shared: Option<SharedGpu>) -> crate::core::Result<Display> {
399
400        // Ensure the global event loop exists; create it on the first call.
401        #[cfg(not(target_os = "macos"))]
402        if GLOBAL_EVENT_LOOP.get().is_none() {
403            let el_result = {
404                #[cfg(target_os = "linux")]
405                {
406                    use winit::platform::x11::EventLoopBuilderExtX11;
407                    winit::event_loop::EventLoop::builder()
408                        .with_any_thread(true)
409                        .build()
410                }
411                #[cfg(not(target_os = "linux"))]
412                {
413                    winit::event_loop::EventLoop::builder().build()
414                }
415            };
416            match el_result {
417                Ok(el) => { let _ = GLOBAL_EVENT_LOOP.set(Mutex::new(SendableEventLoop(el))); }
418                Err(_) if GLOBAL_EVENT_LOOP.get().is_some() => {} // concurrent init won
419                Err(e) => return Err(crate::core::Error::BackendError(Error::OsError(
420                    format!("Failed to create event loop: {:?}", e)
421                ))),
422            }
423        }
424
425        let window_attributes = winit::window::WindowAttributes::default()
426            .with_inner_size(winit::dpi::PhysicalSize::new(descriptor.width, descriptor.height))
427            .with_title(&descriptor.title)
428            .with_transparent(descriptor.transparent)
429            .with_decorations(descriptor.decorations)
430            .with_visible(descriptor.visible)
431            .with_resizable(true);
432
433        #[allow(deprecated)]
434        let window = {
435            #[cfg(not(target_os = "macos"))]
436            {
437                GLOBAL_EVENT_LOOP.get()
438                    .ok_or_else(|| crate::core::Error::BackendError(Error::OsError("Event loop unavailable".to_string())))?
439                    .lock().unwrap().0
440                    .create_window(window_attributes)
441                    .map_err(|e| crate::core::Error::BackendError(Error::WindowCreation(format!("Failed to create window: {:?}", e))))?
442            }
443            #[cfg(target_os = "macos")]
444            {
445                return Err(crate::core::Error::BackendError(Error::NotAvailable(
446                    "Multi-window / non-main-thread display creation not supported on macOS".to_string()
447                )));
448            }
449        };
450        let window = Arc::new(window);
451
452        // Reuse GPU resources from shared context, or create them for the first display.
453        let (instance, adapter, device, queue) = if let Some(s) = shared {
454            (s.instance, s.adapter, s.device, s.queue)
455        } else {
456            let instance = Arc::new(wgpu::Instance::new(wgpu::InstanceDescriptor {
457                backends: wgpu::Backends::all(),
458                ..wgpu::InstanceDescriptor::new_without_display_handle()
459            }));
460
461            // Create a temporary surface just for adapter selection.
462            let tmp_surface = instance.create_surface(window.clone()).map_err(crate::core::Error::from)?;
463
464            let adapter = Arc::new(instance.request_adapter(&wgpu::RequestAdapterOptions {
465                power_preference: if descriptor.vsync {
466                    wgpu::PowerPreference::HighPerformance
467                } else {
468                    wgpu::PowerPreference::LowPower
469                },
470                force_fallback_adapter: false,
471                compatible_surface: Some(&tmp_surface),
472            }).await.map_err(|_| {
473                crate::core::Error::BackendError(Error::NotAvailable("No suitable GPU adapter found".to_string()))
474            })?);
475
476            let (device, queue) = adapter.request_device(&wgpu::DeviceDescriptor {
477                label: Some("Radiant Device"),
478                required_features: wgpu::Features::empty(),
479                required_limits: wgpu::Limits::default(),
480                ..Default::default()
481            }).await.map_err(|e| {
482                crate::core::Error::BackendError(Error::WgpuError(format!("Failed to request device: {:?}", e)))
483            })?;
484
485            (instance, adapter, Arc::new(device), Arc::new(queue))
486        };
487
488        let surface = instance.create_surface(window.clone()).map_err(crate::core::Error::from)?;
489
490        let size = window.inner_size();
491        let surface_caps = surface.get_capabilities(&*adapter);
492
493        let present_mode = if descriptor.vsync {
494            if surface_caps.present_modes.contains(&wgpu::PresentMode::Mailbox) {
495                wgpu::PresentMode::Mailbox
496            } else {
497                wgpu::PresentMode::Fifo
498            }
499        } else if surface_caps.present_modes.contains(&wgpu::PresentMode::Immediate) {
500            wgpu::PresentMode::Immediate
501        } else {
502            wgpu::PresentMode::FifoRelaxed
503        };
504
505        let config = wgpu::SurfaceConfiguration {
506            usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
507            format: surface_caps.formats[0],
508            width: size.width,
509            height: size.height,
510            present_mode,
511            desired_maximum_frame_latency: 2,
512            alpha_mode: wgpu::CompositeAlphaMode::Auto,
513            view_formats: vec![],
514        };
515        surface.configure(&*device, &config);
516
517        let blit_resources = BlitResources::new(&device, config.format);
518
519        let inner = Arc::new(DisplayInner {
520            window,
521            instance,
522            adapter,
523            surface,
524            device,
525            queue,
526            config: Mutex::new(config),
527            blit_resources,
528        });
529
530        Ok(Display { inner })
531    }
532
533    pub fn draw(self: &Self) -> WgpuFrame {
534        let frame = loop {
535            match self.inner.surface.get_current_texture() {
536                wgpu::CurrentSurfaceTexture::Success(frame) | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => break frame,
537                wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
538                    let size = self.inner.window.inner_size();
539                    let mut config = self.inner.config.lock().unwrap();
540                    config.width = size.width.max(1);
541                    config.height = size.height.max(1);
542                    self.inner.surface.configure(&self.inner.device, &config);
543                }
544                e => panic!("Failed to get swapchain frame: {:?}", e),
545            }
546        };
547        let view = Arc::new(frame.texture.create_view(&wgpu::TextureViewDescriptor::default()));
548
549        let command_encoder = self.inner.device.create_command_encoder(
550            &wgpu::CommandEncoderDescriptor { label: Some("Radiant Command Encoder") }
551        );
552
553        WgpuFrame {
554            view,
555            command_encoder,
556            surface_texture: frame,
557            clear_color: None,
558            first_pass: true,
559            device: self.inner.device.clone(),
560            queue: self.inner.queue.clone(),
561            blit_resources: self.inner.blit_resources.clone(),
562        }
563    }
564
565    pub fn framebuffer_dimensions(self: &Self) -> crate::core::Point2<u32> {
566        let size = self.inner.window.inner_size();
567        (size.width, size.height)
568    }
569
570    pub fn window_dimensions(self: &Self) -> crate::core::Point2<u32> {
571        let size = self.inner.window.inner_size();
572        (size.width, size.height)
573    }
574
575    pub fn set_cursor_position(self: &Self, position: crate::core::Point2<i32>) {
576        let _ = self.inner.window.set_cursor_position(winit::dpi::PhysicalPosition::new(position.0 as f64, position.1 as f64));
577    }
578
579    pub fn set_cursor_state(self: &Self, state: crate::core::CursorState) {
580        use crate::core::CursorState as CS;
581        use winit::window::CursorGrabMode;
582        match state {
583            CS::Normal => {
584                self.inner.window.set_cursor_grab(CursorGrabMode::None).ok();
585                self.inner.window.set_cursor_visible(true);
586            }
587            CS::Hide => {
588                self.inner.window.set_cursor_visible(false);
589            }
590            CS::Grab => {
591                self.inner.window.set_cursor_grab(CursorGrabMode::Confined).ok();
592                self.inner.window.set_cursor_visible(false);
593            }
594        }
595    }
596
597    pub fn set_fullscreen(self: &Self, monitor: Option<crate::core::Monitor>) -> bool {
598        if let Some(monitor) = monitor {
599            self.inner.window.set_fullscreen(Some(winit::window::Fullscreen::Borderless(Some(monitor.inner.inner.winit_monitor))));
600        } else {
601            self.inner.window.set_fullscreen(None);
602        }
603        true
604    }
605
606    pub fn poll_events<F>(self: &Self, mut callback: F) where F: FnMut(crate::core::Event) {
607        #[cfg(not(target_os = "macos"))]
608        {
609            use winit::platform::pump_events::EventLoopExtPumpEvents;
610
611            // Pump the event loop and collect events for every window. Releasing the
612            // event-loop lock before touching WINDOW_EVENTS avoids any lock-order issues.
613            let collected = GLOBAL_EVENT_LOOP.get().and_then(|el_mutex| {
614                el_mutex.lock().ok().map(|mut el| {
615                    let mut collector = EventCollector {
616                        window_events: Vec::new(),
617                        mouse_delta: None,
618                    };
619                    el.0.pump_app_events(Some(std::time::Duration::ZERO), &mut collector);
620                    collector
621                })
622            });
623
624            // Distribute the just-collected events into the per-window queues.
625            let queues = WINDOW_EVENTS.get_or_init(|| Mutex::new(HashMap::new()));
626            if let Some(collector) = collected {
627                if let Ok(mut map) = queues.lock() {
628                    for (win_id, event) in collector.window_events {
629                        map.entry(win_id).or_default().push(event);
630                    }
631                    if let Some((dx, dy)) = collector.mouse_delta {
632                        for events in map.values_mut() {
633                            events.push(crate::core::Event::MouseDelta(dx, dy));
634                        }
635                    }
636                }
637            }
638
639            // Drain this window's events and forward them to the caller.
640            if let Ok(mut map) = queues.lock() {
641                if let Some(events) = map.remove(&self.inner.window.id()) {
642                    for event in events {
643                        callback(event);
644                    }
645                }
646            }
647        }
648        // macOS: pump_app_events unavailable; poll_events remains a no-op
649        #[cfg(target_os = "macos")]
650        let _ = callback;
651    }
652
653    pub fn show(self: &Self) {
654        self.inner.window.set_visible(true);
655    }
656
657    pub fn hide(self: &Self) {
658        self.inner.window.set_visible(false);
659    }
660
661    pub fn set_title(self: &Self, title: &str) {
662        self.inner.window.set_title(title);
663    }
664}
665
666// --------------
667// Frame
668// --------------
669
670pub struct WgpuFrame {
671    view: Arc<wgpu::TextureView>,
672    command_encoder: wgpu::CommandEncoder,
673    surface_texture: wgpu::SurfaceTexture,
674    clear_color: Option<[f64; 4]>,
675    first_pass: bool,
676    device: Arc<wgpu::Device>,
677    queue: Arc<wgpu::Queue>,
678    blit_resources: Arc<BlitResources>,
679}
680
681impl WgpuFrame {
682    pub fn clear(self: &mut Self, color: crate::core::Color) {
683        let crate::core::Color(r, g, b, a) = color;
684        self.clear_color = Some([r as f64, g as f64, b as f64, a as f64]);
685    }
686
687    pub fn finish(mut self: Self) {
688        // If no render pass was issued (e.g. empty frame), apply the pending clear now.
689        if self.first_pass {
690            if let Some(c) = self.clear_color {
691                let encoder = &mut self.command_encoder;
692                let view = &*self.view;
693                let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
694                    label: Some("Clear Pass"),
695                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
696                        view,
697                        depth_slice: None,
698                        resolve_target: None,
699                        ops: wgpu::Operations {
700                            load: wgpu::LoadOp::Clear(wgpu::Color { r: c[0], g: c[1], b: c[2], a: c[3] }),
701                            store: wgpu::StoreOp::Store,
702                        },
703                    })],
704                    depth_stencil_attachment: None,
705                    timestamp_writes: None,
706                    occlusion_query_set: None,
707                    multiview_mask: None,
708                });
709            }
710        }
711        self.queue.submit(std::iter::once(self.command_encoder.finish()));
712        self.surface_texture.present();
713        // device.poll(Wait) is omitted: it is unnecessary for rendering and panics on
714        // device loss (e.g. when the window surface is torn down on close).
715    }
716
717    pub fn dimensions(self: &Self) -> crate::core::Point2<u32> {
718        let size = self.surface_texture.texture.size();
719        (size.width, size.height)
720    }
721
722    pub fn copy_from_texture(self: &mut Self, source: &crate::core::Texture, filter: crate::core::TextureFilter) {
723        use crate::core::Mat4Trait;
724        let uniforms = TextureUniforms {
725            u_view: <[[f32; 4]; 4] as Mat4Trait<f32>>::viewport(1.0, 1.0),
726            u_model: <[[f32; 4]; 4] as Mat4Trait<f32>>::identity(),
727            _rd_color: [1.0, 1.0, 1.0, 1.0],
728            _rd_offset: [0.0, 0.0],
729            _rd_dimensions: [1.0, 1.0],
730            _rd_flags: [0.0, 0.0, 0.0, 0.0],
731        };
732        let load_op = self.next_load_op();
733        let view = self.view.clone();
734        let blit = self.blit_resources.clone();
735        let sampler = Context::sampler_for_filter(&blit.nearest_sampler, &blit.linear_sampler, filter);
736        render_texture_quad(
737            &mut self.command_encoder,
738            &view,
739            &self.device,
740            &self.queue,
741            &uniforms,
742            &BLIT_QUAD,
743            &*source.handle.view,
744            sampler,
745            &blit.pipeline,
746            &blit.bind_group_layout,
747            load_op,
748        );
749    }
750
751    pub fn copy_rect(self: &mut Self, _source_rect: crate::core::Rect<i32>, _target_rect: crate::core::Rect<i32>, _filter: crate::core::TextureFilter) {
752        // frame-to-frame rect blit: not needed for current examples
753    }
754
755    pub fn copy_rect_from_texture(self: &mut Self, source: &crate::core::Texture, source_rect: crate::core::Rect<i32>, target_rect: crate::core::Rect<i32>, filter: crate::core::TextureFilter) {
756        use crate::core::Mat4Trait;
757
758        let tw = source.handle.width as f32;
759        let th = source.handle.height as f32;
760        let frame_size = self.surface_texture.texture.size();
761        let fw = frame_size.width as f32;
762        let fh = frame_size.height as f32;
763
764        // Source UV rect
765        let sx = (source_rect.0).0 as f32;
766        let sy = (source_rect.0).1 as f32;
767        let sw = (source_rect.1).0 as f32;
768        let sh = (source_rect.1).1 as f32;
769        let u0 = sx / tw;
770        let u1 = (sx + sw) / tw;
771        let v0 = sy / th;
772        let v1 = (sy + sh) / th;
773
774        // Destination rect in [0,1] normalized frame space
775        let dx = (target_rect.0).0 as f32;
776        let dy = (target_rect.0).1 as f32;
777        let dw = (target_rect.1).0 as f32;
778        let dh = (target_rect.1).1 as f32;
779
780        let quad = [
781            Vertex { position: [0.0, 0.0], texture_uv: [u0, v0], ..Vertex::default() },
782            Vertex { position: [1.0, 0.0], texture_uv: [u1, v0], ..Vertex::default() },
783            Vertex { position: [0.0, 1.0], texture_uv: [u0, v1], ..Vertex::default() },
784            Vertex { position: [1.0, 1.0], texture_uv: [u1, v1], ..Vertex::default() },
785        ];
786        let uniforms = TextureUniforms {
787            u_view: <[[f32; 4]; 4] as Mat4Trait<f32>>::viewport(1.0, 1.0),
788            u_model: <[[f32; 4]; 4] as Mat4Trait<f32>>::identity(),
789            _rd_color: [1.0, 1.0, 1.0, 1.0],
790            _rd_offset: [dx / fw, dy / fh],
791            _rd_dimensions: [dw / fw, dh / fh],
792            _rd_flags: [0.0, 0.0, 0.0, 0.0],
793        };
794        let load_op = self.next_load_op();
795        let view = self.view.clone();
796        let blit = self.blit_resources.clone();
797        let sampler = Context::sampler_for_filter(&blit.nearest_sampler, &blit.linear_sampler, filter);
798        render_texture_quad(
799            &mut self.command_encoder,
800            &view,
801            &self.device,
802            &self.queue,
803            &uniforms,
804            &quad,
805            &*source.handle.view,
806            sampler,
807            &blit.pipeline,
808            &blit.bind_group_layout,
809            load_op,
810        );
811    }
812
813    // Returns the LoadOp for the next render pass and clears the first_pass flag.
814    fn next_load_op(&mut self) -> wgpu::LoadOp<wgpu::Color> {
815        if self.first_pass {
816            self.first_pass = false;
817            if let Some(c) = self.clear_color {
818                return wgpu::LoadOp::Clear(wgpu::Color { r: c[0], g: c[1], b: c[2], a: c[3] });
819            }
820        }
821        wgpu::LoadOp::Load
822    }
823
824    /// Draw sprites to this frame's surface.
825    pub fn draw_sprites(
826        &mut self,
827        vertices: &[Vertex],
828        dirty: bool,
829        layer_hint: usize,
830        component: u32,
831        blend: wgpu::BlendState,
832        font_texture_view: &wgpu::TextureView,
833        tex_arrays: &[crate::core::RawFrameArray],
834        sprite_uniforms: &SpriteUniforms,
835        context: &mut Context,
836        custom_program: Option<&Program>,
837    ) {
838        let num_vertices = vertices.len();
839        let num_sprites = num_vertices / 4;
840        if num_vertices == 0 {
841            return;
842        }
843
844        let (vb_index, vb_dirty) = context.select_vertex_buffer(layer_hint, num_vertices);
845
846        if dirty || vb_dirty {
847            let vertex_bytes = unsafe {
848                std::slice::from_raw_parts(
849                    vertices.as_ptr() as *const u8,
850                    num_vertices * std::mem::size_of::<Vertex>(),
851                )
852            };
853            self.queue.write_buffer(&context.vertex_buffers[vb_index].buffer, 0, vertex_bytes);
854        }
855
856        context.update_index_buffer(num_sprites);
857
858        let uniform_bytes = unsafe {
859            std::slice::from_raw_parts(
860                sprite_uniforms as *const SpriteUniforms as *const u8,
861                std::mem::size_of::<SpriteUniforms>(),
862            )
863        };
864        let uniform_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
865            label: Some("Sprite Uniform Buffer"),
866            size: uniform_bytes.len() as u64,
867            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
868            mapped_at_creation: false,
869        });
870        self.queue.write_buffer(&uniform_buffer, 0, uniform_bytes);
871
872        // _rd_comp: u32 padded to 16 bytes for uniform buffer alignment
873        let comp_data = [component, 0u32, 0u32, 0u32];
874        let comp_bytes = unsafe {
875            std::slice::from_raw_parts(comp_data.as_ptr() as *const u8, 16)
876        };
877        let comp_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
878            label: Some("Component Uniform Buffer"),
879            size: 16,
880            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
881            mapped_at_creation: false,
882        });
883        self.queue.write_buffer(&comp_buffer, 0, comp_bytes);
884
885        let sprite_bgl = custom_program.map_or(&context.sprite_bind_group_layout, |p| &*p.bind_group_layout);
886        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
887            label: Some("Sprite Bind Group"),
888            layout: sprite_bgl,
889            entries: &[
890                wgpu::BindGroupEntry {
891                    binding: 0,
892                    resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &uniform_buffer, offset: 0, size: None }),
893                },
894                wgpu::BindGroupEntry {
895                    binding: 1,
896                    resource: wgpu::BindingResource::TextureView(font_texture_view),
897                },
898                wgpu::BindGroupEntry {
899                    binding: 2,
900                    resource: wgpu::BindingResource::Sampler(&context.nearest_sampler),
901                },
902                wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(&tex_arrays[1].data.view) },
903                wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::TextureView(&tex_arrays[2].data.view) },
904                wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::TextureView(&tex_arrays[3].data.view) },
905                wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::TextureView(&tex_arrays[4].data.view) },
906                wgpu::BindGroupEntry { binding: 7, resource: wgpu::BindingResource::TextureView(&tex_arrays[5].data.view) },
907                wgpu::BindGroupEntry {
908                    binding: 8,
909                    resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &comp_buffer, offset: 0, size: None }),
910                },
911            ],
912        });
913
914        let pipeline = match custom_program {
915            Some(prog) => prog.get_or_create_pipeline(blend, context.format),
916            None => context.get_or_create_sprite_pipeline(blend, context.format),
917        };
918        let load_op = self.next_load_op();
919        let view = self.view.clone();
920
921        let mut render_pass = self.command_encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
922            label: Some("Radiant Sprite Pass"),
923            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
924                view: &view,
925                depth_slice: None,
926                resolve_target: None,
927                ops: wgpu::Operations { load: load_op, store: wgpu::StoreOp::Store },
928            })],
929            depth_stencil_attachment: None,
930            timestamp_writes: None,
931            occlusion_query_set: None,
932            multiview_mask: None,
933        });
934
935        render_pass.set_pipeline(&pipeline);
936        render_pass.set_blend_constant(wgpu::Color::TRANSPARENT);
937        render_pass.set_bind_group(0, &bind_group, &[]);
938        render_pass.set_vertex_buffer(0, context.vertex_buffers[vb_index].buffer.slice(..));
939        render_pass.set_index_buffer(context.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
940        render_pass.draw_indexed(0..(num_sprites as u32 * 6), 0, 0..1);
941    }
942
943    /// Draw a textured quad to this frame.
944    pub fn draw_quad(
945        &mut self,
946        uniforms: &TextureUniforms,
947        tex_view: &wgpu::TextureView,
948        filter: crate::core::TextureFilter,
949        blend: wgpu::BlendState,
950        context: &mut Context,
951    ) {
952        let pipeline = context.get_or_create_texture_pipeline(blend, context.format);
953        let sampler = Context::sampler_for_filter(&context.nearest_sampler, &context.linear_sampler, filter);
954        let load_op = self.next_load_op();
955        let view = self.view.clone();
956        render_texture_quad(
957            &mut self.command_encoder,
958            &view,
959            &self.device,
960            &self.queue,
961            uniforms,
962            &BLIT_QUAD,
963            tex_view,
964            sampler,
965            &pipeline,
966            &context.texture_bind_group_layout,
967            load_op,
968        );
969    }
970}
971
972pub type Frame = WgpuFrame;
973
974// --------------
975// Program — custom WGSL pipelines for blur / combine / default shaders
976// --------------
977
978pub(crate) enum ProgramKind {
979    Default,
980    Combine,
981    Sprite,
982}
983
984pub struct Program {
985    pub(crate) kind: ProgramKind,
986    pub(crate) is_custom: bool,
987    device: Arc<wgpu::Device>,
988    vert_module: Arc<wgpu::ShaderModule>,
989    frag_module: Arc<wgpu::ShaderModule>,
990    pub(crate) bind_group_layout: Arc<wgpu::BindGroupLayout>,
991    pipeline_layout: Arc<wgpu::PipelineLayout>,
992    pipelines: std::sync::Mutex<HashMap<(BlendStateKey, wgpu::TextureFormat), Arc<wgpu::RenderPipeline>>>,
993    pub(crate) sampler: Arc<wgpu::Sampler>,
994}
995
996impl Program {
997    pub fn new(context: &Context, fragment_shader: &str) -> crate::core::Result<Program> {
998        Self::new_inner(context, fragment_shader, true)
999    }
1000
1001    fn new_inner(context: &Context, fragment_shader: &str, is_custom: bool) -> crate::core::Result<Program> {
1002        let device = &context.device;
1003
1004        let kind = if fragment_shader.contains("SpriteFragmentInput") {
1005            ProgramKind::Sprite
1006        } else if fragment_shader.contains("sample4") {
1007            ProgramKind::Combine
1008        } else {
1009            ProgramKind::Default
1010        };
1011
1012        let vert_module = Arc::new(device.create_shader_module(wgpu::ShaderModuleDescriptor {
1013            label: Some("Custom Vert"),
1014            source: wgpu::ShaderSource::Wgsl(match kind {
1015                ProgramKind::Sprite => include_str!("../shader/sprite.wgsl").into(),
1016                _ => include_str!("../shader/texture.wgsl").into(),
1017            }),
1018        }));
1019
1020        let frag_module = Arc::new(match kind {
1021            ProgramKind::Sprite | ProgramKind::Default => device.create_shader_module(wgpu::ShaderModuleDescriptor {
1022                label: Some("Custom Frag"),
1023                source: wgpu::ShaderSource::Wgsl(fragment_shader.into()),
1024            }),
1025            ProgramKind::Combine => device.create_shader_module(wgpu::ShaderModuleDescriptor {
1026                label: Some("Combine Frag"),
1027                source: wgpu::ShaderSource::Wgsl(fragment_shader.into()),
1028            }),
1029        });
1030
1031        let vert_frag = wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT;
1032        // Custom programs may read texture_uniforms (binding 0) in the fragment stage.
1033        // Built-in programs only use it in the vertex stage.
1034        let uniform_visibility = if is_custom { vert_frag } else { wgpu::ShaderStages::VERTEX };
1035
1036        let bind_group_layout = Arc::new(match kind {
1037            ProgramKind::Combine => device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1038                label: Some("Combine BGL"),
1039                entries: &[
1040                    wgpu::BindGroupLayoutEntry {
1041                        binding: 0,
1042                        visibility: vert_frag,
1043                        ty: wgpu::BindingType::Buffer {
1044                            ty: wgpu::BufferBindingType::Uniform,
1045                            has_dynamic_offset: false,
1046                            min_binding_size: None,
1047                        },
1048                        count: None,
1049                    },
1050                    Context::bgl_texture_2d(1, wgpu::ShaderStages::FRAGMENT),
1051                    Context::bgl_texture_2d(2, wgpu::ShaderStages::FRAGMENT),
1052                    Context::bgl_texture_2d(3, wgpu::ShaderStages::FRAGMENT),
1053                    Context::bgl_texture_2d(4, wgpu::ShaderStages::FRAGMENT),
1054                    Context::bgl_texture_2d(5, wgpu::ShaderStages::FRAGMENT),
1055                    Context::bgl_sampler(6, wgpu::ShaderStages::FRAGMENT),
1056                ],
1057            }),
1058            ProgramKind::Default => device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1059                label: Some("Default Custom BGL"),
1060                entries: &[
1061                    wgpu::BindGroupLayoutEntry {
1062                        binding: 0,
1063                        visibility: uniform_visibility,
1064                        ty: wgpu::BindingType::Buffer {
1065                            ty: wgpu::BufferBindingType::Uniform,
1066                            has_dynamic_offset: false,
1067                            min_binding_size: None,
1068                        },
1069                        count: None,
1070                    },
1071                    Context::bgl_texture_2d(1, wgpu::ShaderStages::FRAGMENT),
1072                    Context::bgl_sampler(2, wgpu::ShaderStages::FRAGMENT),
1073                ],
1074            }),
1075            ProgramKind::Sprite => device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1076                label: Some("Custom Sprite BGL"),
1077                entries: &[
1078                    Context::bgl_uniform(0, wgpu::ShaderStages::VERTEX),
1079                    Context::bgl_texture_2d(1, wgpu::ShaderStages::FRAGMENT),
1080                    Context::bgl_sampler(2, wgpu::ShaderStages::FRAGMENT),
1081                    Context::bgl_texture_2d_array(3, wgpu::ShaderStages::FRAGMENT),
1082                    Context::bgl_texture_2d_array(4, wgpu::ShaderStages::FRAGMENT),
1083                    Context::bgl_texture_2d_array(5, wgpu::ShaderStages::FRAGMENT),
1084                    Context::bgl_texture_2d_array(6, wgpu::ShaderStages::FRAGMENT),
1085                    Context::bgl_texture_2d_array(7, wgpu::ShaderStages::FRAGMENT),
1086                    Context::bgl_uniform(8, wgpu::ShaderStages::FRAGMENT),
1087                ],
1088            }),
1089        });
1090
1091        let pipeline_layout = Arc::new(device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1092            label: Some("Custom Pipeline Layout"),
1093            bind_group_layouts: &[Some(&*bind_group_layout)],
1094            immediate_size: 0,
1095        }));
1096
1097        let sampler = Arc::new(device.create_sampler(&wgpu::SamplerDescriptor {
1098            label: Some("Custom Program Sampler"),
1099            mag_filter: wgpu::FilterMode::Linear,
1100            min_filter: wgpu::FilterMode::Linear,
1101            address_mode_u: wgpu::AddressMode::ClampToEdge,
1102            address_mode_v: wgpu::AddressMode::ClampToEdge,
1103            address_mode_w: wgpu::AddressMode::ClampToEdge,
1104            ..Default::default()
1105        }));
1106
1107        Ok(Program {
1108            kind,
1109            is_custom,
1110            device: context.device.clone(),
1111            vert_module,
1112            frag_module,
1113            bind_group_layout,
1114            pipeline_layout,
1115            pipelines: std::sync::Mutex::new(HashMap::new()),
1116            sampler,
1117        })
1118    }
1119
1120    pub fn new_default_texture(context: &Context) -> crate::core::Result<Program> {
1121        Self::new_inner(context, include_str!("../shader/texture_frag.wgsl"), false)
1122    }
1123
1124    pub fn get_or_create_pipeline(&self, blend: wgpu::BlendState, format: wgpu::TextureFormat) -> Arc<wgpu::RenderPipeline> {
1125        let key = (BlendStateKey::from(&blend), format);
1126        let mut pipelines = self.pipelines.lock().unwrap();
1127        if !pipelines.contains_key(&key) {
1128            let pipeline = Arc::new(match self.kind {
1129                ProgramKind::Sprite => Context::build_sprite_pipeline(
1130                    &self.device, &self.vert_module, &self.frag_module,
1131                    &self.pipeline_layout, format, blend,
1132                ),
1133                _ => Context::build_texture_pipeline(
1134                    &self.device, &self.vert_module, &self.frag_module,
1135                    &self.pipeline_layout, format, blend,
1136                ),
1137            });
1138            pipelines.insert(key, pipeline);
1139        }
1140        pipelines[&key].clone()
1141    }
1142}
1143
1144// --------------
1145// Uniform buffer structs (must match WGSL layout exactly)
1146// --------------
1147
1148#[repr(C)]
1149#[derive(Copy, Clone, Debug)]
1150pub(crate) struct SpriteUniforms {
1151    pub u_view: [[f32; 4]; 4],
1152    pub u_model: [[f32; 4]; 4],
1153    pub _rd_color: [f32; 4],
1154}
1155
1156#[repr(C)]
1157#[derive(Copy, Clone, Debug)]
1158pub(crate) struct TextureUniforms {
1159    pub u_view: [[f32; 4]; 4],       // 64 bytes
1160    pub u_model: [[f32; 4]; 4],      // 64 bytes
1161    pub _rd_color: [f32; 4],          // 16 bytes
1162    pub _rd_offset: [f32; 2],         // 8 bytes
1163    pub _rd_dimensions: [f32; 2],     // 8 bytes
1164    pub _rd_flags: [f32; 4],          // 16 bytes (custom shader params: [horizontal, brightness, 0, 0])
1165    // total: 176 bytes — matches WGSL struct
1166}
1167
1168impl TextureUniforms {
1169    /// Identity transform — renders the quad at (0,0)–(1,1) with no color tint.
1170    pub fn identity() -> Self {
1171        TextureUniforms {
1172            u_view: crate::core::Mat4::viewport(1.0, 1.0).into(),
1173            u_model: crate::core::Mat4::<f32>::identity().into(),
1174            _rd_color: [1.0, 1.0, 1.0, 1.0],
1175            _rd_offset: [0.0, 0.0],
1176            _rd_dimensions: [1.0, 1.0],
1177            _rd_flags: [0.0, 0.0, 0.0, 0.0],
1178        }
1179    }
1180}
1181
1182// --------------
1183// Monitor
1184// --------------
1185
1186#[derive(Clone)]
1187pub struct Monitor {
1188    pub inner: MonitorInner,
1189}
1190
1191#[derive(Clone)]
1192pub struct MonitorInner {
1193    pub winit_monitor: winit::monitor::MonitorHandle,
1194}
1195
1196impl Monitor {
1197    pub fn get_dimensions(self: &Self) -> crate::core::Point2<u32> {
1198        let size = self.inner.winit_monitor.size();
1199        (size.width, size.height)
1200    }
1201
1202    pub fn get_name(self: &Self) -> Option<String> {
1203        self.inner.winit_monitor.name()
1204    }
1205}
1206
1207pub struct MonitorIterator {
1208    monitors: std::vec::IntoIter<winit::monitor::MonitorHandle>,
1209}
1210
1211impl MonitorIterator {
1212    pub fn new() -> Self {
1213        MonitorIterator { monitors: Vec::new().into_iter() }
1214    }
1215}
1216
1217impl Iterator for MonitorIterator {
1218    type Item = Monitor;
1219    fn next(&mut self) -> Option<Monitor> {
1220        self.monitors.next().map(|m| Monitor { inner: MonitorInner { winit_monitor: m } })
1221    }
1222}
1223
1224// --------------
1225// Texture2d
1226// --------------
1227
1228pub struct Texture2d {
1229    texture: Arc<wgpu::Texture>,
1230    pub(crate) view: Arc<wgpu::TextureView>,
1231    device: Arc<wgpu::Device>,
1232    queue: Arc<wgpu::Queue>,
1233    pub(crate) width: u32,
1234    pub(crate) height: u32,
1235    bytes_per_pixel: u32,
1236    blit_res: Arc<BlitResources>,
1237}
1238
1239impl Texture2d {
1240    pub fn new(context: &Context, width: u32, height: u32, format: crate::core::TextureFormat, data: Option<crate::core::RawFrame>) -> Self {
1241        let wgpu_format = Self::convert_format(format);
1242        let bpp = Self::bytes_per_pixel_for(format);
1243
1244        let texture = context.device.create_texture(&wgpu::TextureDescriptor {
1245            label: Some("Radiant Texture2d"),
1246            size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
1247            mip_level_count: 1,
1248            sample_count: 1,
1249            dimension: wgpu::TextureDimension::D2,
1250            format: wgpu_format,
1251            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::RENDER_ATTACHMENT,
1252            view_formats: &[],
1253        });
1254
1255        let view = Arc::new(texture.create_view(&wgpu::TextureViewDescriptor::default()));
1256
1257        if let Some(raw) = data {
1258            context.queue.write_texture(
1259                wgpu::TexelCopyTextureInfo {
1260                    texture: &texture,
1261                    mip_level: 0,
1262                    origin: wgpu::Origin3d::ZERO,
1263                    aspect: wgpu::TextureAspect::All,
1264                },
1265                &raw.data,
1266                wgpu::TexelCopyBufferLayout {
1267                    offset: 0,
1268                    bytes_per_row: Some(raw.width * raw.channels as u32),
1269                    rows_per_image: Some(raw.height),
1270                },
1271                wgpu::Extent3d { width: raw.width, height: raw.height, depth_or_array_layers: 1 },
1272            );
1273        }
1274
1275        let blit_res = BlitResources::new(&context.device, wgpu_format);
1276
1277        Texture2d {
1278            texture: Arc::new(texture),
1279            view,
1280            device: context.device.clone(),
1281            queue: context.queue.clone(),
1282            width,
1283            height,
1284            bytes_per_pixel: bpp,
1285            blit_res,
1286        }
1287    }
1288
1289    pub fn clear(self: &Self, color: crate::core::Color) {
1290        let crate::core::Color(r, g, b, a) = color;
1291        let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Texture Clear") });
1292        {
1293            let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1294                label: Some("Texture Clear Pass"),
1295                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1296                    view: &self.view,
1297                    depth_slice: None,
1298                    resolve_target: None,
1299                    ops: wgpu::Operations {
1300                        load: wgpu::LoadOp::Clear(wgpu::Color { r: r as f64, g: g as f64, b: b as f64, a: a as f64 }),
1301                        store: wgpu::StoreOp::Store,
1302                    },
1303                })],
1304                depth_stencil_attachment: None,
1305                timestamp_writes: None,
1306                occlusion_query_set: None,
1307                multiview_mask: None,
1308            });
1309        }
1310        self.queue.submit(std::iter::once(encoder.finish()));
1311    }
1312
1313    pub fn write(self: &Self, rect: &crate::core::Rect<u32>, data: &Vec<u8>) {
1314        let x = (rect.0).0;
1315        let y = (rect.0).1;
1316        let w = (rect.1).0 - x;
1317        let h = (rect.1).1 - y;
1318        if w == 0 || h == 0 {
1319            return;
1320        }
1321        self.queue.write_texture(
1322            wgpu::TexelCopyTextureInfo {
1323                texture: &self.texture,
1324                mip_level: 0,
1325                origin: wgpu::Origin3d { x, y, z: 0 },
1326                aspect: wgpu::TextureAspect::All,
1327            },
1328            data,
1329            wgpu::TexelCopyBufferLayout {
1330                offset: 0,
1331                bytes_per_row: Some(w * self.bytes_per_pixel),
1332                rows_per_image: Some(h),
1333            },
1334            wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
1335        );
1336    }
1337
1338    pub fn copy_from(self: &Self, src_texture: &crate::core::Texture, filter: crate::core::TextureFilter) {
1339        let uniforms = TextureUniforms::identity();
1340        let sampler = Context::sampler_for_filter(&self.blit_res.nearest_sampler, &self.blit_res.linear_sampler, filter);
1341        let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Tex CopyFrom") });
1342        render_texture_quad(
1343            &mut encoder, &self.view,
1344            &self.device, &self.queue,
1345            &uniforms, &BLIT_QUAD,
1346            &*src_texture.handle.view,
1347            sampler,
1348            &self.blit_res.pipeline,
1349            &self.blit_res.bind_group_layout,
1350            wgpu::LoadOp::Load,
1351        );
1352        self.queue.submit(std::iter::once(encoder.finish()));
1353    }
1354
1355    pub fn copy_rect_from(self: &Self, src_texture: &crate::core::Texture, source_rect: crate::core::Rect<i32>, target_rect: crate::core::Rect<i32>, filter: crate::core::TextureFilter) {
1356        let tw = src_texture.handle.width as f32;
1357        let th = src_texture.handle.height as f32;
1358        let dw_full = self.width as f32;
1359        let dh_full = self.height as f32;
1360
1361        let sx = (source_rect.0).0 as f32; let sy = (source_rect.0).1 as f32;
1362        let sw = (source_rect.1).0 as f32; let sh = (source_rect.1).1 as f32;
1363        let u0 = sx / tw; let u1 = (sx + sw) / tw;
1364        let v0 = sy / th; let v1 = (sy + sh) / th;
1365
1366        let dx = (target_rect.0).0 as f32; let dy = (target_rect.0).1 as f32;
1367        let dw = (target_rect.1).0 as f32; let dh = (target_rect.1).1 as f32;
1368
1369        let quad = [
1370            Vertex { position: [0.0, 0.0], texture_uv: [u0, v0], ..Vertex::default() },
1371            Vertex { position: [1.0, 0.0], texture_uv: [u1, v0], ..Vertex::default() },
1372            Vertex { position: [0.0, 1.0], texture_uv: [u0, v1], ..Vertex::default() },
1373            Vertex { position: [1.0, 1.0], texture_uv: [u1, v1], ..Vertex::default() },
1374        ];
1375        let uniforms = TextureUniforms {
1376            u_view: crate::core::Mat4::viewport(1.0, 1.0).into(),
1377            u_model: crate::core::Mat4::<f32>::identity().into(),
1378            _rd_color: [1.0, 1.0, 1.0, 1.0],
1379            _rd_offset: [dx / dw_full, dy / dh_full],
1380            _rd_dimensions: [dw / dw_full, dh / dh_full],
1381            _rd_flags: [0.0, 0.0, 0.0, 0.0],
1382        };
1383        let sampler = Context::sampler_for_filter(&self.blit_res.nearest_sampler, &self.blit_res.linear_sampler, filter);
1384        let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Tex CopyRectFrom") });
1385        render_texture_quad(
1386            &mut encoder, &self.view,
1387            &self.device, &self.queue,
1388            &uniforms, &quad,
1389            &*src_texture.handle.view,
1390            sampler,
1391            &self.blit_res.pipeline,
1392            &self.blit_res.bind_group_layout,
1393            wgpu::LoadOp::Load,
1394        );
1395        self.queue.submit(std::iter::once(encoder.finish()));
1396    }
1397
1398    pub fn copy_from_frame(self: &Self, _src_frame: &WgpuFrame, _filter: crate::core::TextureFilter) {}
1399    pub fn copy_rect_from_frame(self: &Self, _src_frame: &WgpuFrame, _source_rect: crate::core::Rect<i32>, _target_rect: crate::core::Rect<i32>, _filter: crate::core::TextureFilter) {}
1400
1401    fn convert_format(format: crate::core::TextureFormat) -> wgpu::TextureFormat {
1402        use crate::core::TextureFormat as RF;
1403        match format {
1404            RF::U8              => wgpu::TextureFormat::R8Unorm,
1405            RF::U16             => wgpu::TextureFormat::R16Uint,
1406            RF::U8U8            => wgpu::TextureFormat::Rg8Unorm,
1407            RF::U16U16          => wgpu::TextureFormat::Rg16Uint,
1408            RF::U10U10U10       => wgpu::TextureFormat::Rgb10a2Unorm,
1409            RF::U12U12U12       => wgpu::TextureFormat::Rgba8Unorm,     // approximation
1410            RF::U16U16U16       => wgpu::TextureFormat::Rgba16Uint,     // approximation
1411            RF::U2U2U2U2        => wgpu::TextureFormat::Rgba8Unorm,     // approximation
1412            RF::U4U4U4U4        => wgpu::TextureFormat::Rgba8Unorm,     // approximation
1413            RF::U5U5U5U1        => wgpu::TextureFormat::Rgba8Unorm,     // approximation
1414            RF::U8U8U8U8        => wgpu::TextureFormat::Rgba8Unorm,
1415            RF::U10U10U10U2     => wgpu::TextureFormat::Rgb10a2Unorm,
1416            RF::U12U12U12U12    => wgpu::TextureFormat::Rgba16Uint,     // approximation
1417            RF::U16U16U16U16    => wgpu::TextureFormat::Rgba16Uint,
1418            RF::I16I16I16I16    => wgpu::TextureFormat::Rgba16Sint,
1419            RF::F16             => wgpu::TextureFormat::R16Float,
1420            RF::F16F16          => wgpu::TextureFormat::Rg16Float,
1421            RF::F16F16F16F16    => wgpu::TextureFormat::Rgba16Float,
1422            RF::F32             => wgpu::TextureFormat::R32Float,
1423            RF::F32F32          => wgpu::TextureFormat::Rg32Float,
1424            RF::F32F32F32F32    => wgpu::TextureFormat::Rgba32Float,
1425            RF::F11F11F10       => wgpu::TextureFormat::Rg11b10Ufloat,
1426        }
1427    }
1428
1429    fn bytes_per_pixel_for(format: crate::core::TextureFormat) -> u32 {
1430        use crate::core::TextureFormat as RF;
1431        match format {
1432            RF::U8 | RF::F16 => 1,
1433            RF::U8U8 | RF::U16 | RF::F16F16 | RF::F32 => 2,
1434            RF::U8U8U8U8 | RF::U10U10U10U2 | RF::U10U10U10 | RF::U12U12U12
1435            | RF::U2U2U2U2 | RF::U4U4U4U4 | RF::U5U5U5U1 => 4,
1436            RF::U16U16 | RF::F32F32 => 4,
1437            RF::U16U16U16 | RF::U12U12U12U12 | RF::U16U16U16U16
1438            | RF::I16I16I16I16 | RF::F16F16F16F16 => 8,
1439            RF::F11F11F10 => 4,
1440            RF::F32F32F32F32 => 16,
1441        }
1442    }
1443}
1444
1445// --------------
1446// Texture2dArray
1447// --------------
1448
1449pub struct Texture2dArray {
1450    pub(crate) view: wgpu::TextureView,
1451}
1452
1453impl Texture2dArray {
1454    pub fn new(context: &Context, raw: &Vec<crate::core::RawFrame>) -> Self {
1455        let layer_count = if raw.is_empty() { 1 } else { raw.len() as u32 };
1456        let width = raw.first().map_or(1, |f| f.width);
1457        let height = raw.first().map_or(1, |f| f.height);
1458
1459        let texture = context.device.create_texture(&wgpu::TextureDescriptor {
1460            label: Some("Radiant Texture2dArray"),
1461            size: wgpu::Extent3d { width, height, depth_or_array_layers: layer_count },
1462            mip_level_count: 1,
1463            sample_count: 1,
1464            dimension: wgpu::TextureDimension::D2,
1465            format: wgpu::TextureFormat::Rgba8Unorm,
1466            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1467            view_formats: &[],
1468        });
1469
1470        let view = texture.create_view(&wgpu::TextureViewDescriptor {
1471            label: Some("Texture Array View"),
1472            dimension: Some(wgpu::TextureViewDimension::D2Array),
1473            ..Default::default()
1474        });
1475
1476        for (i, frame) in raw.iter().enumerate() {
1477            context.queue.write_texture(
1478                wgpu::TexelCopyTextureInfo {
1479                    texture: &texture,
1480                    mip_level: 0,
1481                    origin: wgpu::Origin3d { x: 0, y: 0, z: i as u32 },
1482                    aspect: wgpu::TextureAspect::All,
1483                },
1484                &frame.data,
1485                wgpu::TexelCopyBufferLayout {
1486                    offset: 0,
1487                    bytes_per_row: Some(frame.width * frame.channels as u32),
1488                    rows_per_image: Some(frame.height),
1489                },
1490                wgpu::Extent3d { width: frame.width, height: frame.height, depth_or_array_layers: 1 },
1491            );
1492        }
1493
1494        Texture2dArray { view }
1495    }
1496}
1497
1498// --------------
1499// Context
1500// --------------
1501
1502struct VertexBufferCacheItem {
1503    hint: usize,
1504    age: usize,
1505    buffer: wgpu::Buffer,
1506    capacity: usize,
1507}
1508
1509pub struct Context {
1510    pub(crate) instance: Arc<wgpu::Instance>,
1511    pub(crate) adapter: Arc<wgpu::Adapter>,
1512    pub(crate) device: Arc<wgpu::Device>,
1513    pub(crate) queue: Arc<wgpu::Queue>,
1514    format: wgpu::TextureFormat,
1515    index_buffer: wgpu::Buffer,
1516    index_count: usize,
1517    vertex_buffers: Vec<VertexBufferCacheItem>,
1518    pub(crate) nearest_sampler: wgpu::Sampler,
1519    pub(crate) linear_sampler: wgpu::Sampler,
1520    pub(crate) sprite_bind_group_layout: wgpu::BindGroupLayout,
1521    pub(crate) texture_bind_group_layout: wgpu::BindGroupLayout,
1522    // Shader modules kept alive for lazily creating pipelines by blend mode
1523    sprite_vert_module: wgpu::ShaderModule,
1524    sprite_frag_module: wgpu::ShaderModule,
1525    texture_vert_module: wgpu::ShaderModule,
1526    texture_frag_module: wgpu::ShaderModule,
1527    sprite_pipeline_layout: wgpu::PipelineLayout,
1528    texture_pipeline_layout: wgpu::PipelineLayout,
1529    sprite_pipelines: HashMap<(BlendStateKey, wgpu::TextureFormat), Arc<wgpu::RenderPipeline>>,
1530    texture_pipelines: HashMap<(BlendStateKey, wgpu::TextureFormat), Arc<wgpu::RenderPipeline>>,
1531    // 1×1 white Rgba8Unorm texture used as placeholder when draw_rect has no source texture
1532    pub(crate) placeholder_view: Arc<wgpu::TextureView>,
1533}
1534
1535impl Context {
1536    pub fn shared_gpu(&self) -> SharedGpu {
1537        SharedGpu {
1538            instance: self.instance.clone(),
1539            adapter: self.adapter.clone(),
1540            device: self.device.clone(),
1541            queue: self.queue.clone(),
1542        }
1543    }
1544
1545    pub fn new(display: &Display, initial_capacity: usize) -> Self {
1546        let format = display.inner.config.lock().unwrap().format;
1547        let instance = display.inner.instance.clone();
1548        let adapter = display.inner.adapter.clone();
1549        let device = display.inner.device.clone();
1550        let queue = display.inner.queue.clone();
1551
1552        let index_buffer = Self::create_index_buffer(&device, &queue, initial_capacity);
1553
1554        let nearest_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1555            label: Some("Nearest Sampler"),
1556            mag_filter: wgpu::FilterMode::Nearest,
1557            min_filter: wgpu::FilterMode::Nearest,
1558            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
1559            address_mode_u: wgpu::AddressMode::ClampToEdge,
1560            address_mode_v: wgpu::AddressMode::ClampToEdge,
1561            address_mode_w: wgpu::AddressMode::ClampToEdge,
1562            ..Default::default()
1563        });
1564
1565        let linear_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1566            label: Some("Linear Sampler"),
1567            mag_filter: wgpu::FilterMode::Linear,
1568            min_filter: wgpu::FilterMode::Linear,
1569            mipmap_filter: wgpu::MipmapFilterMode::Linear,
1570            address_mode_u: wgpu::AddressMode::ClampToEdge,
1571            address_mode_v: wgpu::AddressMode::ClampToEdge,
1572            address_mode_w: wgpu::AddressMode::ClampToEdge,
1573            ..Default::default()
1574        });
1575
1576        // Sprite bind group layout: uniforms, font tex, sampler, 5 tex arrays, comp uniform
1577        let sprite_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1578            label: Some("Sprite Bind Group Layout"),
1579            entries: &[
1580                Self::bgl_uniform(0, wgpu::ShaderStages::VERTEX),
1581                Self::bgl_texture_2d(1, wgpu::ShaderStages::FRAGMENT),
1582                Self::bgl_sampler(2, wgpu::ShaderStages::FRAGMENT),
1583                Self::bgl_texture_2d_array(3, wgpu::ShaderStages::FRAGMENT),
1584                Self::bgl_texture_2d_array(4, wgpu::ShaderStages::FRAGMENT),
1585                Self::bgl_texture_2d_array(5, wgpu::ShaderStages::FRAGMENT),
1586                Self::bgl_texture_2d_array(6, wgpu::ShaderStages::FRAGMENT),
1587                Self::bgl_texture_2d_array(7, wgpu::ShaderStages::FRAGMENT),
1588                Self::bgl_uniform(8, wgpu::ShaderStages::FRAGMENT),
1589            ],
1590        });
1591
1592        // Texture bind group layout: uniforms, source tex, sampler
1593        let texture_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1594            label: Some("Texture Bind Group Layout"),
1595            entries: &[
1596                Self::bgl_uniform(0, wgpu::ShaderStages::VERTEX),
1597                Self::bgl_texture_2d(1, wgpu::ShaderStages::FRAGMENT),
1598                Self::bgl_sampler(2, wgpu::ShaderStages::FRAGMENT),
1599            ],
1600        });
1601
1602        // Shader modules
1603        let sprite_vert_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1604            label: Some("Sprite Vertex Shader"),
1605            source: wgpu::ShaderSource::Wgsl(include_str!("../shader/sprite.wgsl").into()),
1606        });
1607        let sprite_frag_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1608            label: Some("Sprite Fragment Shader"),
1609            source: wgpu::ShaderSource::Wgsl(include_str!("../shader/sprite_frag.wgsl").into()),
1610        });
1611        let texture_vert_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1612            label: Some("Texture Vertex Shader"),
1613            source: wgpu::ShaderSource::Wgsl(include_str!("../shader/texture.wgsl").into()),
1614        });
1615        let texture_frag_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1616            label: Some("Texture Fragment Shader"),
1617            source: wgpu::ShaderSource::Wgsl(include_str!("../shader/texture_frag.wgsl").into()),
1618        });
1619
1620        let sprite_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1621            label: Some("Sprite Pipeline Layout"),
1622            bind_group_layouts: &[Some(&sprite_bind_group_layout)],
1623            immediate_size: 0,
1624        });
1625        let texture_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1626            label: Some("Texture Pipeline Layout"),
1627            bind_group_layouts: &[Some(&texture_bind_group_layout)],
1628            immediate_size: 0,
1629        });
1630
1631        // 1×1 white placeholder texture
1632        let placeholder_tex = device.create_texture(&wgpu::TextureDescriptor {
1633            label: Some("Placeholder Texture"),
1634            size: wgpu::Extent3d { width: 1, height: 1, depth_or_array_layers: 1 },
1635            mip_level_count: 1,
1636            sample_count: 1,
1637            dimension: wgpu::TextureDimension::D2,
1638            format: wgpu::TextureFormat::Rgba8Unorm,
1639            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1640            view_formats: &[],
1641        });
1642        queue.write_texture(
1643            wgpu::TexelCopyTextureInfo { texture: &placeholder_tex, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
1644            &[255u8, 255, 255, 255],
1645            wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(4), rows_per_image: Some(1) },
1646            wgpu::Extent3d { width: 1, height: 1, depth_or_array_layers: 1 },
1647        );
1648        let placeholder_view = Arc::new(placeholder_tex.create_view(&wgpu::TextureViewDescriptor::default()));
1649
1650        // Pre-create the default alpha-blending pipelines
1651        let mut sprite_pipelines = HashMap::new();
1652        let mut texture_pipelines = HashMap::new();
1653
1654        let default_blend = wgpu::BlendState::ALPHA_BLENDING;
1655        let blend_key = BlendStateKey::from(&default_blend);
1656        let composite_key = (blend_key, format);
1657        sprite_pipelines.insert(composite_key, Arc::new(Self::build_sprite_pipeline(
1658            &device, &sprite_vert_module, &sprite_frag_module, &sprite_pipeline_layout, format, default_blend,
1659        )));
1660        texture_pipelines.insert(composite_key, Arc::new(Self::build_texture_pipeline(
1661            &device, &texture_vert_module, &texture_frag_module, &texture_pipeline_layout, format, default_blend,
1662        )));
1663
1664        Context {
1665            instance,
1666            adapter,
1667            device,
1668            queue,
1669            format,
1670            index_buffer,
1671            index_count: initial_capacity * 6,
1672            vertex_buffers: Vec::new(),
1673            nearest_sampler,
1674            linear_sampler,
1675            sprite_bind_group_layout,
1676            texture_bind_group_layout,
1677            sprite_vert_module,
1678            sprite_frag_module,
1679            texture_vert_module,
1680            texture_frag_module,
1681            sprite_pipeline_layout,
1682            texture_pipeline_layout,
1683            sprite_pipelines,
1684            texture_pipelines,
1685            placeholder_view,
1686        }
1687    }
1688
1689    // Bind group layout entry helpers
1690    fn bgl_uniform(binding: u32, visibility: wgpu::ShaderStages) -> wgpu::BindGroupLayoutEntry {
1691        wgpu::BindGroupLayoutEntry {
1692            binding,
1693            visibility,
1694            ty: wgpu::BindingType::Buffer {
1695                ty: wgpu::BufferBindingType::Uniform,
1696                has_dynamic_offset: false,
1697                min_binding_size: None,
1698            },
1699            count: None,
1700        }
1701    }
1702    fn bgl_texture_2d(binding: u32, visibility: wgpu::ShaderStages) -> wgpu::BindGroupLayoutEntry {
1703        wgpu::BindGroupLayoutEntry {
1704            binding,
1705            visibility,
1706            ty: wgpu::BindingType::Texture {
1707                sample_type: wgpu::TextureSampleType::Float { filterable: true },
1708                view_dimension: wgpu::TextureViewDimension::D2,
1709                multisampled: false,
1710            },
1711            count: None,
1712        }
1713    }
1714    fn bgl_texture_2d_array(binding: u32, visibility: wgpu::ShaderStages) -> wgpu::BindGroupLayoutEntry {
1715        wgpu::BindGroupLayoutEntry {
1716            binding,
1717            visibility,
1718            ty: wgpu::BindingType::Texture {
1719                sample_type: wgpu::TextureSampleType::Float { filterable: true },
1720                view_dimension: wgpu::TextureViewDimension::D2Array,
1721                multisampled: false,
1722            },
1723            count: None,
1724        }
1725    }
1726    fn bgl_sampler(binding: u32, visibility: wgpu::ShaderStages) -> wgpu::BindGroupLayoutEntry {
1727        wgpu::BindGroupLayoutEntry {
1728            binding,
1729            visibility,
1730            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1731            count: None,
1732        }
1733    }
1734
1735    pub fn get_or_create_sprite_pipeline(&mut self, blend: wgpu::BlendState, target_format: wgpu::TextureFormat) -> Arc<wgpu::RenderPipeline> {
1736        let key = (BlendStateKey::from(&blend), target_format);
1737        if !self.sprite_pipelines.contains_key(&key) {
1738            let pipeline = Arc::new(Self::build_sprite_pipeline(
1739                &self.device, &self.sprite_vert_module, &self.sprite_frag_module,
1740                &self.sprite_pipeline_layout, target_format, blend,
1741            ));
1742            self.sprite_pipelines.insert(key, pipeline);
1743        }
1744        self.sprite_pipelines[&key].clone()
1745    }
1746
1747    pub fn get_or_create_texture_pipeline(&mut self, blend: wgpu::BlendState, target_format: wgpu::TextureFormat) -> Arc<wgpu::RenderPipeline> {
1748        let key = (BlendStateKey::from(&blend), target_format);
1749        if !self.texture_pipelines.contains_key(&key) {
1750            let pipeline = Arc::new(Self::build_texture_pipeline(
1751                &self.device, &self.texture_vert_module, &self.texture_frag_module,
1752                &self.texture_pipeline_layout, target_format, blend,
1753            ));
1754            self.texture_pipelines.insert(key, pipeline);
1755        }
1756        self.texture_pipelines[&key].clone()
1757    }
1758
1759    fn build_sprite_pipeline(
1760        device: &wgpu::Device,
1761        vert: &wgpu::ShaderModule,
1762        frag: &wgpu::ShaderModule,
1763        layout: &wgpu::PipelineLayout,
1764        format: wgpu::TextureFormat,
1765        blend: wgpu::BlendState,
1766    ) -> wgpu::RenderPipeline {
1767        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1768            label: Some("Sprite Pipeline"),
1769            layout: Some(layout),
1770            vertex: wgpu::VertexState {
1771                module: vert,
1772                entry_point: Some("main"),
1773                buffers: &[wgpu::VertexBufferLayout {
1774                    array_stride: VERTEX_STRIDE,
1775                    step_mode: wgpu::VertexStepMode::Vertex,
1776                    attributes: &wgpu::vertex_attr_array![
1777                        0 => Float32x2,  // position
1778                        1 => Float32x2,  // offset
1779                        2 => Float32,    // rotation
1780                        3 => Float32x4,  // color
1781                        4 => Uint32,     // bucket_id
1782                        5 => Uint32,     // texture_id
1783                        6 => Float32x2,  // texture_uv
1784                        7 => Uint32,     // components
1785                    ],
1786                }],
1787                compilation_options: Default::default(),
1788            },
1789            fragment: Some(wgpu::FragmentState {
1790                module: frag,
1791                entry_point: Some("main"),
1792                targets: &[Some(wgpu::ColorTargetState {
1793                    format,
1794                    blend: Some(blend),
1795                    write_mask: wgpu::ColorWrites::ALL,
1796                })],
1797                compilation_options: Default::default(),
1798            }),
1799            primitive: wgpu::PrimitiveState {
1800                topology: wgpu::PrimitiveTopology::TriangleList,
1801                cull_mode: None,
1802                ..Default::default()
1803            },
1804            depth_stencil: None,
1805            multisample: wgpu::MultisampleState::default(),
1806            multiview_mask: None,
1807            cache: None,
1808        })
1809    }
1810
1811    fn build_texture_pipeline(
1812        device: &wgpu::Device,
1813        vert: &wgpu::ShaderModule,
1814        frag: &wgpu::ShaderModule,
1815        layout: &wgpu::PipelineLayout,
1816        format: wgpu::TextureFormat,
1817        blend: wgpu::BlendState,
1818    ) -> wgpu::RenderPipeline {
1819        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1820            label: Some("Texture Pipeline"),
1821            layout: Some(layout),
1822            vertex: wgpu::VertexState {
1823                module: vert,
1824                entry_point: Some("main"),
1825                buffers: &[wgpu::VertexBufferLayout {
1826                    array_stride: VERTEX_STRIDE,
1827                    step_mode: wgpu::VertexStepMode::Vertex,
1828                    // Only position (@location 0) and texture_uv (@location 6) are read by the shader.
1829                    // Extra attributes in the buffer are silently ignored by wgpu.
1830                    attributes: &[
1831                        wgpu::VertexAttribute {
1832                            format: wgpu::VertexFormat::Float32x2,
1833                            offset: 0,
1834                            shader_location: 0,
1835                        },
1836                        wgpu::VertexAttribute {
1837                            format: wgpu::VertexFormat::Float32x2,
1838                            offset: TEXTURE_UV_OFFSET,
1839                            shader_location: 6,
1840                        },
1841                    ],
1842                }],
1843                compilation_options: Default::default(),
1844            },
1845            fragment: Some(wgpu::FragmentState {
1846                module: frag,
1847                entry_point: Some("main"),
1848                targets: &[Some(wgpu::ColorTargetState {
1849                    format,
1850                    blend: Some(blend),
1851                    write_mask: wgpu::ColorWrites::ALL,
1852                })],
1853                compilation_options: Default::default(),
1854            }),
1855            primitive: wgpu::PrimitiveState {
1856                topology: wgpu::PrimitiveTopology::TriangleList,
1857                cull_mode: None,
1858                ..Default::default()
1859            },
1860            depth_stencil: None,
1861            multisample: wgpu::MultisampleState::default(),
1862            multiview_mask: None,
1863            cache: None,
1864        })
1865    }
1866
1867    fn create_index_buffer(device: &Arc<wgpu::Device>, queue: &Arc<wgpu::Queue>, max_sprites: usize) -> wgpu::Buffer {
1868        let mut data: Vec<u32> = Vec::with_capacity(max_sprites * 6);
1869        for i in 0..max_sprites {
1870            let base = i as u32 * 4;
1871            data.extend_from_slice(&[base, base+1, base+2, base+1, base+3, base+2]);
1872        }
1873        let buffer = device.create_buffer(&wgpu::BufferDescriptor {
1874            label: Some("Index Buffer"),
1875            size: (data.len() * 4) as u64,
1876            usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1877            mapped_at_creation: false,
1878        });
1879        let bytes: Vec<u8> = data.iter().flat_map(|v| v.to_ne_bytes()).collect();
1880        queue.write_buffer(&buffer, 0, &bytes);
1881        buffer
1882    }
1883
1884    pub fn select_vertex_buffer(&mut self, buffer_hint: usize, num_vertices: usize) -> (usize, bool) {
1885        for buffer in self.vertex_buffers.iter_mut() {
1886            buffer.age += 1;
1887        }
1888        if let Some(id) = self.vertex_buffers.iter().position(|item| item.hint == buffer_hint && item.capacity >= num_vertices) {
1889            self.vertex_buffers[id].age = 0;
1890            (id, false)
1891        } else if self.vertex_buffers.len() < MAX_BUFFERS {
1892            let buffer = Self::create_vertex_buffer(&self.device, num_vertices);
1893            self.vertex_buffers.push(VertexBufferCacheItem { hint: buffer_hint, age: 0, buffer, capacity: num_vertices });
1894            (self.vertex_buffers.len() - 1, true)
1895        } else {
1896            let (id, _) = self.vertex_buffers.iter().enumerate().max_by(|&(_, a), &(_, b)| a.age.cmp(&b.age)).unwrap();
1897            let buffer = Self::create_vertex_buffer(&self.device, num_vertices);
1898            self.vertex_buffers[id] = VertexBufferCacheItem { hint: buffer_hint, age: 0, buffer, capacity: num_vertices };
1899            (id, true)
1900        }
1901    }
1902
1903    fn create_vertex_buffer(device: &Arc<wgpu::Device>, num_vertices: usize) -> wgpu::Buffer {
1904        device.create_buffer(&wgpu::BufferDescriptor {
1905            label: Some("Vertex Buffer"),
1906            size: (num_vertices * std::mem::size_of::<Vertex>()) as u64,
1907            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1908            mapped_at_creation: false,
1909        })
1910    }
1911
1912    pub fn update_index_buffer(&mut self, max_sprites: usize) {
1913        if max_sprites * 6 > self.index_count {
1914            self.index_buffer = Self::create_index_buffer(&self.device, &self.queue, max_sprites);
1915            self.index_count = max_sprites * 6;
1916        }
1917    }
1918
1919    pub fn blendmode_to_wgpu(blendmode: &crate::core::BlendMode) -> wgpu::BlendState {
1920        use crate::core::{BlendingFunction, LinearBlendingFactor};
1921
1922        fn convert(factor: LinearBlendingFactor) -> wgpu::BlendFactor {
1923            match factor {
1924                LinearBlendingFactor::Zero                  => wgpu::BlendFactor::Zero,
1925                LinearBlendingFactor::One                   => wgpu::BlendFactor::One,
1926                LinearBlendingFactor::SourceColor           => wgpu::BlendFactor::Src,
1927                LinearBlendingFactor::OneMinusSourceColor   => wgpu::BlendFactor::OneMinusSrc,
1928                LinearBlendingFactor::DestinationColor      => wgpu::BlendFactor::Dst,
1929                LinearBlendingFactor::OneMinusDestinationColor => wgpu::BlendFactor::OneMinusDst,
1930                LinearBlendingFactor::SourceAlpha           => wgpu::BlendFactor::SrcAlpha,
1931                LinearBlendingFactor::SourceAlphaSaturate   => wgpu::BlendFactor::SrcAlphaSaturated,
1932                LinearBlendingFactor::OneMinusSourceAlpha   => wgpu::BlendFactor::OneMinusSrcAlpha,
1933                LinearBlendingFactor::DestinationAlpha      => wgpu::BlendFactor::DstAlpha,
1934                LinearBlendingFactor::OneMinusDestinationAlpha => wgpu::BlendFactor::OneMinusDstAlpha,
1935                LinearBlendingFactor::ConstantColor         => wgpu::BlendFactor::Constant,
1936                LinearBlendingFactor::OneMinusConstantColor => wgpu::BlendFactor::OneMinusConstant,
1937                LinearBlendingFactor::ConstantAlpha         => wgpu::BlendFactor::Constant,
1938                LinearBlendingFactor::OneMinusConstantAlpha => wgpu::BlendFactor::OneMinusConstant,
1939            }
1940        }
1941
1942        fn blend_component(func: BlendingFunction) -> wgpu::BlendComponent {
1943            match func {
1944                BlendingFunction::AlwaysReplace => wgpu::BlendComponent::REPLACE,
1945                BlendingFunction::Min => wgpu::BlendComponent { operation: wgpu::BlendOperation::Min, src_factor: wgpu::BlendFactor::One, dst_factor: wgpu::BlendFactor::One },
1946                BlendingFunction::Max => wgpu::BlendComponent { operation: wgpu::BlendOperation::Max, src_factor: wgpu::BlendFactor::One, dst_factor: wgpu::BlendFactor::One },
1947                BlendingFunction::Addition { source, destination } => wgpu::BlendComponent { operation: wgpu::BlendOperation::Add, src_factor: convert(source), dst_factor: convert(destination) },
1948                BlendingFunction::Subtraction { source, destination } => wgpu::BlendComponent { operation: wgpu::BlendOperation::Subtract, src_factor: convert(source), dst_factor: convert(destination) },
1949                BlendingFunction::ReverseSubtraction { source, destination } => wgpu::BlendComponent { operation: wgpu::BlendOperation::ReverseSubtract, src_factor: convert(source), dst_factor: convert(destination) },
1950            }
1951        }
1952
1953        wgpu::BlendState {
1954            color: blend_component(blendmode.color),
1955            alpha: blend_component(blendmode.alpha),
1956        }
1957    }
1958
1959    pub fn sampler_for_filter<'a>(nearest: &'a wgpu::Sampler, linear: &'a wgpu::Sampler, filter: crate::core::TextureFilter) -> &'a wgpu::Sampler {
1960        if filter == crate::core::TextureFilter::Linear { linear } else { nearest }
1961    }
1962
1963}
1964
1965// --------------
1966// Shared render helper
1967// --------------
1968
1969/// Index buffer for a two-triangle quad (vertices 0-3 forming a rectangle).
1970const QUAD_INDICES: [u32; 6] = [0, 1, 2, 1, 3, 2];
1971
1972/// Blit quad: covers (0,0)–(1,1) with V non-flipped (v=0 at image top = screen top).
1973/// Used for all texture quad rendering in wgpu (which stores textures with v=0 at top).
1974const BLIT_QUAD: [Vertex; 4] = [
1975    Vertex { position: [0.0, 0.0], offset: [0.0, 0.0], rotation: 0.0, color: [1.0, 1.0, 1.0, 1.0], bucket_id: 0, texture_id: 0, texture_uv: [0.0, 0.0], components: 0 },
1976    Vertex { position: [1.0, 0.0], offset: [0.0, 0.0], rotation: 0.0, color: [1.0, 1.0, 1.0, 1.0], bucket_id: 0, texture_id: 0, texture_uv: [1.0, 0.0], components: 0 },
1977    Vertex { position: [0.0, 1.0], offset: [0.0, 0.0], rotation: 0.0, color: [1.0, 1.0, 1.0, 1.0], bucket_id: 0, texture_id: 0, texture_uv: [0.0, 1.0], components: 0 },
1978    Vertex { position: [1.0, 1.0], offset: [0.0, 0.0], rotation: 0.0, color: [1.0, 1.0, 1.0, 1.0], bucket_id: 0, texture_id: 0, texture_uv: [1.0, 1.0], components: 0 },
1979];
1980
1981/// Issue a textured quad draw into `target_view` using the texture pipeline.
1982fn render_texture_quad(
1983    encoder: &mut wgpu::CommandEncoder,
1984    target_view: &wgpu::TextureView,
1985    device: &wgpu::Device,
1986    queue: &wgpu::Queue,
1987    uniforms: &TextureUniforms,
1988    quad: &[Vertex; 4],
1989    tex_view: &wgpu::TextureView,
1990    sampler: &wgpu::Sampler,
1991    pipeline: &wgpu::RenderPipeline,
1992    layout: &wgpu::BindGroupLayout,
1993    load_op: wgpu::LoadOp<wgpu::Color>,
1994) {
1995    let indices = QUAD_INDICES;
1996
1997    let vb = device.create_buffer(&wgpu::BufferDescriptor {
1998        label: Some("Quad VB"),
1999        size: (4 * std::mem::size_of::<Vertex>()) as u64,
2000        usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2001        mapped_at_creation: false,
2002    });
2003    let ib = device.create_buffer(&wgpu::BufferDescriptor {
2004        label: Some("Quad IB"),
2005        size: (6 * 4) as u64,
2006        usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
2007        mapped_at_creation: false,
2008    });
2009    let ub = device.create_buffer(&wgpu::BufferDescriptor {
2010        label: Some("Quad UB"),
2011        size: std::mem::size_of::<TextureUniforms>() as u64,
2012        usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2013        mapped_at_creation: false,
2014    });
2015
2016    let vertex_bytes = unsafe {
2017        std::slice::from_raw_parts(quad.as_ptr() as *const u8, 4 * std::mem::size_of::<Vertex>())
2018    };
2019    let index_bytes = unsafe {
2020        std::slice::from_raw_parts(indices.as_ptr() as *const u8, 6 * 4)
2021    };
2022    let uniform_bytes = unsafe {
2023        std::slice::from_raw_parts(uniforms as *const TextureUniforms as *const u8, std::mem::size_of::<TextureUniforms>())
2024    };
2025
2026    queue.write_buffer(&vb, 0, vertex_bytes);
2027    queue.write_buffer(&ib, 0, index_bytes);
2028    queue.write_buffer(&ub, 0, uniform_bytes);
2029
2030    let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2031        label: Some("Quad Bind Group"),
2032        layout,
2033        entries: &[
2034            wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &ub, offset: 0, size: None }) },
2035            wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(tex_view) },
2036            wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(sampler) },
2037        ],
2038    });
2039
2040    let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2041        label: Some("Quad Render Pass"),
2042        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2043            view: target_view,
2044            depth_slice: None,
2045            resolve_target: None,
2046            ops: wgpu::Operations { load: load_op, store: wgpu::StoreOp::Store },
2047        })],
2048        depth_stencil_attachment: None,
2049        timestamp_writes: None,
2050        occlusion_query_set: None,
2051        multiview_mask: None,
2052    });
2053    pass.set_pipeline(pipeline);
2054    pass.set_bind_group(0, &bind_group, &[]);
2055    pass.set_vertex_buffer(0, vb.slice(..));
2056    pass.set_index_buffer(ib.slice(..), wgpu::IndexFormat::Uint32);
2057    pass.draw_indexed(0..6, 0, 0..1);
2058}
2059
2060fn render_combine_quad(
2061    encoder: &mut wgpu::CommandEncoder,
2062    target_view: &wgpu::TextureView,
2063    device: &wgpu::Device,
2064    queue: &wgpu::Queue,
2065    uniforms: &TextureUniforms,
2066    textures: [&wgpu::TextureView; 5],
2067    sampler: &wgpu::Sampler,
2068    pipeline: &wgpu::RenderPipeline,
2069    layout: &wgpu::BindGroupLayout,
2070    load_op: wgpu::LoadOp<wgpu::Color>,
2071) {
2072    let indices = QUAD_INDICES;
2073    let vb = device.create_buffer(&wgpu::BufferDescriptor {
2074        label: Some("Combine VB"),
2075        size: (4 * std::mem::size_of::<Vertex>()) as u64,
2076        usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2077        mapped_at_creation: false,
2078    });
2079    let ib = device.create_buffer(&wgpu::BufferDescriptor {
2080        label: Some("Combine IB"),
2081        size: (6 * 4) as u64,
2082        usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
2083        mapped_at_creation: false,
2084    });
2085    let ub = device.create_buffer(&wgpu::BufferDescriptor {
2086        label: Some("Combine UB"),
2087        size: std::mem::size_of::<TextureUniforms>() as u64,
2088        usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2089        mapped_at_creation: false,
2090    });
2091    let vertex_bytes = unsafe {
2092        std::slice::from_raw_parts(BLIT_QUAD.as_ptr() as *const u8, 4 * std::mem::size_of::<Vertex>())
2093    };
2094    let index_bytes = unsafe {
2095        std::slice::from_raw_parts(indices.as_ptr() as *const u8, 6 * 4)
2096    };
2097    let uniform_bytes = unsafe {
2098        std::slice::from_raw_parts(uniforms as *const TextureUniforms as *const u8, std::mem::size_of::<TextureUniforms>())
2099    };
2100    queue.write_buffer(&vb, 0, vertex_bytes);
2101    queue.write_buffer(&ib, 0, index_bytes);
2102    queue.write_buffer(&ub, 0, uniform_bytes);
2103
2104    let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
2105        label: Some("Combine Bind Group"),
2106        layout,
2107        entries: &[
2108            wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &ub, offset: 0, size: None }) },
2109            wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(textures[0]) },
2110            wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(textures[1]) },
2111            wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(textures[2]) },
2112            wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::TextureView(textures[3]) },
2113            wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::TextureView(textures[4]) },
2114            wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::Sampler(sampler) },
2115        ],
2116    });
2117
2118    let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2119        label: Some("Combine Render Pass"),
2120        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2121            view: target_view,
2122            depth_slice: None,
2123            resolve_target: None,
2124            ops: wgpu::Operations { load: load_op, store: wgpu::StoreOp::Store },
2125        })],
2126        depth_stencil_attachment: None,
2127        timestamp_writes: None,
2128        occlusion_query_set: None,
2129        multiview_mask: None,
2130    });
2131    pass.set_pipeline(pipeline);
2132    pass.set_bind_group(0, &bind_group, &[]);
2133    pass.set_vertex_buffer(0, vb.slice(..));
2134    pass.set_index_buffer(ib.slice(..), wgpu::IndexFormat::Uint32);
2135    pass.draw_indexed(0..6, 0, 0..1);
2136}
2137
2138fn draw_rect_custom<T>(
2139    target: &crate::core::RenderTarget,
2140    program: &crate::core::Program,
2141    backend_prog: &Program,
2142    backend_context: &mut Context,
2143    wgpu_blend: wgpu::BlendState,
2144    info: &crate::core::DrawBuilder<T>,
2145    view_matrix: crate::core::Mat4,
2146    model_matrix: crate::core::Mat4,
2147    color: crate::core::Color,
2148    texture: Option<&crate::core::Texture>,
2149) {
2150    use crate::core::Point2Trait;
2151    use crate::core::Uniform;
2152
2153    // Pack named scalar uniforms into _rd_flags
2154    let horizontal = match program.uniforms.0.get("horizontal") {
2155        Some(Uniform::Bool(b)) => if *b { 1.0f32 } else { 0.0 },
2156        _ => 0.0,
2157    };
2158    let brightness = match program.uniforms.0.get("brightness") {
2159        Some(Uniform::Float(f)) => *f,
2160        _ => 1.0,
2161    };
2162
2163    let uniforms = TextureUniforms {
2164        u_view: view_matrix.into(),
2165        u_model: model_matrix.into(),
2166        _rd_color: color.into(),
2167        _rd_offset: info.rect.0.as_array(),
2168        _rd_dimensions: info.rect.1.as_array(),
2169        _rd_flags: [horizontal, brightness, 0.0, 0.0],
2170    };
2171
2172    match backend_prog.kind {
2173        ProgramKind::Default => {
2174            let tex_view: Arc<wgpu::TextureView> = if let Some(tex) = texture {
2175                tex.handle.view.clone()
2176            } else {
2177                backend_context.placeholder_view.clone()
2178            };
2179
2180            match &target.0 {
2181                crate::core::RenderTargetInner::Frame(frame_rc) => {
2182                    let pipeline = backend_prog.get_or_create_pipeline(wgpu_blend, backend_context.format);
2183                    let mut frame = frame_rc.borrow_mut();
2184                    let frame = frame.as_mut().expect("No frame prepared");
2185                    let load_op = frame.next_load_op();
2186                    let view = frame.view.clone();
2187                    render_texture_quad(
2188                        &mut frame.command_encoder, &view,
2189                        &frame.device, &frame.queue,
2190                        &uniforms, &BLIT_QUAD, &*tex_view,
2191                        &*backend_prog.sampler,
2192                        &pipeline, &backend_prog.bind_group_layout,
2193                        load_op,
2194                    );
2195                }
2196                crate::core::RenderTargetInner::Texture(dest_texture) => {
2197                    let dest_view = dest_texture.handle.view.clone();
2198                    let dest_format = dest_texture.handle.texture.format();
2199                    let pipeline = backend_prog.get_or_create_pipeline(wgpu_blend, dest_format);
2200                    let mut encoder = backend_context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Custom RTT") });
2201                    render_texture_quad(
2202                        &mut encoder, &dest_view,
2203                        &backend_context.device, &backend_context.queue,
2204                        &uniforms, &BLIT_QUAD, &*tex_view,
2205                        &*backend_prog.sampler,
2206                        &pipeline, &backend_prog.bind_group_layout,
2207                        wgpu::LoadOp::Load,
2208                    );
2209                    backend_context.queue.submit(std::iter::once(encoder.finish()));
2210                }
2211                crate::core::RenderTargetInner::None => {}
2212            }
2213        }
2214        ProgramKind::Combine => {
2215            // Extract sample0–sample4 from the program's uniform list
2216            let placeholder = &backend_context.placeholder_view;
2217            let get_tex = |name: &str| -> Arc<wgpu::TextureView> {
2218                match program.uniforms.0.get(name) {
2219                    Some(Uniform::Texture(t)) => t.handle.view.clone(),
2220                    _ => placeholder.clone(),
2221                }
2222            };
2223            let s0 = get_tex("sample0");
2224            let s1 = get_tex("sample1");
2225            let s2 = get_tex("sample2");
2226            let s3 = get_tex("sample3");
2227            let s4 = get_tex("sample4");
2228            let textures = [&*s0, &*s1, &*s2, &*s3, &*s4];
2229
2230            match &target.0 {
2231                crate::core::RenderTargetInner::Frame(frame_rc) => {
2232                    let pipeline = backend_prog.get_or_create_pipeline(wgpu_blend, backend_context.format);
2233                    let mut frame = frame_rc.borrow_mut();
2234                    let frame = frame.as_mut().expect("No frame prepared");
2235                    let load_op = frame.next_load_op();
2236                    let view = frame.view.clone();
2237                    render_combine_quad(
2238                        &mut frame.command_encoder, &view,
2239                        &frame.device, &frame.queue,
2240                        &uniforms, textures,
2241                        &*backend_prog.sampler,
2242                        &pipeline, &backend_prog.bind_group_layout,
2243                        load_op,
2244                    );
2245                }
2246                crate::core::RenderTargetInner::Texture(dest_texture) => {
2247                    let dest_view = dest_texture.handle.view.clone();
2248                    let dest_format = dest_texture.handle.texture.format();
2249                    let pipeline = backend_prog.get_or_create_pipeline(wgpu_blend, dest_format);
2250                    let mut encoder = backend_context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Combine RTT") });
2251                    render_combine_quad(
2252                        &mut encoder, &dest_view,
2253                        &backend_context.device, &backend_context.queue,
2254                        &uniforms, textures,
2255                        &*backend_prog.sampler,
2256                        &pipeline, &backend_prog.bind_group_layout,
2257                        wgpu::LoadOp::Load,
2258                    );
2259                    backend_context.queue.submit(std::iter::once(encoder.finish()));
2260                }
2261                crate::core::RenderTargetInner::None => {}
2262            }
2263        }
2264        ProgramKind::Sprite => {
2265            // Sprite programs are not used for fill/rect draws; texture_program handles those.
2266        }
2267    }
2268}
2269
2270// --------------
2271// Public draw functions called from radiant_core
2272// --------------
2273
2274pub fn draw_layer(target: &crate::core::RenderTarget, program: &crate::core::Program, context: &mut crate::core::ContextData, layer: &crate::core::Layer, component: u32) {
2275    let vertices = layer.vertices();
2276    let vertices = vertices.deref();
2277    let dirty = layer.undirty();
2278    let layer_id = layer.id();
2279
2280    let view_matrix = *layer.view_matrix().deref().deref();
2281    let model_matrix = *layer.model_matrix().deref().deref();
2282    let color = layer.color().deref().clone();
2283    let blendmode = layer.blendmode().clone();
2284    let wgpu_blend = Context::blendmode_to_wgpu(&blendmode);
2285
2286    let sprite_uniforms = SpriteUniforms {
2287        u_view: view_matrix,
2288        u_model: model_matrix,
2289        _rd_color: color.into(),
2290    };
2291
2292    let font_view = context.font_texture.as_ref().map(|t| t.view.clone());
2293    let custom_sprite_prog = program.sprite_program.as_deref()
2294        .filter(|p| matches!(p.kind, ProgramKind::Sprite));
2295
2296    let backend_context = context.backend_context.as_mut().unwrap();
2297
2298    match &target.0 {
2299        crate::core::RenderTargetInner::Frame(frame_rc) => {
2300            let mut frame = frame_rc.borrow_mut();
2301            let frame = frame.as_mut().expect("No frame prepared");
2302            let font_view_ref = font_view.as_ref().map(|v| v.as_ref());
2303            if let Some(fv) = font_view_ref {
2304                frame.draw_sprites(
2305                    vertices, dirty, layer_id, component,
2306                    wgpu_blend, fv, &context.tex_arrays,
2307                    &sprite_uniforms, backend_context, custom_sprite_prog,
2308                );
2309            }
2310        }
2311        crate::core::RenderTargetInner::Texture(texture) => {
2312            // Render to texture using a temporary command encoder
2313            let dest_format = texture.handle.texture.format();
2314            let font_view_ref = font_view.as_ref().map(|v| v.as_ref());
2315            if let Some(fv) = font_view_ref {
2316                draw_sprites_to_texture(
2317                    vertices, dirty, layer_id, component,
2318                    wgpu_blend, fv, &context.tex_arrays,
2319                    &sprite_uniforms, backend_context, &*texture.handle.view, dest_format,
2320                    custom_sprite_prog,
2321                );
2322            }
2323        }
2324        crate::core::RenderTargetInner::None => {}
2325    }
2326}
2327
2328/// Draw sprites directly into a texture view (for render-to-texture).
2329fn draw_sprites_to_texture(
2330    vertices: &[Vertex],
2331    dirty: bool,
2332    layer_hint: usize,
2333    component: u32,
2334    blend: wgpu::BlendState,
2335    font_texture_view: &wgpu::TextureView,
2336    tex_arrays: &[crate::core::RawFrameArray],
2337    sprite_uniforms: &SpriteUniforms,
2338    context: &mut Context,
2339    target_view: &wgpu::TextureView,
2340    dest_format: wgpu::TextureFormat,
2341    custom_program: Option<&Program>,
2342) {
2343    let num_vertices = vertices.len();
2344    let num_sprites = num_vertices / 4;
2345    if num_vertices == 0 {
2346        return;
2347    }
2348
2349    let (vb_index, vb_dirty) = context.select_vertex_buffer(layer_hint, num_vertices);
2350
2351    let mut encoder = context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("RTT Encoder") });
2352
2353    if dirty || vb_dirty {
2354        let vertex_bytes = unsafe {
2355            std::slice::from_raw_parts(vertices.as_ptr() as *const u8, num_vertices * std::mem::size_of::<Vertex>())
2356        };
2357        context.queue.write_buffer(&context.vertex_buffers[vb_index].buffer, 0, vertex_bytes);
2358    }
2359    context.update_index_buffer(num_sprites);
2360
2361    let uniform_bytes = unsafe {
2362        std::slice::from_raw_parts(sprite_uniforms as *const SpriteUniforms as *const u8, std::mem::size_of::<SpriteUniforms>())
2363    };
2364    let uniform_buffer = context.device.create_buffer(&wgpu::BufferDescriptor {
2365        label: None, size: uniform_bytes.len() as u64,
2366        usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false,
2367    });
2368    context.queue.write_buffer(&uniform_buffer, 0, uniform_bytes);
2369
2370    let comp_data = [component, 0u32, 0u32, 0u32];
2371    let comp_bytes = unsafe { std::slice::from_raw_parts(comp_data.as_ptr() as *const u8, 16) };
2372    let comp_buffer = context.device.create_buffer(&wgpu::BufferDescriptor {
2373        label: None, size: 16,
2374        usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false,
2375    });
2376    context.queue.write_buffer(&comp_buffer, 0, comp_bytes);
2377
2378    let sprite_bgl = custom_program.map_or(&context.sprite_bind_group_layout, |p| &*p.bind_group_layout);
2379    let bind_group = context.device.create_bind_group(&wgpu::BindGroupDescriptor {
2380        label: None,
2381        layout: sprite_bgl,
2382        entries: &[
2383            wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &uniform_buffer, offset: 0, size: None }) },
2384            wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(font_texture_view) },
2385            wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&context.nearest_sampler) },
2386            wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(&tex_arrays[1].data.view) },
2387            wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::TextureView(&tex_arrays[2].data.view) },
2388            wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::TextureView(&tex_arrays[3].data.view) },
2389            wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::TextureView(&tex_arrays[4].data.view) },
2390            wgpu::BindGroupEntry { binding: 7, resource: wgpu::BindingResource::TextureView(&tex_arrays[5].data.view) },
2391            wgpu::BindGroupEntry { binding: 8, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &comp_buffer, offset: 0, size: None }) },
2392        ],
2393    });
2394
2395    let pipeline = match custom_program {
2396        Some(prog) => prog.get_or_create_pipeline(blend, dest_format),
2397        None => context.get_or_create_sprite_pipeline(blend, dest_format),
2398    };
2399    {
2400        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2401            label: Some("RTT Sprite Pass"),
2402            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2403                view: target_view,
2404                depth_slice: None,
2405                resolve_target: None,
2406                ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store },
2407            })],
2408            depth_stencil_attachment: None,
2409            timestamp_writes: None,
2410            occlusion_query_set: None,
2411            multiview_mask: None,
2412        });
2413        pass.set_pipeline(&pipeline);
2414        pass.set_blend_constant(wgpu::Color::TRANSPARENT);
2415        pass.set_bind_group(0, &bind_group, &[]);
2416        pass.set_vertex_buffer(0, context.vertex_buffers[vb_index].buffer.slice(..));
2417        pass.set_index_buffer(context.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
2418        pass.draw_indexed(0..(num_sprites as u32 * 6), 0, 0..1);
2419    }
2420    context.queue.submit(std::iter::once(encoder.finish()));
2421}
2422
2423pub fn draw_rect<T>(
2424    target: &crate::core::RenderTarget,
2425    program: &crate::core::Program,
2426    context: &mut crate::core::ContextData,
2427    blend: crate::core::BlendMode,
2428    info: crate::core::DrawBuilder<T>,
2429    view_matrix: crate::core::Mat4,
2430    model_matrix: crate::core::Mat4,
2431    color: crate::core::Color,
2432    texture: Option<&crate::core::Texture>,
2433) {
2434    use crate::core::Point2Trait;
2435
2436    let wgpu_blend = Context::blendmode_to_wgpu(&blend);
2437
2438    let backend_context = context.backend_context.as_mut().unwrap();
2439
2440    // Custom programs use a dedicated draw path that packs uniforms and uses the program's pipeline.
2441    let backend_prog = program.texture_program.clone();
2442    if backend_prog.is_custom {
2443        draw_rect_custom(target, program, &backend_prog, backend_context, wgpu_blend, &info, view_matrix, model_matrix, color, texture);
2444        return;
2445    }
2446
2447    let uniforms = TextureUniforms {
2448        u_view: view_matrix.into(),
2449        u_model: model_matrix.into(),
2450        _rd_color: color.into(),
2451        _rd_offset: info.rect.0.as_array(),
2452        _rd_dimensions: info.rect.1.as_array(),
2453        _rd_flags: [0.0, 0.0, 0.0, 0.0],
2454    };
2455
2456    // Resolve texture view: use provided texture or 1×1 white placeholder
2457    let tex_view: Arc<wgpu::TextureView> = if let Some(tex) = texture {
2458        tex.handle.view.clone()
2459    } else {
2460        backend_context.placeholder_view.clone()
2461    };
2462
2463    let filter = texture.map_or(crate::core::TextureFilter::Nearest, |t| t.magnify);
2464
2465    match &target.0 {
2466        crate::core::RenderTargetInner::Frame(frame_rc) => {
2467            let mut frame = frame_rc.borrow_mut();
2468            let frame = frame.as_mut().expect("No frame prepared");
2469            frame.draw_quad(&uniforms, &*tex_view, filter, wgpu_blend, backend_context);
2470        }
2471        crate::core::RenderTargetInner::Texture(dest_texture) => {
2472            let dest_view = dest_texture.handle.view.clone();
2473            let dest_format = dest_texture.handle.texture.format();
2474            let pipeline = backend_context.get_or_create_texture_pipeline(wgpu_blend, dest_format);
2475            let sampler = Context::sampler_for_filter(&backend_context.nearest_sampler, &backend_context.linear_sampler, filter);
2476            let mut encoder = backend_context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("DrawRect RTT") });
2477            render_texture_quad(
2478                &mut encoder,
2479                &dest_view,
2480                &backend_context.device,
2481                &backend_context.queue,
2482                &uniforms,
2483                &BLIT_QUAD,
2484                &*tex_view,
2485                sampler,
2486                &pipeline,
2487                &backend_context.texture_bind_group_layout,
2488                wgpu::LoadOp::Load,
2489            );
2490            backend_context.queue.submit(std::iter::once(encoder.finish()));
2491        }
2492        crate::core::RenderTargetInner::None => {}
2493    }
2494}