1use anyhow::{Context, Result};
8use bytemuck::{Pod, Zeroable};
9use std::collections::{HashMap, HashSet};
10use std::num::NonZeroU64;
11use std::path::{Path, PathBuf};
12use std::sync::Arc;
13use wgpu::util::DeviceExt;
14use winit::window::Window;
15
16use crate::assets::load_image_any;
17use crate::image_manager::{ImageHandle, ImageKey, ImageManager};
18use crate::layer::{
19 ClipRect, RenderFrame, RenderSprite, SpriteBlend, SpriteFit, SpriteSizeMode, WipeRenderPlan,
20};
21use crate::mesh3d::{MeshAsset, load_mesh_asset};
22use crate::render_math::sprite_quad_geometry_rect;
23use crate::runtime::FrameCaptureBackend;
24
25mod emote;
26mod mipmap;
27
28use crate::render_plan::*;
29
30impl Vertex {
31 const ATTRS: [wgpu::VertexAttribute; 10] = [
35 wgpu::VertexAttribute {
36 offset: 0,
37 shader_location: 0,
38 format: wgpu::VertexFormat::Float32x3,
39 },
40 wgpu::VertexAttribute {
41 offset: 12,
42 shader_location: 1,
43 format: wgpu::VertexFormat::Float32x2,
44 },
45 wgpu::VertexAttribute {
46 offset: 28,
47 shader_location: 2,
48 format: wgpu::VertexFormat::Float32,
49 },
50 wgpu::VertexAttribute {
51 offset: 144,
52 shader_location: 3,
53 format: wgpu::VertexFormat::Float32x4,
54 },
55 wgpu::VertexAttribute {
56 offset: 160,
57 shader_location: 4,
58 format: wgpu::VertexFormat::Float32x4,
59 },
60 wgpu::VertexAttribute {
61 offset: 224,
62 shader_location: 5,
63 format: wgpu::VertexFormat::Float32x4,
64 },
65 wgpu::VertexAttribute {
66 offset: 240,
67 shader_location: 6,
68 format: wgpu::VertexFormat::Float32x4,
69 },
70 wgpu::VertexAttribute {
71 offset: 256,
72 shader_location: 7,
73 format: wgpu::VertexFormat::Float32x4,
74 },
75 wgpu::VertexAttribute {
76 offset: 288,
77 shader_location: 8,
78 format: wgpu::VertexFormat::Float32x4,
79 },
80 wgpu::VertexAttribute {
81 offset: 304,
82 shader_location: 9,
83 format: wgpu::VertexFormat::Float32x4,
84 },
85 ];
86
87 fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
88 wgpu::VertexBufferLayout {
89 array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
90 step_mode: wgpu::VertexStepMode::Vertex,
91 attributes: &Self::ATTRS,
92 }
93 }
94}
95
96struct VertexSprite2d;
97
98impl VertexSprite2d {
99 const ATTRS: [wgpu::VertexAttribute; 6] = wgpu::vertex_attr_array![
100 0 => Float32x3,
101 1 => Float32x2,
102 2 => Float32x2,
103 3 => Float32,
104 4 => Float32x4,
105 5 => Float32x4
106 ];
107
108 fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
109 wgpu::VertexBufferLayout {
110 array_stride: std::mem::size_of::<VertexSprite2dData>() as wgpu::BufferAddress,
111 step_mode: wgpu::VertexStepMode::Vertex,
112 attributes: &Self::ATTRS,
113 }
114 }
115}
116
117#[derive(Debug)]
118pub struct Renderer {
119 pub surface: wgpu::Surface<'static>,
120 pub instance: wgpu::Instance,
126 pub adapter: wgpu::Adapter,
129 pub device: wgpu::Device,
130 pub queue: wgpu::Queue,
131 pub config: wgpu::SurfaceConfiguration,
132 wait_display_vsync: bool,
135 logical_width: f32,
136 logical_height: f32,
137 scale_factor: f32,
138 surface_viewport: SurfaceViewport,
139
140 pipelines: HashMap<RenderPipelineKey, wgpu::RenderPipeline>,
141 bind_group_layout: wgpu::BindGroupLayout,
142 shader: wgpu::ShaderModule,
143 pipeline_layout: wgpu::PipelineLayout,
144 wipe_bind_group_layout: wgpu::BindGroupLayout,
145 wipe_pipeline: wgpu::RenderPipeline,
146 page_wipe_bind_group_layout: wgpu::BindGroupLayout,
147 page_wipe_pipeline: wgpu::RenderPipeline,
148
149 vertex_buf: wgpu::Buffer,
150 vertex_capacity: usize,
151 vertex_sprite2d_buf: wgpu::Buffer,
152
153 vs_uniform_buf: wgpu::Buffer,
157 vs_uniform_capacity: usize,
158 vs_uniform_stride: usize,
159 vs_uniform_staging: Vec<u8>,
160 zero_bone_uniform_buf: wgpu::Buffer,
161
162 textures: HashMap<ImageKey, GpuTexture>,
164 mipmap_generator: mipmap::MipmapGenerator,
165 external_textures: HashMap<PathBuf, GpuTexture>,
166 default_aux: GpuTexture,
167 fog_sampler: wgpu::Sampler,
170 mesh_sampler: wgpu::Sampler,
172 normal_sampler: wgpu::Sampler,
173 toon_sampler: wgpu::Sampler,
174 shadow_sampler: wgpu::Sampler,
175 depth: DepthTexture,
176 surface_depth: DepthTexture,
177 scene_a: RenderTargetTexture,
178 scene_b: RenderTargetTexture,
179 wipe_a: RenderTargetTexture,
180 wipe_b: RenderTargetTexture,
181 wipe_mask_cache: Option<(WipeMaskCacheKey, GpuTexture)>,
182 shadow_map: RenderTargetTexture,
183 shadow_depth: DepthTexture,
184 offscreen_ready: bool,
187 shadow_ready: bool,
189
190 sprite2d_verts: Vec<VertexSprite2dData>,
191 plan: FramePlanner,
193 draw_gpu_slots: Vec<DrawGpuSlot>,
194 shared_draw_bind_groups: HashMap<DrawBindKey, Arc<wgpu::BindGroup>>,
195 draw_bind_epoch: u64,
196 debug_frame_serial: u64,
197 emote_compositor: emote::EmoteCompositor,
198}
199
200fn align_up_usize(value: usize, alignment: usize) -> usize {
201 debug_assert!(alignment > 0);
202 let rem = value % alignment;
203 if rem == 0 {
204 value
205 } else {
206 value.saturating_add(alignment - rem)
207 }
208}
209
210#[derive(Debug, Clone, Copy, Default)]
211pub struct RendererMemoryStats {
212 pub image_texture_bytes: u64,
213 pub external_texture_bytes: u64,
214 pub internal_color_target_bytes: u64,
215 pub internal_depth_target_bytes: u64,
216 pub renderer_gpu_buffer_bytes: u64,
217 pub frame_arena_capacity_bytes: usize,
218 pub image_texture_count: usize,
219 pub external_texture_count: usize,
220 pub cached_render_pipeline_count: usize,
221}
222
223#[derive(Debug, Clone)]
224pub struct RendererDebugTexture {
225 pub key: String,
226 pub kind: String,
227 pub label: String,
228 pub usage: String,
229 pub usage_count: usize,
230 pub width: u32,
231 pub height: u32,
232 pub version: u64,
233 pub rgba: Vec<u8>,
234}
235
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
237enum RendererDebugRenderTarget {
238 SceneA,
239 SceneB,
240 ShadowMap,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, Hash)]
244enum RendererDebugTextureKey {
245 DefaultAux,
246 Image(ImageKey),
247 External(PathBuf),
248 RenderTarget(RendererDebugRenderTarget),
249}
250
251#[derive(Debug, Clone)]
252struct PendingRendererDebugTexture {
253 order: usize,
254 kind: String,
255 label: String,
256 usage: Vec<String>,
257 width: u32,
258 height: u32,
259 version: u64,
260}
261
262#[derive(Debug)]
263struct DepthTexture {
264 _tex: wgpu::Texture,
265 view: wgpu::TextureView,
266}
267
268#[derive(Debug)]
269struct GpuTexture {
270 _tex: wgpu::Texture,
271 view: wgpu::TextureView,
272 sampler: wgpu::Sampler,
273 width: u32,
274 height: u32,
275 version: u64,
276}
277
278#[derive(Debug)]
279struct RenderTargetTexture {
280 _tex: wgpu::Texture,
281 view: wgpu::TextureView,
282 sampler: wgpu::Sampler,
283 width: u32,
284 height: u32,
285 format: wgpu::TextureFormat,
286}
287
288#[derive(Debug, Clone, Copy)]
289enum InternalColorTarget {
290 SceneA,
291 SceneB,
292 WipeA,
293 WipeB,
294 ShadowMap,
295}
296
297#[derive(Debug, Clone, Copy)]
298enum DepthTarget {
299 None,
300 Main,
301 Surface,
302 Shadow,
303}
304
305#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
306enum BackdropTarget {
307 SceneA,
308 SceneB,
309}
310
311fn backdrop_to_internal(target: BackdropTarget) -> InternalColorTarget {
312 match target {
313 BackdropTarget::SceneA => InternalColorTarget::SceneA,
314 BackdropTarget::SceneB => InternalColorTarget::SceneB,
315 }
316}
317
318fn opposite_backdrop(target: BackdropTarget) -> BackdropTarget {
319 match target {
320 BackdropTarget::SceneA => BackdropTarget::SceneB,
321 BackdropTarget::SceneB => BackdropTarget::SceneA,
322 }
323}
324
325#[derive(Debug, Clone, Copy)]
326enum ColorTarget<'a> {
327 External(&'a wgpu::TextureView),
328 Internal(InternalColorTarget),
329}
330
331#[derive(Debug, Clone, PartialEq, Eq, Hash)]
332struct DrawBindKey {
333 image_id: Option<ImageKey>,
334 emote_render_id: Option<u64>,
335 mesh_texture_path: Option<PathBuf>,
336 mesh_normal_texture_path: Option<PathBuf>,
337 mesh_toon_texture_path: Option<PathBuf>,
338 mask_image_id: Option<ImageKey>,
339 tonecurve_image_id: Option<ImageKey>,
340 fog_image_id: Option<ImageKey>,
341 wipe_src_image_id: Option<ImageKey>,
342 overlay_backdrop: Option<BackdropTarget>,
343 mesh_base_sampler: bool,
344 use_bone_uniform: bool,
345}
346
347impl DrawBindKey {
348 fn from_command(cmd: &DrawCommand, overlay_backdrop: Option<BackdropTarget>) -> Self {
349 Self {
350 image_id: cmd.image_id.as_ref().map(|id| id.key()),
351 emote_render_id: cmd.emote_render_id,
352 mesh_texture_path: cmd.mesh_texture_path.clone(),
353 mesh_normal_texture_path: cmd.mesh_normal_texture_path.clone(),
354 mesh_toon_texture_path: cmd.mesh_toon_texture_path.clone(),
355 mask_image_id: cmd.mask_image_id.as_ref().map(|id| id.key()),
356 tonecurve_image_id: cmd.tonecurve_image_id.as_ref().map(|id| id.key()),
357 fog_image_id: cmd.fog_image_id.as_ref().map(|id| id.key()),
358 wipe_src_image_id: cmd.wipe_src_image_id.as_ref().map(|id| id.key()),
359 overlay_backdrop: if matches!(
360 cmd.pipeline_key.technique.special,
361 TechniqueSpecial::Overlay
362 ) {
363 overlay_backdrop
364 } else {
365 None
366 },
367 mesh_base_sampler: matches!(
368 cmd.draw_kind,
369 MeshDrawKind::StaticMesh | MeshDrawKind::SkinnedMesh | MeshDrawKind::ShadowCaster
370 ),
371 use_bone_uniform: draw_uses_bone_uniform(cmd),
372 }
373 }
374}
375
376#[derive(Debug)]
377struct DrawGpuSlot {
378 bone_uniform_buf: Option<wgpu::Buffer>,
379 bind_group: Option<Arc<wgpu::BindGroup>>,
380 bind_key: Option<DrawBindKey>,
381 bind_epoch: u64,
382}
383
384fn draw_uses_bone_uniform(cmd: &DrawCommand) -> bool {
385 cmd.bone_uniform_index.is_some()
386}
387
388#[derive(Debug, Clone, Copy, Default)]
389struct EffectGlobalValPackSemantic {
390 use_bone_uniform: bool,
391 use_shadow_tex: bool,
392 use_normal_tex: bool,
393 use_toon_tex: bool,
394}
395
396#[derive(Debug)]
397struct EffectResolvedResources<'a> {
398 base: &'a GpuTexture,
399 mask: &'a GpuTexture,
400 tone: &'a GpuTexture,
401 fog: &'a GpuTexture,
402 normal: &'a GpuTexture,
403 toon: &'a GpuTexture,
404 aux_view: &'a wgpu::TextureView,
405 aux_sampler: &'a wgpu::Sampler,
406 shadow_view: &'a wgpu::TextureView,
407 shadow_sampler: &'a wgpu::Sampler,
408 global_vals: EffectGlobalValPackSemantic,
409}
410
411fn d3d_front_face() -> wgpu::FrontFace {
412 wgpu::FrontFace::Cw
415}
416
417type DebugTextureReadback = (u32, u32, u64, Vec<u8>);
418
419fn present_mode_for_wait_display_vsync(
420 wait_display_vsync: bool,
421 supported: &[wgpu::PresentMode],
422) -> wgpu::PresentMode {
423 if wait_display_vsync {
424 return wgpu::PresentMode::Fifo;
427 }
428
429 if supported.contains(&wgpu::PresentMode::Immediate) {
434 wgpu::PresentMode::Immediate
435 } else if supported.contains(&wgpu::PresentMode::Mailbox) {
436 wgpu::PresentMode::Mailbox
437 } else {
438 wgpu::PresentMode::Fifo
439 }
440}
441
442impl Renderer {
443 pub fn adapter_name(&self) -> String {
447 self.adapter.get_info().name
448 }
449
450 pub async fn new<W>(window: W) -> Result<Self>
451 where
452 W: std::ops::Deref<Target = dyn Window> + Into<wgpu::SurfaceTarget<'static>>,
453 {
454 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
455 let backends = wgpu::Backends::GL;
456 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
457 let backends = wgpu::Backends::all();
458
459 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
460 backends,
461 ..Default::default()
462 });
463
464 let size = window.surface_size();
465 let scale_factor = window.scale_factor() as f32;
466 let surface = instance.create_surface(window).context("create_surface")?;
467 Self::new_from_instance_surface(instance, surface, size.width, size.height, scale_factor)
468 .await
469 }
470
471 #[cfg(any(target_os = "android", target_os = "ios"))]
472 pub async unsafe fn new_from_raw_handles(
473 raw_display_handle: raw_window_handle::RawDisplayHandle,
474 raw_window_handle: raw_window_handle::RawWindowHandle,
475 width: u32,
476 height: u32,
477 scale_factor: f32,
478 ) -> Result<Self> {
479 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
480 backends: wgpu::Backends::all(),
481 ..Default::default()
482 });
483 let surface = instance
484 .create_surface_unsafe(wgpu::SurfaceTargetUnsafe::RawHandle {
485 raw_display_handle,
486 raw_window_handle,
487 })
488 .context("create_surface_unsafe")?;
489 Self::new_from_instance_surface(instance, surface, width, height, scale_factor).await
490 }
491
492 pub unsafe fn replace_surface_from_raw_handles(
507 &mut self,
508 raw_display_handle: raw_window_handle::RawDisplayHandle,
509 raw_window_handle: raw_window_handle::RawWindowHandle,
510 width: u32,
511 height: u32,
512 ) -> Result<()> {
513 let surface = unsafe {
514 self.instance
515 .create_surface_unsafe(wgpu::SurfaceTargetUnsafe::RawHandle {
516 raw_display_handle,
517 raw_window_handle,
518 })
519 }
520 .context("create_surface_unsafe (replace)")?;
521 let caps = surface.get_capabilities(&self.adapter);
525 if !caps.formats.contains(&self.config.format) {
526 anyhow::bail!(
527 "replacement surface does not support format {:?} (adapter formats: {:?})",
528 self.config.format,
529 caps.formats
530 );
531 }
532 let replacement_present_mode =
533 present_mode_for_wait_display_vsync(self.wait_display_vsync, &caps.present_modes);
534 if !caps.alpha_modes.contains(&self.config.alpha_mode) {
535 anyhow::bail!(
536 "replacement surface does not support alpha mode {:?} (adapter modes: {:?})",
537 self.config.alpha_mode,
538 caps.alpha_modes
539 );
540 }
541 if !caps.usages.contains(self.config.usage) {
542 anyhow::bail!(
543 "replacement surface does not support usage {:?} (adapter usages: {:?})",
544 self.config.usage,
545 caps.usages
546 );
547 }
548 self.config.present_mode = replacement_present_mode;
549 self.surface = surface;
550 let scale_factor = self.scale_factor;
551 self.resize_with_scale(width.max(1), height.max(1), scale_factor);
552 Ok(())
553 }
554
555 async fn new_from_instance_surface(
556 instance: wgpu::Instance,
557 surface: wgpu::Surface<'static>,
558 width: u32,
559 height: u32,
560 scale_factor: f32,
561 ) -> Result<Self> {
562 let adapter = instance
563 .request_adapter(&wgpu::RequestAdapterOptions {
564 power_preference: wgpu::PowerPreference::HighPerformance,
565 compatible_surface: Some(&surface),
566 force_fallback_adapter: false,
567 })
568 .await
569 .context("request_adapter")?;
570
571 let (device, queue) = adapter
572 .request_device(
573 &wgpu::DeviceDescriptor {
574 label: Some("siglus-bg-device"),
575 required_features: wgpu::Features::empty(),
576 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
577 required_limits: wgpu::Limits::downlevel_webgl2_defaults(),
578 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
579 required_limits: wgpu::Limits::default(),
580 },
581 None,
582 )
583 .await
584 .context("request_device")?;
585
586 let surface_caps = surface.get_capabilities(&adapter);
587 let format = surface_caps
592 .formats
593 .iter()
594 .copied()
595 .find(|f| !f.is_srgb())
596 .unwrap_or(surface_caps.formats[0]);
597 if format.is_srgb() {
598 log::error!(
599 "adapter exposes no non-sRGB surface format; D3D9 byte-space blending cannot be reproduced exactly (using {format:?})"
600 );
601 }
602
603 let scale_factor = if scale_factor.is_finite() && scale_factor > 0.0 {
604 scale_factor
605 } else {
606 1.0
607 };
608 let width = width.max(1);
609 let height = height.max(1);
610 let logical_width = ((width as f32) / scale_factor).max(1.0);
611 let logical_height = ((height as f32) / scale_factor).max(1.0);
612 let alpha_mode = surface_caps
613 .alpha_modes
614 .iter()
615 .copied()
616 .find(|m| *m == wgpu::CompositeAlphaMode::Opaque)
617 .unwrap_or(surface_caps.alpha_modes[0]);
618 let wait_display_vsync = true;
619 let present_mode =
620 present_mode_for_wait_display_vsync(wait_display_vsync, &surface_caps.present_modes);
621 let config = wgpu::SurfaceConfiguration {
622 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
623 format,
624 width,
625 height,
626 present_mode,
627 alpha_mode,
628 view_formats: vec![],
629 desired_maximum_frame_latency: 2,
630 };
631 surface.configure(&device, &config);
632
633 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
634 label: Some("siglus-sprite-bgl"),
635 entries: &[
636 wgpu::BindGroupLayoutEntry {
637 binding: 0,
638 visibility: wgpu::ShaderStages::FRAGMENT,
639 ty: wgpu::BindingType::Texture {
640 multisampled: false,
641 view_dimension: wgpu::TextureViewDimension::D2,
642 sample_type: wgpu::TextureSampleType::Float { filterable: true },
643 },
644 count: None,
645 },
646 wgpu::BindGroupLayoutEntry {
647 binding: 1,
648 visibility: wgpu::ShaderStages::FRAGMENT,
649 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
650 count: None,
651 },
652 wgpu::BindGroupLayoutEntry {
653 binding: 2,
654 visibility: wgpu::ShaderStages::FRAGMENT,
655 ty: wgpu::BindingType::Texture {
656 multisampled: false,
657 view_dimension: wgpu::TextureViewDimension::D2,
658 sample_type: wgpu::TextureSampleType::Float { filterable: true },
659 },
660 count: None,
661 },
662 wgpu::BindGroupLayoutEntry {
663 binding: 3,
664 visibility: wgpu::ShaderStages::FRAGMENT,
665 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
666 count: None,
667 },
668 wgpu::BindGroupLayoutEntry {
669 binding: 4,
670 visibility: wgpu::ShaderStages::FRAGMENT,
671 ty: wgpu::BindingType::Texture {
672 multisampled: false,
673 view_dimension: wgpu::TextureViewDimension::D2,
674 sample_type: wgpu::TextureSampleType::Float { filterable: true },
675 },
676 count: None,
677 },
678 wgpu::BindGroupLayoutEntry {
679 binding: 5,
680 visibility: wgpu::ShaderStages::FRAGMENT,
681 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
682 count: None,
683 },
684 wgpu::BindGroupLayoutEntry {
685 binding: 6,
686 visibility: wgpu::ShaderStages::FRAGMENT,
687 ty: wgpu::BindingType::Texture {
688 multisampled: false,
689 view_dimension: wgpu::TextureViewDimension::D2,
690 sample_type: wgpu::TextureSampleType::Float { filterable: true },
691 },
692 count: None,
693 },
694 wgpu::BindGroupLayoutEntry {
695 binding: 7,
696 visibility: wgpu::ShaderStages::FRAGMENT,
697 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
698 count: None,
699 },
700 wgpu::BindGroupLayoutEntry {
701 binding: 8,
702 visibility: wgpu::ShaderStages::FRAGMENT,
703 ty: wgpu::BindingType::Texture {
704 multisampled: false,
705 view_dimension: wgpu::TextureViewDimension::D2,
706 sample_type: wgpu::TextureSampleType::Float { filterable: true },
707 },
708 count: None,
709 },
710 wgpu::BindGroupLayoutEntry {
711 binding: 9,
712 visibility: wgpu::ShaderStages::FRAGMENT,
713 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
714 count: None,
715 },
716 wgpu::BindGroupLayoutEntry {
717 binding: 10,
718 visibility: wgpu::ShaderStages::FRAGMENT,
719 ty: wgpu::BindingType::Texture {
720 multisampled: false,
721 view_dimension: wgpu::TextureViewDimension::D2,
722 sample_type: wgpu::TextureSampleType::Float { filterable: true },
723 },
724 count: None,
725 },
726 wgpu::BindGroupLayoutEntry {
727 binding: 11,
728 visibility: wgpu::ShaderStages::FRAGMENT,
729 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
730 count: None,
731 },
732 wgpu::BindGroupLayoutEntry {
733 binding: 12,
734 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
735 ty: wgpu::BindingType::Buffer {
736 ty: wgpu::BufferBindingType::Uniform,
737 has_dynamic_offset: true,
738 min_binding_size: NonZeroU64::new(std::mem::size_of::<VsUniform>() as u64),
739 },
740 count: None,
741 },
742 wgpu::BindGroupLayoutEntry {
743 binding: 13,
744 visibility: wgpu::ShaderStages::VERTEX,
745 ty: wgpu::BindingType::Buffer {
746 ty: wgpu::BufferBindingType::Uniform,
747 has_dynamic_offset: false,
748 min_binding_size: None,
749 },
750 count: None,
751 },
752 wgpu::BindGroupLayoutEntry {
753 binding: 14,
754 visibility: wgpu::ShaderStages::FRAGMENT,
755 ty: wgpu::BindingType::Texture {
756 multisampled: false,
757 view_dimension: wgpu::TextureViewDimension::D2,
758 sample_type: wgpu::TextureSampleType::Float { filterable: true },
759 },
760 count: None,
761 },
762 wgpu::BindGroupLayoutEntry {
763 binding: 15,
764 visibility: wgpu::ShaderStages::FRAGMENT,
765 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
766 count: None,
767 },
768 wgpu::BindGroupLayoutEntry {
769 binding: 16,
770 visibility: wgpu::ShaderStages::FRAGMENT,
771 ty: wgpu::BindingType::Texture {
772 multisampled: false,
773 view_dimension: wgpu::TextureViewDimension::D2,
774 sample_type: wgpu::TextureSampleType::Float { filterable: true },
775 },
776 count: None,
777 },
778 wgpu::BindGroupLayoutEntry {
779 binding: 17,
780 visibility: wgpu::ShaderStages::FRAGMENT,
781 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
782 count: None,
783 },
784 ],
785 });
786
787 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
788 let shader_source = wgpu::ShaderSource::Wgsl(wasm_shader_source().into());
789 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
790 let shader_source = wgpu::ShaderSource::Wgsl(SHADER.into());
791 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
792 label: Some("siglus-sprite-shader"),
793 source: shader_source,
794 });
795
796 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
797 label: Some("siglus-sprite-pipeline-layout"),
798 bind_group_layouts: &[&bind_group_layout],
799 push_constant_ranges: &[],
800 });
801 let (wipe_bind_group_layout, wipe_pipeline) = create_wipe_pipeline(&device, config.format);
802 let (page_wipe_bind_group_layout, page_wipe_pipeline) =
803 create_page_wipe_pipeline(&device, config.format);
804
805 let vertex_capacity = 48;
809 let vertex_buf = device.create_buffer(&wgpu::BufferDescriptor {
810 label: Some("siglus-sprite-vertex-buf"),
811 size: (vertex_capacity * std::mem::size_of::<Vertex>()) as wgpu::BufferAddress,
812 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
813 mapped_at_creation: false,
814 });
815 let vertex_sprite2d_buf = device.create_buffer(&wgpu::BufferDescriptor {
816 label: Some("siglus-sprite2d-vertex-buf"),
817 size: (vertex_capacity * std::mem::size_of::<VertexSprite2dData>())
818 as wgpu::BufferAddress,
819 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
820 mapped_at_creation: false,
821 });
822
823 let uniform_alignment = device.limits().min_uniform_buffer_offset_alignment.max(1) as usize;
824 let vs_uniform_stride = align_up_usize(std::mem::size_of::<VsUniform>(), uniform_alignment);
825 let vs_uniform_capacity = 64usize;
826 let vs_uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
827 label: Some("siglus-vs-uniform-arena"),
828 size: (vs_uniform_stride * vs_uniform_capacity) as wgpu::BufferAddress,
829 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
830 mapped_at_creation: false,
831 });
832 let zero_bone_uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
833 label: Some("siglus-zero-bone-uniform"),
834 contents: bytemuck::bytes_of(&BoneUniform::zero()),
835 usage: wgpu::BufferUsages::UNIFORM,
836 });
837
838 let mipmap_generator = mipmap::MipmapGenerator::new(&device);
839 let default_aux =
840 create_solid_texture(&device, &queue, &mipmap_generator, [255, 255, 255, 255])?;
841 let fog_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
842 label: Some("siglus-cfx-fog-sampler"),
843 address_mode_u: wgpu::AddressMode::ClampToEdge,
844 address_mode_v: wgpu::AddressMode::ClampToEdge,
845 address_mode_w: wgpu::AddressMode::ClampToEdge,
846 mag_filter: wgpu::FilterMode::Linear,
847 min_filter: wgpu::FilterMode::Linear,
848 mipmap_filter: wgpu::FilterMode::Nearest,
849 lod_min_clamp: 0.0,
850 lod_max_clamp: 0.0,
851 ..Default::default()
852 });
853 let mesh_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
854 label: Some("siglus-tona3-mesh-clamp-sampler"),
855 address_mode_u: wgpu::AddressMode::ClampToEdge,
856 address_mode_v: wgpu::AddressMode::ClampToEdge,
857 address_mode_w: wgpu::AddressMode::ClampToEdge,
858 mag_filter: wgpu::FilterMode::Linear,
859 min_filter: wgpu::FilterMode::Linear,
860 mipmap_filter: wgpu::FilterMode::Linear,
861 ..Default::default()
862 });
863 let normal_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
864 label: Some("siglus-tona3-normal-clamp-sampler"),
865 address_mode_u: wgpu::AddressMode::ClampToEdge,
866 address_mode_v: wgpu::AddressMode::ClampToEdge,
867 address_mode_w: wgpu::AddressMode::ClampToEdge,
868 mag_filter: wgpu::FilterMode::Linear,
869 min_filter: wgpu::FilterMode::Linear,
870 mipmap_filter: wgpu::FilterMode::Nearest,
871 lod_min_clamp: 0.0,
872 lod_max_clamp: 0.0,
873 ..Default::default()
874 });
875 let toon_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
876 label: Some("siglus-tona3-toon-clamp-sampler"),
877 address_mode_u: wgpu::AddressMode::ClampToEdge,
878 address_mode_v: wgpu::AddressMode::ClampToEdge,
879 address_mode_w: wgpu::AddressMode::ClampToEdge,
880 mag_filter: wgpu::FilterMode::Linear,
881 min_filter: wgpu::FilterMode::Linear,
882 mipmap_filter: wgpu::FilterMode::Nearest,
883 lod_min_clamp: 0.0,
884 lod_max_clamp: 0.0,
885 ..Default::default()
886 });
887 let shadow_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
888 label: Some("siglus-tona3-shadow-point-sampler"),
889 address_mode_u: wgpu::AddressMode::ClampToEdge,
890 address_mode_v: wgpu::AddressMode::ClampToEdge,
891 address_mode_w: wgpu::AddressMode::ClampToEdge,
892 mag_filter: wgpu::FilterMode::Nearest,
893 min_filter: wgpu::FilterMode::Nearest,
894 mipmap_filter: wgpu::FilterMode::Nearest,
895 lod_min_clamp: 0.0,
896 lod_max_clamp: 0.0,
897 ..Default::default()
898 });
899 let internal_width = 1;
904 let internal_height = 1;
905 let depth = create_depth_texture(&device, internal_width, internal_height);
906 let surface_depth = create_depth_texture(&device, config.width, config.height);
907 let scene_a = create_render_target_texture(
908 &device,
909 internal_width,
910 internal_height,
911 config.format,
912 "siglus-scene-a",
913 );
914 let scene_b = create_render_target_texture(
915 &device,
916 internal_width,
917 internal_height,
918 config.format,
919 "siglus-scene-b",
920 );
921 let wipe_a = create_render_target_texture(
922 &device,
923 internal_width,
924 internal_height,
925 config.format,
926 "siglus-wipe-a",
927 );
928 let wipe_b = create_render_target_texture(
929 &device,
930 internal_width,
931 internal_height,
932 config.format,
933 "siglus-wipe-b",
934 );
935 let shadow_map =
936 create_render_target_texture(&device, 1, 1, config.format, "siglus-shadow-map");
937 let shadow_depth = create_depth_texture_with_format(
938 &device,
939 1,
940 1,
941 wgpu::TextureFormat::Depth16Unorm,
942 "siglus-shadow-depth-d16",
943 );
944
945 let surface_viewport = SurfaceViewport::full(config.width, config.height);
946 let emote_compositor = emote::EmoteCompositor::new(&device);
947 Ok(Self {
948 instance,
949 adapter,
950 surface,
951 device,
952 queue,
953 config,
954 wait_display_vsync,
955 logical_width,
956 logical_height,
957 scale_factor: scale_factor.max(1.0),
958 surface_viewport,
959 pipelines: HashMap::new(),
960 bind_group_layout,
961 shader,
962 pipeline_layout,
963 wipe_bind_group_layout,
964 wipe_pipeline,
965 page_wipe_bind_group_layout,
966 page_wipe_pipeline,
967 vertex_buf,
968 vertex_capacity,
969 vertex_sprite2d_buf,
970 vs_uniform_buf,
971 vs_uniform_capacity,
972 vs_uniform_stride,
973 vs_uniform_staging: Vec::new(),
974 zero_bone_uniform_buf,
975 textures: HashMap::new(),
976 external_textures: HashMap::new(),
977 default_aux,
978 mipmap_generator,
979 fog_sampler,
980 mesh_sampler,
981 normal_sampler,
982 toon_sampler,
983 shadow_sampler,
984 depth,
985 surface_depth,
986 scene_a,
987 scene_b,
988 wipe_a,
989 wipe_b,
990 wipe_mask_cache: None,
991 shadow_map,
992 shadow_depth,
993 offscreen_ready: false,
994 shadow_ready: false,
995 sprite2d_verts: Vec::new(),
996 plan: FramePlanner::default(),
997 draw_gpu_slots: Vec::new(),
998 shared_draw_bind_groups: HashMap::new(),
999 draw_bind_epoch: 1,
1000 debug_frame_serial: 0,
1001 emote_compositor,
1002 })
1003 }
1004
1005 fn ensure_offscreen_targets(&mut self) {
1008 if !self.offscreen_ready {
1009 self.offscreen_ready = true;
1010 self.recreate_logical_render_targets();
1011 }
1012 }
1013
1014 fn ensure_shadow_targets(&mut self) {
1016 if self.shadow_ready {
1017 return;
1018 }
1019 self.shadow_ready = true;
1020 self.shadow_map = create_render_target_texture(
1021 &self.device,
1022 2048,
1023 2048,
1024 self.config.format,
1025 "siglus-shadow-map",
1026 );
1027 self.shadow_depth = create_depth_texture_with_format(
1028 &self.device,
1029 2048,
1030 2048,
1031 wgpu::TextureFormat::Depth16Unorm,
1032 "siglus-shadow-depth-d16",
1033 );
1034 self.clear_draw_bindings();
1036 }
1037
1038 fn recreate_logical_render_targets(&mut self) {
1039 let (width, height) = if self.offscreen_ready {
1040 (
1041 self.logical_width.max(1.0).round() as u32,
1042 self.logical_height.max(1.0).round() as u32,
1043 )
1044 } else {
1045 (1, 1)
1046 };
1047 self.depth = create_depth_texture(&self.device, width, height);
1048 self.scene_a = create_render_target_texture(
1049 &self.device,
1050 width,
1051 height,
1052 self.config.format,
1053 "siglus-scene-a",
1054 );
1055 self.scene_b = create_render_target_texture(
1056 &self.device,
1057 width,
1058 height,
1059 self.config.format,
1060 "siglus-scene-b",
1061 );
1062 self.wipe_a = create_render_target_texture(
1063 &self.device,
1064 width,
1065 height,
1066 self.config.format,
1067 "siglus-wipe-a",
1068 );
1069 self.wipe_b = create_render_target_texture(
1070 &self.device,
1071 width,
1072 height,
1073 self.config.format,
1074 "siglus-wipe-b",
1075 );
1076 self.wipe_mask_cache = None;
1077 self.clear_draw_bindings();
1078 }
1079
1080 pub fn set_wait_display_vsync(&mut self, wait_display_vsync: bool) {
1087 if self.wait_display_vsync == wait_display_vsync {
1088 return;
1089 }
1090 self.wait_display_vsync = wait_display_vsync;
1091
1092 let caps = self.surface.get_capabilities(&self.adapter);
1093 let present_mode =
1094 present_mode_for_wait_display_vsync(wait_display_vsync, &caps.present_modes);
1095 if !wait_display_vsync && present_mode != wgpu::PresentMode::Immediate {
1096 if present_mode == wgpu::PresentMode::Fifo {
1097 log::warn!(
1098 "VSync-off requested by Siglus script, but this surface exposes neither Immediate nor Mailbox; falling back to Fifo"
1099 );
1100 } else {
1101 log::warn!(
1102 "VSync-off requested by Siglus script, but Immediate is unavailable; using {:?}",
1103 present_mode
1104 );
1105 }
1106 }
1107
1108 if self.config.present_mode == present_mode {
1109 return;
1110 }
1111 self.config.present_mode = present_mode;
1112 self.surface.configure(&self.device, &self.config);
1113 }
1114
1115 pub fn scale_factor(&self) -> f32 {
1116 self.scale_factor
1117 }
1118
1119 pub fn surface_viewport(&self) -> (u32, u32, u32, u32) {
1120 (
1121 self.surface_viewport.x,
1122 self.surface_viewport.y,
1123 self.surface_viewport.w,
1124 self.surface_viewport.h,
1125 )
1126 }
1127
1128 pub fn resize(&mut self, width: u32, height: u32) {
1129 self.resize_with_scale(width, height, self.scale_factor);
1130 }
1131
1132 pub fn resize_with_scale(&mut self, width: u32, height: u32, scale_factor: f32) {
1133 let sf = Self::valid_scale_factor(scale_factor);
1134 self.resize_targets(width, height, sf, width as f32 / sf, height as f32 / sf);
1135 self.surface_viewport = SurfaceViewport::full(self.config.width, self.config.height);
1136 }
1137
1138 fn valid_scale_factor(scale_factor: f32) -> f32 {
1139 if scale_factor.is_finite() && scale_factor > 0.0 {
1140 scale_factor
1141 } else {
1142 1.0
1143 }
1144 }
1145
1146 fn resize_targets(
1147 &mut self,
1148 width: u32,
1149 height: u32,
1150 scale_factor: f32,
1151 logical_width: f32,
1152 logical_height: f32,
1153 ) {
1154 if width == 0 || height == 0 {
1155 return;
1156 }
1157 let surface_changed = self.config.width != width || self.config.height != height;
1158 let previous_logical_size = self.logical_size();
1159 self.scale_factor = Self::valid_scale_factor(scale_factor);
1160 self.logical_width = logical_width.max(1.0);
1161 self.logical_height = logical_height.max(1.0);
1162 self.config.width = width;
1163 self.config.height = height;
1164 self.surface.configure(&self.device, &self.config);
1166 if surface_changed {
1167 self.surface_depth = create_depth_texture(&self.device, width, height);
1168 }
1169 if previous_logical_size != self.logical_size() {
1170 self.recreate_logical_render_targets();
1171 }
1172 }
1173
1174 pub fn resize_with_logical_viewport(
1175 &mut self,
1176 surface_width: u32,
1177 surface_height: u32,
1178 scale_factor: f32,
1179 logical_width: u32,
1180 logical_height: u32,
1181 viewport_x: u32,
1182 viewport_y: u32,
1183 viewport_width: u32,
1184 viewport_height: u32,
1185 ) {
1186 self.resize_targets(
1187 surface_width,
1188 surface_height,
1189 scale_factor,
1190 logical_width.max(1) as f32,
1191 logical_height.max(1) as f32,
1192 );
1193 let max_w = self.config.width;
1194 let max_h = self.config.height;
1195 let x = viewport_x.min(max_w.saturating_sub(1));
1196 let y = viewport_y.min(max_h.saturating_sub(1));
1197 let w = viewport_width.max(1).min(max_w.saturating_sub(x).max(1));
1198 let h = viewport_height.max(1).min(max_h.saturating_sub(y).max(1));
1199 self.surface_viewport = SurfaceViewport { x, y, w, h };
1200 }
1201
1202 pub fn logical_size(&self) -> (u32, u32) {
1203 (
1204 self.logical_width.max(1.0).round() as u32,
1205 self.logical_height.max(1.0).round() as u32,
1206 )
1207 }
1208
1209 pub fn render_sprites(
1210 &mut self,
1211 images: &ImageManager,
1212 sprites: &[RenderSprite],
1213 ) -> Result<()> {
1214 self.render_frame(images, &RenderFrame::ordinary(sprites.to_vec()))
1215 }
1216
1217 pub fn render_frame(&mut self, images: &ImageManager, frame_plan: &RenderFrame) -> Result<()> {
1218 let frame = match self.surface.get_current_texture() {
1219 Ok(frame) => frame,
1220 Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
1221 self.surface.configure(&self.device, &self.config);
1227 return Ok(());
1228 }
1229 Err(wgpu::SurfaceError::OutOfMemory) => {
1230 anyhow::bail!("surface out of memory");
1231 }
1232 Err(wgpu::SurfaceError::Timeout) => return Ok(()),
1233 Err(err) => return Err(err).context("get_current_texture"),
1234 };
1235 let view = frame
1236 .texture
1237 .create_view(&wgpu::TextureViewDescriptor::default());
1238 self.debug_frame_serial = self.debug_frame_serial.wrapping_add(1);
1239 {
1240 let mut live_emote_ids = HashSet::new();
1241 let mut collect = |sprites: &[RenderSprite]| {
1242 for entry in sprites {
1243 if let Some(packet) = entry.sprite.emote_render.as_deref() {
1244 live_emote_ids.insert(packet.render_id);
1245 }
1246 }
1247 };
1248 if let Some(wipe) = frame_plan.wipe.as_ref() {
1249 collect(&wipe.under);
1250 collect(&wipe.current);
1251 collect(&wipe.next);
1252 collect(&wipe.over);
1253 } else {
1254 collect(&frame_plan.sprites);
1255 }
1256 self.emote_compositor.retain_render_ids(&live_emote_ids);
1257 }
1258
1259 let needs_scene_texture = frame_plan.wipe.is_some()
1267 || frame_plan
1268 .sprites
1269 .iter()
1270 .any(|entry| matches!(entry.sprite.blend, SpriteBlend::Overlay));
1271
1272 if needs_scene_texture {
1273 let final_target = self.render_frame_to_internal(images, frame_plan)?;
1274 let blit_range = self.prepare_blit_vertices()?;
1275 let mut encoder = self
1276 .device
1277 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1278 label: Some("siglus-present-encoder"),
1279 });
1280 self.render_copy_pass(
1281 &mut encoder,
1282 ColorTarget::External(&view),
1283 final_target,
1284 blit_range,
1285 )?;
1286 self.submit(encoder);
1287 } else {
1288 self.render_ordinary_frame_to_surface(images, &frame_plan.sprites, &view)?;
1289 }
1290
1291 frame.present();
1292 Ok(())
1293 }
1294
1295 fn render_ordinary_frame_to_surface(
1296 &mut self,
1297 images: &ImageManager,
1298 sprites: &[RenderSprite],
1299 view: &wgpu::TextureView,
1300 ) -> Result<()> {
1301 let _ = self.prepare_draws(images, sprites, true)?;
1302 let draw_count = self.plan.draws.len();
1303 let mut encoder = self
1304 .device
1305 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1306 label: Some("siglus-direct-present-encoder"),
1307 });
1308
1309 let shadow_indices: Vec<usize> = self
1310 .plan
1311 .draws
1312 .iter()
1313 .enumerate()
1314 .filter_map(|(idx, cmd)| cmd.shadow_cast.then_some(idx))
1315 .collect();
1316 if !shadow_indices.is_empty() {
1317 self.ensure_shadow_targets();
1318 self.render_command_slice(
1319 &mut encoder,
1320 ColorTarget::Internal(InternalColorTarget::ShadowMap),
1321 DepthTarget::Shadow,
1322 0..0,
1323 wgpu::LoadOp::Clear(wgpu::Color::WHITE),
1324 true,
1325 None,
1326 None,
1327 )?;
1328 for idx in shadow_indices {
1329 self.render_command_slice(
1330 &mut encoder,
1331 ColorTarget::Internal(InternalColorTarget::ShadowMap),
1332 DepthTarget::Shadow,
1333 idx..idx + 1,
1334 wgpu::LoadOp::Load,
1335 false,
1336 None,
1337 Some(TechniqueSpecial::Shadow),
1338 )?;
1339 }
1340 }
1341
1342 self.render_command_slice(
1343 &mut encoder,
1344 ColorTarget::External(view),
1345 DepthTarget::Surface,
1346 0..draw_count,
1347 wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1348 true,
1349 None,
1350 None,
1351 )?;
1352 self.submit(encoder);
1353 Ok(())
1354 }
1355
1356 fn prepare_draws(
1357 &mut self,
1358 images: &ImageManager,
1359 sprites: &[RenderSprite],
1360 external_target: bool,
1361 ) -> Result<std::ops::Range<u32>> {
1362 self.plan.verts.clear();
1363 self.plan.draws.clear();
1364 self.plan.draw_bone_uniforms.clear();
1365
1366 let win_w = self.logical_width.max(1.0);
1367 let win_h = self.logical_height.max(1.0);
1368 let (surface_w, surface_h, surface_viewport) = if external_target {
1369 (self.config.width, self.config.height, self.surface_viewport)
1370 } else {
1371 let width = self.logical_width.max(1.0).round() as u32;
1372 let height = self.logical_height.max(1.0).round() as u32;
1373 (width, height, SurfaceViewport::full(width, height))
1374 };
1375
1376 for s in sprites {
1377 if let Some(packet) = s.sprite.emote_render.as_deref() {
1378 self.emote_compositor
1379 .prepare(&self.device, &self.queue, packet)?;
1380 }
1381 }
1382 self.plan.plan(
1383 images,
1384 sprites,
1385 &PlanTarget {
1386 win_w,
1387 win_h,
1388 surface_w,
1389 surface_h,
1390 surface_viewport,
1391 },
1392 )?;
1393
1394 let blit_range = append_fullscreen_blit_vertices(&mut self.plan.verts);
1395
1396 self.upload_prepared_vertices()?;
1397 self.upload_draw_uniforms();
1398
1399 let mut live_image_ids = HashSet::new();
1400 for cmd in &self.plan.draws {
1401 if let Some(ref id) = cmd.image_id {
1402 live_image_ids.insert(id.key());
1403 }
1404 if let Some(ref id) = cmd.mask_image_id {
1405 live_image_ids.insert(id.key());
1406 }
1407 if let Some(ref id) = cmd.tonecurve_image_id {
1408 live_image_ids.insert(id.key());
1409 }
1410 if let Some(ref id) = cmd.fog_image_id {
1411 live_image_ids.insert(id.key());
1412 }
1413 if let Some(ref id) = cmd.wipe_src_image_id {
1414 live_image_ids.insert(id.key());
1415 }
1416 }
1417 for id in live_image_ids {
1418 self.ensure_texture_uploaded(images, id)?;
1419 }
1420 self.organize_textures(images);
1421
1422 self.ensure_draw_pipelines();
1423 self.ensure_pipeline(sprite2d_copy_render_pipeline_key(), "siglus-sprite2d-copy");
1424
1425 Ok(blit_range)
1426 }
1427
1428 fn prepare_blit_vertices(&mut self) -> Result<std::ops::Range<u32>> {
1429 self.plan.verts.clear();
1430 self.plan.draws.clear();
1431 let range = append_fullscreen_blit_vertices(&mut self.plan.verts);
1432 self.upload_prepared_vertices()?;
1433 self.ensure_pipeline(sprite2d_copy_render_pipeline_key(), "siglus-sprite2d-copy");
1434 Ok(range)
1435 }
1436
1437 fn render_frame_to_internal(
1438 &mut self,
1439 images: &ImageManager,
1440 frame: &RenderFrame,
1441 ) -> Result<BackdropTarget> {
1442 self.ensure_offscreen_targets();
1443 if let Some(wipe) = frame.wipe.as_ref() {
1444 let current = self.render_sprite_list_to_scene_pair(images, &wipe.current, None)?;
1445 self.copy_internal_target(backdrop_to_internal(current), InternalColorTarget::WipeA);
1446 let next = self.render_sprite_list_to_scene_pair(images, &wipe.next, None)?;
1447 self.copy_internal_target(backdrop_to_internal(next), InternalColorTarget::WipeB);
1448 let under = self.render_sprite_list_to_scene_pair(images, &wipe.under, None)?;
1449 let composed = self.render_wipe_composite(images, wipe, under)?;
1450 self.render_sprite_list_to_scene_pair(images, &wipe.over, Some(composed))
1451 } else {
1452 self.render_sprite_list_to_scene_pair(images, &frame.sprites, None)
1453 }
1454 }
1455
1456 fn render_sprite_list_to_scene_pair(
1457 &mut self,
1458 images: &ImageManager,
1459 sprites: &[RenderSprite],
1460 initial: Option<BackdropTarget>,
1461 ) -> Result<BackdropTarget> {
1462 let blit_range = self.prepare_draws(images, sprites, false)?;
1463 let draw_count = self.plan.draws.len();
1464 let mut encoder = self
1465 .device
1466 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1467 label: Some("siglus-offscreen-scene-encoder"),
1468 });
1469
1470 let shadow_indices: Vec<usize> = self
1471 .plan
1472 .draws
1473 .iter()
1474 .enumerate()
1475 .filter_map(|(idx, cmd)| cmd.shadow_cast.then_some(idx))
1476 .collect();
1477 if !shadow_indices.is_empty() {
1478 self.ensure_shadow_targets();
1479 self.render_command_slice(
1480 &mut encoder,
1481 ColorTarget::Internal(InternalColorTarget::ShadowMap),
1482 DepthTarget::Shadow,
1483 0..0,
1484 wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1485 true,
1486 None,
1487 None,
1488 )?;
1489 for idx in shadow_indices {
1490 self.render_command_slice(
1491 &mut encoder,
1492 ColorTarget::Internal(InternalColorTarget::ShadowMap),
1493 DepthTarget::Shadow,
1494 idx..idx + 1,
1495 wgpu::LoadOp::Load,
1496 false,
1497 None,
1498 Some(TechniqueSpecial::Shadow),
1499 )?;
1500 }
1501 }
1502
1503 let mut current = initial.unwrap_or(BackdropTarget::SceneA);
1504 let initial_color_load = if initial.is_some() {
1505 wgpu::LoadOp::Load
1506 } else {
1507 wgpu::LoadOp::Clear(wgpu::Color::BLACK)
1508 };
1509 self.render_command_slice(
1510 &mut encoder,
1511 ColorTarget::Internal(backdrop_to_internal(current)),
1512 DepthTarget::Main,
1513 0..0,
1514 initial_color_load,
1515 true,
1516 None,
1517 None,
1518 )?;
1519
1520 let mut index = 0usize;
1521 while index < draw_count {
1522 let is_overlay = matches!(
1523 self.plan.draws[index].pipeline_key.technique.special,
1524 TechniqueSpecial::Overlay
1525 );
1526 let start = index;
1527 while index < draw_count
1528 && matches!(
1529 self.plan.draws[index].pipeline_key.technique.special,
1530 TechniqueSpecial::Overlay
1531 ) == is_overlay
1532 {
1533 index += 1;
1534 }
1535 if is_overlay {
1536 let dst = opposite_backdrop(current);
1537 self.render_copy_pass(
1538 &mut encoder,
1539 ColorTarget::Internal(backdrop_to_internal(dst)),
1540 current,
1541 blit_range.clone(),
1542 )?;
1543 self.render_command_slice(
1544 &mut encoder,
1545 ColorTarget::Internal(backdrop_to_internal(dst)),
1546 DepthTarget::Main,
1547 start..index,
1548 wgpu::LoadOp::Load,
1549 false,
1550 Some(current),
1551 None,
1552 )?;
1553 current = dst;
1554 } else {
1555 self.render_command_slice(
1556 &mut encoder,
1557 ColorTarget::Internal(backdrop_to_internal(current)),
1558 DepthTarget::Main,
1559 start..index,
1560 wgpu::LoadOp::Load,
1561 false,
1562 None,
1563 None,
1564 )?;
1565 }
1566 }
1567 self.submit(encoder);
1568 Ok(current)
1569 }
1570
1571 fn copy_internal_target(&self, src: InternalColorTarget, dst: InternalColorTarget) {
1572 let src_tex = self.internal_target_ref(src);
1573 let dst_tex = self.internal_target_ref(dst);
1574 let width = src_tex.width.min(dst_tex.width);
1575 let height = src_tex.height.min(dst_tex.height);
1576 let mut encoder = self
1577 .device
1578 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1579 label: Some("siglus-copy-internal-target"),
1580 });
1581 encoder.copy_texture_to_texture(
1582 wgpu::ImageCopyTexture {
1583 texture: &src_tex._tex,
1584 mip_level: 0,
1585 origin: wgpu::Origin3d::ZERO,
1586 aspect: wgpu::TextureAspect::All,
1587 },
1588 wgpu::ImageCopyTexture {
1589 texture: &dst_tex._tex,
1590 mip_level: 0,
1591 origin: wgpu::Origin3d::ZERO,
1592 aspect: wgpu::TextureAspect::All,
1593 },
1594 wgpu::Extent3d {
1595 width,
1596 height,
1597 depth_or_array_layers: 1,
1598 },
1599 );
1600 self.submit(encoder);
1601 }
1602
1603 fn ensure_generated_wipe_mask(&mut self, wipe: &WipeRenderPlan) -> Result<()> {
1604 let width = self.logical_width.max(1.0).round() as u32;
1605 let height = self.logical_height.max(1.0).round() as u32;
1606 let key = WipeMaskCacheKey {
1607 wipe_type: wipe.wipe_type,
1608 option: wipe.option.clone(),
1609 width,
1610 height,
1611 seed: wipe.random_seed,
1612 };
1613 if self
1614 .wipe_mask_cache
1615 .as_ref()
1616 .is_some_and(|(cached, _)| cached == &key)
1617 {
1618 return Ok(());
1619 }
1620 let Some(image) = generated_wipe_mask(wipe, width, height) else {
1621 self.wipe_mask_cache = None;
1622 return Ok(());
1623 };
1624 let texture = create_gpu_texture(
1625 &self.device,
1626 &self.queue,
1627 &self.mipmap_generator,
1628 "siglus-generated-wipe-mask",
1629 &image,
1630 wipe.random_seed as u64,
1631 )?;
1632 self.wipe_mask_cache = Some((key, texture));
1633 Ok(())
1634 }
1635
1636 fn render_wipe_composite(
1637 &mut self,
1638 images: &ImageManager,
1639 wipe: &WipeRenderPlan,
1640 under: BackdropTarget,
1641 ) -> Result<BackdropTarget> {
1642 if matches!(wipe.wipe_type, 300 | 301) {
1643 return self.render_page_wipe(wipe, under);
1644 }
1645
1646 if let Some(ref id) = wipe.mask_image_id {
1647 self.ensure_texture_uploaded(images, id.key())?;
1648 } else {
1649 self.ensure_generated_wipe_mask(wipe)?;
1650 }
1651
1652 let uniform = wipe_uniform(
1653 wipe,
1654 self.logical_width.max(1.0),
1655 self.logical_height.max(1.0),
1656 );
1657 let uniform_buffer = self
1658 .device
1659 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1660 label: Some("siglus-wipe-uniform"),
1661 contents: bytemuck::bytes_of(&uniform),
1662 usage: wgpu::BufferUsages::UNIFORM,
1663 });
1664
1665 let under_texture = self.backdrop_target_ref(under);
1666 let current_texture = &self.wipe_a;
1667 let next_texture = &self.wipe_b;
1668 let external_mask = wipe
1669 .mask_image_id
1670 .as_ref()
1671 .and_then(|id| self.textures.get(&id.key()));
1672 let generated_mask = self.wipe_mask_cache.as_ref().map(|(_, texture)| texture);
1673 let mask_texture = external_mask
1674 .or(generated_mask)
1675 .unwrap_or(&self.default_aux);
1676 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1677 label: Some("siglus-wipe-bind-group"),
1678 layout: &self.wipe_bind_group_layout,
1679 entries: &[
1680 wgpu::BindGroupEntry {
1681 binding: 0,
1682 resource: wgpu::BindingResource::TextureView(&under_texture.view),
1683 },
1684 wgpu::BindGroupEntry {
1685 binding: 1,
1686 resource: wgpu::BindingResource::Sampler(&under_texture.sampler),
1687 },
1688 wgpu::BindGroupEntry {
1689 binding: 2,
1690 resource: wgpu::BindingResource::TextureView(¤t_texture.view),
1691 },
1692 wgpu::BindGroupEntry {
1693 binding: 3,
1694 resource: wgpu::BindingResource::Sampler(¤t_texture.sampler),
1695 },
1696 wgpu::BindGroupEntry {
1697 binding: 4,
1698 resource: wgpu::BindingResource::TextureView(&next_texture.view),
1699 },
1700 wgpu::BindGroupEntry {
1701 binding: 5,
1702 resource: wgpu::BindingResource::Sampler(&next_texture.sampler),
1703 },
1704 wgpu::BindGroupEntry {
1705 binding: 6,
1706 resource: wgpu::BindingResource::TextureView(&mask_texture.view),
1707 },
1708 wgpu::BindGroupEntry {
1709 binding: 7,
1710 resource: wgpu::BindingResource::Sampler(&mask_texture.sampler),
1711 },
1712 wgpu::BindGroupEntry {
1713 binding: 8,
1714 resource: uniform_buffer.as_entire_binding(),
1715 },
1716 ],
1717 });
1718
1719 let target = opposite_backdrop(under);
1720 let target_view = &self.backdrop_target_ref(target).view;
1721 let viewport = SurfaceViewport::full(
1722 self.logical_width.max(1.0).round() as u32,
1723 self.logical_height.max(1.0).round() as u32,
1724 );
1725 let mut encoder = self
1726 .device
1727 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1728 label: Some("siglus-wipe-composite-encoder"),
1729 });
1730 {
1731 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1732 label: Some("siglus-wipe-composite-pass"),
1733 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1734 view: target_view,
1735 resolve_target: None,
1736 ops: wgpu::Operations {
1737 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1738 store: wgpu::StoreOp::Store,
1739 },
1740 })],
1741 depth_stencil_attachment: None,
1742 timestamp_writes: None,
1743 occlusion_query_set: None,
1744 });
1745 pass.set_viewport(
1746 viewport.x as f32,
1747 viewport.y as f32,
1748 viewport.w as f32,
1749 viewport.h as f32,
1750 0.0,
1751 1.0,
1752 );
1753 pass.set_pipeline(&self.wipe_pipeline);
1754 pass.set_bind_group(0, &bind_group, &[]);
1755 pass.draw(0..3, 0..1);
1756 }
1757 self.submit(encoder);
1758 Ok(target)
1759 }
1760
1761 fn render_page_wipe(
1762 &mut self,
1763 wipe: &WipeRenderPlan,
1764 under: BackdropTarget,
1765 ) -> Result<BackdropTarget> {
1766 let target = opposite_backdrop(under);
1770 self.copy_internal_target(backdrop_to_internal(under), backdrop_to_internal(target));
1771 self.render_page_wipe_geometry(wipe, target)?;
1772 Ok(target)
1773 }
1774
1775 fn render_page_wipe_geometry(
1776 &mut self,
1777 wipe: &WipeRenderPlan,
1778 target: BackdropTarget,
1779 ) -> Result<()> {
1780 let draws = build_page_wipe_draws(
1781 wipe,
1782 self.logical_width.max(1.0),
1783 self.logical_height.max(1.0),
1784 );
1785 if draws.is_empty() {
1786 return Ok(());
1787 }
1788
1789 let mut buffers = Vec::with_capacity(draws.len());
1790 let mut bind_groups = Vec::with_capacity(draws.len());
1791 for draw in &draws {
1792 let buffer = self
1793 .device
1794 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1795 label: Some("siglus-page-wipe-vertex-buffer"),
1796 contents: bytemuck::cast_slice(&draw.vertices),
1797 usage: wgpu::BufferUsages::VERTEX,
1798 });
1799 let texture = if draw.use_current {
1800 &self.wipe_a
1801 } else {
1802 &self.wipe_b
1803 };
1804 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
1805 label: Some("siglus-page-wipe-bind-group"),
1806 layout: &self.page_wipe_bind_group_layout,
1807 entries: &[
1808 wgpu::BindGroupEntry {
1809 binding: 0,
1810 resource: wgpu::BindingResource::TextureView(&texture.view),
1811 },
1812 wgpu::BindGroupEntry {
1813 binding: 1,
1814 resource: wgpu::BindingResource::Sampler(&texture.sampler),
1815 },
1816 ],
1817 });
1818 buffers.push(buffer);
1819 bind_groups.push(bind_group);
1820 }
1821
1822 let target_view = &self.backdrop_target_ref(target).view;
1823 let viewport = SurfaceViewport::full(
1824 self.logical_width.max(1.0).round() as u32,
1825 self.logical_height.max(1.0).round() as u32,
1826 );
1827 let mut encoder = self
1828 .device
1829 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1830 label: Some("siglus-page-wipe-encoder"),
1831 });
1832 {
1833 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1834 label: Some("siglus-page-wipe-pass"),
1835 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1836 view: target_view,
1837 resolve_target: None,
1838 ops: wgpu::Operations {
1839 load: wgpu::LoadOp::Load,
1840 store: wgpu::StoreOp::Store,
1841 },
1842 })],
1843 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1844 view: &self.depth.view,
1845 depth_ops: Some(wgpu::Operations {
1846 load: wgpu::LoadOp::Clear(1.0),
1847 store: wgpu::StoreOp::Store,
1848 }),
1849 stencil_ops: None,
1850 }),
1851 timestamp_writes: None,
1852 occlusion_query_set: None,
1853 });
1854 pass.set_viewport(
1855 viewport.x as f32,
1856 viewport.y as f32,
1857 viewport.w as f32,
1858 viewport.h as f32,
1859 0.0,
1860 1.0,
1861 );
1862 pass.set_pipeline(&self.page_wipe_pipeline);
1863 for (index, draw) in draws.iter().enumerate() {
1864 pass.set_bind_group(0, &bind_groups[index], &[]);
1865 pass.set_vertex_buffer(0, buffers[index].slice(..));
1866 pass.draw(0..draw.vertices.len() as u32, 0..1);
1867 }
1868 }
1869 self.submit(encoder);
1870 Ok(())
1871 }
1872
1873 fn debug_collect_render_chain_textures(
1874 &self,
1875 ) -> HashMap<RendererDebugTextureKey, PendingRendererDebugTexture> {
1876 let mut pending: HashMap<RendererDebugTextureKey, PendingRendererDebugTexture> =
1877 HashMap::new();
1878
1879 for (draw_idx, cmd) in self.plan.draws.iter().enumerate() {
1880 let role_prefix = format!("draw[{draw_idx}]");
1881 self.debug_add_base_texture_usage(&mut pending, cmd, &format!("{role_prefix}.base"));
1882 self.debug_add_image_texture_usage(
1883 &mut pending,
1884 cmd.mask_image_id.as_ref(),
1885 "image",
1886 &format!("{role_prefix}.mask"),
1887 );
1888 self.debug_add_image_texture_usage(
1889 &mut pending,
1890 cmd.tonecurve_image_id.as_ref(),
1891 "image",
1892 &format!("{role_prefix}.tonecurve"),
1893 );
1894 self.debug_add_image_texture_usage(
1895 &mut pending,
1896 cmd.fog_image_id.as_ref(),
1897 "image",
1898 &format!("{role_prefix}.fog"),
1899 );
1900 self.debug_add_aux_texture_usage(&mut pending, cmd, &format!("{role_prefix}.aux"));
1901 self.debug_add_external_texture_usage(
1902 &mut pending,
1903 cmd.mesh_normal_texture_path.as_deref(),
1904 "external",
1905 &format!("{role_prefix}.normal"),
1906 );
1907 self.debug_add_external_texture_usage(
1908 &mut pending,
1909 cmd.mesh_toon_texture_path.as_deref(),
1910 "external",
1911 &format!("{role_prefix}.toon"),
1912 );
1913 if cmd.pipeline_key.use_depth
1914 || cmd.shadow_cast
1915 || cmd.mesh_material_key.as_ref().is_some_and(|k| k.shadow)
1916 {
1917 self.debug_add_render_target_usage(
1918 &mut pending,
1919 RendererDebugRenderTarget::ShadowMap,
1920 &format!("{role_prefix}.shadow"),
1921 );
1922 }
1923 }
1924
1925 if self.plan.draws.iter().any(|cmd| {
1926 matches!(
1927 cmd.pipeline_key.technique.special,
1928 TechniqueSpecial::Overlay
1929 )
1930 }) {
1931 self.debug_add_render_target_usage(
1932 &mut pending,
1933 RendererDebugRenderTarget::SceneA,
1934 "overlay.backdrop.scene_a",
1935 );
1936 self.debug_add_render_target_usage(
1937 &mut pending,
1938 RendererDebugRenderTarget::SceneB,
1939 "overlay.backdrop.scene_b",
1940 );
1941 }
1942 if self.plan.draws.is_empty() {
1943 self.debug_add_default_aux_usage(&mut pending, "empty-frame.default_aux");
1944 }
1945
1946 pending
1947 }
1948
1949 pub fn debug_render_chain_texture_metadata(&self) -> Vec<RendererDebugTexture> {
1953 let pending = self.debug_collect_render_chain_textures();
1954 let mut items = Vec::with_capacity(pending.len());
1955 for (key, meta) in pending.into_iter() {
1956 items.push((
1957 meta.order,
1958 RendererDebugTexture {
1959 key: Self::debug_texture_key_string(&key),
1960 kind: meta.kind,
1961 label: meta.label,
1962 usage: meta.usage.join("; "),
1963 usage_count: meta.usage.len(),
1964 width: meta.width,
1965 height: meta.height,
1966 version: meta.version,
1967 rgba: Vec::new(),
1968 },
1969 ));
1970 }
1971 items.sort_by_key(|(order, _)| *order);
1972 items.into_iter().map(|(_, item)| item).collect()
1973 }
1974
1975 pub fn debug_read_render_chain_textures(&self) -> Result<Vec<RendererDebugTexture>> {
1978 let pending = self.debug_collect_render_chain_textures();
1979 let mut items = Vec::with_capacity(pending.len());
1980 for (key, meta) in pending.into_iter() {
1981 let Some((width, height, version, rgba)) = self.debug_read_texture_by_key(&key)? else {
1982 continue;
1983 };
1984 items.push((
1985 meta.order,
1986 RendererDebugTexture {
1987 key: Self::debug_texture_key_string(&key),
1988 kind: meta.kind,
1989 label: meta.label,
1990 usage: meta.usage.join("; "),
1991 usage_count: meta.usage.len(),
1992 width,
1993 height,
1994 version,
1995 rgba,
1996 },
1997 ));
1998 }
1999 items.sort_by_key(|(order, _)| *order);
2000 Ok(items.into_iter().map(|(_, item)| item).collect())
2001 }
2002
2003 fn debug_add_pending_texture_usage(
2004 &self,
2005 pending: &mut HashMap<RendererDebugTextureKey, PendingRendererDebugTexture>,
2006 key: RendererDebugTextureKey,
2007 kind: &str,
2008 label: String,
2009 width: u32,
2010 height: u32,
2011 version: u64,
2012 usage: &str,
2013 ) {
2014 let order = pending.len();
2015 let entry = pending
2016 .entry(key)
2017 .or_insert_with(|| PendingRendererDebugTexture {
2018 order,
2019 kind: kind.to_string(),
2020 label,
2021 usage: Vec::new(),
2022 width,
2023 height,
2024 version,
2025 });
2026 if !entry.usage.iter().any(|s| s == usage) {
2027 entry.usage.push(usage.to_string());
2028 }
2029 }
2030
2031 fn debug_add_default_aux_usage(
2032 &self,
2033 pending: &mut HashMap<RendererDebugTextureKey, PendingRendererDebugTexture>,
2034 usage: &str,
2035 ) {
2036 self.debug_add_pending_texture_usage(
2037 pending,
2038 RendererDebugTextureKey::DefaultAux,
2039 "default",
2040 "default_aux".to_string(),
2041 self.default_aux.width,
2042 self.default_aux.height,
2043 self.default_aux.version,
2044 usage,
2045 );
2046 }
2047
2048 fn debug_add_image_texture_usage(
2049 &self,
2050 pending: &mut HashMap<RendererDebugTextureKey, PendingRendererDebugTexture>,
2051 image_id: Option<&ImageHandle>,
2052 kind: &str,
2053 usage: &str,
2054 ) {
2055 if let Some(id) = image_id
2056 && let Some(tex) = self.textures.get(&id.key())
2057 {
2058 self.debug_add_pending_texture_usage(
2059 pending,
2060 RendererDebugTextureKey::Image(id.key()),
2061 kind,
2062 format!("ImageHandle({})", id.index()),
2063 tex.width,
2064 tex.height,
2065 tex.version,
2066 usage,
2067 );
2068 return;
2069 }
2070 self.debug_add_default_aux_usage(pending, usage);
2071 }
2072
2073 fn debug_add_external_texture_usage(
2074 &self,
2075 pending: &mut HashMap<RendererDebugTextureKey, PendingRendererDebugTexture>,
2076 path: Option<&Path>,
2077 kind: &str,
2078 usage: &str,
2079 ) {
2080 if let Some(path) = path
2081 && let Some(tex) = self.external_textures.get(path)
2082 {
2083 self.debug_add_pending_texture_usage(
2084 pending,
2085 RendererDebugTextureKey::External(path.to_path_buf()),
2086 kind,
2087 path.display().to_string(),
2088 tex.width,
2089 tex.height,
2090 tex.version,
2091 usage,
2092 );
2093 return;
2094 }
2095 self.debug_add_default_aux_usage(pending, usage);
2096 }
2097
2098 fn debug_add_render_target_usage(
2099 &self,
2100 pending: &mut HashMap<RendererDebugTextureKey, PendingRendererDebugTexture>,
2101 target: RendererDebugRenderTarget,
2102 usage: &str,
2103 ) {
2104 let rt = self.debug_render_target_ref(target);
2105 self.debug_add_pending_texture_usage(
2106 pending,
2107 RendererDebugTextureKey::RenderTarget(target),
2108 "render-target",
2109 match target {
2110 RendererDebugRenderTarget::SceneA => "scene_a".to_string(),
2111 RendererDebugRenderTarget::SceneB => "scene_b".to_string(),
2112 RendererDebugRenderTarget::ShadowMap => "shadow_map".to_string(),
2113 },
2114 rt.width,
2115 rt.height,
2116 self.debug_frame_serial,
2117 usage,
2118 );
2119 }
2120
2121 fn debug_add_base_texture_usage(
2122 &self,
2123 pending: &mut HashMap<RendererDebugTextureKey, PendingRendererDebugTexture>,
2124 cmd: &DrawCommand,
2125 usage: &str,
2126 ) {
2127 if let Some(path) = cmd.mesh_texture_path.as_deref()
2128 && let Some(tex) = self.external_textures.get(path)
2129 {
2130 self.debug_add_pending_texture_usage(
2131 pending,
2132 RendererDebugTextureKey::External(path.to_path_buf()),
2133 "external",
2134 path.display().to_string(),
2135 tex.width,
2136 tex.height,
2137 tex.version,
2138 usage,
2139 );
2140 return;
2141 }
2142 self.debug_add_image_texture_usage(pending, cmd.image_id.as_ref(), "image", usage);
2143 }
2144
2145 fn debug_add_aux_texture_usage(
2146 &self,
2147 pending: &mut HashMap<RendererDebugTextureKey, PendingRendererDebugTexture>,
2148 cmd: &DrawCommand,
2149 usage: &str,
2150 ) {
2151 if matches!(
2152 cmd.pipeline_key.technique.special,
2153 TechniqueSpecial::Overlay
2154 ) {
2155 self.debug_add_render_target_usage(pending, RendererDebugRenderTarget::SceneA, usage);
2156 self.debug_add_render_target_usage(pending, RendererDebugRenderTarget::SceneB, usage);
2157 return;
2158 }
2159 self.debug_add_image_texture_usage(pending, cmd.wipe_src_image_id.as_ref(), "image", usage);
2160 }
2161
2162 fn debug_render_target_ref(&self, target: RendererDebugRenderTarget) -> &RenderTargetTexture {
2163 match target {
2164 RendererDebugRenderTarget::SceneA => &self.scene_a,
2165 RendererDebugRenderTarget::SceneB => &self.scene_b,
2166 RendererDebugRenderTarget::ShadowMap => &self.shadow_map,
2167 }
2168 }
2169
2170 fn debug_texture_key_string(key: &RendererDebugTextureKey) -> String {
2171 match key {
2172 RendererDebugTextureKey::DefaultAux => "default_aux".to_string(),
2173 RendererDebugTextureKey::Image(id) => format!("image:{id}"),
2174 RendererDebugTextureKey::External(path) => format!("external:{}", path.display()),
2175 RendererDebugTextureKey::RenderTarget(RendererDebugRenderTarget::SceneA) => {
2176 "render-target:scene_a".to_string()
2177 }
2178 RendererDebugTextureKey::RenderTarget(RendererDebugRenderTarget::SceneB) => {
2179 "render-target:scene_b".to_string()
2180 }
2181 RendererDebugTextureKey::RenderTarget(RendererDebugRenderTarget::ShadowMap) => {
2182 "render-target:shadow_map".to_string()
2183 }
2184 }
2185 }
2186
2187 fn debug_read_texture_by_key(
2188 &self,
2189 key: &RendererDebugTextureKey,
2190 ) -> Result<Option<DebugTextureReadback>> {
2191 match key {
2192 RendererDebugTextureKey::DefaultAux => Ok(Some((
2193 self.default_aux.width,
2194 self.default_aux.height,
2195 self.default_aux.version,
2196 self.debug_read_texture_rgba(
2197 &self.default_aux._tex,
2198 self.default_aux.width,
2199 self.default_aux.height,
2200 wgpu::TextureFormat::Rgba8Unorm,
2201 )?,
2202 ))),
2203 RendererDebugTextureKey::Image(id) => {
2204 let Some(tex) = self.textures.get(id) else {
2205 return Ok(None);
2206 };
2207 Ok(Some((
2208 tex.width,
2209 tex.height,
2210 tex.version,
2211 self.debug_read_texture_rgba(
2212 &tex._tex,
2213 tex.width,
2214 tex.height,
2215 wgpu::TextureFormat::Rgba8Unorm,
2216 )?,
2217 )))
2218 }
2219 RendererDebugTextureKey::External(path) => {
2220 let Some(tex) = self.external_textures.get(path) else {
2221 return Ok(None);
2222 };
2223 Ok(Some((
2224 tex.width,
2225 tex.height,
2226 tex.version,
2227 self.debug_read_texture_rgba(
2228 &tex._tex,
2229 tex.width,
2230 tex.height,
2231 wgpu::TextureFormat::Rgba8Unorm,
2232 )?,
2233 )))
2234 }
2235 RendererDebugTextureKey::RenderTarget(target) => {
2236 let rt = self.debug_render_target_ref(*target);
2237 Ok(Some((
2238 rt.width,
2239 rt.height,
2240 self.debug_frame_serial,
2241 self.debug_read_texture_rgba(&rt._tex, rt.width, rt.height, rt.format)?,
2242 )))
2243 }
2244 }
2245 }
2246
2247 fn debug_read_texture_rgba(
2248 &self,
2249 texture: &wgpu::Texture,
2250 width: u32,
2251 height: u32,
2252 format: wgpu::TextureFormat,
2253 ) -> Result<Vec<u8>> {
2254 if width == 0 || height == 0 {
2255 return Ok(Vec::new());
2256 }
2257 let bytes_per_pixel = 4u32;
2258 let unpadded_bytes_per_row = width.saturating_mul(bytes_per_pixel);
2259 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
2260 let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
2261 let output_buffer_size = padded_bytes_per_row as u64 * height as u64;
2262 let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
2263 label: Some("siglus-debug-texture-readback"),
2264 size: output_buffer_size,
2265 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2266 mapped_at_creation: false,
2267 });
2268 let mut encoder = self
2269 .device
2270 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2271 label: Some("siglus-debug-texture-readback-encoder"),
2272 });
2273 encoder.copy_texture_to_buffer(
2274 wgpu::ImageCopyTexture {
2275 texture,
2276 mip_level: 0,
2277 origin: wgpu::Origin3d::ZERO,
2278 aspect: wgpu::TextureAspect::All,
2279 },
2280 wgpu::ImageCopyBuffer {
2281 buffer: &output_buffer,
2282 layout: wgpu::ImageDataLayout {
2283 offset: 0,
2284 bytes_per_row: Some(padded_bytes_per_row),
2285 rows_per_image: Some(height),
2286 },
2287 },
2288 wgpu::Extent3d {
2289 width,
2290 height,
2291 depth_or_array_layers: 1,
2292 },
2293 );
2294 self.submit(encoder);
2295
2296 let buffer_slice = output_buffer.slice(..);
2297 let (tx, rx) = std::sync::mpsc::channel();
2298 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
2299 let _ = tx.send(result);
2300 });
2301 self.device.poll(wgpu::Maintain::Wait);
2302 rx.recv()
2303 .context("wait for debug texture readback")?
2304 .context("map debug texture readback")?;
2305 let data = buffer_slice.get_mapped_range();
2306 let mut rgba = vec![0u8; (width as usize) * (height as usize) * 4];
2307 for y in 0..height as usize {
2308 let src_offset = y * padded_bytes_per_row as usize;
2309 let dst_offset = y * unpadded_bytes_per_row as usize;
2310 let src = &data[src_offset..src_offset + unpadded_bytes_per_row as usize];
2311 let dst = &mut rgba[dst_offset..dst_offset + unpadded_bytes_per_row as usize];
2312 match format {
2313 wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb => {
2314 for (src_px, dst_px) in src
2315 .as_chunks::<4>()
2316 .0
2317 .iter()
2318 .zip(dst.as_chunks_mut::<4>().0.iter_mut())
2319 {
2320 dst_px[0] = src_px[2];
2321 dst_px[1] = src_px[1];
2322 dst_px[2] = src_px[0];
2323 dst_px[3] = src_px[3];
2324 }
2325 }
2326 wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba8UnormSrgb => {
2327 dst.copy_from_slice(src);
2328 }
2329 other => {
2330 anyhow::bail!("unsupported debug texture readback format: {other:?}");
2331 }
2332 }
2333 }
2334 drop(data);
2335 output_buffer.unmap();
2336 Ok(rgba)
2337 }
2338
2339 fn ensure_external_texture(&mut self, path: &Path) -> Option<()> {
2340 if self.external_textures.contains_key(path) {
2341 return Some(());
2342 }
2343 let img = load_image_any(path, 0).ok()?;
2344 let tex = create_gpu_texture(
2345 &self.device,
2346 &self.queue,
2347 &self.mipmap_generator,
2348 &format!("siglus-external-texture-{}", self.external_textures.len()),
2349 &img,
2350 0,
2351 )
2352 .ok()?;
2353 self.external_textures.insert(path.to_path_buf(), tex);
2354 Some(())
2355 }
2356
2357 fn ensure_draw_pipelines(&mut self) {
2358 for draw_idx in 0..self.plan.draws.len() {
2359 let (pipeline_key, pipeline_label, shadow) = {
2360 let cmd = &self.plan.draws[draw_idx];
2361 let pipeline_key = cmd.pipeline_key.render_pipeline_key();
2362 let pipeline_label = (!self.pipelines.contains_key(&pipeline_key))
2363 .then(|| format!("siglus-{}", technique_name_for_pipeline(&cmd.pipeline_key)));
2364 let shadow = cmd.shadow_cast.then(|| {
2365 let key = cmd.pipeline_key.shadow_render_pipeline_key();
2366 let label = (!self.pipelines.contains_key(&key)).then(|| {
2367 cmd.shadow_pipeline_name.as_deref().map_or_else(
2368 || format!("siglus-{}", key.program.short_name()),
2369 |name| format!("siglus-{name}#{}", key.program.short_name()),
2370 )
2371 });
2372 (key, label)
2373 });
2374 (pipeline_key, pipeline_label, shadow)
2375 };
2376
2377 if let Some(label) = pipeline_label {
2378 self.ensure_pipeline(pipeline_key, &label);
2379 }
2380 if let Some((shadow_key, Some(label))) = shadow {
2381 self.ensure_pipeline(shadow_key, &label);
2382 }
2383 }
2384 }
2385
2386 fn ensure_pipeline(&mut self, key: RenderPipelineKey, label: &str) {
2387 if self.pipelines.contains_key(&key) {
2388 return;
2389 }
2390 let blend_state = if let Some(blend) = key.blend {
2391 Some(match blend {
2392 SpriteBlend::Normal => wgpu::BlendState {
2393 color: wgpu::BlendComponent {
2394 src_factor: wgpu::BlendFactor::SrcAlpha,
2395 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
2396 operation: wgpu::BlendOperation::Add,
2397 },
2398 alpha: wgpu::BlendComponent {
2399 src_factor: wgpu::BlendFactor::One,
2400 dst_factor: wgpu::BlendFactor::One,
2401 operation: wgpu::BlendOperation::Add,
2402 },
2403 },
2404 SpriteBlend::Add => wgpu::BlendState {
2405 color: wgpu::BlendComponent {
2406 src_factor: wgpu::BlendFactor::SrcAlpha,
2407 dst_factor: wgpu::BlendFactor::One,
2408 operation: wgpu::BlendOperation::Add,
2409 },
2410 alpha: wgpu::BlendComponent {
2411 src_factor: wgpu::BlendFactor::One,
2412 dst_factor: wgpu::BlendFactor::One,
2413 operation: wgpu::BlendOperation::Add,
2414 },
2415 },
2416 SpriteBlend::Sub => wgpu::BlendState {
2417 color: wgpu::BlendComponent {
2418 src_factor: wgpu::BlendFactor::SrcAlpha,
2419 dst_factor: wgpu::BlendFactor::One,
2420 operation: wgpu::BlendOperation::ReverseSubtract,
2421 },
2422 alpha: wgpu::BlendComponent {
2423 src_factor: wgpu::BlendFactor::One,
2424 dst_factor: wgpu::BlendFactor::One,
2425 operation: wgpu::BlendOperation::Add,
2426 },
2427 },
2428 SpriteBlend::Mul => wgpu::BlendState {
2429 color: wgpu::BlendComponent {
2430 src_factor: wgpu::BlendFactor::Zero,
2431 dst_factor: wgpu::BlendFactor::Src,
2432 operation: wgpu::BlendOperation::Add,
2433 },
2434 alpha: wgpu::BlendComponent {
2435 src_factor: wgpu::BlendFactor::One,
2436 dst_factor: wgpu::BlendFactor::One,
2437 operation: wgpu::BlendOperation::Add,
2438 },
2439 },
2440 SpriteBlend::Screen => wgpu::BlendState {
2441 color: wgpu::BlendComponent {
2442 src_factor: wgpu::BlendFactor::One,
2443 dst_factor: wgpu::BlendFactor::OneMinusSrc,
2444 operation: wgpu::BlendOperation::Add,
2445 },
2446 alpha: wgpu::BlendComponent {
2447 src_factor: wgpu::BlendFactor::One,
2448 dst_factor: wgpu::BlendFactor::One,
2449 operation: wgpu::BlendOperation::Add,
2450 },
2451 },
2452 SpriteBlend::Overlay => wgpu::BlendState {
2453 color: wgpu::BlendComponent {
2454 src_factor: wgpu::BlendFactor::One,
2455 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
2456 operation: wgpu::BlendOperation::Add,
2457 },
2458 alpha: wgpu::BlendComponent::OVER,
2459 },
2460 })
2461 } else {
2462 None
2463 };
2464
2465 let pipeline = self
2466 .device
2467 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
2468 label: Some(label),
2469 layout: Some(&self.pipeline_layout),
2470 vertex: wgpu::VertexState {
2471 module: &self.shader,
2472 entry_point: key.program.vertex_entry(),
2473 buffers: &[if key.program.uses_sprite2d_layout() {
2474 VertexSprite2d::layout()
2475 } else {
2476 Vertex::layout()
2477 }],
2478 compilation_options: Default::default(),
2479 },
2480 fragment: Some(wgpu::FragmentState {
2481 module: &self.shader,
2482 entry_point: key.program.fragment_entry(),
2483 targets: &[Some(wgpu::ColorTargetState {
2484 format: self.config.format,
2485 blend: blend_state,
2486 write_mask: wgpu::ColorWrites::ALL,
2487 })],
2488 compilation_options: Default::default(),
2489 }),
2490 primitive: wgpu::PrimitiveState {
2491 topology: wgpu::PrimitiveTopology::TriangleList,
2492 strip_index_format: None,
2493 front_face: d3d_front_face(),
2494 cull_mode: if key.cull_back {
2495 Some(wgpu::Face::Back)
2496 } else {
2497 None
2498 },
2499 polygon_mode: wgpu::PolygonMode::Fill,
2500 unclipped_depth: false,
2501 conservative: false,
2502 },
2503 depth_stencil: key.depth.map(|depth| wgpu::DepthStencilState {
2504 format: if matches!(
2505 key.program,
2506 EffectProgram::ShadowStatic | EffectProgram::ShadowSkinned
2507 ) {
2508 wgpu::TextureFormat::Depth16Unorm
2509 } else {
2510 wgpu::TextureFormat::Depth32Float
2511 },
2512 depth_write_enabled: depth.depth_write,
2513 depth_compare: if depth.use_depth {
2514 wgpu::CompareFunction::LessEqual
2515 } else {
2516 wgpu::CompareFunction::Always
2517 },
2518 stencil: wgpu::StencilState::default(),
2519 bias: wgpu::DepthBiasState::default(),
2520 }),
2521 multisample: wgpu::MultisampleState::default(),
2522 multiview: None,
2523 });
2524 self.pipelines.insert(key, pipeline);
2525 }
2526
2527 fn internal_target_ref(&self, target: InternalColorTarget) -> &RenderTargetTexture {
2528 match target {
2529 InternalColorTarget::SceneA => &self.scene_a,
2530 InternalColorTarget::SceneB => &self.scene_b,
2531 InternalColorTarget::WipeA => &self.wipe_a,
2532 InternalColorTarget::WipeB => &self.wipe_b,
2533 InternalColorTarget::ShadowMap => &self.shadow_map,
2534 }
2535 }
2536
2537 fn color_target_view<'a>(&'a self, target: ColorTarget<'a>) -> &'a wgpu::TextureView {
2538 match target {
2539 ColorTarget::External(view) => view,
2540 ColorTarget::Internal(InternalColorTarget::SceneA) => &self.scene_a.view,
2541 ColorTarget::Internal(InternalColorTarget::SceneB) => &self.scene_b.view,
2542 ColorTarget::Internal(InternalColorTarget::WipeA) => &self.wipe_a.view,
2543 ColorTarget::Internal(InternalColorTarget::WipeB) => &self.wipe_b.view,
2544 ColorTarget::Internal(InternalColorTarget::ShadowMap) => &self.shadow_map.view,
2545 }
2546 }
2547
2548 fn depth_target_view(&self, target: DepthTarget) -> Option<&wgpu::TextureView> {
2549 match target {
2550 DepthTarget::None => None,
2551 DepthTarget::Main => Some(&self.depth.view),
2552 DepthTarget::Surface => Some(&self.surface_depth.view),
2553 DepthTarget::Shadow => Some(&self.shadow_depth.view),
2554 }
2555 }
2556
2557 fn backdrop_target_ref(&self, target: BackdropTarget) -> &RenderTargetTexture {
2558 match target {
2559 BackdropTarget::SceneA => &self.scene_a,
2560 BackdropTarget::SceneB => &self.scene_b,
2561 }
2562 }
2563
2564 fn render_command_slice(
2565 &mut self,
2566 encoder: &mut wgpu::CommandEncoder,
2567 color_target: ColorTarget<'_>,
2568 depth_target: DepthTarget,
2569 range: std::ops::Range<usize>,
2570 color_load: wgpu::LoadOp<wgpu::Color>,
2571 clear_depth: bool,
2572 overlay_backdrop: Option<BackdropTarget>,
2573 force_special: Option<TechniqueSpecial>,
2574 ) -> Result<()> {
2575 let external_paths: Vec<PathBuf> = range
2580 .clone()
2581 .flat_map(|idx| {
2582 let cmd = &self.plan.draws[idx];
2583 [
2584 cmd.mesh_texture_path.clone(),
2585 cmd.mesh_normal_texture_path.clone(),
2586 cmd.mesh_toon_texture_path.clone(),
2587 ]
2588 .into_iter()
2589 .flatten()
2590 })
2591 .collect();
2592 for path in external_paths {
2593 let _ = self.ensure_external_texture(&path);
2594 }
2595 for draw_idx in range.clone() {
2596 self.prepare_draw_gpu_slot(draw_idx, overlay_backdrop)?;
2597 }
2598
2599 let viewport = match color_target {
2600 ColorTarget::External(_) => self.surface_viewport,
2601 ColorTarget::Internal(InternalColorTarget::SceneA)
2602 | ColorTarget::Internal(InternalColorTarget::SceneB)
2603 | ColorTarget::Internal(InternalColorTarget::WipeA)
2604 | ColorTarget::Internal(InternalColorTarget::WipeB) => SurfaceViewport::full(
2605 self.logical_width.max(1.0).round() as u32,
2606 self.logical_height.max(1.0).round() as u32,
2607 ),
2608 ColorTarget::Internal(InternalColorTarget::ShadowMap) => {
2609 SurfaceViewport::full(self.shadow_map.width, self.shadow_map.height)
2610 }
2611 };
2612 let color_view = self.color_target_view(color_target);
2613 let depth_view = self.depth_target_view(depth_target);
2614 let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2615 label: Some("siglus-sprite-pass"),
2616 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2617 view: color_view,
2618 resolve_target: None,
2619 ops: wgpu::Operations {
2620 load: color_load,
2621 store: wgpu::StoreOp::Store,
2622 },
2623 })],
2624 depth_stencil_attachment: depth_view.map(|view| {
2625 wgpu::RenderPassDepthStencilAttachment {
2626 view,
2627 depth_ops: Some(wgpu::Operations {
2628 load: if clear_depth {
2629 wgpu::LoadOp::Clear(1.0)
2630 } else {
2631 wgpu::LoadOp::Load
2632 },
2633 store: wgpu::StoreOp::Store,
2634 }),
2635 stencil_ops: None,
2636 }
2637 }),
2638 timestamp_writes: None,
2639 occlusion_query_set: None,
2640 });
2641 rp.set_vertex_buffer(0, self.vertex_buf.slice(..));
2642 rp.set_viewport(
2643 viewport.x as f32,
2644 viewport.y as f32,
2645 viewport.w as f32,
2646 viewport.h as f32,
2647 0.0,
2648 1.0,
2649 );
2650
2651 for draw_idx in range {
2652 let cmd = &self.plan.draws[draw_idx];
2653 let effective_key = if force_special.is_some() {
2654 cmd.pipeline_key.shadow_render_pipeline_key()
2655 } else {
2656 cmd.pipeline_key.render_pipeline_key()
2657 };
2658 if let Some(pipeline) = self.pipelines.get(&effective_key) {
2659 rp.set_pipeline(pipeline);
2660 }
2661 if effective_key.program.uses_sprite2d_layout() {
2662 rp.set_vertex_buffer(0, self.vertex_sprite2d_buf.slice(..));
2663 } else {
2664 rp.set_vertex_buffer(0, self.vertex_buf.slice(..));
2665 }
2666 let bind_group = self.draw_gpu_slots[draw_idx]
2667 .bind_group
2668 .as_ref()
2669 .expect("draw gpu slot prepared before render pass");
2670 let dynamic_offset = draw_idx
2671 .checked_mul(self.vs_uniform_stride)
2672 .and_then(|offset| u32::try_from(offset).ok())
2673 .expect("draw uniform dynamic offset exceeds wgpu u32 range");
2674 rp.set_bind_group(0, bind_group.as_ref(), &[dynamic_offset]);
2675 if let Some(sci) = cmd.scissor {
2676 rp.set_scissor_rect(sci.x, sci.y, sci.w, sci.h);
2677 } else {
2678 rp.set_scissor_rect(viewport.x, viewport.y, viewport.w, viewport.h);
2679 }
2680 rp.draw(cmd.range.clone(), 0..1);
2681 }
2682 Ok(())
2683 }
2684
2685 fn resolve_effect_resources_for_draw<'a>(
2686 &'a self,
2687 cmd: &'a DrawCommand,
2688 overlay_backdrop: Option<&'a RenderTargetTexture>,
2689 ) -> EffectResolvedResources<'a> {
2690 let emote_base = cmd
2691 .emote_render_id
2692 .and_then(|id| self.emote_compositor.texture(id));
2693 let base = if let Some(texture) = emote_base {
2694 texture
2695 } else if let Some(path) = cmd.mesh_texture_path.as_deref() {
2696 self.external_textures
2697 .get(path)
2698 .or_else(|| {
2699 cmd.image_id
2700 .as_ref()
2701 .and_then(|id| self.textures.get(&id.key()))
2702 })
2703 .unwrap_or(&self.default_aux)
2704 } else {
2705 cmd.image_id
2706 .as_ref()
2707 .and_then(|id| self.textures.get(&id.key()))
2708 .unwrap_or(&self.default_aux)
2709 };
2710 let mask = cmd
2711 .mask_image_id
2712 .as_ref()
2713 .and_then(|id| self.textures.get(&id.key()))
2714 .unwrap_or(&self.default_aux);
2715 let tone = cmd
2716 .tonecurve_image_id
2717 .as_ref()
2718 .and_then(|id| self.textures.get(&id.key()))
2719 .unwrap_or(&self.default_aux);
2720 let fog = cmd
2721 .fog_image_id
2722 .as_ref()
2723 .and_then(|id| self.textures.get(&id.key()))
2724 .unwrap_or(&self.default_aux);
2725 let normal = cmd
2726 .mesh_normal_texture_path
2727 .as_deref()
2728 .and_then(|p| self.external_textures.get(p))
2729 .unwrap_or(&self.default_aux);
2730 let toon = cmd
2731 .mesh_toon_texture_path
2732 .as_deref()
2733 .and_then(|p| self.external_textures.get(p))
2734 .unwrap_or(&self.default_aux);
2735 let (aux_view, aux_sampler) = if matches!(
2736 cmd.pipeline_key.technique.special,
2737 TechniqueSpecial::Overlay
2738 ) {
2739 if let Some(backdrop) = overlay_backdrop {
2740 (&backdrop.view, &backdrop.sampler)
2741 } else {
2742 (&self.default_aux.view, &self.default_aux.sampler)
2743 }
2744 } else if let Some(ref id) = cmd.wipe_src_image_id {
2745 if let Some(tex) = self.textures.get(&id.key()) {
2746 (&tex.view, &tex.sampler)
2747 } else {
2748 (&self.default_aux.view, &self.default_aux.sampler)
2749 }
2750 } else {
2751 (&self.default_aux.view, &self.default_aux.sampler)
2752 };
2753 let global_vals = EffectGlobalValPackSemantic {
2754 use_bone_uniform: matches!(
2755 cmd.draw_kind,
2756 MeshDrawKind::SkinnedMesh | MeshDrawKind::ShadowCaster
2757 ) && cmd.mesh_material_key.as_ref().is_some_and(|k| k.skinned),
2758 use_shadow_tex: cmd.pipeline_key.use_depth
2759 || cmd.shadow_cast
2760 || cmd.mesh_material_key.as_ref().is_some_and(|k| k.shadow),
2761 use_normal_tex: cmd
2762 .mesh_material_key
2763 .as_ref()
2764 .is_some_and(|k| k.use_normal_tex),
2765 use_toon_tex: cmd
2766 .mesh_material_key
2767 .as_ref()
2768 .is_some_and(|k| k.use_toon_tex),
2769 };
2770 EffectResolvedResources {
2771 base,
2772 mask,
2773 tone,
2774 fog,
2775 normal,
2776 toon,
2777 aux_view,
2778 aux_sampler,
2779 shadow_view: &self.shadow_map.view,
2780 shadow_sampler: &self.shadow_sampler,
2781 global_vals,
2782 }
2783 }
2784
2785 fn render_copy_pass(
2786 &self,
2787 encoder: &mut wgpu::CommandEncoder,
2788 color_target: ColorTarget<'_>,
2789 src: BackdropTarget,
2790 blit_range: std::ops::Range<u32>,
2791 ) -> Result<()> {
2792 let color_view = self.color_target_view(color_target);
2793 let src = self.backdrop_target_ref(src);
2794 let key = sprite2d_copy_render_pipeline_key();
2795 let target_is_external = matches!(color_target, ColorTarget::External(_));
2796 let uniform_width = if target_is_external {
2797 self.config.width as f32
2798 } else {
2799 self.logical_width.max(1.0)
2800 };
2801 let uniform_height = if target_is_external {
2802 self.config.height as f32
2803 } else {
2804 self.logical_height.max(1.0)
2805 };
2806 let vs_uniform = plain_sprite2d_uniform(uniform_width, uniform_height);
2807 self.queue
2808 .write_buffer(&self.vs_uniform_buf, 0, bytemuck::bytes_of(&vs_uniform));
2809 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
2810 label: Some("siglus-copy-bg"),
2811 layout: &self.bind_group_layout,
2812 entries: &[
2813 wgpu::BindGroupEntry {
2814 binding: 0,
2815 resource: wgpu::BindingResource::TextureView(&src.view),
2816 },
2817 wgpu::BindGroupEntry {
2818 binding: 1,
2819 resource: wgpu::BindingResource::Sampler(&src.sampler),
2820 },
2821 wgpu::BindGroupEntry {
2822 binding: 2,
2823 resource: wgpu::BindingResource::TextureView(&self.default_aux.view),
2824 },
2825 wgpu::BindGroupEntry {
2826 binding: 3,
2827 resource: wgpu::BindingResource::Sampler(&self.default_aux.sampler),
2828 },
2829 wgpu::BindGroupEntry {
2830 binding: 4,
2831 resource: wgpu::BindingResource::TextureView(&self.default_aux.view),
2832 },
2833 wgpu::BindGroupEntry {
2834 binding: 5,
2835 resource: wgpu::BindingResource::Sampler(&self.default_aux.sampler),
2836 },
2837 wgpu::BindGroupEntry {
2838 binding: 6,
2839 resource: wgpu::BindingResource::TextureView(&self.default_aux.view),
2840 },
2841 wgpu::BindGroupEntry {
2842 binding: 7,
2843 resource: wgpu::BindingResource::Sampler(&self.default_aux.sampler),
2844 },
2845 wgpu::BindGroupEntry {
2846 binding: 8,
2847 resource: wgpu::BindingResource::TextureView(&self.default_aux.view),
2848 },
2849 wgpu::BindGroupEntry {
2850 binding: 9,
2851 resource: wgpu::BindingResource::Sampler(&self.default_aux.sampler),
2852 },
2853 wgpu::BindGroupEntry {
2854 binding: 10,
2855 resource: wgpu::BindingResource::TextureView(&self.shadow_map.view),
2856 },
2857 wgpu::BindGroupEntry {
2858 binding: 11,
2859 resource: wgpu::BindingResource::Sampler(&self.shadow_sampler),
2860 },
2861 wgpu::BindGroupEntry {
2862 binding: 12,
2863 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
2864 buffer: &self.vs_uniform_buf,
2865 offset: 0,
2866 size: NonZeroU64::new(std::mem::size_of::<VsUniform>() as u64),
2867 }),
2868 },
2869 wgpu::BindGroupEntry {
2870 binding: 13,
2871 resource: self.zero_bone_uniform_buf.as_entire_binding(),
2872 },
2873 wgpu::BindGroupEntry {
2874 binding: 14,
2875 resource: wgpu::BindingResource::TextureView(&self.default_aux.view),
2876 },
2877 wgpu::BindGroupEntry {
2878 binding: 15,
2879 resource: wgpu::BindingResource::Sampler(&self.normal_sampler),
2880 },
2881 wgpu::BindGroupEntry {
2882 binding: 16,
2883 resource: wgpu::BindingResource::TextureView(&self.default_aux.view),
2884 },
2885 wgpu::BindGroupEntry {
2886 binding: 17,
2887 resource: wgpu::BindingResource::Sampler(&self.toon_sampler),
2888 },
2889 ],
2890 });
2891 let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2892 label: Some("siglus-copy-pass"),
2893 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2894 view: color_view,
2895 resolve_target: None,
2896 ops: wgpu::Operations {
2897 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
2898 store: wgpu::StoreOp::Store,
2899 },
2900 })],
2901 depth_stencil_attachment: None,
2902 timestamp_writes: None,
2903 occlusion_query_set: None,
2904 });
2905 if let Some(pipeline) = self.pipelines.get(&key) {
2906 rp.set_pipeline(pipeline);
2907 }
2908 let viewport = if target_is_external {
2909 self.surface_viewport
2910 } else {
2911 SurfaceViewport::full(
2912 self.logical_width.max(1.0).round() as u32,
2913 self.logical_height.max(1.0).round() as u32,
2914 )
2915 };
2916 rp.set_viewport(
2917 viewport.x as f32,
2918 viewport.y as f32,
2919 viewport.w as f32,
2920 viewport.h as f32,
2921 0.0,
2922 1.0,
2923 );
2924 rp.set_vertex_buffer(0, self.vertex_sprite2d_buf.slice(..));
2925 rp.set_bind_group(0, &bind_group, &[0]);
2926 rp.set_scissor_rect(viewport.x, viewport.y, viewport.w, viewport.h);
2927 rp.draw(blit_range, 0..1);
2928 Ok(())
2929 }
2930
2931 fn ensure_vertex_capacity(&mut self, needed: usize) -> Result<()> {
2932 if needed <= self.vertex_capacity {
2933 return Ok(());
2934 }
2935
2936 let mut new_cap = self.vertex_capacity.max(48);
2941 while new_cap < needed {
2942 new_cap = new_cap.saturating_mul(2);
2943 }
2944 new_cap = align_up_usize(new_cap, 6);
2945 self.vertex_capacity = new_cap;
2946
2947 self.vertex_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
2948 label: Some("siglus-sprite-vertex-buf"),
2949 size: (new_cap * std::mem::size_of::<Vertex>()) as wgpu::BufferAddress,
2950 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2951 mapped_at_creation: false,
2952 });
2953 self.vertex_sprite2d_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
2954 label: Some("siglus-sprite2d-vertex-buf"),
2955 size: (new_cap * std::mem::size_of::<VertexSprite2dData>()) as wgpu::BufferAddress,
2956 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
2957 mapped_at_creation: false,
2958 });
2959 Ok(())
2960 }
2961
2962 fn upload_prepared_vertices(&mut self) -> Result<()> {
2963 self.ensure_vertex_capacity(self.plan.verts.len())?;
2964
2965 self.sprite2d_verts.clear();
2969 self.sprite2d_verts.extend(
2970 self.plan
2971 .verts
2972 .iter()
2973 .copied()
2974 .map(VertexSprite2dData::from),
2975 );
2976 if !self.sprite2d_verts.is_empty() {
2977 self.queue.write_buffer(
2978 &self.vertex_sprite2d_buf,
2979 0,
2980 bytemuck::cast_slice(&self.sprite2d_verts),
2981 );
2982 }
2983
2984 let needs_mesh_vertices = self
2985 .plan
2986 .draws
2987 .iter()
2988 .any(|cmd| !cmd.pipeline_key.program.uses_sprite2d_layout() || cmd.shadow_cast);
2989 if needs_mesh_vertices && !self.plan.verts.is_empty() {
2990 self.queue
2991 .write_buffer(&self.vertex_buf, 0, bytemuck::cast_slice(&self.plan.verts));
2992 }
2993 Ok(())
2994 }
2995
2996 fn ensure_vs_uniform_capacity(&mut self, needed: usize) {
2997 if needed <= self.vs_uniform_capacity {
2998 return;
2999 }
3000 let mut new_cap = self.vs_uniform_capacity.max(64);
3001 while new_cap < needed {
3002 new_cap = new_cap.saturating_mul(2);
3003 }
3004 self.vs_uniform_capacity = new_cap;
3005 self.vs_uniform_buf = self.device.create_buffer(&wgpu::BufferDescriptor {
3006 label: Some("siglus-vs-uniform-arena"),
3007 size: (self.vs_uniform_stride * new_cap) as wgpu::BufferAddress,
3008 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
3009 mapped_at_creation: false,
3010 });
3011
3012 self.clear_draw_bindings();
3015 }
3016
3017 fn upload_draw_uniforms(&mut self) {
3018 let draw_count = self.plan.draws.len();
3019 if draw_count == 0 {
3020 return;
3021 }
3022 self.ensure_vs_uniform_capacity(draw_count);
3023 let byte_len = self.vs_uniform_stride.saturating_mul(draw_count);
3024 self.vs_uniform_staging.clear();
3025 self.vs_uniform_staging.resize(byte_len, 0);
3026 for (idx, cmd) in self.plan.draws.iter().enumerate() {
3027 let offset = idx * self.vs_uniform_stride;
3028 let bytes = bytemuck::bytes_of(&cmd.vs_uniform);
3029 self.vs_uniform_staging[offset..offset + bytes.len()].copy_from_slice(bytes);
3030 }
3031 self.queue
3032 .write_buffer(&self.vs_uniform_buf, 0, &self.vs_uniform_staging);
3033 }
3034
3035 fn ensure_draw_gpu_slots(&mut self, needed: usize) {
3036 if self.draw_gpu_slots.len() >= needed {
3037 return;
3038 }
3039 self.draw_gpu_slots.resize_with(needed, || DrawGpuSlot {
3043 bone_uniform_buf: None,
3044 bind_group: None,
3045 bind_key: None,
3046 bind_epoch: 0,
3047 });
3048 }
3049
3050 fn prepare_draw_gpu_slot(
3051 &mut self,
3052 draw_idx: usize,
3053 overlay_backdrop: Option<BackdropTarget>,
3054 ) -> Result<()> {
3055 self.ensure_draw_gpu_slots(draw_idx + 1);
3056
3057 let use_bone_uniform = draw_uses_bone_uniform(&self.plan.draws[draw_idx]);
3058 if use_bone_uniform {
3059 if self.draw_gpu_slots[draw_idx].bone_uniform_buf.is_none() {
3060 self.draw_gpu_slots[draw_idx].bone_uniform_buf =
3061 Some(self.device.create_buffer(&wgpu::BufferDescriptor {
3062 label: Some("siglus-bone-uniform-slot"),
3063 size: std::mem::size_of::<BoneUniform>() as wgpu::BufferAddress,
3064 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
3065 mapped_at_creation: false,
3066 }));
3067 }
3068 let bone_buf = self.draw_gpu_slots[draw_idx]
3069 .bone_uniform_buf
3070 .as_ref()
3071 .expect("skinned draw allocated a bone uniform buffer");
3072 let bone_uniform_index = self.plan.draws[draw_idx]
3073 .bone_uniform_index
3074 .expect("skinned draw missing bone palette index");
3075 let bone_uniform = self
3076 .plan
3077 .draw_bone_uniforms
3078 .get(bone_uniform_index as usize)
3079 .expect("skinned draw bone palette index out of range");
3080 self.queue
3081 .write_buffer(bone_buf, 0, bytemuck::bytes_of(bone_uniform));
3082 }
3083
3084 let cmd = &self.plan.draws[draw_idx];
3085 let bind_key = DrawBindKey::from_command(cmd, overlay_backdrop);
3086 let cacheable = cmd.emote_render_id.is_none();
3091 let slot = &self.draw_gpu_slots[draw_idx];
3092 let needs_bind_group = !cacheable
3093 || slot.bind_group.is_none()
3094 || slot.bind_epoch != self.draw_bind_epoch
3095 || slot.bind_key.as_ref() != Some(&bind_key);
3096 if !needs_bind_group {
3097 return Ok(());
3098 }
3099
3100 if cacheable
3105 && !use_bone_uniform
3106 && let Some(bind_group) = self.shared_draw_bind_groups.get(&bind_key).cloned()
3107 {
3108 let slot = &mut self.draw_gpu_slots[draw_idx];
3109 slot.bind_group = Some(bind_group);
3110 slot.bind_key = Some(bind_key);
3111 slot.bind_epoch = self.draw_bind_epoch;
3112 return Ok(());
3113 }
3114
3115 let semantics = self.resolve_effect_resources_for_draw(
3116 cmd,
3117 overlay_backdrop.map(|target| self.backdrop_target_ref(target)),
3118 );
3119 let base_sampler = if bind_key.mesh_base_sampler {
3120 &self.mesh_sampler
3121 } else {
3122 &semantics.base.sampler
3123 };
3124 let bone_uniform_buf = if use_bone_uniform {
3125 self.draw_gpu_slots[draw_idx]
3126 .bone_uniform_buf
3127 .as_ref()
3128 .expect("skinned draw bone buffer missing")
3129 } else {
3130 &self.zero_bone_uniform_buf
3131 };
3132 let bind_group = Arc::new(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
3133 label: Some("siglus-sprite-bg-slot"),
3134 layout: &self.bind_group_layout,
3135 entries: &[
3136 wgpu::BindGroupEntry {
3137 binding: 0,
3138 resource: wgpu::BindingResource::TextureView(&semantics.base.view),
3139 },
3140 wgpu::BindGroupEntry {
3141 binding: 1,
3142 resource: wgpu::BindingResource::Sampler(base_sampler),
3143 },
3144 wgpu::BindGroupEntry {
3145 binding: 2,
3146 resource: wgpu::BindingResource::TextureView(&semantics.mask.view),
3147 },
3148 wgpu::BindGroupEntry {
3149 binding: 3,
3150 resource: wgpu::BindingResource::Sampler(&semantics.mask.sampler),
3151 },
3152 wgpu::BindGroupEntry {
3153 binding: 4,
3154 resource: wgpu::BindingResource::TextureView(&semantics.tone.view),
3155 },
3156 wgpu::BindGroupEntry {
3157 binding: 5,
3158 resource: wgpu::BindingResource::Sampler(&semantics.tone.sampler),
3159 },
3160 wgpu::BindGroupEntry {
3161 binding: 6,
3162 resource: wgpu::BindingResource::TextureView(semantics.aux_view),
3163 },
3164 wgpu::BindGroupEntry {
3165 binding: 7,
3166 resource: wgpu::BindingResource::Sampler(semantics.aux_sampler),
3167 },
3168 wgpu::BindGroupEntry {
3169 binding: 8,
3170 resource: wgpu::BindingResource::TextureView(&semantics.fog.view),
3171 },
3172 wgpu::BindGroupEntry {
3173 binding: 9,
3174 resource: wgpu::BindingResource::Sampler(&self.fog_sampler),
3175 },
3176 wgpu::BindGroupEntry {
3177 binding: 10,
3178 resource: wgpu::BindingResource::TextureView(semantics.shadow_view),
3179 },
3180 wgpu::BindGroupEntry {
3181 binding: 11,
3182 resource: wgpu::BindingResource::Sampler(&self.shadow_sampler),
3183 },
3184 wgpu::BindGroupEntry {
3185 binding: 12,
3186 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
3187 buffer: &self.vs_uniform_buf,
3188 offset: 0,
3189 size: NonZeroU64::new(std::mem::size_of::<VsUniform>() as u64),
3190 }),
3191 },
3192 wgpu::BindGroupEntry {
3193 binding: 13,
3194 resource: bone_uniform_buf.as_entire_binding(),
3195 },
3196 wgpu::BindGroupEntry {
3197 binding: 14,
3198 resource: wgpu::BindingResource::TextureView(&semantics.normal.view),
3199 },
3200 wgpu::BindGroupEntry {
3201 binding: 15,
3202 resource: wgpu::BindingResource::Sampler(&self.normal_sampler),
3203 },
3204 wgpu::BindGroupEntry {
3205 binding: 16,
3206 resource: wgpu::BindingResource::TextureView(&semantics.toon.view),
3207 },
3208 wgpu::BindGroupEntry {
3209 binding: 17,
3210 resource: wgpu::BindingResource::Sampler(&self.toon_sampler),
3211 },
3212 ],
3213 }));
3214
3215 if cacheable && !use_bone_uniform {
3216 self.shared_draw_bind_groups
3217 .insert(bind_key.clone(), Arc::clone(&bind_group));
3218 }
3219 let slot = &mut self.draw_gpu_slots[draw_idx];
3220 slot.bind_group = Some(bind_group);
3221 slot.bind_key = cacheable.then_some(bind_key);
3222 slot.bind_epoch = self.draw_bind_epoch;
3223 Ok(())
3224 }
3225
3226 pub fn clear_runtime_image_textures(&mut self) {
3233 self.plan.draws.clear();
3234 self.plan.draw_bone_uniforms.clear();
3235 self.plan.draw_bone_uniforms.shrink_to_fit();
3236 self.textures.clear();
3237 self.clear_draw_bindings();
3238 }
3239
3240 fn clear_draw_bindings(&mut self) {
3241 for slot in &mut self.draw_gpu_slots {
3242 slot.bind_group = None;
3243 slot.bind_key = None;
3244 }
3245 self.shared_draw_bind_groups.clear();
3246 self.draw_bind_epoch = self.draw_bind_epoch.wrapping_add(1).max(1);
3247 }
3248
3249 pub fn texture_cache_bytes(&self) -> u64 {
3250 self.textures
3251 .values()
3252 .map(|tex| u64::from(tex.width) * u64::from(tex.height) * 4 * 4 / 3)
3253 .sum()
3254 }
3255
3256 pub fn debug_memory_stats(&self) -> RendererMemoryStats {
3261 let image_texture_bytes = self.texture_cache_bytes();
3262 let external_texture_bytes = self
3263 .external_textures
3264 .values()
3265 .map(|tex| u64::from(tex.width) * u64::from(tex.height) * 4 * 4 / 3)
3266 .sum();
3267 let color_bytes = |rt: &RenderTargetTexture| u64::from(rt.width) * u64::from(rt.height) * 4;
3268 let internal_color_target_bytes = color_bytes(&self.scene_a)
3269 + color_bytes(&self.scene_b)
3270 + color_bytes(&self.wipe_a)
3271 + color_bytes(&self.wipe_b)
3272 + color_bytes(&self.shadow_map);
3273 let internal_depth_target_bytes =
3276 u64::from(self.scene_a.width) * u64::from(self.scene_a.height) * 4
3277 + u64::from(self.config.width) * u64::from(self.config.height) * 4
3278 + u64::from(self.shadow_map.width) * u64::from(self.shadow_map.height) * 2;
3279 let renderer_gpu_buffer_bytes = (self.vertex_capacity * std::mem::size_of::<Vertex>())
3280 as u64
3281 + (self.vertex_capacity * std::mem::size_of::<VertexSprite2dData>()) as u64
3282 + (self.vs_uniform_stride * self.vs_uniform_capacity) as u64
3283 + std::mem::size_of::<BoneUniform>() as u64
3284 + self
3285 .draw_gpu_slots
3286 .iter()
3287 .filter(|slot| slot.bone_uniform_buf.is_some())
3288 .count() as u64
3289 * std::mem::size_of::<BoneUniform>() as u64;
3290 let frame_arena_capacity_bytes = self.plan.verts.capacity() * std::mem::size_of::<Vertex>()
3291 + self.sprite2d_verts.capacity() * std::mem::size_of::<VertexSprite2dData>()
3292 + self.plan.draws.capacity() * std::mem::size_of::<DrawCommand>()
3293 + self.plan.draw_bone_uniforms.capacity() * std::mem::size_of::<BoneUniform>()
3294 + self.vs_uniform_staging.capacity();
3295 RendererMemoryStats {
3296 image_texture_bytes,
3297 external_texture_bytes,
3298 internal_color_target_bytes,
3299 internal_depth_target_bytes,
3300 renderer_gpu_buffer_bytes,
3301 frame_arena_capacity_bytes,
3302 image_texture_count: self.textures.len(),
3303 external_texture_count: self.external_textures.len(),
3304 cached_render_pipeline_count: self.pipelines.len(),
3305 }
3306 }
3307
3308 fn organize_textures(&mut self, images: &ImageManager) {
3309 let before = self.textures.len();
3310 self.textures.retain(|id, _| images.contains(*id));
3311 if self.textures.len() != before {
3312 self.clear_draw_bindings();
3315 }
3316 }
3317
3318 fn ensure_texture_uploaded(&mut self, images: &ImageManager, key: ImageKey) -> Result<()> {
3319 let Some(id) = images.image_handle(key) else {
3320 return Ok(());
3321 };
3322 let Some((img, version)) = images.get_entry(&id) else {
3323 return Ok(());
3324 };
3325 if let Some(mut tex) = self.textures.remove(&id.key()) {
3326 if tex.version != version {
3327 if tex.width == img.width && tex.height == img.height {
3328 self.update_texture(&tex, &img)?;
3329 tex.version = version;
3330 } else {
3331 tex = create_gpu_texture(
3332 &self.device,
3333 &self.queue,
3334 &self.mipmap_generator,
3335 &format!("siglus-texture-{}", id.index()),
3336 &img,
3337 version,
3338 )?;
3339 self.clear_draw_bindings();
3340 }
3341 }
3342 self.textures.insert(id.key(), tex);
3343 } else {
3344 let tex = create_gpu_texture(
3345 &self.device,
3346 &self.queue,
3347 &self.mipmap_generator,
3348 &format!("siglus-texture-{}", id.index()),
3349 &img,
3350 version,
3351 )?;
3352 self.textures.insert(id.key(), tex);
3353 }
3354 Ok(())
3355 }
3356
3357 fn update_texture(&self, tex: &GpuTexture, img: &crate::assets::RgbaImage) -> Result<()> {
3358 if tex.width != img.width || tex.height != img.height {
3359 return Ok(());
3360 }
3361 upload_texture_pixels(&self.queue, &tex._tex, img);
3362 if let Some(mipmaps) = self.mipmap_generator.generate(&self.device, &tex._tex) {
3363 self.queue.submit(Some(mipmaps));
3368 }
3369 Ok(())
3370 }
3371
3372 fn submit(&self, encoder: wgpu::CommandEncoder) {
3373 self.queue.submit(Some(encoder.finish()));
3374 }
3375}
3376impl FrameCaptureBackend for Renderer {
3377 fn capture_render_frame(
3378 &mut self,
3379 images: &ImageManager,
3380 frame: &RenderFrame,
3381 logical_width: u32,
3382 logical_height: u32,
3383 ) -> Result<crate::assets::RgbaImage> {
3384 let renderer_width = self.logical_width.max(1.0).round() as u32;
3385 let renderer_height = self.logical_height.max(1.0).round() as u32;
3386 if renderer_width != logical_width.max(1) || renderer_height != logical_height.max(1) {
3387 anyhow::bail!(
3388 "capture logical size mismatch: renderer={}x{}, runtime={}x{}",
3389 renderer_width,
3390 renderer_height,
3391 logical_width,
3392 logical_height,
3393 );
3394 }
3395 let final_target = self.render_frame_to_internal(images, frame)?;
3396 let target = self.backdrop_target_ref(final_target);
3397 let rgba =
3398 self.debug_read_texture_rgba(&target._tex, target.width, target.height, target.format)?;
3399 Ok(crate::assets::RgbaImage {
3400 width: target.width,
3401 height: target.height,
3402 center_x: 0,
3403 center_y: 0,
3404 rgba,
3405 })
3406 }
3407}
3408
3409fn create_solid_texture(
3410 device: &wgpu::Device,
3411 queue: &wgpu::Queue,
3412 mipmap_generator: &mipmap::MipmapGenerator,
3413 rgba: [u8; 4],
3414) -> Result<GpuTexture> {
3415 let img = crate::assets::RgbaImage {
3416 width: 1,
3417 height: 1,
3418 center_x: 0,
3419 center_y: 0,
3420 rgba: rgba.to_vec(),
3421 };
3422 create_gpu_texture(
3423 device,
3424 queue,
3425 mipmap_generator,
3426 "siglus-default-aux",
3427 &img,
3428 0,
3429 )
3430}
3431
3432#[cfg(test)]
3433#[derive(Debug)]
3434struct Rgba8MipLevel {
3435 width: u32,
3436 height: u32,
3437 rgba: Vec<u8>,
3438}
3439
3440#[cfg(test)]
3444fn build_rgba8_mip_chain(width: u32, height: u32, rgba: &[u8]) -> Vec<Rgba8MipLevel> {
3445 if width == 0 || height == 0 || rgba.len() < width as usize * height as usize * 4 {
3446 return Vec::new();
3447 }
3448
3449 let mut levels = vec![Rgba8MipLevel {
3450 width,
3451 height,
3452 rgba: rgba[..width as usize * height as usize * 4].to_vec(),
3453 }];
3454
3455 while levels
3456 .last()
3457 .is_some_and(|level| level.width > 1 || level.height > 1)
3458 {
3459 let prev = levels.last().expect("mip chain contains level zero");
3460 let next_width = (prev.width / 2).max(1);
3461 let next_height = (prev.height / 2).max(1);
3462 let mut next = vec![0u8; next_width as usize * next_height as usize * 4];
3463
3464 for y in 0..next_height {
3465 for x in 0..next_width {
3466 let src_x0 = x.saturating_mul(2);
3467 let src_y0 = y.saturating_mul(2);
3468 let src_x1 = (src_x0 + 1).min(prev.width - 1);
3469 let src_y1 = (src_y0 + 1).min(prev.height - 1);
3470 let coords = [
3471 (src_x0, src_y0),
3472 (src_x1, src_y0),
3473 (src_x0, src_y1),
3474 (src_x1, src_y1),
3475 ];
3476 let dst = ((y * next_width + x) * 4) as usize;
3477 for channel in 0..4usize {
3478 let sum = coords.iter().fold(0u32, |acc, (sx, sy)| {
3479 let src = ((*sy * prev.width + *sx) * 4) as usize + channel;
3480 acc + prev.rgba[src] as u32
3481 });
3482 next[dst + channel] = ((sum + 2) / 4).min(255) as u8;
3483 }
3484 }
3485 }
3486
3487 levels.push(Rgba8MipLevel {
3488 width: next_width,
3489 height: next_height,
3490 rgba: next,
3491 });
3492 }
3493
3494 levels
3495}
3496
3497fn create_gpu_texture(
3498 device: &wgpu::Device,
3499 queue: &wgpu::Queue,
3500 mipmap_generator: &mipmap::MipmapGenerator,
3501 label: &str,
3502 img: &crate::assets::RgbaImage,
3503 version: u64,
3504) -> Result<GpuTexture> {
3505 anyhow::ensure!(img.width > 0 && img.height > 0, "empty texture dimensions");
3506 let pixel_bytes = (img.width as usize)
3507 .checked_mul(img.height as usize)
3508 .and_then(|n| n.checked_mul(4))
3509 .context("texture size overflow")?;
3510 anyhow::ensure!(img.rgba.len() >= pixel_bytes, "truncated texture pixels");
3511 let mip_level_count = u32::BITS - img.width.max(img.height).leading_zeros();
3512 let tex = device.create_texture(&wgpu::TextureDescriptor {
3513 label: Some(label),
3514 size: wgpu::Extent3d {
3515 width: img.width,
3516 height: img.height,
3517 depth_or_array_layers: 1,
3518 },
3519 mip_level_count,
3520 sample_count: 1,
3521 dimension: wgpu::TextureDimension::D2,
3522 format: wgpu::TextureFormat::Rgba8Unorm,
3523 usage: wgpu::TextureUsages::TEXTURE_BINDING
3524 | wgpu::TextureUsages::COPY_DST
3525 | wgpu::TextureUsages::COPY_SRC
3526 | wgpu::TextureUsages::RENDER_ATTACHMENT,
3527 view_formats: &[],
3528 });
3529
3530 upload_texture_pixels(queue, &tex, img);
3531 if let Some(mipmaps) = mipmap_generator.generate(device, &tex) {
3532 queue.submit(Some(mipmaps));
3533 }
3534
3535 let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
3536 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
3537 label: Some("siglus-sampler"),
3538 address_mode_u: wgpu::AddressMode::ClampToEdge,
3539 address_mode_v: wgpu::AddressMode::ClampToEdge,
3540 address_mode_w: wgpu::AddressMode::ClampToEdge,
3541 mag_filter: wgpu::FilterMode::Linear,
3542 min_filter: wgpu::FilterMode::Linear,
3543 mipmap_filter: wgpu::FilterMode::Linear,
3544 ..Default::default()
3545 });
3546
3547 Ok(GpuTexture {
3548 _tex: tex,
3549 view,
3550 sampler,
3551 width: img.width,
3552 height: img.height,
3553 version,
3554 })
3555}
3556
3557fn upload_texture_pixels(
3558 queue: &wgpu::Queue,
3559 texture: &wgpu::Texture,
3560 img: &crate::assets::RgbaImage,
3561) {
3562 queue.write_texture(
3563 wgpu::ImageCopyTexture {
3564 texture,
3565 mip_level: 0,
3566 origin: wgpu::Origin3d::ZERO,
3567 aspect: wgpu::TextureAspect::All,
3568 },
3569 &img.rgba,
3570 wgpu::ImageDataLayout {
3571 offset: 0,
3572 bytes_per_row: Some(4 * img.width),
3573 rows_per_image: Some(img.height),
3574 },
3575 wgpu::Extent3d {
3576 width: img.width,
3577 height: img.height,
3578 depth_or_array_layers: 1,
3579 },
3580 );
3581}
3582
3583fn create_render_target_texture(
3584 device: &wgpu::Device,
3585 width: u32,
3586 height: u32,
3587 format: wgpu::TextureFormat,
3588 label: &str,
3589) -> RenderTargetTexture {
3590 let tex = device.create_texture(&wgpu::TextureDescriptor {
3591 label: Some(label),
3592 size: wgpu::Extent3d {
3593 width: width.max(1),
3594 height: height.max(1),
3595 depth_or_array_layers: 1,
3596 },
3597 mip_level_count: 1,
3598 sample_count: 1,
3599 dimension: wgpu::TextureDimension::D2,
3600 format,
3601 usage: wgpu::TextureUsages::TEXTURE_BINDING
3602 | wgpu::TextureUsages::RENDER_ATTACHMENT
3603 | wgpu::TextureUsages::COPY_SRC
3604 | wgpu::TextureUsages::COPY_DST,
3605 view_formats: &[],
3606 });
3607 let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
3608 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
3609 label: Some("siglus-render-target-sampler"),
3610 address_mode_u: wgpu::AddressMode::ClampToEdge,
3611 address_mode_v: wgpu::AddressMode::ClampToEdge,
3612 address_mode_w: wgpu::AddressMode::ClampToEdge,
3613 mag_filter: wgpu::FilterMode::Linear,
3614 min_filter: wgpu::FilterMode::Linear,
3615 mipmap_filter: wgpu::FilterMode::Nearest,
3616 ..Default::default()
3617 });
3618 RenderTargetTexture {
3619 _tex: tex,
3620 view,
3621 sampler,
3622 width: width.max(1),
3623 height: height.max(1),
3624 format,
3625 }
3626}
3627
3628fn create_depth_texture(device: &wgpu::Device, width: u32, height: u32) -> DepthTexture {
3629 create_depth_texture_with_format(
3630 device,
3631 width,
3632 height,
3633 wgpu::TextureFormat::Depth32Float,
3634 "siglus-depth",
3635 )
3636}
3637
3638fn create_depth_texture_with_format(
3639 device: &wgpu::Device,
3640 width: u32,
3641 height: u32,
3642 format: wgpu::TextureFormat,
3643 label: &str,
3644) -> DepthTexture {
3645 let tex = device.create_texture(&wgpu::TextureDescriptor {
3646 label: Some(label),
3647 size: wgpu::Extent3d {
3648 width: width.max(1),
3649 height: height.max(1),
3650 depth_or_array_layers: 1,
3651 },
3652 mip_level_count: 1,
3653 sample_count: 1,
3654 dimension: wgpu::TextureDimension::D2,
3655 format,
3656 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
3657 view_formats: &[],
3658 });
3659 let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
3660 DepthTexture { _tex: tex, view }
3661}
3662
3663#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
3664fn wasm_shader_source() -> String {
3665 SHADER.to_string()
3666}
3667
3668fn create_wipe_pipeline(
3669 device: &wgpu::Device,
3670 format: wgpu::TextureFormat,
3671) -> (wgpu::BindGroupLayout, wgpu::RenderPipeline) {
3672 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
3673 label: Some("siglus-wipe-bind-group-layout"),
3674 entries: &[
3675 texture_layout_entry(0),
3676 sampler_layout_entry(1),
3677 texture_layout_entry(2),
3678 sampler_layout_entry(3),
3679 texture_layout_entry(4),
3680 sampler_layout_entry(5),
3681 texture_layout_entry(6),
3682 sampler_layout_entry(7),
3683 wgpu::BindGroupLayoutEntry {
3684 binding: 8,
3685 visibility: wgpu::ShaderStages::FRAGMENT,
3686 ty: wgpu::BindingType::Buffer {
3687 ty: wgpu::BufferBindingType::Uniform,
3688 has_dynamic_offset: false,
3689 min_binding_size: None,
3690 },
3691 count: None,
3692 },
3693 ],
3694 });
3695 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
3696 label: Some("siglus-wipe-shader"),
3697 source: wgpu::ShaderSource::Wgsl(WIPE_SHADER.into()),
3698 });
3699 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
3700 label: Some("siglus-wipe-pipeline-layout"),
3701 bind_group_layouts: &[&layout],
3702 push_constant_ranges: &[],
3703 });
3704 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
3705 label: Some("siglus-wipe-pipeline"),
3706 layout: Some(&pipeline_layout),
3707 vertex: wgpu::VertexState {
3708 module: &shader,
3709 entry_point: "vs_main",
3710 compilation_options: wgpu::PipelineCompilationOptions::default(),
3711 buffers: &[],
3712 },
3713 fragment: Some(wgpu::FragmentState {
3714 module: &shader,
3715 entry_point: "fs_main",
3716 compilation_options: wgpu::PipelineCompilationOptions::default(),
3717 targets: &[Some(wgpu::ColorTargetState {
3718 format,
3719 blend: None,
3720 write_mask: wgpu::ColorWrites::ALL,
3721 })],
3722 }),
3723 primitive: wgpu::PrimitiveState {
3724 topology: wgpu::PrimitiveTopology::TriangleList,
3725 strip_index_format: None,
3726 front_face: wgpu::FrontFace::Ccw,
3727 cull_mode: None,
3728 polygon_mode: wgpu::PolygonMode::Fill,
3729 unclipped_depth: false,
3730 conservative: false,
3731 },
3732 depth_stencil: None,
3733 multisample: wgpu::MultisampleState::default(),
3734 multiview: None,
3735 });
3736 (layout, pipeline)
3737}
3738
3739fn create_page_wipe_pipeline(
3740 device: &wgpu::Device,
3741 format: wgpu::TextureFormat,
3742) -> (wgpu::BindGroupLayout, wgpu::RenderPipeline) {
3743 let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
3744 label: Some("siglus-page-wipe-bind-group-layout"),
3745 entries: &[texture_layout_entry(0), sampler_layout_entry(1)],
3746 });
3747 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
3748 label: Some("siglus-page-wipe-shader"),
3749 source: wgpu::ShaderSource::Wgsl(PAGE_WIPE_SHADER.into()),
3750 });
3751 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
3752 label: Some("siglus-page-wipe-pipeline-layout"),
3753 bind_group_layouts: &[&layout],
3754 push_constant_ranges: &[],
3755 });
3756 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
3757 label: Some("siglus-page-wipe-pipeline"),
3758 layout: Some(&pipeline_layout),
3759 vertex: wgpu::VertexState {
3760 module: &shader,
3761 entry_point: "vs_main",
3762 compilation_options: wgpu::PipelineCompilationOptions::default(),
3763 buffers: &[wgpu::VertexBufferLayout {
3764 array_stride: std::mem::size_of::<PageWipeVertex>() as wgpu::BufferAddress,
3765 step_mode: wgpu::VertexStepMode::Vertex,
3766 attributes: &[
3767 wgpu::VertexAttribute {
3768 format: wgpu::VertexFormat::Float32x4,
3769 offset: 0,
3770 shader_location: 0,
3771 },
3772 wgpu::VertexAttribute {
3773 format: wgpu::VertexFormat::Float32x2,
3774 offset: 16,
3775 shader_location: 1,
3776 },
3777 ],
3778 }],
3779 },
3780 fragment: Some(wgpu::FragmentState {
3781 module: &shader,
3782 entry_point: "fs_main",
3783 compilation_options: wgpu::PipelineCompilationOptions::default(),
3784 targets: &[Some(wgpu::ColorTargetState {
3785 format,
3786 blend: Some(wgpu::BlendState::ALPHA_BLENDING),
3787 write_mask: wgpu::ColorWrites::ALL,
3788 })],
3789 }),
3790 primitive: wgpu::PrimitiveState {
3791 topology: wgpu::PrimitiveTopology::TriangleList,
3792 strip_index_format: None,
3793 front_face: wgpu::FrontFace::Ccw,
3794 cull_mode: Some(wgpu::Face::Back),
3795 polygon_mode: wgpu::PolygonMode::Fill,
3796 unclipped_depth: false,
3797 conservative: false,
3798 },
3799 depth_stencil: Some(wgpu::DepthStencilState {
3800 format: wgpu::TextureFormat::Depth32Float,
3801 depth_write_enabled: true,
3802 depth_compare: wgpu::CompareFunction::LessEqual,
3803 stencil: wgpu::StencilState::default(),
3804 bias: wgpu::DepthBiasState::default(),
3805 }),
3806 multisample: wgpu::MultisampleState::default(),
3807 multiview: None,
3808 });
3809 (layout, pipeline)
3810}
3811
3812const PAGE_WIPE_SHADER: &str = include_str!("shaders/page_wipe.wgsl");
3813
3814#[cfg(test)]
3815mod present_mode_tests {
3816 use super::present_mode_for_wait_display_vsync;
3817
3818 #[test]
3819 fn vsync_wait_uses_fifo() {
3820 let modes = [wgpu::PresentMode::Fifo, wgpu::PresentMode::Immediate];
3821 assert_eq!(
3822 present_mode_for_wait_display_vsync(true, &modes),
3823 wgpu::PresentMode::Fifo
3824 );
3825 }
3826
3827 #[test]
3828 fn vsync_off_prefers_immediate_then_mailbox_then_fifo() {
3829 let all = [
3830 wgpu::PresentMode::Fifo,
3831 wgpu::PresentMode::Mailbox,
3832 wgpu::PresentMode::Immediate,
3833 ];
3834 assert_eq!(
3835 present_mode_for_wait_display_vsync(false, &all),
3836 wgpu::PresentMode::Immediate
3837 );
3838 assert_eq!(
3839 present_mode_for_wait_display_vsync(
3840 false,
3841 &[wgpu::PresentMode::Fifo, wgpu::PresentMode::Mailbox],
3842 ),
3843 wgpu::PresentMode::Mailbox
3844 );
3845 assert_eq!(
3846 present_mode_for_wait_display_vsync(false, &[wgpu::PresentMode::Fifo]),
3847 wgpu::PresentMode::Fifo
3848 );
3849 }
3850}
3851
3852#[cfg(test)]
3853mod depth_state_tests {
3854 use super::{d3d_front_face, depth_write_enabled};
3855
3856 #[test]
3857 fn translucent_3d_sprites_test_depth_without_writing_it() {
3858 assert!(depth_write_enabled(true, false));
3859 assert!(!depth_write_enabled(true, true));
3860 assert!(!depth_write_enabled(false, false));
3861 }
3862
3863 #[test]
3864 fn tona3_ccw_cull_uses_clockwise_front_faces() {
3865 assert!(matches!(d3d_front_face(), wgpu::FrontFace::Cw));
3866 }
3867}
3868
3869fn texture_layout_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
3870 wgpu::BindGroupLayoutEntry {
3871 binding,
3872 visibility: wgpu::ShaderStages::FRAGMENT,
3873 ty: wgpu::BindingType::Texture {
3874 multisampled: false,
3875 view_dimension: wgpu::TextureViewDimension::D2,
3876 sample_type: wgpu::TextureSampleType::Float { filterable: true },
3877 },
3878 count: None,
3879 }
3880}
3881
3882fn sampler_layout_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
3883 wgpu::BindGroupLayoutEntry {
3884 binding,
3885 visibility: wgpu::ShaderStages::FRAGMENT,
3886 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
3887 count: None,
3888 }
3889}
3890
3891const WIPE_SHADER: &str = include_str!("shaders/wipe.wgsl");
3892
3893const SHADER: &str = include_str!("shaders/sprite.wgsl");