//! Sprite-Pass: das 2D-Overlay (UI) als getintete Textur-Quads. //! //! Immediate-Mode: der Vertex-Buffer wird jeden Frame neu befüllt (kleine //! Datenmenge). Geometrie kommt von [`crate::render::ui`] als fertige //! [`SpriteVertex`]-Liste + [`SpriteBatch`]es (ein Batch je Textur). Die //! Texturen (weißer Pixel, Font-Atlas, …) werden einmal beim Bau //! hochgeladen — wie beim Szenen-Pass: Decode (CPU) ≠ Upload (GPU). use crate::engine::tga::Image; #[repr(C)] #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] pub(crate) struct SpriteVertex { /// Interne Pixel, Ursprung oben-links. pub(crate) pos: [f32; 2], pub(crate) uv: [f32; 2], /// Tint, mit dem Texel multipliziert (RGBA, 0..1). pub(crate) color: [f32; 4], } /// Ein zusammenhängender Vertex-Bereich, der mit *einer* Textur gezeichnet /// wird (zwei Dreiecke je Quad, also Vielfache von 6). #[derive(Clone, Copy)] pub(crate) struct SpriteBatch { pub(crate) texture: usize, pub(crate) start: u32, pub(crate) count: u32, } #[repr(C)] #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] struct Uniforms { inv_res: [f32; 2], _pad: [f32; 2], } /// Obergrenze des dynamischen Vertex-Buffers. 8192 Verts = ~1365 Quads pro /// Frame — reichlich für Dialog-Panels, Text und Cursor. const MAX_VERTS: u64 = 8192; const VERTEX_LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout { array_stride: size_of::() as u64, step_mode: wgpu::VertexStepMode::Vertex, attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Float32x4], }; pub struct SpritePass { pipeline: wgpu::RenderPipeline, vbuf: wgpu::Buffer, uniform_bind: wgpu::BindGroup, // group 0 tex_binds: Vec, // group 1, pro Textur /// Im aktuellen Frame hochgeladene Vertex-Anzahl (für `draw`-Schutz). loaded: u32, } impl SpritePass { pub fn new( device: &wgpu::Device, queue: &wgpu::Queue, color_format: wgpu::TextureFormat, depth_format: wgpu::TextureFormat, internal: [f32; 2], textures: &[Image], ) -> Self { let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("sprite"), source: wgpu::ShaderSource::Wgsl(include_str!("sprite.wgsl").into()), }); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("sprite"), layout: None, vertex: wgpu::VertexState { module: &shader, entry_point: Some("vs_main"), compilation_options: Default::default(), buffers: &[VERTEX_LAYOUT], }, fragment: Some(wgpu::FragmentState { module: &shader, entry_point: Some("fs_main"), compilation_options: Default::default(), targets: &[Some(wgpu::ColorTargetState { format: color_format, // Standard-Alpha-Blending fürs Overlay. blend: Some(wgpu::BlendState::ALPHA_BLENDING), write_mask: wgpu::ColorWrites::ALL, })], }), primitive: wgpu::PrimitiveState::default(), // Der interne Pass hat ein Tiefen-Attachment → die Pipeline muss // ein kompatibles Format deklarieren. UI testet/schreibt aber // keine Tiefe (Always, kein Write) → liegt immer obenauf. depth_stencil: Some(wgpu::DepthStencilState { format: depth_format, depth_write_enabled: Some(false), depth_compare: Some(wgpu::CompareFunction::Always), stencil: wgpu::StencilState::default(), bias: wgpu::DepthBiasState::default(), }), multisample: wgpu::MultisampleState::default(), multiview_mask: None, cache: None, }); let vbuf = device.create_buffer(&wgpu::BufferDescriptor { label: Some("sprite vertices"), size: MAX_VERTS * size_of::() as u64, usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); let ubuf = device.create_buffer(&wgpu::BufferDescriptor { label: Some("sprite uniforms"), size: size_of::() as u64, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); let u = Uniforms { inv_res: [1.0 / internal[0], 1.0 / internal[1]], _pad: [0.0; 2] }; queue.write_buffer(&ubuf, 0, bytemuck::bytes_of(&u)); let uniform_bind = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("sprite uniforms"), layout: &pipeline.get_bind_group_layout(0), entries: &[wgpu::BindGroupEntry { binding: 0, resource: ubuf.as_entire_binding() }], }); // Nearest + Clamp: harte Pixel, kein Wrap an den Atlas-Rändern. let sampler = device.create_sampler(&wgpu::SamplerDescriptor { label: Some("sprite nearest"), address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, mag_filter: wgpu::FilterMode::Nearest, min_filter: wgpu::FilterMode::Nearest, ..Default::default() }); let tex_layout = pipeline.get_bind_group_layout(1); let tex_binds = textures.iter() .map(|img| upload_texture(device, queue, &tex_layout, &sampler, img)) .collect(); Self { pipeline, vbuf, uniform_bind, tex_binds, loaded: 0 } } /// Vertex-Daten dieses Frames hochladen — vor dem Render-Pass rufen. pub fn prepare(&mut self, queue: &wgpu::Queue, verts: &[SpriteVertex]) { let n = (verts.len() as u64).min(MAX_VERTS) as usize; queue.write_buffer(&self.vbuf, 0, bytemuck::cast_slice(&verts[..n])); self.loaded = n as u32; } pub fn draw(&self, pass: &mut wgpu::RenderPass, batches: &[SpriteBatch]) { if self.loaded == 0 { return; } pass.set_pipeline(&self.pipeline); pass.set_bind_group(0, &self.uniform_bind, &[]); pass.set_vertex_buffer(0, self.vbuf.slice(..)); for b in batches { if b.start + b.count > self.loaded { continue; } pass.set_bind_group(1, &self.tex_binds[b.texture], &[]); pass.draw(b.start..b.start + b.count, 0..1); } } } /// Ein RGBA8-`Image` als GPU-Textur hochladen und die Bind-Group (Textur + /// Sampler, group 1) bauen. fn upload_texture( device: &wgpu::Device, queue: &wgpu::Queue, layout: &wgpu::BindGroupLayout, sampler: &wgpu::Sampler, img: &Image, ) -> wgpu::BindGroup { let size = wgpu::Extent3d { width: img.width, height: img.height, depth_or_array_layers: 1 }; let texture = device.create_texture(&wgpu::TextureDescriptor { label: Some("sprite texture"), size, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Rgba8Unorm, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, view_formats: &[], }); queue.write_texture( wgpu::TexelCopyTextureInfo { texture: &texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All, }, &img.rgba, wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(img.width * 4), rows_per_image: Some(img.height), }, size, ); let view = texture.create_view(&Default::default()); device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("sprite texture"), layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&view) }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(sampler) }, ], }) }