From fc6fc48002717669e4376f228e4b317e04255e07 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Mon, 13 Oct 2025 16:31:44 +0200 Subject: [PATCH 01/69] fix: mac renderer start --- engine/pill_renderer/src/renderer.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index ae2a8280..a51b1910 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -307,14 +307,36 @@ impl State { let (device, queue) = adapter.request_device(&device_descriptor,None).await.unwrap(); // Specify surface configuration - let format = wgpu::TextureFormat::Rgba8UnormSrgb; + let preferred_format = wgpu::TextureFormat::Rgba8UnormSrgb; + + // Get supported present modes and choose the best one + let surface_caps = surface.get_capabilities(&adapter); + let present_mode = if surface_caps.present_modes.contains(&wgpu::PresentMode::Mailbox) { + wgpu::PresentMode::Mailbox + } else if surface_caps.present_modes.contains(&wgpu::PresentMode::Immediate) { + wgpu::PresentMode::Immediate + } else { + wgpu::PresentMode::Fifo + }; + + // Choose the best supported format + let format = if surface_caps.formats.contains(&preferred_format) { + preferred_format + } else if surface_caps.formats.contains(&wgpu::TextureFormat::Bgra8UnormSrgb) { + wgpu::TextureFormat::Bgra8UnormSrgb + } else if surface_caps.formats.contains(&wgpu::TextureFormat::Bgra8Unorm) { + wgpu::TextureFormat::Bgra8Unorm + } else { + surface_caps.formats[0] // Use first available format + }; + let surface_configuration = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, // Defines how the swap_chain's underlying textures will be used format: format, // Defines how the swap_chain's textures will be stored on the gpu width: window_size.width, height: window_size.height, desired_maximum_frame_latency: 2, - present_mode: wgpu::PresentMode::Mailbox, // Defines how to sync the surface with the display + present_mode: present_mode, // Defines how to sync the surface with the display alpha_mode: wgpu::CompositeAlphaMode::Auto, view_formats: vec![format], }; From 3c6d25651fa99c1efd6aa86890476258c1faaa58 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Mon, 13 Oct 2025 16:08:26 +0200 Subject: [PATCH 02/69] fix: launcher, create new project --- engine/pill_launcher/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/pill_launcher/src/main.rs b/engine/pill_launcher/src/main.rs index ae2137f0..f5614023 100644 --- a/engine/pill_launcher/src/main.rs +++ b/engine/pill_launcher/src/main.rs @@ -172,7 +172,7 @@ fn remove_files_starting_with(directory_path: &PathBuf, file_name_prefix: &str) // --- Actions --- fn create_game_project(game_project_parent_directory_path: &PathBuf, game_name: &String) -> Result<()> { - const TEMPLATE_NAME: &str = "Pill-Default"; + const TEMPLATE_NAME: &str = "pill_default"; let game_project_directory_path = game_project_parent_directory_path.join(game_name); if game_project_directory_path.exists() { From 64bcc21eb70e916907682eacc55e108da7d046d4 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Mon, 13 Oct 2025 16:32:15 +0200 Subject: [PATCH 03/69] fix: template res names --- .../res/templates/pill_default/src/game.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/engine/pill_launcher/res/templates/pill_default/src/game.rs b/engine/pill_launcher/res/templates/pill_default/src/game.rs index c9be30c9..7c6ae385 100644 --- a/engine/pill_launcher/res/templates/pill_default/src/game.rs +++ b/engine/pill_launcher/res/templates/pill_default/src/game.rs @@ -30,21 +30,21 @@ impl PillGame for Game { engine.add_system("PillRotation", pill_rotation_system)?; // Add meshes - let pill_mesh = Mesh::new("Pill", "models/Pill.obj".into()); + let pill_mesh = Mesh::new("Pill", "models/pill.obj".into()); let pill_mesh_handle = engine.add_resource(pill_mesh)?; // Add textures - let pill_color_texture = Texture::new("PillColor", TextureType::Color, ResourceLoadType::Path("textures/PillColor.png".into())); + let pill_color_texture = Texture::new("PillColor", TextureType::Color, ResourceLoadType::Path("textures/pill_color.png".into())); let pill_color_texture_handle = engine.add_resource::(pill_color_texture)?; - let pill_normal_texture = Texture::new("PillNormal", TextureType::Normal, ResourceLoadType::Path("textures/PillNormal.png".into())); + let pill_normal_texture = Texture::new("PillNormal", TextureType::Normal, ResourceLoadType::Path("textures/pill_normal.png".into())); let pill_normal_texture_handle = engine.add_resource::(pill_normal_texture)?; // Add materials let mut pill_material = Material::new("Pill"); - pill_material.set_texture("Color", pill_color_texture_handle)?; - pill_material.set_texture("Normal", pill_normal_texture_handle)?; - pill_material.set_color("Tint", Color::new( 1.0, 1.0, 1.0))?; - pill_material.set_scalar("Specularity", 0.5)?; + pill_material.set_texture("color", pill_color_texture_handle)?; + pill_material.set_texture("normal", pill_normal_texture_handle)?; + pill_material.set_color("tint", Color::new( 1.0, 1.0, 1.0))?; + pill_material.set_scalar("specularity", 0.5)?; let pill_material_handle = engine.add_resource::(pill_material)?; // Create camera entity From e4832ac44c3f95248cb47ed927fd21e333a76dd0 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Mon, 13 Oct 2025 23:00:22 +0200 Subject: [PATCH 04/69] feat: renderer design --- engine/pill_renderer/DESIGN.md | 240 +++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 engine/pill_renderer/DESIGN.md diff --git a/engine/pill_renderer/DESIGN.md b/engine/pill_renderer/DESIGN.md new file mode 100644 index 00000000..c740677c --- /dev/null +++ b/engine/pill_renderer/DESIGN.md @@ -0,0 +1,240 @@ +# Pill Renderer vNext (wgpu) – Minimal MVP, API-Correct + +### Goals + +- Lean MVP aligned with wgpu/Vulkan semantics (no GL-era patterns). +- Master pipeline from `WindowComponent`, renders nothing by default. +- Mesh visible by Milestone 2 via a single pass; complexity grows incrementally. +- Separate resource mutation from drawing; small, stable public API. + +### Hardware/API alignment from TALK_0.md (applied to MVP) + +- Separate data modification from drawing: precreate PSOs/bind groups; upload dynamic data once per pass. +- Use bind groups (no per-draw shader resource setup); convention: set 0 (globals), set 1 (material), set 2 (optional). +- Dynamic offsets over per-draw map/unmap; per-frame ring buffer; obey min uniform offset alignment (256 bytes on wgpu). +- Avoid base-instance tricks and push-constant reliance; stick to uniform/SSBO with dynamic offsets. +- Pack meshes later; MVP uses per-mesh vertex/index buffers; introduce packing after correctness (Milestone 8 optional). + +### Surface/window init via WindowComponent (wgpu-correct) + +- Choose `SurfaceConfiguration` from capabilities: + - Format: pick first sRGB-capable if present (prefer `Rgba8UnormSrgb`), else first supported. + - Usage: `TextureUsages::RENDER_ATTACHMENT` (+ `TEXTURE_BINDING` for offscreen targets later). + - Present mode: `Fifo` for vsync=true; for vsync=false try `Mailbox` then `Immediate`, else fall back to `Fifo`. + - Alpha: default `CompositeAlphaMode::Auto`. +- Handle `SurfaceError` properly: `Lost|Outdated` → reconfigure; `Timeout` → skip frame; `OutOfMemory` → exit gracefully. +- Depth: create a depth texture per size change (`Depth24Plus` or `Depth32Float`), usage `RENDER_ATTACHMENT`. + +### Core minimal public API (stable names; MVP may leave some unimplemented until their milestone) + +```rust +pub struct Renderer; +impl Renderer { + pub fn new_from_window_component(window: &WindowComponent) -> anyhow::Result; + pub fn master(&mut self) -> MasterPipeline; + + // Views (Milestone 3) + pub fn get_view_from_camera(&self, cam: &CameraView) -> ViewId; + pub fn get_view_from_entities<'a, I>(&self, cam: &CameraView, entities: I) -> ViewId + where I: IntoIterator>; + + // Resource access (Milestone 2) + pub fn resources(&mut self) -> ResourceManager; +} + +pub struct MasterPipeline; +impl MasterPipeline { + pub fn add_pass(&mut self, pass: RenderPassDesc) -> PassId; + pub fn remove_pass(&mut self, id: PassId); + pub fn set_order(&mut self, new_order: &[PassId]); +} + +pub struct RenderPassDesc { + pub name: &'static str, + pub target: TargetDesc, // swapchain or offscreen texture + pub clear: ClearDesc, + pub subpipeline: Subpipeline, +} + +pub enum TargetDesc { Swapchain, Texture { tex: Handle, level: u32, layer: u32 } } + +pub struct Viewport { pub x: u32, pub y: u32, pub w: u32, pub h: u32 } + +pub struct ClearDesc { pub color: Option<[f32;4]>, pub depth: Option, pub stencil: Option } + +pub struct Subpipeline { + pub pipeline: Handle, // PSO (wgpu::RenderPipeline) + pub globals: Handle, // set 0 + pub draws: DrawRecipe, // how to record draws +} + +pub enum DrawRecipe { + FromView { view: ViewId, material_set: Handle, shader_set: Option> }, + Inline(Box), +} + +pub struct DrawListBuilder; +impl DrawListBuilder { + pub fn set_pipeline(&mut self, pso: Handle); + pub fn set_bind_groups_with_offsets( + &mut self, + g0: Handle, + g1: Option>, g2: Option>, + offs0: Option, offs1: Option, offs2: Option + ); + pub fn set_mesh(&mut self, mesh: Handle, base_vertex: u32, base_index: u32); + pub fn push_per_draw(&mut self, data: &T) -> u32; // returns 256B-aligned dynamic offset + pub fn draw_indexed(&mut self, index_count: u32, first_index: u32); +} + +pub struct ResourceManager; +impl ResourceManager { + pub fn create_texture(&mut self, desc: TextureDesc) -> Handle; + pub fn create_buffer(&mut self, desc: BufferDesc) -> Handle; + pub fn create_bind_group(&mut self, desc: BindGroupDesc) -> Handle; + pub fn create_pipeline(&mut self, desc: GraphicsPipelineDesc) -> Handle; + pub fn upload_mesh(&mut self, mesh: CpuMesh) -> Handle; +} + +// Milestone 3 +pub struct CameraView { pub view: glam::Mat4, pub proj: glam::Mat4, pub frustum: Frustum } +#[repr(transparent)] pub struct ViewId(u32); + +pub struct EntityRenderProxy<'a> { + pub world_from_object: glam::Mat4, + pub mesh: Handle, + pub material: Handle, + pub bounds_ws: Aabb, + pub user_data: &'a dyn std::any::Any, +} +``` + +### WGSL shader correctness (M2 baseline) + +- Use WGSL (portable across native/web). Uniforms must respect WGSL alignment; per-draw UBO struct padded to 256 bytes. +- Example (unlit color) minimal pair: +```wgsl +// globals @group(0) @binding(0) +struct Globals { view_proj: mat4x4 }; +@group(0) @binding(0) var G: Globals; + +struct VSIn { @location(0) pos: vec3; }; +struct VSOut { @builtin(position) pos: vec4; }; + +@vertex fn vs_main(input: VSIn) -> VSOut { + var out: VSOut; + out.pos = G.view_proj * vec4(input.pos, 1.0); + return out; +} + +@fragment fn fs_main() -> @location(0) vec4 { return vec4(1.0, 0.7, 0.3, 1.0); } +``` + + +### Milestones (incremental, debuggable) with API-correctness notes + +1) Bootstrap: Black Window + +- Expected result: Black frame; vsync honored; resizes OK. +- Client API: `Renderer::new_from_window_component(&wc)?;` run loop; no passes. +- Implementables: + - winit + wgpu init; capabilities-driven `SurfaceConfiguration`. + - Depth texture created but unused yet; recreate on resize. + - Surface error handling (Lost/Outdated/Timeout/OutOfMemory). +- wgpu correctness: + - Use `surface.get_capabilities(&adapter)` for format/present mode. + - Set `usage: RENDER_ATTACHMENT` and correct `alpha_mode`. + +2) Hello Mesh: Single Pass, Inline Draw + +- Expected result: Pill mesh or fallback triangle; fixed MVP (identity or simple ortho) hardcoded; depth test on. +- Client API: Create mesh + pipeline; `Inline` pass records one draw. +- Implementables: + - `ResourceManager`: create vertex/index buffers with `VERTEX|INDEX|COPY_DST` usages. + - Minimal pipeline: WGSL VS/FS, color target = surface format, depth = `Depth24Plus`. + - `DrawListBuilder` encodes: set pipeline, set vertex/index buffers, set bind groups, draw. +- wgpu correctness: + - Index format `Uint32` or `Uint16` consistent with buffer. + - Pipeline layout: explicit `BindGroupLayout` for globals even if empty; avoid implicit layout. + - Clear ops via `RenderPassColorAttachment` and depth attachment. + +3) Camera + FromView Culling + +- Expected result: Camera matrices applied; frustum culling removes offscreen entities. +- Client API: Build `CameraView`; `get_view_from_entities`; pass uses `FromView`. +- Implementables: + - CPU frustum planes; AABB test; build draw list from filtered entities. + - Per-entity MVP uniform placed in per-frame uniform buffer; 256B alignment. +- wgpu correctness: + - Uniform buffer alignment (offset multiple of 256); `BindGroupLayoutEntry` for uniform with `has_dynamic_offset: true`. + - Call `set_bind_group(group_index, &bg, &[offset])` matching the layout. + +4) Materials + Textures (set 1) + +- Expected result: Textured mesh with material BG; per-entity tint supported from UBO. +- Client API: `create_texture`, `create_bind_group(material_desc)`; assign material handle. +- Implementables: + - Texture creation with `TEXTURE_BINDING | COPY_DST`; view with correct `TextureViewDescriptor`. + - Sampler (filtering); material BG layout: `@group(1) @binding(0) texture_2d`, `@group(1) @binding(1) sampler`. + - FS samples sRGB texture; output to sRGB surface. +- wgpu correctness: + - Use sRGB surface format and sRGB textures for correct gamma; or convert in shader. + +5) Per-frame Ring Buffer + Dynamic Offsets + +- Expected result: No per-draw map/unmap; dynamic uniform offsets. +- Client API: `push_per_draw` returns aligned offset; pass offsets via `set_bind_groups_with_offsets`. +- Implementables: + - CPU staging buffer (MAP_WRITE) per frame; `queue.write_buffer` into GPU-visible uniform buffer. + - Layout entries with `has_dynamic_offset=true` only for dynamic bindings; offsets are multiples of 256. +- wgpu correctness: + - Avoid mapping GPU-only buffers; prefer `write_buffer` for simplicity. + +6) Multi-pass + Offscreen Targets + Quad Composition + +- Expected result: Offscreen render (T0) → FSQ/quads composite to swapchain; quads can place T0 anywhere in the framebuffer. +- Client API: Add two passes: `Texture` then `Swapchain`; post uses `Inline` to draw a quad (or fullscreen triangle) sampling T0 with UVs that map to desired target rect. +- Implementables: + - Offscreen target texture with `RENDER_ATTACHMENT | TEXTURE_BINDING`; correct view format; sampler. + - Composition shader samples T0; quad vertex data/uniforms carry position/size; no viewport/scissor used. +- wgpu correctness: + - Presentable texture cannot be sampled; offscreen must be separate texture. + +7) Resource Pools + Hot/Cold Split + +- Expected result: Opaque `Handle` pools; safe reuse; minimal hot metadata. +- Client API: Unchanged; `ResourceManager` returns handles. +- Implementables: + - Freelist + generation; hot (wgpu handles + light metadata) vs cold (descs, debug names). +- wgpu correctness: + - Keep GPU handles alive until all pass encoders referencing them are submitted. + +8) Draw List Optimization + +- Expected result: Fewer state changes; sorted by PSO/material. +- Client API: Optional sort keys; otherwise internal. +- Implementables: + - Build sort keys; stable sort; encode draws. +- wgpu correctness: + - Ensure binds set when changing PSO/material; match pipeline layouts. + +### Client usage snapshots + +- M1: just create renderer from `WindowComponent`. +- M2: create mesh + pipeline; one Inline pass to draw. +- M3: create `CameraView`, build `ViewId`, use `FromView`. +- M4–M6: add material BG + textures; add offscreen + post; use viewport rects. + +### Files to add later + +- `engine/pill_renderer/DESIGN.md` (this document) +- `engine/pill_renderer/src/renderer.rs` +- `engine/pill_renderer/src/pass.rs` +- `engine/pill_renderer/src/resources/*` +- `engine/pill_renderer/src/view.rs` + +### References + +- wgpu: `https://docs.rs/wgpu/latest/wgpu/` +- winit: `https://docs.rs/winit/latest/winit/` +- WGSL: `https://www.w3.org/TR/WGSL/` \ No newline at end of file From 8ee28c424e0a7621eedeeefc5a005ba44a97089c Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Mon, 13 Oct 2025 23:00:53 +0200 Subject: [PATCH 05/69] docs --- engine/pill_renderer/PLAN_v0.md | 294 +++++++ engine/pill_renderer/PLAN_v1.md | 240 ++++++ engine/pill_renderer/TALK_0.md | 1357 +++++++++++++++++++++++++++++++ 3 files changed, 1891 insertions(+) create mode 100644 engine/pill_renderer/PLAN_v0.md create mode 100644 engine/pill_renderer/PLAN_v1.md create mode 100644 engine/pill_renderer/TALK_0.md diff --git a/engine/pill_renderer/PLAN_v0.md b/engine/pill_renderer/PLAN_v0.md new file mode 100644 index 00000000..27766f20 --- /dev/null +++ b/engine/pill_renderer/PLAN_v0.md @@ -0,0 +1,294 @@ +# Pill Renderer vNext (wgpu) API Design + +### Goals + +- Modern, low-overhead rendering on wgpu (no GL-era patterns). +- Master pipeline initializes the window from a global `WindowComponent` and renders nothing by default. +- Users can define subpipelines/passes in game code, add them at runtime, and control targets/rects. +- Views: `get_view(camera)` and `get_view(entities)` produce culled renderables for passes. +- Stable API with prebuilt pipelines, bind groups, and generational handles. + +### Principles distilled from TALK_0.md + +- **Separate data modification from drawing**: precreate pipelines, bind groups; upload dynamic data per-pass in batches. +- **Generational handles + hot/cold SoA** for resources (textures, buffers, pipelines, materials). +- **Three bind group slots** convention (globals, material, shader-specific), dynamic offsets for temp data. +- **Minimize API calls per draw**: packed meshes, prebound resources, small draw metadata. + +### High-level architecture + +``` ++---------------------------+ +---------------------------+ +| game.rs (user code) | | pill_renderer (wgpu) | +| - builds passes | uses | - Renderer | +| - defines views +------->| - MasterPipeline | +| - registers renderables | | - PassBuilder/PassGraph | ++---------------------------+ | - ResourceManager (pools) | + | - ViewBuilder + Culling | + | - FrameTempAllocator | + +---------------------------+ +``` + +### Surface/window init via WindowComponent + +- `WindowComponent` fields consumed at init: `width`, `height`, `vsync`, `fullscreen/windowed`, `title`. +- Map to winit + wgpu `SurfaceConfiguration`: + - `vsync=true` → `PresentMode::Fifo`. + - `vsync=false` → prefer `Mailbox` else `Immediate` if supported; fall back to `Fifo`. + - Auto-pick `TextureFormat` via `surface.get_capabilities().formats[0]` unless overridden. + +### Core types (public API) + +```rust +// Handles (32-bit index + 32-bit gen) +#[repr(transparent)] pub struct Handle(u64); + +// Renderer creation +pub struct Renderer; +impl Renderer { + pub fn new_from_window_component(window: &WindowComponent) -> anyhow::Result; + pub fn master(&mut self) -> MasterPipeline; + + // Views + pub fn get_view_from_camera(&self, cam: &CameraView) -> ViewId; + pub fn get_view_from_entities<'a, I>(&self, cam: &CameraView, entities: I) -> ViewId + where I: IntoIterator>; +} + +// Master pipeline is empty; users add passes at runtime +pub struct MasterPipeline; +impl MasterPipeline { + pub fn add_pass(&mut self, pass: RenderPassDesc) -> PassId; + pub fn remove_pass(&mut self, id: PassId); + pub fn set_order(&mut self, new_order: &[PassId]); +} + +// Pass description (immutable after build) +pub struct RenderPassDesc { + pub name: &'static str, + pub target: TargetDesc, // swapchain or texture + pub viewport: Option,// rect in window + pub clear: ClearDesc, // per-attachment clears + pub subpipeline: Subpipeline, // PSO/shaders, draw list recipe +} + +pub enum TargetDesc { Swapchain, Texture { tex: Handle, level: u32, layer: u32 } } + +pub struct Viewport { pub x: u32, pub y: u32, pub w: u32, pub h: u32 } + +pub struct ClearDesc { pub color: Option<[f32;4]>, pub depth: Option, pub stencil: Option } + +// Subpipeline: bind groups, pipeline, and a draw callback/recipe +pub struct Subpipeline { + pub pipeline: Handle, // PSO + pub globals: Handle, // set 0 + pub draws: DrawRecipe, // how to build draw list per frame +} + +pub enum DrawRecipe { + // Renderer builds draws from a View (culled scene) + FromView { view: ViewId, material_set: Handle /* set 1 */, shader_set: Option> /* set 2 */ }, + // User supplies a closure to record draws + Inline(Box), +} + +// View building and culling +pub struct CameraView { pub view: glam::Mat4, pub proj: glam::Mat4, pub frustum: Frustum } +#[repr(transparent)] pub struct ViewId(u32); + +pub struct EntityRenderProxy<'a> { + pub world_from_object: glam::Mat4, + pub mesh: Handle, + pub material: Handle, + pub bounds_ws: Aabb, // for culling + pub user_data: &'a dyn std::any::Any, // optional +} + +// Draw list +pub struct DrawListBuilder; // state-sorted writes; per-frame temp data +impl DrawListBuilder { + pub fn set_pipeline(&mut self, pso: Handle); + pub fn set_bind_groups(&mut self, g0: Handle, g1: Option>, g2: Option>); + pub fn set_mesh(&mut self, mesh: Handle, base_v: u32, base_i: u32); + pub fn push_per_draw(&mut self, data: &T) -> u32; // returns dynamic offset + pub fn draw_indexed(&mut self, index_count: u32, first_index: u32); +} + +// Resource creation (pre-pass) +pub struct ResourceManager; +impl ResourceManager { + pub fn create_texture(&mut self, desc: TextureDesc) -> Handle; + pub fn create_buffer(&mut self, desc: BufferDesc) -> Handle; + pub fn create_bind_group(&mut self, desc: BindGroupDesc) -> Handle; + pub fn create_pipeline(&mut self, desc: GraphicsPipelineDesc) -> Handle; + pub fn upload_mesh(&mut self, mesh: CpuMesh) -> Handle; // packed mesh heap +} +``` + +### Data ownership and performance + +- All resources live in **typed generational pools** with hot/cold split: + - Hot: GPU handles, minimal render-time metadata (format, size, bind group ids). + - Cold: debug name, creation descriptors, CPU-side helpers. +- No per-draw map/unmap. Dynamic per-draw data is uploaded through a per-frame **ring buffer** (COPY_DST) and bound via **dynamic offsets**. +- Bind group convention: + - set 0: pass globals (camera, sun, shadow maps). + - set 1: material (textures + material UBO/SSBO). + - set 2: shader-specific (optional). + +### Pass graph and execution + +``` +Frame N: ++-------------+ +-------------+ +-------------+ +| Pass A | --> | Pass B | --> | Pass C | +| target=T0 | | target=T1 | | target=Swap | ++-------------+ +-------------+ +-------------+ + writes T0 reads T0 reads T1 +``` + +- Default: linear order; optional read-after-write deps declared in `RenderPassDesc` for future graphing. +- Each pass gets a viewport; swapchain always presents full frame, viewport limits draw area. + +### How users create renderables from scene entities + +- Provide two adapters: + - `renderer.get_view_from_camera(cam)` → returns `ViewId` with built frustum. + - `renderer.get_view_from_entities(cam, iter)` → users pass an iterator of `EntityRenderProxy`; renderer does frustum culling and produces a `DrawList` internally for `DrawRecipe::FromView` passes. +- Users can also bypass views and push draws manually via `DrawRecipe::Inline` for full-screen quads, gizmos, etc. + +### Client usage (game.rs excerpts) + +```rust +fn start(&self, engine: &mut Engine) -> Result<()> { + // Window setup via component + let wc = WindowComponent { width: 1280, height: 720, vsync: true, windowed: true, title: "Pill".into() }; + + // Create renderer + let mut renderer = pill_renderer::Renderer::new_from_window_component(&wc)?; + + // Create resources + let mut rm = renderer.resources(); + let mesh = rm.upload_mesh(load_obj("models/pill.obj")?); + let globals_layout = layouts::globals(); // provided by engine or shader gen + let material_bg = rm.create_bind_group(material_desc()); + let pipeline = rm.create_pipeline(pipeline_desc(globals_layout, material_layout(), swapchain_format(&renderer))); + + // Build camera + let cam = CameraView::from_transform_fov(transform, fov_y, aspect, near, far); + + // Define a view from entities + let view = renderer.get_view_from_entities(&cam, engine.iter_renderables()?); + + // Build an opaque pass targeting the swapchain + let pass_id = renderer.master().add_pass(RenderPassDesc { + name: "Opaque", + target: TargetDesc::Swapchain, + viewport: Some(Viewport { x: 0, y: 0, w: wc.width, h: wc.height }), + clear: ClearDesc { color: Some([0.02,0.02,0.03,1.0]), depth: Some(1.0), stencil: None }, + subpipeline: Subpipeline { + pipeline, + globals: build_globals_bg(&mut rm, &cam)?, + draws: DrawRecipe::FromView { view, material_set: material_bg, shader_set: None }, + }, + }); + + // Fullscreen quad post-process pass (manual draws) + let fsq_pso = rm.create_pipeline(fsq_pipeline_desc(swapchain_format(&renderer))); + renderer.master().add_pass(RenderPassDesc { + name: "Post", + target: TargetDesc::Swapchain, + viewport: None, + clear: ClearDesc { color: None, depth: None, stencil: None }, + subpipeline: Subpipeline { + pipeline: fsq_pso, + globals: default_globals(), + draws: DrawRecipe::Inline(Box::new(|dl| { + dl.set_pipeline(fsq_pso); + dl.set_bind_groups(globals_bg, None, None); + dl.set_mesh(fullscreen_triangle_mesh(), 0, 0); + dl.draw_indexed(3, 0); + })), + }, + }); + + Ok(()) +} +``` + +### WGPU details and constraints + +- Dynamic offsets require matching `BindGroupLayoutEntry` with `has_dynamic_offset: true`. +- Per-frame ring buffer: allocate once `MAP_WRITE | COPY_SRC` staging on CPU, `queue.write_buffer` into GPU-visible dynamic UBO/SSBO chunks. +- Swapchain rects: presentation is full-surface; use viewport/scissor to restrict drawing area. +- Pipeline creation uses `depth24plus` or `depth32float` depth formats; precreate all PSOs at startup. + +### Files to add (later implementation) + +- `engine/pill_renderer/DESIGN.md` (this document) +- `engine/pill_renderer/src/renderer.rs` (Renderer + MasterPipeline) +- `engine/pill_renderer/src/pass.rs` (RenderPassDesc, Subpipeline, DrawListBuilder) +- `engine/pill_renderer/src/resources/*` (pools, handles, upload) +- `engine/pill_renderer/src/view.rs` (CameraView, culling) + + +### Milestones (incremental, debuggable) + +1) Bootstrap: Black Window +- Expected result: Resizable window presents a black frame based on `WindowComponent` (vsync, size). No passes. +- Expected client API: + - `let mut renderer = Renderer::new_from_window_component(&wc)?;` then run engine loop; no passes added. +- Implementables: + - winit + wgpu init; `SurfaceConfiguration` using present mode from vsync; acquire→clear→present. + - `MasterPipeline` exists but is empty; resize handled. + +2) Hello Mesh: Single Pass, Inline Draw +- Expected result: Pill mesh on screen; fallback to built-in triangle if asset missing. Fixed MVP in shader. +- Expected client API: + - Create mesh + pipeline; `master.add_pass(RenderPassDesc { draws: Inline(|dl| { set_pipeline; set_mesh; draw_indexed; }) })`. +- Implementables: + - Minimal `ResourceManager` for buffers, shader module, pipeline; depth buffer; unlit shader. + - `DrawListBuilder`: `set_pipeline`, `set_mesh`, `draw_indexed`. + +3) Camera + FromView Culling +- Expected result: Camera matrices applied; cull out-of-frustum entities. +- Expected client API: + - Build `CameraView`; `renderer.get_view_from_entities(&cam, iter)`; pass uses `DrawRecipe::FromView { view, material_set, .. }`. +- Implementables: + - Frustum planes; `EntityRenderProxy`; per-entity MVP uniform; basic sort by PSO/material. + +4) Materials + Textures (set 1) +- Expected result: Textured opaque geometry via material bind group (set 1); optional per-entity tint/scalar. +- Expected client API: + - `create_texture`, `create_bind_group(material_desc)`; assign to entities. +- Implementables: + - Texture upload (RGBA8 sRGB), sampler; material BG layout; shader samples `albedo`. + +5) Per-frame Ring Buffer + Dynamic Offsets +- Expected result: Per-draw uniforms via dynamic offsets; no per-draw map/unmap. +- Expected client API: + - `let off = dl.push_per_draw(&PerDraw); set_bind_groups(globals, Some(material), None);` offsets applied. +- Implementables: + - CPU staging + `queue.write_buffer`; layouts with `has_dynamic_offset: true`; per-frame ring. + +6) Multi-pass + Offscreen Targets + Viewport Rects +- Expected result: Render to offscreen texture, post-process FSQ to swapchain; passes honor viewport/scissor rects. +- Expected client API: + - Two passes: `TargetDesc::Texture` then `TargetDesc::Swapchain`; post pass via `Inline`. +- Implementables: + - Texture render targets; FSQ pipeline; `fullscreen_triangle_mesh`. + +7) Resource Pools + Hot/Cold Split +- Expected result: Opaque `Handle` with generational reuse; minimal hot metadata. +- Expected client API: + - Same API; resource creation returns `Handle`. +- Implementables: + - Typed pools, freelist + generation; hot/cold structs. + +8) Draw List Optimization +- Expected result: Fewer PSO/material switches; sorted draws. +- Expected client API: + - Optional user sort key; default internal order. +- Implementables: + - Build sort keys; reorder before encoding; expose state-change counters. + diff --git a/engine/pill_renderer/PLAN_v1.md b/engine/pill_renderer/PLAN_v1.md new file mode 100644 index 00000000..c740677c --- /dev/null +++ b/engine/pill_renderer/PLAN_v1.md @@ -0,0 +1,240 @@ +# Pill Renderer vNext (wgpu) – Minimal MVP, API-Correct + +### Goals + +- Lean MVP aligned with wgpu/Vulkan semantics (no GL-era patterns). +- Master pipeline from `WindowComponent`, renders nothing by default. +- Mesh visible by Milestone 2 via a single pass; complexity grows incrementally. +- Separate resource mutation from drawing; small, stable public API. + +### Hardware/API alignment from TALK_0.md (applied to MVP) + +- Separate data modification from drawing: precreate PSOs/bind groups; upload dynamic data once per pass. +- Use bind groups (no per-draw shader resource setup); convention: set 0 (globals), set 1 (material), set 2 (optional). +- Dynamic offsets over per-draw map/unmap; per-frame ring buffer; obey min uniform offset alignment (256 bytes on wgpu). +- Avoid base-instance tricks and push-constant reliance; stick to uniform/SSBO with dynamic offsets. +- Pack meshes later; MVP uses per-mesh vertex/index buffers; introduce packing after correctness (Milestone 8 optional). + +### Surface/window init via WindowComponent (wgpu-correct) + +- Choose `SurfaceConfiguration` from capabilities: + - Format: pick first sRGB-capable if present (prefer `Rgba8UnormSrgb`), else first supported. + - Usage: `TextureUsages::RENDER_ATTACHMENT` (+ `TEXTURE_BINDING` for offscreen targets later). + - Present mode: `Fifo` for vsync=true; for vsync=false try `Mailbox` then `Immediate`, else fall back to `Fifo`. + - Alpha: default `CompositeAlphaMode::Auto`. +- Handle `SurfaceError` properly: `Lost|Outdated` → reconfigure; `Timeout` → skip frame; `OutOfMemory` → exit gracefully. +- Depth: create a depth texture per size change (`Depth24Plus` or `Depth32Float`), usage `RENDER_ATTACHMENT`. + +### Core minimal public API (stable names; MVP may leave some unimplemented until their milestone) + +```rust +pub struct Renderer; +impl Renderer { + pub fn new_from_window_component(window: &WindowComponent) -> anyhow::Result; + pub fn master(&mut self) -> MasterPipeline; + + // Views (Milestone 3) + pub fn get_view_from_camera(&self, cam: &CameraView) -> ViewId; + pub fn get_view_from_entities<'a, I>(&self, cam: &CameraView, entities: I) -> ViewId + where I: IntoIterator>; + + // Resource access (Milestone 2) + pub fn resources(&mut self) -> ResourceManager; +} + +pub struct MasterPipeline; +impl MasterPipeline { + pub fn add_pass(&mut self, pass: RenderPassDesc) -> PassId; + pub fn remove_pass(&mut self, id: PassId); + pub fn set_order(&mut self, new_order: &[PassId]); +} + +pub struct RenderPassDesc { + pub name: &'static str, + pub target: TargetDesc, // swapchain or offscreen texture + pub clear: ClearDesc, + pub subpipeline: Subpipeline, +} + +pub enum TargetDesc { Swapchain, Texture { tex: Handle, level: u32, layer: u32 } } + +pub struct Viewport { pub x: u32, pub y: u32, pub w: u32, pub h: u32 } + +pub struct ClearDesc { pub color: Option<[f32;4]>, pub depth: Option, pub stencil: Option } + +pub struct Subpipeline { + pub pipeline: Handle, // PSO (wgpu::RenderPipeline) + pub globals: Handle, // set 0 + pub draws: DrawRecipe, // how to record draws +} + +pub enum DrawRecipe { + FromView { view: ViewId, material_set: Handle, shader_set: Option> }, + Inline(Box), +} + +pub struct DrawListBuilder; +impl DrawListBuilder { + pub fn set_pipeline(&mut self, pso: Handle); + pub fn set_bind_groups_with_offsets( + &mut self, + g0: Handle, + g1: Option>, g2: Option>, + offs0: Option, offs1: Option, offs2: Option + ); + pub fn set_mesh(&mut self, mesh: Handle, base_vertex: u32, base_index: u32); + pub fn push_per_draw(&mut self, data: &T) -> u32; // returns 256B-aligned dynamic offset + pub fn draw_indexed(&mut self, index_count: u32, first_index: u32); +} + +pub struct ResourceManager; +impl ResourceManager { + pub fn create_texture(&mut self, desc: TextureDesc) -> Handle; + pub fn create_buffer(&mut self, desc: BufferDesc) -> Handle; + pub fn create_bind_group(&mut self, desc: BindGroupDesc) -> Handle; + pub fn create_pipeline(&mut self, desc: GraphicsPipelineDesc) -> Handle; + pub fn upload_mesh(&mut self, mesh: CpuMesh) -> Handle; +} + +// Milestone 3 +pub struct CameraView { pub view: glam::Mat4, pub proj: glam::Mat4, pub frustum: Frustum } +#[repr(transparent)] pub struct ViewId(u32); + +pub struct EntityRenderProxy<'a> { + pub world_from_object: glam::Mat4, + pub mesh: Handle, + pub material: Handle, + pub bounds_ws: Aabb, + pub user_data: &'a dyn std::any::Any, +} +``` + +### WGSL shader correctness (M2 baseline) + +- Use WGSL (portable across native/web). Uniforms must respect WGSL alignment; per-draw UBO struct padded to 256 bytes. +- Example (unlit color) minimal pair: +```wgsl +// globals @group(0) @binding(0) +struct Globals { view_proj: mat4x4 }; +@group(0) @binding(0) var G: Globals; + +struct VSIn { @location(0) pos: vec3; }; +struct VSOut { @builtin(position) pos: vec4; }; + +@vertex fn vs_main(input: VSIn) -> VSOut { + var out: VSOut; + out.pos = G.view_proj * vec4(input.pos, 1.0); + return out; +} + +@fragment fn fs_main() -> @location(0) vec4 { return vec4(1.0, 0.7, 0.3, 1.0); } +``` + + +### Milestones (incremental, debuggable) with API-correctness notes + +1) Bootstrap: Black Window + +- Expected result: Black frame; vsync honored; resizes OK. +- Client API: `Renderer::new_from_window_component(&wc)?;` run loop; no passes. +- Implementables: + - winit + wgpu init; capabilities-driven `SurfaceConfiguration`. + - Depth texture created but unused yet; recreate on resize. + - Surface error handling (Lost/Outdated/Timeout/OutOfMemory). +- wgpu correctness: + - Use `surface.get_capabilities(&adapter)` for format/present mode. + - Set `usage: RENDER_ATTACHMENT` and correct `alpha_mode`. + +2) Hello Mesh: Single Pass, Inline Draw + +- Expected result: Pill mesh or fallback triangle; fixed MVP (identity or simple ortho) hardcoded; depth test on. +- Client API: Create mesh + pipeline; `Inline` pass records one draw. +- Implementables: + - `ResourceManager`: create vertex/index buffers with `VERTEX|INDEX|COPY_DST` usages. + - Minimal pipeline: WGSL VS/FS, color target = surface format, depth = `Depth24Plus`. + - `DrawListBuilder` encodes: set pipeline, set vertex/index buffers, set bind groups, draw. +- wgpu correctness: + - Index format `Uint32` or `Uint16` consistent with buffer. + - Pipeline layout: explicit `BindGroupLayout` for globals even if empty; avoid implicit layout. + - Clear ops via `RenderPassColorAttachment` and depth attachment. + +3) Camera + FromView Culling + +- Expected result: Camera matrices applied; frustum culling removes offscreen entities. +- Client API: Build `CameraView`; `get_view_from_entities`; pass uses `FromView`. +- Implementables: + - CPU frustum planes; AABB test; build draw list from filtered entities. + - Per-entity MVP uniform placed in per-frame uniform buffer; 256B alignment. +- wgpu correctness: + - Uniform buffer alignment (offset multiple of 256); `BindGroupLayoutEntry` for uniform with `has_dynamic_offset: true`. + - Call `set_bind_group(group_index, &bg, &[offset])` matching the layout. + +4) Materials + Textures (set 1) + +- Expected result: Textured mesh with material BG; per-entity tint supported from UBO. +- Client API: `create_texture`, `create_bind_group(material_desc)`; assign material handle. +- Implementables: + - Texture creation with `TEXTURE_BINDING | COPY_DST`; view with correct `TextureViewDescriptor`. + - Sampler (filtering); material BG layout: `@group(1) @binding(0) texture_2d`, `@group(1) @binding(1) sampler`. + - FS samples sRGB texture; output to sRGB surface. +- wgpu correctness: + - Use sRGB surface format and sRGB textures for correct gamma; or convert in shader. + +5) Per-frame Ring Buffer + Dynamic Offsets + +- Expected result: No per-draw map/unmap; dynamic uniform offsets. +- Client API: `push_per_draw` returns aligned offset; pass offsets via `set_bind_groups_with_offsets`. +- Implementables: + - CPU staging buffer (MAP_WRITE) per frame; `queue.write_buffer` into GPU-visible uniform buffer. + - Layout entries with `has_dynamic_offset=true` only for dynamic bindings; offsets are multiples of 256. +- wgpu correctness: + - Avoid mapping GPU-only buffers; prefer `write_buffer` for simplicity. + +6) Multi-pass + Offscreen Targets + Quad Composition + +- Expected result: Offscreen render (T0) → FSQ/quads composite to swapchain; quads can place T0 anywhere in the framebuffer. +- Client API: Add two passes: `Texture` then `Swapchain`; post uses `Inline` to draw a quad (or fullscreen triangle) sampling T0 with UVs that map to desired target rect. +- Implementables: + - Offscreen target texture with `RENDER_ATTACHMENT | TEXTURE_BINDING`; correct view format; sampler. + - Composition shader samples T0; quad vertex data/uniforms carry position/size; no viewport/scissor used. +- wgpu correctness: + - Presentable texture cannot be sampled; offscreen must be separate texture. + +7) Resource Pools + Hot/Cold Split + +- Expected result: Opaque `Handle` pools; safe reuse; minimal hot metadata. +- Client API: Unchanged; `ResourceManager` returns handles. +- Implementables: + - Freelist + generation; hot (wgpu handles + light metadata) vs cold (descs, debug names). +- wgpu correctness: + - Keep GPU handles alive until all pass encoders referencing them are submitted. + +8) Draw List Optimization + +- Expected result: Fewer state changes; sorted by PSO/material. +- Client API: Optional sort keys; otherwise internal. +- Implementables: + - Build sort keys; stable sort; encode draws. +- wgpu correctness: + - Ensure binds set when changing PSO/material; match pipeline layouts. + +### Client usage snapshots + +- M1: just create renderer from `WindowComponent`. +- M2: create mesh + pipeline; one Inline pass to draw. +- M3: create `CameraView`, build `ViewId`, use `FromView`. +- M4–M6: add material BG + textures; add offscreen + post; use viewport rects. + +### Files to add later + +- `engine/pill_renderer/DESIGN.md` (this document) +- `engine/pill_renderer/src/renderer.rs` +- `engine/pill_renderer/src/pass.rs` +- `engine/pill_renderer/src/resources/*` +- `engine/pill_renderer/src/view.rs` + +### References + +- wgpu: `https://docs.rs/wgpu/latest/wgpu/` +- winit: `https://docs.rs/winit/latest/winit/` +- WGSL: `https://www.w3.org/TR/WGSL/` \ No newline at end of file diff --git a/engine/pill_renderer/TALK_0.md b/engine/pill_renderer/TALK_0.md new file mode 100644 index 00000000..11a88864 --- /dev/null +++ b/engine/pill_renderer/TALK_0.md @@ -0,0 +1,1357 @@ +Hi, I am Sebastian Aaltonen. I have over 20 years of experience in graphics +programming. In the past have been working at Ubisoft and Unity building their cross +platform rendering technologies. + +I joined HypeHype one year ago with a mission to rewrite their mobile rendering +technology. Today I am going to be talking about the first milestone of that project: +rewriting the low level graphics API and the platform backends. + +HypeHype is a mobile game development platform. You create games directly on the +touchscreen and upload them to our cloud server. + +Gamers use a Tik Tok-style feed to browse the games. The games are instant +loading. This is a big technical challenge. Both the game binary size and the loading +code have to be highly optimized. To make the initial binary smaller, we store data in +highly compressed form and also lean on streaming. + +HypeHype has up to 8 player multiplayer. Multiplayer features and the player count +will increase in the future, once our cloud game server infrastructure is deployed. + +We have a full blown game editor inside the mobile app. Visual scripting system is +used for writing the game logic. Players can spectate creators creating games, and +multiple creators can collaboratively create games together at real time. It’s a bit like +Google Docs, but for game creation. Test play is instant and all the spectators join +multiplayer test session as players. This improves the iteration time drastically. + +We of course have a full set of social features, including chat, leaderboards, replays +and similar. + +HypeHype is mainly targeting mobile devices and tablets. But we also have a web +client and native PC and Mac applications. + +I have a console development background at Ubisoft, so I like to compare mobile +devices to the past console generations to understand them better. + +Xbox 360 and PS3 are nowadays equal to low-mid tier mobile devices in GPU +performance. This is excellent news, since these consoles offered the greatest visual +jump we have seen between console generations so far: We got HD output +resolution, and were able to implement proper HDR lighting pipelines, physically +based material models and image post processing for the first time. All of that is +possible today on mainstream mobile devices. And we can scale that down to bottom +tier devices at 30 fps with upscaling. + +When you look at the high end, you see latest 1000$+ phones reaching Xbox One +and PS4 levels of performance already. However these phones run at higher native +resolution and are thermally constrained, thus in reality, we can’t quite reach that +generation of visual fidelity yet on mobile devices in real games. And we don’t even +want to, since that would make the devices hot and drain their battery in couple of +hours. + +HypeHype games have been limited to simple visuals: Stylized untextured objects, +simple gamma space lighting and tiny scenes with short view distance. This has been +fine for simple hyper-casual games. + +This is however a big limitation to the platform, so we started building a new renderer +from the scratch one year ago. The visual fidelity target for the new renderer is to +match the best looking Xbox 360 and PS3 games. We will be bringing full PBR +pipeline with modern lighting, shadowing and post processing techniques. We will be +targeting larger game worlds and longer draw distances to allow more game genres +to be built properly on the platform. + +This is all nice of course, but we have to be really careful about the performance cost +of all these new improvements. We still want to be running HypeHype games at +locked 60 fps on mid tier mobile phones without throttling the devices. This is a big +concern for us and is the main reason why we are focusing heavily on performance in +our new rendering architecture. + +If you compare current mainstream phones with Xbox 360, you notice a lot of +similarities. + +Both designs have slow shared main memory. Bandwidth is the main limiting factor. +Both designs also employ techniques to reduce the memory bandwidth usage. The +most important one being on-chip storage for render targets. On Xbox 360 you had a +10MB EDRAM buffer for your whole render target. On mobile phones, you have a +smaller on-chip tile memory. Both technologies solve similar problems. Overdraw +doesn’t require extra memory bandwidth and Z-buffering and blending happens fully +on chip. On mobile phones, you also have framebuffer fetch, allowing you to load +back the previous pixel from the same render target location without a memory round +trip. The newer Xbox One console also was equipped with read-write ESRAM +allowing similar optimizations. + +Since the main memory is slow, you want to avoid resolving render targets as much +as possible. You want to minimize the amount of render passes. Doing multiple things +at once is the key to good performance. Modern mobile phones also have framebuffer +compression, to reduce the render target resolve and sampling bandwidth cost. This +is a good addition, but doesn’t fully solve the problem. ASTC texture compression +also helps. It offers better quality and smaller footprint than DXT5 back in the day. + +Mobile phones also have double rate fp16 math. This helps, since you don’t want to +lean on memory lookups on bandwidth starved devices. And there’s now better lower +precision HDR framebuffer formats available. + +But some old limitations still remain: Mobile GPUs are still designed around uniform +buffers. SSBO loads from dynamic addresses are still slow. If you are able to +scalarize your memory access patterns you hitting the performance sweet spot. This +limits the algorithms we are able to implement efficiently. Many mobile phones also +write vertex varyings to main memory, which costs significant amount of precious +bandwidth. Optimizing the size of the varyings is key for good performance on these +devices. + +I was talking about GPU-driven renderer already 8 years ago at SIGGRAPH, we +presented the core ideas such as the clustered rendering and the 2-pass occlusion +culling which have become a de-facto standard nowadays. + +Recently Nanite by Epic made GPU-driven rendering available for mainstream. They +combine V-buffer, material classification, analytic derivatives and software rasterizer +to make GPU-driven rendering robust enough for generic engine. + +However, there’s still a lot of unsolved performance problems with GPU-driven +rendering on mainstream mobile GPUs. + +Mobile GPUs are not optimized yet for SSBO loads. AMD and Nvidia optimized their +data paths couple of generations ago when they added ray-tracing. Ray-tracing +access patterns are dynamic and you can’t lean on tiny on-chip buffers for vertex +attributes anymore. We still need to wait for mobile GPUs with similar optimizations to +become mainstream. + +V-buffer requires you to run the vertex shader 3 times per pixel, and this includes +fetching all the vertex attributes of these 3 vertices. You also need to fetch all the +instance data and material data from dynamic location. This is over 20 non-uniform +memory loads in the pixel shader. Mobile chips aren’t simply designed for this kinds +of memory heavy workloads. + +No current mobile GPU supports framebuffer compression for compute shader writes. +Compute shader is the most efficient way to implement full screen material passes in +deferred V-buffer shading. If you do that on mobile, you waste a lot of bandwidth. + +64 bit atomics are commonly used in software rasterizers. You pack the Z value in +high bits and payload in low bits and let the atomic resolve the closest surface. +There’s no 64 bit atomic support in mobile GPUs. SampleGrad is also slow. ⅛ rate or +even slower. Which makes deferred texturing with analytic gradients quite costly. And +there support for wave intrinsics is spotty and you even have emulated groupshared +memory on some low end devices. + +As a result, traditional CPU-based rendering is still the best be for mainstream mobile +phones today. We could do 10,000 draw calls on Xbox 360 back in the day at 60 fps. +To reach that goal on mobile devices today, we need to write very well optimized +rendering code. + +Let’s talk about our roadmap. + +We split the renderer rewrite into two stages. First we rewrote the low level gfx API +and all the platform specific backend code. In order to run both the old and the new +backends in tandem, we introduce a minimal wrapper with ifdefs, so that we can keep +shipping the old rendering code and switch between the new and the old to compare +them. We have already deleted 200 files of old rendering code and recently started +tearing down the wrapper and replacing it with direct calls to the new platform API. + +This presentation will focus on the low level platform API and the backends. I will be +later talking about our new high level rendering code. Our design allows us to refactor +these pieces completely independently of each other. I will come back to that topic +later in the presentation. + +The first thing we need to decide is the platform abstraction level. What code is +platform specific and what code is platform independent. + +Game engines generally limit platform specific code to the lowest levels of the stack. +This minimizes the amount of code that is platform specific and reduces the +implementation and maintenance cost. However some engine and renderer specifics +tend to leak to the lowest level platform code. + +If you look at mobile apps, the platforms specific code tends to reach a bit higher +levels in the stack. For example the popular Google Flutter app framework is +developed by multiple platform specific teams. They usually ship new features first on +mobiles and later to desktops. Android and iOS don’t have full feature parity either. +Their high level rendering code is different on desktop and mobile platforms, including +Mac and iOS, even though they both use the same Metal API. + +Many mobile apps bring the code separation even higher. There’s often completely +separate iOS and Android team with their dedicated code bases. Most of the business +logic in these apps tends to be running in a cloud server, which is of course shared +and maintained by a third team. + +HypeHype is a real time game engine, so we of course need to have all of our world +state locally. Games must run identically on all devices and cross play must work +across all devices. The old HypeHype gfx code base had duplicated shaders for +Metal and some duplicated higher level code as well. This bloated the test matrix, +added maintenance cost and made adding new features slow. This was the first thing +I wanted to solve. The goal was to bring the platform API to even lower level +compared to existing game engines. + +We want as small amount of platform specific code as possible. This leads to a +design where we tightly wrap the existing low level gfx APIs. + +The design work started by cross referencing Vulkan, Metal and WebGPU docs. I was +already familiar with all of these APIs, which made the work easier and less error +prone. + +When writing a wrapper, you first want to find the common set of features. These are +often straightforward to wrap. The difficulties arise when there’s differences in the API +design. Care must be taken in order to abstract these differences in a way that is +performance optimal. We chose to use Metal 2.0 because it’s closer to Vulkan and +WebGPU and it provides placement heaps, argument buffers and manual fences, +allowing us to extract a bit more performance out of Apple devices too. We also +support MoltenVK to make cross platform development easier, but we don’t ship it +since our Metal 2.0 backend is roughly 40% faster for CPU. + +In order to make the API more compact, we trim all deprecated stuff that nobody uses +anymore. These things were failed experiments that never lived up to our +expectations regarding performance. Vertex buffers are an interesting topic. At +Ubisoft, we deprecated vertex buffers in our GPU-driven renderer already 8 years + +ago. But at HypeHype we still support vertex buffers, since some mobile GPU shader +compilers generate better code for them. Also we are still using WebGL2 in our web +client, since WebGPU coverage is not yet good enough. I will likely be removing +vertex buffers from the API in a few years. + +Single set of shaders is crucial for tech artist productivity. We use modern open +source tools such as SPIRV-Cross to cross compile our shaders to all target +platforms. + +Let’s talk about the design goals of this new platform API. + +First, we want it to be a standalone library. Designed and maintained independently +from the HypeHype engine. It needs to have an stable API that doesn’t change often. + +I have seen a lot of graphics platform abstractions during my career and the problem +in most abstractions is that user land concepts creep into the hardware API. Having a +mesh and material in the platform code is the most common issue. This is problematic +since mesh and material both have change pressure. Meshlets and bindless textures +are the future. We don’t want to commit to a certain way of presenting them. Mesh +can be simply represented as an index buffer binding + N vertex buffer bindings, and +material can be represented as a bind group containing multiple texture descriptors +and a buffer for value data. + +Automatic the uniform handling might feel like a good idea in the beginning, but +eventually you want to add stuff like geometry instancing and now you need to +refactor your backend code to change the data layout. Or even worse, add a new fast +path to complicate the API. And eventually you add a new fast path for GPU-driven +rendering too, bloating the API further. In our design, the user land code is +responsible for setting up all the data! + +Zero extra API overhead is another crucial design core pillar for us. The platform +interface should not add significant cost. It should be as easy to use as DX11, but +always as efficient as hand written optimized DX12. A wrong solution is to copy the +DX11 API as is. This way you end up emulating the DX11 driver in your code base, +and rest assured that Nvidia and AMD does this better than your team. Thus your +modern bankends are slower than DX11. The reason for this is that you have too fine +grained inputs, too fine grained render state, lots of shadow state and data copies. +PSO and render state tracking and caching is a big performance drain, and slow +software command buffer design usually adds to the cost. + +So, we have very strict performance standards for our API, but at the same time we +want it to be as easy to use as DX11. How can we achieve this? + +We need a good process for designing the API. + +The traditional way would be to spend months researching the API documentations +and writing a big technical design document describing the new API in detail, splitting +in into tasks and estimating the implementation time for each task for each backend. + +The issue with this approach is that you lock in the design too early and it’s hard to +change later. Small nitty gritty details matter a lot in platform specific graphics code. +You can’t really understand the performance impact of all the corner cases without +writing any code. Now you will notice those issues when there’s a lot of production +ready code written. It’s too hard to justify a full rewrite of plans and code at this point. + +Agile test driven development has the opposite problem. You are focusing on what +you need in the next sprints. You implement small independent pieces of code that +have full test coverage. The assumption is that once you put these pieces together, +you have good architecture. But you didn’t even do any architecture design. More +pieces equals more interfaces, equals more communication overhead. It is difficult to + +reach optimal performance with this kind of programming practice. And it’s even more +difficult to throw away lots of production ready code with full test coverage and lots of +story points spent once you notice that the architecture needs a big overhaul to meet +the performance goals. + +Our solution for this problem is to use a highly iterative design process. + +I start by writing mock user land code. I use my prior expertise and start writing my +dream graphics code, assuming I have the perfect API. That API doesn’t exist yet, but +I keep writing mock code until I am happy with it. I write code for creating all the +resources I need for rendering, textures, shaders, buffers and so and then I write a +small draw loop using these resources. The draw loop is called multiple times, with +some resources mutated to implement animation. It’s important to design both +dynamic and static data paths early. + +Once I am happy with the first iteration of the user land code, I write a mock platform +API for it. This is just a hollow API at this point. There’s no backend implementation. +But it allows me so start using the compiler to do syntax checking and autocomplete. +Now I can really start experimenting with the API to see how good it feels to use. I will +of course refactor all the time when I find even slightest need for it. I add missing +mock use cases and go through the Vulkan, Metal and WebGPU API docs to ensure +that I have not missed anything important. + +Then I will do a performance check for all the user land code. I have a good +understanding how all the platform APIs work, so I think what kind of implementation + +each API call would require in Vulkan and Metal and WebGPU backends. If the +implementation is trivial then it’s fine. If the implementation requires extra data copies, +hash map lookups, memory allocations or other expensive operations, then I scrap +that design and rewrite that part of the API to be more efficient. As you remember, our +goal is to be as fast as hand optimized DX12 in every single case. We can’t do that if +our API doesn’t map perfectly to the underlying hardware API. + +Once we are happy with the performance of our mock code, we start implementing +the backends. We of course notice nitty gritty details that we missed during this +process and immediately refactor the mock code and the mock API when this +happens. We don’t write heavy test suite just yet, as that would slow down our +iteration time. Instead we are leaning on Vulkan and Metal validation layers to provide +us thousands of test cases for free. We hook the validation layer error callback to our +automated tests to ensure our code keeps functioning when we refactor it. + +The last topic about API design I wanted to discuss today is doing things at right +frequency and granularity. + +A big problem in rendering code tends to be that expensive operations are done at +too high frequency. This also tends to add tracking cost to the hot draw loop. + +Games have a lot of temporal coherency. You load the game world and you slowly +mutate it every frame. Most of the data stays the same. Also the camera is most of +the time moving slowly. Human brain needs temporal coherency between the frames +to see smooth movement. This is great for us! We want to exploit it! + +Let’s take a look at all the stuff happening: Loading the game world and all the shader +PSOs happens in the beginning. If you have a larger level, you also load textures, +meshes and materials when you move around. Most of the objects are spawned in +the beginning, but sections of the level might be spawned during streaming, enemies, +loot, projectiles, etc are generally spawned throughout the game, but not that many +every frame. The only really high frequency operation is culling all the objects and +drawing the visible objects. The culling and draw loops are the most time sensitive +loops in your whole code base. + +I have highlighted problem cases with red. People tend to do processing related to +these inside their hot draw loop. + +Modifying material bindings is not common. How often do you replace the normal +map of an already loaded material? How often do you change the shader that’s used +to render an object? How often you change the render state that is used to render an +object? Pretty much never, except in some special effects. Animating object color and +object transform are more common operations. A small subset of objects animate +every frame. We only want to pay for these things when they happen. Not for every +draw call. + +Our solution for this problem is to fully separate all data modification from drawing. All +the data is ready before the draw loop. + +Pipeline state objects (PSO) should be built at application startup or at level load time. +Building PSOs at runtime causes stuttering. In our philosophy shader variants are +authored by coders and tech artists and hand optimized. There’s only a limited +amount of them. This is similar to that id-Software does and provides very good +performance. + +We store the PSO handle directly to each object’s visual component. We don’t need +hash map lookups to obtain it every frame. + +We precreate all the bind groups (descriptor sets). Material descriptor sets contain all +their textures and buffer for value data. We store the material bind group handle to +objects visual component. This avoids a hashmap lookup and makes it possible to +efficiently change the material bindings with a single Vulkan, Metal and WebGPU +command. + +Separating persistent and dynamic data is important. Persistent data is uploaded at +startup and delta updated when changed. I had a talk about this topic two years ago + +at REAC 2011. You can refer to that presentation if you want more information about +that topic. + +Dynamic data should be batch uploaded once per pass instead of using map/unmap +per draw call. Global data should be separated from per-draw data to minimize the +wasted bandwidth cost. + +Resource synchronization is a big CPU cost in many engines. Our current solution is +simple: When a render pass begins, we transition the render target to writable layout. +At the end of the render pass, we transition it back to sampled texture layout. This +way all the textures (both static and dynamic) are always in sampler readable layout. +We never need to do per-draw call resource tracking at all. This saves significant +amount of CPU cycles. + +And now we are ready to talk about the implementation details. + +A renderer needs textures, buffers, shaders and several other resource objects. We +need a good way to store these objects and ensure they are safe to use. + +The modern C++ practice would be to use smart pointers, reference counting and +RAII (resource acquisition is initialization). + +Frankly, these are too slow for us. Reference counted smart pointers tie the life time +of a reference and the backing memory together. This results in a lot of small memory +allocations. Memory allocations are expensive in current highly multithreaded +systems. Allocations are also randomly scattered around the system memory, making +data access patterns worse and increasing the cache misses. Copying reference +counted smart pointers requires two atomics (add, sub), since we are in a +multithreaded system. Ownership can be shared between threads. + +There’s also safety issues. Ref counting makes object lifetime vague. Hard to reason +about. It might die in any thread. RAII objects such as listeners cause destructors to +have side effects. Example: Object ref count runs in another thread, destructor of +listener de-registers it from an array. Another thread is just iterating that array. + +CRASH! To avoid this crash, you have to protect part of the destructor with a mutex. +This means that every time you delete an object, you need a mutex lock. This is very +expensive. HypeHype is loading and unloading games rapidly in the feed. We can’t +afford slow loading and tear down code! + +Our solution for this problem (and most other problems too) is to use arrays! + +There’s one big allocation containing all objects of the same type. Array index is a +surprisingly nice data handle. If I have an array of textures, I can simply ask texture at +index 4. Index is POD data. It’s trivial to copy around. I can pass it to worker threads +safely too. As long as that thread doesn’t have access to the array, they can do +nothing dangerous with the indices. This allows us to write culling and draw stream +generation tasks with no safety concerns. These threads simply take array indices +from one place and combine them to form draw calls. No access to the data arrays is +needed at all. + +But there’s a critical flaw: An array index doesn’t guarantee object lifetime. We of +course reuse slots in our data arrays. Data could have died and slot reused... + +To solve the problem, we need to replace our array indices with generational handles. +Let’s discuss what this means. + +A pool is similar to our data array. It is an typed array of objects, but now it also has +an additional generation counter array. Generation counter tells how many times the +slot has been reused. Counter is increased when the current data in that slot is freed. + +We also have a freelist in the pool. A freelist is simply a linear array containing the +indices of each free slots. It has stack semantics. When you allocate a new object, +you pop a free index from the top. When you delete an object you push the index of +the freed slot on top of the free list. These are both fast O(1) operations. When the +freelist runs out, we double the size of the pool. This is safe as nobody is allowed to +have direct pointer references to the data in the array. All references are done using +handles. + +The handle is just a POD struct. It contains the array index just like in the previous +slide, but now we also have the generation counter next to it. This is in total 32 bit (for +example 16+16 bit split) or 64 bits (32 + 32 bit), depending how many simultaneously +active resources you require and how short are the lifetimes of the objects. In +HypeHype we use 32 bit (16+16) handles for all graphics resources, this is enough for + +65536 resources of each particular type. + +Pool offers a getter API, which takes the handle as a parameter. It reads pool’s +generation counter array at handle index and compares it to the handle’s generation +counter. If they match, you get the data. If they don’t you get a null pointer. + +This results in weak reference semantics. It’s completely safe to use stale handles. +You just get a null pointer back. A null check is a predictable branch, and is almost +free on modern CPUs. The branch prediction fails once when the handle is deleted. +At that point you clean up yourself too. Weak references result in coding practice that +doesn’t required callbacks. Callbacks require buffering or mutexes to avoid race +conditions in multithreaded systems. + +One of our main goals was to make the API as easy to use as DX11. This requires us +to bundle auxiliary data in our data structs in the pools. In Vulkan the VkTexture +handle doesn’t know anything about itself, which is annoying if you try to write your +rendering code in pure Vulkan. We want our texture struct to know it’s size, format, +data pointer for writing, allocator for deleting it and so forth. + +This auxiliary data is required for low frequency tasks such as modifying the resource +and deleting the resource. Since our design principle is to separate resource +modifications from drawing, we are accessing this data only when the resource is +modified or deleted. This means that putting the auxiliary data in the same struct as +the data required in the hot draw loop is not cache efficient. The draw loop will load +data to L1$ that is not used. I hate trade-offs between performance and usability. + +Our solution for this problem is to use SoA layout inside the pools. We identify which +data is required every frame in the hot draw loops and put that data in one struct and +the remaining low frequency auxiliary data in another struct. The pool now has two +data arrays instead of one. We can use the same array index in the handle to access +either of the data arrays (or both). This way we only need to load the hot data to +caches in the performance critical draw loop. The auxiliary data struct is only loaded +at low frequency, solving our performance issue with L1$ cache utilization. + +Now we have a good way to store and refer to graphics resources. The next topic is +creating the resources. + +Creating graphics resources in Vulkan and DX12 is cumbersome. You need to fill big +structs that contain other big structs. Some of these structs also contain pointers to +arrays of structs too. This makes it possible to shoot yourself in the foot with +temporary object life times. + +The most common existing solution for this problem is using builder pattern for +resource descriptors: The builder object contains good default state for the descriptor. +It offers an API to mutate itself to set all the fields you want to change. Once you are +ready, you call build function to get the final descriptor struct. This is easy to use, but +the codegen, especially in debug mode is far from perfect. At HypeHype we use +debug mode a lot during development, so we want it to be fast too. + +Our solution for this problem is to use C++20 designated struct initializers in +combination with C++11 struct aggregate initialization. These two features in +combination allow us to set default values to each struct in a trivial way. Look at the +code example box below. If you want to override one of these defaults, you use the +designated struct initializer syntax to override the values of named fields. The syntax + +is super clean and codegen is perfect. + +To solve the array data cleanly, we have to write our own span class. C++20 built-in +span class doesn’t support initializer lists, because initializer lists have very short life +time. They die immediately after the statement. It was too dangerous to allow putting +initializer list inside a span in the generic case. However we use this only in a special +case, and we have a solution for it: C++ const && function parameter only accepts +temporary unnamed objects. C++ guarantees that temporary objects in function +parameter list live long enough to finish the function call. This gives us enough +guarantees to safely store initializer lists inside spans in our resource descriptor +structs. + +And this is how it looks in practice. + +Let’s start with the left side: First we are creating a vertex buffer and a texture. The +syntax here is nice and we are only declaring fields that differ from the struct default +values. + +If you look at the bottom left, you see us declaring a material. This is a bind group. +The bind group has an array of textures: albedo, normal and properties. We are using +initializer list here to provide the array. This makes the syntax super clean. And it’s +worth noting that this array doesn’t require any heap allocations. The initializer list and +the whole descriptor struct lives in the stack. It is never copied. We just pass a +reference to it in the resource creation function call. This is as fast as raw DX12 or +raw Vulkan. + +One the right side you see us initializing a more complex resource. This looks a bit +like json. We have named fields, arrays and fields and arrays inside each other with +proper indentation. This is much more easier to write and read compared to raw DX12 +and Vulkan. Yet still we pay no runtime cost. There’s still no memory allocations or +data copies. Everything is pure stack data. + +Now that we have a good way to create and store resources, we need to allocate +GPU memory for them. + +I prefer to use temporary memory whenever possible. Temporary memory doesn’t +fragment your memory pools and allocating it is as simple as adding a number to a +counter. + +We use 128MB memory heaps in our bump allocator. The heaps are stored in a ring. +If the bump allocator reaches the tail, we allocate a new heap block. Once we reach +stable state, there’s no heap allocations happening at all. We create a platform +specific buffer handle for each GPU heap we create. This buffer handle maps the +whole heap. This way we don’t need to create platform specific buffer objects at +runtime. Our buffer struct simply contains an heap index and an offset. It’s super +efficient to construct them at runtime and pass to user. + +As an extra optimization, we provide the user land a concrete bump allocator object. +This has a function to allocate N bytes. This function inlines perfectly to the caller. It +simply increments a counter and then tests whether the counter is over the heap +block boundary. This check is a predictable branch. When the block runs out, we call +a virtual function in th gfx API to obtain the new temp allocator block. This happens + +only once for 128MB of data, making it highly efficient. + +Since WebGPU doesn’t yet have 100% coverage, we had to add WebGL2 support +during the project. We use the same temp allocator abstraction for WebGL2. User +land code doesn’t need to know whether the returned pointer is a CPU pointer or a +GPU pointer. In WebGL2 we use 8MB CPU side temp buffers and we copy these +buffers using a single glBufferSubData at beginning of each render pass. This +amortizes the cost of data updates, and is a big performance win over calling +map/unmap per draw call. + +We do persistent allocations only when needed, since persistent allocation is always +much slower than temporary. + +I implemented a two-level segregated fit algorithm. This is O(1) hard real time +allocator. It uses a two level bitfield and two lzcnt instructions to find the bin. Bin size +classes follow floating point distribution. This guarantees that overhead percentage is +always small, independent of the size class. Delete operation is similar to allocate. +But in addition you are checking neighbor pointers on both sides and merging empty +memory regions. This is also O(1). + +We use the same allocator for both Vulkan and Metal 2.0 (placement heaps). I open +sourced the offset allocator. It can be used for sub-allocating GPU heaps or buffers, +and generally anything that requires a contiguous range of elements (and doesn’t +require CPU memory backing for embedded metadata). + +One of the biggest differences our design has towards other renderers is user land +bind groups (descriptor sets in Vulkan terminology). + +The traditional way is to have separate bindings for each texture and buffer. Before +drawing you set all bindings separately. The gfx backend has to combine these +bindings in shader specific layout and create respective bind groups (WebGPU), +descriptor sets (Vulkan), argument buffers (Metal) or descriptor tables (DX12). These +bind group objects are GPU objects and are expensive to create. IHVs recommend +you to precreate all GPU objects to avoid stalls and memory fragmentation issues. + +The common workaround is to cache bind groups in a hash map in the backend. All +bindings are hashed and a lookup is made. If the bind group exists, then it is reused +instead of created. The problem with this approach is that hashing is expensive and +hash map lookups randomize your memory access pattern. If you are rendering from +multiple threads, you might even need to protect your bind group hash with a mutex, +making it even more expensive. + +Our solution is to bring bind groups directly to user land: User creates immutable bind +groups ahead of time. For example a material bind group contains 5 textures and one +uniform buffer (filled with value data). You get a handle, which you use to bind the + +material. + +Our draw call API exposes three bind group slots to the user land. Vulkan on Android +and WebGPU mandate minimum of four bind group slots. Three first groups are +exposed directly to use land code, matching the GLSL set=X semantics. This is easy +for gfx programmers and tech artists to understand. + +HypeHype higher level rendering code uses an convention to split data to bind groups +by binding frequency. The first group has render pass global bindings (sun light, +camera matrices, shadow maps, etc), the second slot has material bindings, third slot +has shader specific bindings and the last slot is special. + +We use the last slot in Vulkan and WebGPU for dynamic offset bound buffers. This is +important for bump allocated temporary data, such as uniform buffers. Metal API +doesn’t have similar offset update API for argument buffer buffer bindings. Instead we +use Metal setBuffer API to set these dynamic buffers separately, and use setOffset +API to change their offset. This provides an abstraction that uses the most efficient +code paths on all platform APIs. + +Push constants are emulated on some mobile GPUs. It’s faster to bump allocate your +uniforms and just change the offset. + +I already said that software command buffers are slow, yet we have one :) + +This software command buffer is entirely different to the ones most people are familiar +with. We don’t have any data in the software command buffer. We only have +metadata pointing to already uploaded data. The metadata is also grouped, making it +much smaller than individual bindings and individual state. This allows us to represent +a draw call with only 64 bytes of data, which is just a single CPU cache line. + +Our initial design was to use an array of draw structs. The draw struct contains +handles to the shader (this is a resolved PSO variant including all render state), 3 +user land bind groups, dynamic buffers (for temp allocated offset bound data), index +and vertex buffers and some offsets. Offsets are needed because sub-allocating +resources is usually a big performance win. + +This 64 byte struct is already pretty good, but I wanted to improve it further. I analyzed +the data and noticed that all fields are 32 bits. Optimized rendering uses sorted order +to minimize the costly PSO and render state transitions. When rendering binned +content we notice that most fields don’t change between draw calls. On average only +18 bytes change between the draws. We want to take advantage of this. + +The idea is to store only the fields that change. This leads to a draw stream design. + +We store a 32 bit bitmask in front of each draw call. This bit mask tells which fields in +the draw struct have changed. + +It’s the responsibility of the user land code to write data according to the stream data +API contract. For this we have user land draw stream writer class. It contains a single +draw struct describing the current state and a dirty mask. The draw stream writer +provides an function for setting each field in the struct. These functions check whether +the data value was changed. If yes, then set the corresponding dirty bit and write that +field to the stream. After writing all fields the user calls draw, which simply writes the +dirty bitmask in front of the data values. + +The backend is simple: For each draw call it reads the dirty bitmask. Then it reads +one uint32 from the stream for each bit and calls the corresponding platform API call +to set that binding/state/value. The advantage of this design is that the backend +doesn’t need any state filtering. We have already done that in the user land code. +This is handy on platforms where secondary command buffers are not available or +are slow (some Qualcomm GPUs disable optimizations with secondary command +buffers). We can still generate draw stream using multiple worker threads and offload + +the state filtering cost there. The render thread is as fast as possible, which is a big +win since the platform API calls are slow on mobile devices. We also save roughly 3x +bandwidth versus full blown 64 byte structs. + +Let’s talk about draw call performance. + +This slide represents a quite traditional DX11 and OpenGL style draw loop. For each +draw call we call map/unmap and write uniforms separately. We also bind vertex +buffer and index buffer and we bind our textures and buffers. Here I am simply +binding set 2 (material) and set 3 following the conventions we have. + +In total this is 6 to 7 API calls per draw call. 6 calls when the material doesn’t change +and 7 otherwise. If we bin by material, then we can assume that the number is closer +to 6 than 7. + +This is using the temp allocator to bump allocate uniforms (and other dynamic data). +Now we don’t need to call map/unmap per draw call. This reduces the API call count +to 4-5 per draw call. + +Map/unmap are surprisingly expensive calls. Our old GLES backend was uploading +uniforms per draw call. The biggest difference in our new GLES3 backend (WebGL2) +was the lack of map/unmap per draw and this change alone got around 3x CPU +performance gain for us. + +We didn’t implement per draw map/unmap to our new Vulkan backend (Vulkan +supports persistent mapping), so I can’t unfortunately show you Vulkan numbers +here. + +The next optimization with big impact was packing meshes. We allocate big 128MB +heap blocks and have one platform buffer handle for each. This makes it easy for us +to sub-allocate meshes and simply change the base vertex and base index in each +draw call to change the mesh. + +This way we get rid of two API calls: set vertex buffer and set index buffer. We are +down to 2-3 API calls per draw, which is very nice! + +This optimization improved the CPU throughput on all devices. We saw biggest gains +on desktop GPUs (close to 2x), but mobile GPUs also showed notable gains (30%- +40%). + +The last optimization I want to discuss is base instance. + +Base instance drawing uses identical data layout as instancing uses. You use tightly +packed array of draw data. On mobile uniform buffers have 16KB binding size +limitation. The idea is to change the binding offset once per 16KB, amortizing the cost +of rebinding the temp allocator buffer with a different offset. This cuts our API call +count by 1 and we now have optimal amount of API calls: just the draw itself and the +possible material bind group change. The draw call has base instance parameter, +which we change to point to different slot in the uniform buffer data array. + +So why not use instancing instead? Base instance results in better shader codegen +on many platforms. The reason is that instance ID is dynamic offset. GPUs pack +multiple instances in the same vertex wave, meaning that all data indexed by instance +ID must use vector registers and vector loads. This is a lot of extra register bloat for +loading 4x4 matrices and similar. Base instance on the other hand is a static per-draw +offset. Every lane loads from the same location. This means that compilers can scalar +code paths and/or use fast constant buffer hardware. + +In practice however, we run into various issues. While the base instance codegen is +perfect on PC, on mobile GPUs it’s a mixed bag. Some drivers simply don’t optimize + +this properly. Also this feature has poor coverage. DX12 doesn’t support base +instance at all and WebGL and WebGPU also have no support. So I wouldn’t +recommend this optimization, unless you are shipping only on desktop. Not worth it +for mobiles. + +Let’s take a look at the performance numbers. + +This is using a single render thread. Ten thousand actual draw calls without any +instancing tricks. Each draw call using an unique mesh and unique material. With +bind groups and packed meshes it’s fast to change the material and the mesh. + +I didn’t have time to implement GPU-persistent scene data yet in HypeHype. These +numbers are with per-draw bump allocated uniforms, as described in the previous +slides. + +We are targeting 10k draw calls because that’s what we managed to push 15 years +ago with Xbox 360 at 60 fps. And the results are impressive. Even the low end 99$ +Android phones are close to hitting 60 fps in this stress test. In a real kit bashed UGC +game scene we will have lots of repeated meshes and materials, allowing batching +and reduction in gfx API call counts. We also intend to multithread the rendering. + +On AMD’s modern integrated GPUs (found also in the Steam Deck and ROG Ally +handheld) our renderer can push 10k draws in less than one millisecond. When +multithreading is used, our renderer could push up to 1 million draw calls at 60 fps on +modern AMD and Nvidia GPUs. + +--- + +## SIGGRAPH 2023 Presentation Slides + +### Thin low level gfx API wrapper +- Cross reference Vulkan, Metal and WebGPU docs +- Find the common set of features and differences +- Design performance optimal way to abstract the differences +- Metal 2.0: Placement heaps, argument buffers, fences + - MoltenVK for debugging. Our Metal 2.0 backend is ~40% faster (CPU) + +### Trim deprecated stuff +- Transform feedback, strips, fans +- Geometry shaders, HW tessellation +- Vertex buffers? + - Some mobile devices still benefit (shader codegen) and WebGL2! + +### Single set of shaders +- GLSL and use SPIRV-Cross to cross compile [4] + +--- + +## Hype → NEW Architecture + +``` ++---------------------------------+ +| Business logic | ++---------------------------------+ +| Data model | ++---------------------------------+ +| High level rendering | ++---------------------------------+ +| Shaders | ++---------------------------------+ +| Low level rendering | ++---------------------------------+ +| GFX API calls | ++---------------------------------+ +``` + +--- + +## Platform API Design Goals + +**Standalone library** +- Independently designed and maintained. Stable API +**Avoid higher level concepts creeping into the API** +- No mesh & material: Can be represented as IB+VBs and bind groups +- No automatic data setup or forced data layout +- No fixed draw algorithm: Traditional, instancing. Future = GPU-driven +- **User land code responsible for setting up all the data!** +**"Zero" extra API overhead** +- Design core pillar: As easy as DX11, as fast as optimized DX12 +- Wrong solution: Implement DX11 driver in your code base + - Fine-grained inputs, render state, shadow state, copies + - PSO + render state caching, bind group caching (hash tables) + - Software command buffers + +**Architecture Diagram:** +``` ++---------------------------------+ +| High level rendering code | +| | +| Changes frequently | ++---------------------------------+ + | + | ++---------------------------------+ +| Platform | +| API | ++---------------------------------+ <-- Standalone library + | + | ++---------------------------------+ +| Backends | +| | +| Vulkan. OpenGL ES 3.0 | +| M WebGL. 2.0 | +| WebGPU | ++---------------------------------+ +``` + +--- + +## Our Solution: Separate Data Modification from Drawing + +**PSOs** +- Build all pipelines (all render state combinations) at application startup. Doable since our PSO count is low +- Store the PSO handle to each objects visual component + +**Bind groups (descriptor sets)** +- Create a bind group per material at level load: Contains all texture and buffer bindings +- Store the material bind group handle to each objects visual component +- Changing the material = a single Vulkan, Metal, WebGPU command + +**Data upload** +- **Persistent data:** Upload once at startup. Delta update when data changes. [5] +- **Dynamic Data:** + - Batch upload whole pass: No per-draw map & unmap + - Separate by frequency: Per pass | per draw + +**Resource synchronization** +- Render pass: RT texture transitioned to write and then read +- No state tracking per draw call + +--- + +## Fast & Safe Object Storage & Lifetime Tracking + +**Modern practices: Smart pointers, ref counting and RAII [6]?** +- Too slow: Memory alloc per object. Scatters data around the memory (cache misses). Copy pointer is 2x atomics +- Safety issues: Ref count runs out → RAII side effect → invalidates iterator (another thread). Mutex is expensive! + +**Our solution: Arrays!** +- One big allocation for all objects of the same type +- Array index is a nice data handle + - POD. Trivial to copy and pass around +- Safe to pass to worker threads + - Can't dereference an array index. Needs access to the array +- **PROBLEM:** Old handles referring an array slot that has been reused? + +--- + +## Generational Pools and Handles + +**Pool** +- Typed array of objects + - Every array slot has a generation counter + - Counter is increased when the slot is freed +- Freelist for slot reuse + - An array (stack) of unused pool indices + - Delete object = push index + - Create object = pop index. Resize if needed (no ptrs → safe) + +**Handle** +- POD struct: Array index + generation counter (32/64 bits) +- `pool.get(handle)`: Compare generations. Not match? → return null +- Typed `Handle`. Pool has the same handle type. T is forward declared + +**Weak reference semantics** +- Null check (predictable branch) is almost free on modern CPUs +- Much better than callbacks in multithreaded systems. No races / mutexes! + +**Diagram:** +``` +Pool: ++-----+-----+-----+-----+-----+-----+ +| 0 | 1 | 2 | 3 | 4 | 5 | <-- Indices ++-----+-----+-----+-----+-----+-----+ +|data |data |data |data |data |data | <-- Data ++-----+-----+-----+-----+-----+-----+ +| 1 | 1 | 1 | 5 | 2 | 1 | <-- Generation Counter ++-----+-----+-----+-----+-----+-----+ + | + | + V +Handle: ++-------+-----+ +| index | gen | ++-------+-----+ +| 4 | 2 | ++-------+-----+ + match! +``` + +--- + +## Hot vs Cold Data + +**Easy to use API needs auxiliary data** +- Texture can't be just a VkTexture or MTL::Texture +- Additional data: size, format, data ptr, allocator... +- Needed for low frequency tasks: + - Update, readback, sync, create dependent resources, free memory + +**Rendering needs only the hot data** +- Auxiliary data bloats the struct → critical draw loop L1$ suffers +- Hate compromising performance and usability :( + +**Our solution: Split hot at cold data inside the pool** +- Pool has two types and two arrays: Hot and cold +- Both can be accessed with the same handle (using the same array index) +- Split hot and cold data (investigate). Compromise avoided! + +**SoA Layout Chart:** +``` + 0 1 2 3 4 5 + +-----+-----+-----+-----+-----+-----+ +Hot | hot | hot | hot | hot | hot | hot | (Red row) + +-----+-----+-----+-----+-----+-----+ +Cold| cold| cold| cold| cold| cold| cold| (Blue row) + +-----+-----+-----+-----+-----+-----+ +Gen | 1 | 1 | 1 | 5 | 2 | 1 | (Green row) + +-----+-----+-----+-----+-----+-----+ +``` + +**Lookup Mechanism:** +``` ++-------+-------+ +| index | gen | ++-------+-------+ +| 4 | 2 | (The '2' in 'gen' is highlighted) ++-------+-------+ +``` +Next to this lookup box: "match!" + +--- + +## Fast & Clean C++20 API for Resource Construction + +**Problem Statement:** +- Vulkan and DirectX use big structs to initialize complex resources + - Structs contain other structs and non-owning pointer references to arrays of structs + - Code bloat. No default values. Lifetime of temporary objects causes bugs + +**Existing Solutions:** +- Builder pattern: Debug perf is horrible. Release codegen not optimal either + +**Proposed Solution:** +- Our solution: Use C++20 designated struct initializers [8] + - The best C99 feature finally in C++. Waited 11 years! + - Default values: + - Provided by C++11 struct aggregate initialization + - Extremely clean syntax. Best readability + - Array data? + - Custom span that supports initializer lists + - Safety: const && parameter forces temporaries + +**Struct Definition Example:** +```cpp +struct BufferDesc +{ + const char* debugName = nullptr; + uint32 byteSize = 0; + USAGE usage = USAGE_UNIFORM; + MEMORY memory = MEMORY::CPU; + f::Span initialData; +}; +``` + +--- + +## Resource Construction Examples + +**Buffer Creation:** +```cpp +Handle vertexBuffer = rm->createBuffer({ + .debugName = "cube", + .byteSize = vertexSize * vertexAmo, + .usage = BufferDesc::USAGE_VERTEX, + .memory = MEMORY::GPU_CPU +}); +``` + +**Texture Creation:** +```cpp +Handle texture = rm->createTexture({ + .debugName = "lion.png", + .dimensions = Vector3I(256, 256, 1), + .format = FORMAT::RGBA8_SRGB, + .initialData = Span((uint8*)data, dataSize) +}); +``` + +**BindGroup Creation:** +```cpp +Handle material = m_rm->createBindGroup({ + .debugName = "Car Paint", + .layout = materialBindingsLayout, + .textures = { albedo, normal, properties }, + .buffers = {{.buffer = uniforms, .byteOffset = 64}} +}); +``` + +**Shader/Render Pipeline Creation:** +```cpp +m_shader = rm->createShader({ + .debugName = "mesh_simple", + .VS { .byteCode = shaderVS, .entryFunc = "main" }, + .PS { .byteCode = shaderPS, .entryFunc = "main" }, + .bindGroups = { + { m_globalsBindingsLayout }, // Globals bind group (0) + { materialBindingsLayout }, // Material bind group (1) + }, + .dynamicBuffers = dynamicBindings.getLayout(), + .graphicsState = { + .depthTest = COMPARE::GREATER_OR_EQUAL, // inverse Z + .vertexBufferBindings { + { + // Position vertex buffer (0) + .byteStride = 12, .attributes = { + { .byteOffset = 0, .format = FORMAT::RGB32_FLOAT } + } + }, + { + // 2nd vertex buffer: tangent, normal, color, texcoord + .byteStride = 24, .attributes = { + { .byteOffset = 0, .format = FORMAT::RGBA16_FLOAT }, + { .byteOffset = 8, .format = FORMAT::RGBA16_FLOAT }, + { .byteOffset = 16, .format = FORMAT::RGBA8_UNORM }, + { .byteOffset = 20, .format = FORMAT::RG16_FLOAT } + } + } + } + }, + .renderPassLayout = m_renderPassLayout +}); +``` + +--- + +## Software Command Buffer, but 10x+ Faster... + +**Initial design: Array of draw structs** +- Only contains the "metadata" +- Simple and fast + - 64 bytes = 1 cache line per draw +- Actual data inside buffers (inside groups) + - Write temp data from N threads directly into GPU memory + +**Can we do even better?** +- All fields are 32 bit integers +- Most data doesn't change between draw calls when rendering binned content +- On average 4.5 fields change (~18 bytes) + +**Draw Struct Definition:** +```cpp +struct Draw +{ + Handle shader; // PSO with all render state + Handle bindGroups[3]; + Handle dynamicBuffers; + Handle indexBuffer; + Handle vertexBuffers[3]; + uint32 indexOffset = 0; + uint32 vertexOffset = 0; + uint32 instanceOffset = 0; + uint32 instanceCount = 1; + uint32 dynamicBufferOffsets[2] = {0}; + uint32 triangleCount = 0; +}; +``` + +--- + +## Draw Stream: Data Interface for Draw Calls + +**Core Concept: Store only modified fields using dirty bitmask** + +The Draw Stream optimization reduces bandwidth by streaming only the fields of a `Draw` struct that have changed between draw calls, indicated by a dirty mask. + +### Draw Stream Layout + +**Stream Structure:** +- Each draw call starts with a 32-bit dirty mask indicating which fields changed +- Only modified fields are written to the stream +- Average of ~18 bytes change between draws (vs 64 bytes full struct) + +**Example Stream Encoding:** +``` +Draw 0: [SBBBD IVVVI VIIDT] [shader] [bind group] [index buffer] [vertex buffer] [tri count] +Draw 1: [SBBBD IVVVI VIIDT] [dynbuf offset] +Draw 2: [SBBBD IVVVI VIIDT] [dynbuf offset] +Draw 3: [SBBBD IVVVI VIIDT] [index offset] [vertex offset] [dynbuf offset] +... +``` + +### User Land: Draw Stream Writer Class +- Contains a draw struct (current state) + dirty mask +- Setter for each field: if changed → set dirty bit + write field to stream +- Draw: write dirty mask in front of the draw (stored offset) + +**Benefits:** +- No change tracking in the backend +- ~3x reduced bandwidth vs full 64-byte structs +- Enables efficient multi-threaded draw stream generation + +### Backend: Stream Decoding + +**Processing Flow:** +- For each draw: Read the dirty field bitmask +- For each set bit: Read field and emit a gfx API call +- **Advantages**: No change tracking in the backend. ~3x reduced BW + +### Draw Struct Definition +```cpp +struct Draw +{ + Handle shader; + Handle bindGroups[3]; + Handle dynamicBuffers; + Handle indexBuffer; + Handle vertexBuffers[3]; + uint32 indexOffset = 0; + uint32 vertexOffset = 0; + uint32 instanceOffset = 0; + uint32 instanceCount = 1; + uint32 dynamicBufferOffsets[2] = {0}; + uint32 triangleCount = 0; +}; +``` + +--- + +## Draw Call Performance: Baseline (Worst Case) + +**6-7 API calls per draw** + +### Draw Call Sequence +For each Draw (Draw 0, Draw 1, Draw 2, Draw 3...): +1. `(Bind set 2)` - Material binding +2. `Map + unmap` - Uniform buffer mapping/unmapping +3. `Bind set 3` - Dynamic data binding with offsets + - Draw 0: `Bind set 3` + - Draw 1: `Bind set 3 (96)` + - Draw 2: `Bind set 3 (192)` + - Draw 3: `Bind set 3 (384)` +4. `Bind VB + Bind IB` - Vertex Buffer + Index Buffer binding +5. `Draw` - Actual draw call + +### Per-Draw Data +```cpp +struct PerDrawData { + mat4x3 MVP; // Model-View-Projection matrix + mat3x3 invT; // Inverse Transpose matrix + float3 color; // Object color +}; +``` + +**Note:** Material (set 2) doesn't change every draw + +--- + +## Draw Call Performance: Bump Alloc + Offset Bind + +**4-5 API calls per draw** + +### Optimized Draw Call Sequence +For each Draw (Draw 0, Draw 1, Draw 2, Draw 3...): +1. `(Bind set 2)` - Material binding +2. `Bind set 3 (offset)` - Dynamic data binding with bump allocator offsets + - Draw 0: `Bind set 3 (0)` + - Draw 1: `Bind set 3 (96)` + - Draw 2: `Bind set 3 (192)` + - Draw 3: `Bind set 3 (384)` +3. `Bind VB + Bind IB` - Vertex Buffer + Index Buffer binding +4. `Draw` - Actual draw call + +### Temp Allocator Data Layout +```cpp +struct PerDrawData { + mat4x3 MVP; // Model-View-Projection matrix + mat3x3 invT; // Inverse Transpose matrix + float3 color; // Object color + PADDING; // Memory alignment padding +}; +``` + +**Allocation Pattern:** +- **Draw 0**: Offset 0 - MVP, invT, color, padding +- **Draw 1**: Offset 96 - MVP, invT, color, padding +- **Draw 2**: Offset 192 - MVP, invT, color, padding +- **Draw 3**: Offset 384 - MVP, invT, color, padding + +### Performance Results +- **~3x versus old WebGL / GLES backend** +- **No Vulkan results :(** + +--- + +## Draw Call Performance: Pack Meshes + +**2-3 API calls per draw** + +### Optimized Draw Call Sequence +For each Draw (Draw 0, Draw 1, Draw 2, Draw 3...): +1. `(Bind set 2)` - Material binding +2. `Bind set 3 (offset)` - Dynamic data binding with bump allocator offsets + - Draw 0: `Bind set 3 (0)` + - Draw 1: `Bind set 3 (96)` + - Draw 2: `Bind set 3 (192)` + - Draw 3: `Bind set 3 (384)` +3. `Draw(baseVert, baseIdx)` - Draw call with base vertex and base index + +### Mesh Buffer Layout +``` ++------------------+ +| Mesh 1 | ← Draw 0 (baseVert=0, baseIdx=0) +| vertices + indices| ++------------------+ +| Mesh 2 | ← Draw 1 (baseVert=N, baseIdx=M) +| vertices + indices| ++------------------+ +| Mesh 3 | ← Draw 2 (baseVert=P, baseIdx=Q) +| vertices + indices| ++------------------+ +| Mesh 4 | ← Draw 3 (baseVert=R, baseIdx=S) +| vertices + indices| ++------------------+ +| ... | ++------------------+ +``` + +### Performance Results +- **Nvidia: 2.17x** +- **AMD: 1.75x** +- **Adreno: 1.29x** +- **Mali: 1.40x** + +--- + +## Draw Call Performance: BaseInstance (FAILED) + +**1-2 API calls per draw (amortized)** + +### Instancing Data Layout (No Padding) +- 16KB UBO binding limit +- Change offset every 16KB +- Amortized over >100 draws + +### Better Shader Codegen vs. Instancing +- `gl_InstanceID = dynamic offset` (Instancing) +- `gl_BaseInstance = static offset` (BaseInstance) +- **Benefits:** Enables scalar loads / fast UBO path. Saves vector registers / loads + +### BaseInstance Draw Call Sequence +**Temp Allocator `drawData[]` Layout:** +``` +16KB UBO Region: ++------------------+ +| Instance 0 | ← Draw 0 (baseInstance=0) +| mat4x3 MVP | +| mat3x3 invT | +| float3 color | ++------------------+ +| Instance 1 | ← Draw 1 (baseInstance=1) +| mat4x3 MVP | +| mat3x3 invT | +| float3 color | ++------------------+ +| Instance 2 | ← Draw 2 (baseInstance=2) +| mat4x3 MVP | +| mat3x3 invT | +| float3 color | ++------------------+ +| ... | ++------------------+ +``` + +For each Draw (Draw 0, Draw 1, Draw 2, Draw 3...): +1. `(Bind set 2)` - Material binding +2. `Bind set 3 (offset)` - Dynamic data binding (amortized over 16KB) +3. `Draw(baseVertex, baseIndex, baseInstance=X)` - Instanced draw call + +### Why BaseInstance FAILED +- **Not supported in web / DX12 :(** +- **Mobile shader codegen issues:** Often slower for the GPU :( + +--- + +## Performance Numbers + +**10,000 Draw Calls (Single CPU Thread)** + +### Benchmark Conditions +- 10,000 unique materials +- 10,000 unique meshes +- Single CPU thread +- Actual draw calls (no instancing) +- No GPU persistent scene data +- ~90% time spent in driver + +### Performance Results +| Hardware Configuration | Time (ms) | Notes | +|------------------------|-----------|-------| +| **AMD RDNA2 iGPU + 6800HS 4.7GHz** | **0.85ms** | High-end desktop integrated | +| **Apple iPhone 6s + 1.85GHz** | **11.27ms** | 7 years old | +| **PowerVR GE8320 + A53 2.3GHz** | **20.93ms** | €99 device | +| **ARM Mali G57 MP1 + A75 1.6GHz** | **15.01ms** | €99 device | +| **QC Adreno 610 + Kryo 260 2GHz** | **13.69ms** | €149 device | + +--- + +## References + +### [1] Various hardware performance numbers (Wikipedia) +- **ARM Mali:** https://en.wikipedia.org/wiki/Mali (processor) +- **Qualcomm Adreno:** https://en.wikipedia.org/wiki/Adreno +- **PowerVR:** https://en.wikipedia.org/wiki/PowerVR +- **Nintendo Switch:** https://en.wikipedia.org/wiki/Nintendo_Switch +- **Microsoft Xbox 360:** https://en.wikipedia.org/wiki/Xbox_360 +- **Microsoft Xbox One:** https://en.wikipedia.org/wiki/Xbox_One +- **Sony PS3:** https://en.wikipedia.org/wiki/PlayStation_3 +- **Sony PS4:** https://en.wikipedia.org/wiki/PlayStation_4 + +### [2] Haar, Aaltonen: GPU-Driven Rendering Pipelines, SIGGRAPH 2015: Advances in Real-Time Rendering in Games +https://advances.realtimerendering.com/s2015/aaltonenhaar_siggraph2015_combined_final_footer_220dpi.pdf + +### [3] Karis, Stubbe, Wihlidal: Nanite, A Deep Dive, SIGGRAPH 2021: Advances in Real-Time Rendering in Games +https://advances.realtimerendering.com/s2021/Karis_Nanite_SIGGRAPH_Advances_2021_final.pdf + +### [4] SPIRV-Cross +https://github.com/KhronosGroup/SPIRV-Cross + +### [5] Tatarchuk, Cooper, Aaltonen: Unity Rendering Architecture, Rendering Engine Architecture Conference (REAC), 2023 +https://enginearchitecture.realtimerendering.com/downloads/reac2021 unity rendering engine architecture.pdf + +### [6] RAII, Wikipedia +https://en.wikipedia.org/wiki/Resource acquisition is initialization + +### [7] SoA layout, Wikipedia +https://en.wikipedia.org/wiki/AoS and SoA + +### [8] C++20 designated initializers, cppreference.com +https://en.cppreference.com/w/cpp/language/aggregate_initialization + +### [9] Masmano, Ripoll, Crespo, Real: TLSF: A new memory allocator for real-time systems +http://www.gii.upv.es/tlsf/files/ecrts04 tlsf.pdf + +### [10] Aaltonen: OffsetAllocator, GitHub +https://github.com/sebbbi/OffsetAllocator + + From 816466245ba2d6cfed158ff0d55c3ac3b8ffb940 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Mon, 13 Oct 2025 23:53:31 +0200 Subject: [PATCH 06/69] m1: black window --- engine/pill_renderer/src/m1_bootstrap.rs | 181 +++++++++ engine/pill_renderer/src/renderer.rs | 474 ++++++++++++++++------- 2 files changed, 505 insertions(+), 150 deletions(-) create mode 100644 engine/pill_renderer/src/m1_bootstrap.rs diff --git a/engine/pill_renderer/src/m1_bootstrap.rs b/engine/pill_renderer/src/m1_bootstrap.rs new file mode 100644 index 00000000..269a32fb --- /dev/null +++ b/engine/pill_renderer/src/m1_bootstrap.rs @@ -0,0 +1,181 @@ +use anyhow::Result; + +#[derive(Clone, Debug)] +pub struct WindowComponent { + pub width: u32, + pub height: u32, + pub title: String, + pub vsync: bool, +} + +pub fn run_m1(window: &WindowComponent) -> Result<()> { + // winit event loop + wgpu init per DESIGN.md (M1) + use winit::{ + dpi::LogicalSize, + event::{Event, WindowEvent}, + event_loop::EventLoop, + window::WindowBuilder, + }; + + let event_loop = EventLoop::new().unwrap(); + let winit_window = WindowBuilder::new() + .with_title(window.title.clone()) + .with_inner_size(LogicalSize::new(window.width as f64, window.height as f64)) + .build(&event_loop)?; + let vsync = window.vsync; // NEW + let window = std::sync::Arc::new(winit_window); + + // Instance and surface + let backends = wgpu::util::backend_bits_from_env().unwrap_or_default(); + let dx12_shader_compiler = wgpu::util::dx12_shader_compiler_from_env().unwrap_or_default(); + let gles_minor_version = wgpu::util::gles_minor_version_from_env().unwrap_or_default(); + + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends, + flags: wgpu::InstanceFlags::from_build_config().with_env(), + dx12_shader_compiler, + gles_minor_version, + }); + let surface = instance.create_surface(window.clone()).unwrap(); + + // Adapter + let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::default(), + compatible_surface: Some(&surface), + force_fallback_adapter: false, + })) + .ok_or_else(|| anyhow::anyhow!("No compatible GPU adapter found"))?; + + // Device/queue + let (device, queue) = pollster::block_on(adapter.request_device( + &wgpu::DeviceDescriptor { + label: Some("m1_device"), + required_features: wgpu::Features::empty(), + required_limits: wgpu::Limits::default(), + }, + None, + ))?; + + // Surface configuration per capabilities + let surface_caps = surface.get_capabilities(&adapter); + let preferred_formats = [wgpu::TextureFormat::Rgba8UnormSrgb, wgpu::TextureFormat::Bgra8UnormSrgb]; + let format = preferred_formats + .into_iter() + .find(|f| surface_caps.formats.contains(f)) + .unwrap_or(surface_caps.formats[0]); + + let present_mode = if vsync { + wgpu::PresentMode::Fifo + } else if surface_caps.present_modes.contains(&wgpu::PresentMode::Mailbox) { + wgpu::PresentMode::Mailbox + } else if surface_caps.present_modes.contains(&wgpu::PresentMode::Immediate) { + wgpu::PresentMode::Immediate + } else { + wgpu::PresentMode::Fifo + }; + + let size = window.inner_size(); + let mut config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format, + width: size.width, + height: size.height, + desired_maximum_frame_latency: 2, + present_mode, + alpha_mode: wgpu::CompositeAlphaMode::Auto, + view_formats: vec![format], + }; + surface.configure(&device, &config); + + // Depth texture created but unused yet; recreate on resize + let mut depth_view = create_depth_view(&device, &config); + + event_loop.run(move |event, elwt| { + match event { + Event::WindowEvent { event, window_id } if window_id == window.id() => match event { + WindowEvent::CloseRequested => elwt.exit(), + WindowEvent::Resized(new_size) => { + if new_size.width > 0 && new_size.height > 0 { + config.width = new_size.width; + config.height = new_size.height; + surface.configure(&device, &config); + depth_view = create_depth_view(&device, &config); + } + } + WindowEvent::ScaleFactorChanged { .. } => { + let new_size = window.inner_size(); + if new_size.width > 0 && new_size.height > 0 { + config.width = new_size.width; + config.height = new_size.height; + surface.configure(&device, &config); + depth_view = create_depth_view(&device, &config); + } + } + _ => {} + }, + Event::AboutToWait => { + // Render black frame + let frame = match surface.get_current_texture() { + Ok(frame) => frame, + Err(err) => match err { + wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated => { + surface.configure(&device, &config); + return; + } + wgpu::SurfaceError::Timeout => { + return; + } + wgpu::SurfaceError::OutOfMemory => { + elwt.exit(); + return; + } + }, + }; + let view = frame.texture.create_view(&wgpu::TextureViewDescriptor::default()); + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("m1_encoder") }); + { + let _rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("m1_clear_black"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &depth_view, + depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: wgpu::StoreOp::Store }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + } + queue.submit([encoder.finish()]); + frame.present(); + } + _ => {} + } + })?; + + Ok(()) +} + +fn create_depth_view(device: &wgpu::Device, config: &wgpu::SurfaceConfiguration) -> wgpu::TextureView { + let depth_format = wgpu::TextureFormat::Depth32Float; + let depth = device.create_texture(&wgpu::TextureDescriptor { + label: Some("m1_depth"), + size: wgpu::Extent3d { width: config.width, height: config.height, depth_or_array_layers: 1 }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: depth_format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }); + depth.create_view(&wgpu::TextureViewDescriptor::default()) +} + + diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index a51b1910..f1828ec2 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -3,49 +3,31 @@ // https://github.com/emilk/egui/discussions/3067 use crate::{ - instance::Instance, mesh_drawer::MeshDrawer, renderer_resource_storage::RendererResourceStorage, resources::{ - RendererCamera, - RendererMaterial, - RendererMesh, - RendererPipeline, - RendererTexture, - Vertex - } + instance::Instance, + mesh_drawer::MeshDrawer, + renderer_resource_storage::RendererResourceStorage, + resources::{ + RendererCamera, RendererMaterial, RendererMesh, RendererPipeline, RendererTexture, Vertex, + }, }; use pill_engine::internal::{ - PillRenderer, - EntityHandle, - RenderQueueItem, - TextureType, - MeshData, - MaterialTextureMap, - TransformComponent, - ComponentStorage, - CameraComponent, - MaterialParameterMap, - RendererCameraHandle, - RendererMaterialHandle, - RendererMeshHandle, - RendererPipelineHandle, - RendererTextureHandle, + get_renderer_resource_handle_from_camera_component, CameraComponent, ComponentStorage, + EntityHandle, MaterialParameterMap, MaterialTextureMap, MeshData, PillRenderer, + RenderQueueItem, RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, + RendererPipelineHandle, RendererTextureHandle, TextureType, TransformComponent, RENDER_QUEUE_KEY_ORDER, - get_renderer_resource_handle_from_camera_component, }; -use pill_core::{ - PillSlotMapKey, PillSlotMapKeyData, PillStyle, RendererError, Timer -}; +use pill_core::{PillSlotMapKey, PillSlotMapKeyData, PillStyle, RendererError, Timer}; -use std::{ - iter, mem::size_of, num::NonZeroU32, ops::Range, sync::Arc -}; +use std::{iter, mem::size_of, num::NonZeroU32, ops::Range, sync::Arc}; -use naga::front::glsl; use naga::back::wgsl; +use naga::front::glsl; use anyhow::{Error, Result}; -use log::{ info }; +use log::info; use crate::egui::EguiRenderer; @@ -53,8 +35,11 @@ pub const MAX_INSTANCE_BATCH_SIZE: usize = 10000; // Maximum number of instances pub const INITIAL_INSTANCE_VECTOR_CAPACITY: usize = 10000; // Default resource handle - Master pipeline -pub const MASTER_PIPELINE_HANDLE: RendererPipelineHandle = RendererPipelineHandle { - 0: PillSlotMapKeyData { index: 1, version: unsafe { std::num::NonZeroU32::new_unchecked(1) } } +pub const MASTER_PIPELINE_HANDLE: RendererPipelineHandle = RendererPipelineHandle { + 0: PillSlotMapKeyData { + index: 1, + version: unsafe { std::num::NonZeroU32::new_unchecked(1) }, + }, }; pub struct Renderer { @@ -65,39 +50,39 @@ fn compile_glsl_to_wgsl(source: &str, stage: naga::ShaderStage) -> Result, config: config::Config) -> Self { + fn new(window: Arc, config: config::Config) -> Self { info!("Initializing {}", "Renderer".mobj_style()); let state: State = pollster::block_on(State::new(window, config)); - Self { - state, - } - } + Self { state } + } fn resize(&mut self, new_window_size: winit::dpi::PhysicalSize) { info!("Resizing {} resources", "Renderer".mobj_style()); self.state.resize(new_window_size) } - - fn set_master_pipeline(&mut self, vertex_shader_bytes: &[u8], fragment_shader_bytes: &[u8]) -> Result<()> { - + fn set_master_pipeline( + &mut self, + vertex_shader_bytes: &[u8], + fragment_shader_bytes: &[u8], + ) -> Result<()> { // Create shaders // Convert bytes to string let vertex_shader_source = std::str::from_utf8(vertex_shader_bytes) @@ -105,25 +90,28 @@ impl PillRenderer for Renderer { let fragment_shader_source = std::str::from_utf8(fragment_shader_bytes) .map_err(|e| anyhow::anyhow!("Invalid UTF-8 in fragment shader: {}", e))?; - -// Convert GLSL to WGSL -let vertex_wgsl = compile_glsl_to_wgsl(vertex_shader_source, naga::ShaderStage::Vertex).unwrap(); -let fragment_wgsl = compile_glsl_to_wgsl(fragment_shader_source, naga::ShaderStage::Fragment).unwrap(); - -// Create shader modules with WGSL -let vertex_shader = self.state.device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("master_vertex_shader"), - source: wgpu::ShaderSource::Wgsl(vertex_wgsl.into()), -}); - -let fragment_shader = self.state.device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("master_fragment_shader"), - source: wgpu::ShaderSource::Wgsl(fragment_wgsl.into()), -}); - - - - + // Convert GLSL to WGSL + let vertex_wgsl = + compile_glsl_to_wgsl(vertex_shader_source, naga::ShaderStage::Vertex).unwrap(); + let fragment_wgsl = + compile_glsl_to_wgsl(fragment_shader_source, naga::ShaderStage::Fragment).unwrap(); + + // Create shader modules with WGSL + let vertex_shader = self + .state + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("master_vertex_shader"), + source: wgpu::ShaderSource::Wgsl(vertex_wgsl.into()), + }); + + let fragment_shader = + self.state + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("master_fragment_shader"), + source: wgpu::ShaderSource::Wgsl(fragment_wgsl.into()), + }); // Create master pipeline let master_pipeline = RendererPipeline::new( @@ -132,10 +120,17 @@ let fragment_shader = self.state.device.create_shader_module(wgpu::ShaderModuleD fragment_shader, self.state.color_format, Some(self.state.depth_format), - &[RendererMesh::data_layout_descriptor(), Instance::data_layout_descriptor()], - ).unwrap(); + &[ + RendererMesh::data_layout_descriptor(), + Instance::data_layout_descriptor(), + ], + ) + .unwrap(); - self.state.renderer_resource_storage.pipelines.insert(master_pipeline); + self.state + .renderer_resource_storage + .pipelines + .insert(master_pipeline); Ok(()) } @@ -147,16 +142,41 @@ let fragment_shader = self.state.device.create_shader_module(wgpu::ShaderModuleD Ok(handle) } - fn create_texture(&mut self, name: &str, image_data: &image::DynamicImage, texture_type: TextureType) -> Result { - let texture = RendererTexture::new_texture(&self.state.device, &self.state.queue, Some(name), image_data, texture_type)?; - let handle = self.state.renderer_resource_storage.textures.insert(texture); + fn create_texture( + &mut self, + name: &str, + image_data: &image::DynamicImage, + texture_type: TextureType, + ) -> Result { + let texture = RendererTexture::new_texture( + &self.state.device, + &self.state.queue, + Some(name), + image_data, + texture_type, + )?; + let handle = self + .state + .renderer_resource_storage + .textures + .insert(texture); Ok(handle) } - fn create_material(&mut self, name: &str, textures: &MaterialTextureMap, parameters: &MaterialParameterMap) -> Result { + fn create_material( + &mut self, + name: &str, + textures: &MaterialTextureMap, + parameters: &MaterialParameterMap, + ) -> Result { let pipeline_handle = MASTER_PIPELINE_HANDLE; - let pipeline = self.state.renderer_resource_storage.pipelines.get(pipeline_handle).unwrap(); + let pipeline = self + .state + .renderer_resource_storage + .pipelines + .get(pipeline_handle) + .unwrap(); let material = RendererMaterial::new( &self.state.device, @@ -168,16 +188,26 @@ let fragment_shader = self.state.device.create_shader_module(wgpu::ShaderModuleD textures, &pipeline.material_parameter_bind_group_layout, parameters, - ).unwrap(); + ) + .unwrap(); - let handle = self.state.renderer_resource_storage.materials.insert(material); + let handle = self + .state + .renderer_resource_storage + .materials + .insert(material); Ok(handle) } fn create_camera(&mut self) -> Result { let pipeline_handle = MASTER_PIPELINE_HANDLE; - let pipeline = self.state.renderer_resource_storage.pipelines.get(pipeline_handle).unwrap(); + let pipeline = self + .state + .renderer_resource_storage + .pipelines + .get(pipeline_handle) + .unwrap(); let camera_bind_group_layout = &pipeline.camera_bind_group_layout; let camera = RendererCamera::new(&self.state.device, camera_bind_group_layout)?; let handle = self.state.renderer_resource_storage.cameras.insert(camera); @@ -185,46 +215,81 @@ let fragment_shader = self.state.device.create_shader_module(wgpu::ShaderModuleD Ok(handle) } - fn update_material_textures(&mut self, renderer_material_handle: RendererMaterialHandle, textures: &MaterialTextureMap) -> Result<()> { - RendererMaterial::update_textures(&self.state.device, renderer_material_handle, &mut self.state.renderer_resource_storage, textures) + fn update_material_textures( + &mut self, + renderer_material_handle: RendererMaterialHandle, + textures: &MaterialTextureMap, + ) -> Result<()> { + RendererMaterial::update_textures( + &self.state.device, + renderer_material_handle, + &mut self.state.renderer_resource_storage, + textures, + ) } - fn update_material_parameters(&mut self, renderer_material_handle: RendererMaterialHandle, parameters: &MaterialParameterMap) -> Result<()> { - RendererMaterial::update_parameters(&self.state.device, &self.state.queue, renderer_material_handle, &mut self.state.renderer_resource_storage, parameters) + fn update_material_parameters( + &mut self, + renderer_material_handle: RendererMaterialHandle, + parameters: &MaterialParameterMap, + ) -> Result<()> { + RendererMaterial::update_parameters( + &self.state.device, + &self.state.queue, + renderer_material_handle, + &mut self.state.renderer_resource_storage, + parameters, + ) } fn destroy_mesh(&mut self, renderer_mesh_handle: RendererMeshHandle) -> Result<()> { - self.state.renderer_resource_storage.meshes.remove(renderer_mesh_handle).unwrap(); + self.state + .renderer_resource_storage + .meshes + .remove(renderer_mesh_handle) + .unwrap(); Ok(()) } fn destroy_texture(&mut self, renderer_texture_handle: RendererTextureHandle) -> Result<()> { - self.state.renderer_resource_storage.textures.remove(renderer_texture_handle).unwrap(); + self.state + .renderer_resource_storage + .textures + .remove(renderer_texture_handle) + .unwrap(); Ok(()) } fn destroy_material(&mut self, renderer_material_handle: RendererMaterialHandle) -> Result<()> { - self.state.renderer_resource_storage.materials.remove(renderer_material_handle).unwrap(); + self.state + .renderer_resource_storage + .materials + .remove(renderer_material_handle) + .unwrap(); Ok(()) } fn destroy_camera(&mut self, renderer_camera_handle: RendererCameraHandle) -> Result<()> { - self.state.renderer_resource_storage.cameras.remove(renderer_camera_handle).unwrap(); - + self.state + .renderer_resource_storage + .cameras + .remove(renderer_camera_handle) + .unwrap(); + Ok(()) } fn render( &mut self, active_camera_entity_handle: EntityHandle, - render_queue: &Vec, + render_queue: &Vec, camera_component_storage: &ComponentStorage, transform_component_storage: &ComponentStorage, egui_ui: Box, - timer: &mut Timer + timer: &mut Timer, ) -> Result<()> { self.state.render( active_camera_entity_handle, @@ -232,15 +297,14 @@ let fragment_shader = self.state.device.create_shader_module(wgpu::ShaderModuleD camera_component_storage, transform_component_storage, egui_ui, - timer + timer, ) } - + fn pass_input_to_egui(&mut self, event: &winit::event::WindowEvent) -> Result<()> { self.state.egui_renderer.handle_input(event); Ok(()) } - } pub struct State { @@ -251,7 +315,7 @@ pub struct State { device: wgpu::Device, queue: wgpu::Queue, surface_configuration: wgpu::SurfaceConfiguration, - window_size: winit::dpi::PhysicalSize, + window_size: winit::dpi::PhysicalSize, color_format: wgpu::TextureFormat, depth_format: wgpu::TextureFormat, depth_texture: RendererTexture, @@ -261,7 +325,6 @@ pub struct State { egui_renderer: crate::egui::EguiRenderer, } - impl State { // Creating some of the wgpu types requires async code async fn new(window: Arc, config: config::Config) -> Self { @@ -280,19 +343,25 @@ impl State { gles_minor_version, }); let surface = instance.create_surface(window).unwrap(); - + // Specify adapter options (Options passed here are not guaranteed to work for all devices) - let request_adapter_options = wgpu::RequestAdapterOptions { + let request_adapter_options = wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::default(), compatible_surface: Some(&surface), force_fallback_adapter: false, }; // Create adapter - let adapter = instance.request_adapter(&request_adapter_options).await.unwrap(); + let adapter = instance + .request_adapter(&request_adapter_options) + .await + .unwrap(); let adapter_info = adapter.get_info(); - info!("Using GPU: {} ({:?})", adapter_info.name, adapter_info.backend); - + info!( + "Using GPU: {} ({:?})", + adapter_info.name, adapter_info.backend + ); + let features = wgpu::Features::DEPTH_CLIP_CONTROL; // Create device descriptor @@ -300,36 +369,51 @@ impl State { label: None, required_features: features, // Allows to specify what extra features of GPU that needs to be included (e.g. depth clamping, push constants, texture compression, etc) required_limits: wgpu::Limits::default(), // Allows to specify the limit of certain types of resources that will be used (e.g. max samplers, uniform buffers, etc) - //memory_hints: wgpu::MemoryHints::MemoryUsage, + //memory_hints: wgpu::MemoryHints::MemoryUsage, }; // Create device and queue - let (device, queue) = adapter.request_device(&device_descriptor,None).await.unwrap(); + let (device, queue) = adapter + .request_device(&device_descriptor, None) + .await + .unwrap(); // Specify surface configuration let preferred_format = wgpu::TextureFormat::Rgba8UnormSrgb; - + // Get supported present modes and choose the best one let surface_caps = surface.get_capabilities(&adapter); - let present_mode = if surface_caps.present_modes.contains(&wgpu::PresentMode::Mailbox) { + let present_mode = if surface_caps + .present_modes + .contains(&wgpu::PresentMode::Mailbox) + { wgpu::PresentMode::Mailbox - } else if surface_caps.present_modes.contains(&wgpu::PresentMode::Immediate) { + } else if surface_caps + .present_modes + .contains(&wgpu::PresentMode::Immediate) + { wgpu::PresentMode::Immediate } else { wgpu::PresentMode::Fifo }; - + // Choose the best supported format let format = if surface_caps.formats.contains(&preferred_format) { preferred_format - } else if surface_caps.formats.contains(&wgpu::TextureFormat::Bgra8UnormSrgb) { + } else if surface_caps + .formats + .contains(&wgpu::TextureFormat::Bgra8UnormSrgb) + { wgpu::TextureFormat::Bgra8UnormSrgb - } else if surface_caps.formats.contains(&wgpu::TextureFormat::Bgra8Unorm) { + } else if surface_caps + .formats + .contains(&wgpu::TextureFormat::Bgra8Unorm) + { wgpu::TextureFormat::Bgra8Unorm } else { surface_caps.formats[0] // Use first available format }; - + let surface_configuration = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, // Defines how the swap_chain's underlying textures will be used format: format, // Defines how the swap_chain's textures will be stored on the gpu @@ -348,11 +432,9 @@ impl State { let renderer_resource_storage = RendererResourceStorage::new(&config); // Create depth and color texture - let depth_texture = RendererTexture::new_depth_texture( - &device, - &surface_configuration, - "depth_texture" - ).unwrap(); + let depth_texture = + RendererTexture::new_depth_texture(&device, &surface_configuration, "depth_texture") + .unwrap(); let color_format = surface_configuration.format; let depth_format = wgpu::TextureFormat::Depth32Float; @@ -360,14 +442,9 @@ impl State { // Create drawing state let mesh_drawer = MeshDrawer::new(&device, MAX_INSTANCE_BATCH_SIZE as u32); - let egui_renderer = EguiRenderer::new( - &device, - surface_configuration.format, - None, - 1, - window_ref, - ); - + let egui_renderer = + EguiRenderer::new(&device, surface_configuration.format, None, 1, window_ref); + // Create state Self { // Resources @@ -384,7 +461,7 @@ impl State { mesh_drawer, // Other config, - egui_renderer + egui_renderer, } } @@ -393,26 +470,88 @@ impl State { self.window_size = new_window_size; self.surface_configuration.width = new_window_size.width; self.surface_configuration.height = new_window_size.height; - self.surface.configure(&self.device, &self.surface_configuration); + self.surface + .configure(&self.device, &self.surface_configuration); self.depth_texture = RendererTexture::new_depth_texture( &self.device, &self.surface_configuration, "depth_texture", - ).unwrap(); + ) + .unwrap(); } } - + fn render( - &mut self, + &mut self, active_camera_entity_handle: EntityHandle, - render_queue: &Vec, + render_queue: &Vec, camera_component_storage: &ComponentStorage, transform_component_storage: &ComponentStorage, egui_ui: Box, - timer: &mut Timer - ) -> Result<()> { + timer: &mut Timer, + ) -> Result<()> { + // Get frame or return mapped error if failed + let frame = self.surface.get_current_texture(); + + let frame = match frame { + std::result::Result::Ok(frame) => frame, + std::result::Result::Err(error) => match error { + wgpu::SurfaceError::Lost => return Err(RendererError::SurfaceLost.into()), + wgpu::SurfaceError::OutOfMemory => { + return Err(RendererError::SurfaceOutOfMemory.into()) + } + _ => return Err(RendererError::SurfaceOther.into()), + }, + }; + + let view = frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("m1_encoder"), + }); + { + let _rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("m1_clear_black"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &self.depth_texture.texture_view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + } + self.queue.submit([encoder.finish()]); + frame.present(); + + Ok(()) + } + + fn render_old( + &mut self, + active_camera_entity_handle: EntityHandle, + render_queue: &Vec, + camera_component_storage: &ComponentStorage, + transform_component_storage: &ComponentStorage, + egui_ui: Box, + timer: &mut Timer, + ) -> Result<()> { timer.record("Get frame"); - + // Get frame or return mapped error if failed let frame = self.surface.get_current_texture(); @@ -420,38 +559,73 @@ impl State { std::result::Result::Ok(frame) => frame, std::result::Result::Err(error) => match error { wgpu::SurfaceError::Lost => return Err(RendererError::SurfaceLost.into()), - wgpu::SurfaceError::OutOfMemory => return Err(RendererError::SurfaceOutOfMemory.into()), + wgpu::SurfaceError::OutOfMemory => { + return Err(RendererError::SurfaceOutOfMemory.into()) + } _ => return Err(RendererError::SurfaceOther.into()), }, }; - let view = frame.texture.create_view(&wgpu::TextureViewDescriptor::default()); + let view = frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); timer.record("Get clear color and create render pass attachments"); // Get active camera and update it - let camera_storage = camera_component_storage.data.get(active_camera_entity_handle.data().index as usize).unwrap(); + let camera_storage = camera_component_storage + .data + .get(active_camera_entity_handle.data().index as usize) + .unwrap(); let active_camera_component = camera_storage.as_ref().unwrap(); - let renderer_camera = self.renderer_resource_storage.cameras.get_mut(get_renderer_resource_handle_from_camera_component(active_camera_component)).ok_or(Error::new(RendererError::RendererResourceNotFound))?; - let camera_transform_storage = transform_component_storage.data.get(active_camera_entity_handle.data().index as usize).unwrap(); + let renderer_camera = self + .renderer_resource_storage + .cameras + .get_mut(get_renderer_resource_handle_from_camera_component( + active_camera_component, + )) + .ok_or(Error::new(RendererError::RendererResourceNotFound))?; + let camera_transform_storage = transform_component_storage + .data + .get(active_camera_entity_handle.data().index as usize) + .unwrap(); let active_camera_transform_component = camera_transform_storage.as_ref().unwrap(); - renderer_camera.update(&self.queue, active_camera_component, active_camera_transform_component); - let renderer_camera = self.renderer_resource_storage.cameras.get(get_renderer_resource_handle_from_camera_component(active_camera_component)).unwrap(); + renderer_camera.update( + &self.queue, + active_camera_component, + active_camera_transform_component, + ); + let renderer_camera = self + .renderer_resource_storage + .cameras + .get(get_renderer_resource_handle_from_camera_component( + active_camera_component, + )) + .unwrap(); let clear_color = active_camera_component.clear_color; // Build a command buffer that can be sent to the GPU - let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("render_encoder"), - }); + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("render_encoder"), + }); + + { + // Additional scope to release mutable borrow of encoder done by begin_render_pass - { // Additional scope to release mutable borrow of encoder done by begin_render_pass - // Create color attachment let color_attachment = wgpu::RenderPassColorAttachment { - view: &view, // Specifies what texture to save the colors to + view: &view, // Specifies what texture to save the colors to resolve_target: None, // Specifies what texture will receive the resolved output - ops: wgpu::Operations { // Specifies what to do with the colors on the screen - load: wgpu::LoadOp::Clear(wgpu::Color { r: clear_color.x as f64, g: clear_color.y as f64, b: clear_color.z as f64, a: 1.0, } ), // Specifies how to handle colors stored from the previous frame + ops: wgpu::Operations { + // Specifies what to do with the colors on the screen + load: wgpu::LoadOp::Clear(wgpu::Color { + r: clear_color.x as f64, + g: clear_color.y as f64, + b: clear_color.z as f64, + a: 1.0, + }), // Specifies how to handle colors stored from the previous frame store: wgpu::StoreOp::Store, }, }; @@ -469,19 +643,19 @@ impl State { timer.begin_context("Mesh Drawer"); self.mesh_drawer.record_draw_commands( - &self.queue, + &self.queue, &self.device, - &self.renderer_resource_storage, - color_attachment, - depth_stencil_attachment, + &self.renderer_resource_storage, + color_attachment, + depth_stencil_attachment, &renderer_camera, - &render_queue, + &render_queue, &transform_component_storage, - timer + timer, )?; timer.end_context()?; - } + } timer.begin_context("Egui Draw"); From 455f7ca16d2c535d27fcdefaf7912f03f2bf3f7e Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 00:55:47 +0200 Subject: [PATCH 07/69] feat: m3, render bunch of pills --- engine/pill_engine/src/resources/mesh.rs | 113 +++-- engine/pill_renderer/src/m1_bootstrap.rs | 181 -------- engine/pill_renderer/src/mesh_drawer.rs | 184 +++++--- engine/pill_renderer/src/renderer.rs | 416 ++++++++++++------ .../src/resources/renderer_mesh.rs | 34 +- .../src/resources/renderer_pipeline.rs | 149 ++++--- 6 files changed, 583 insertions(+), 494 deletions(-) delete mode 100644 engine/pill_renderer/src/m1_bootstrap.rs diff --git a/engine/pill_engine/src/resources/mesh.rs b/engine/pill_engine/src/resources/mesh.rs index 616229fc..aa043c1f 100644 --- a/engine/pill_engine/src/resources/mesh.rs +++ b/engine/pill_engine/src/resources/mesh.rs @@ -1,19 +1,20 @@ use crate::{ - engine::Engine, - graphics::{ RendererMeshHandle }, - resources::{ ResourceStorage, Resource }, - ecs::{ DeferredUpdateManagerPointer, MeshRenderingComponent }, config::*, + ecs::{DeferredUpdateManagerPointer, MeshRenderingComponent}, + engine::Engine, + graphics::RendererMeshHandle, + resources::{Resource, ResourceStorage}, }; -use pill_core::{ EngineError, PillSlotMapKey, PillTypeMap, PillTypeMapKey, Vector3f, PillStyle, get_type_name }; +use pill_core::{ + get_type_name, EngineError, PillSlotMapKey, PillStyle, PillTypeMap, PillTypeMapKey, Vector3f, +}; -use std::path::{ Path, PathBuf }; +use anyhow::{Context, Error, Result}; use boolinator::Boolinator; use cgmath::InnerSpace; +use std::path::{Path, PathBuf}; use tobj::LoadOptions; -use anyhow::{Result, Context, Error}; - pill_core::define_new_pill_slotmap_key! { pub struct MeshHandle; @@ -40,7 +41,7 @@ impl Mesh { path, renderer_resource_handle: None, mesh_data: None, - flip_uv_y: false + flip_uv_y: false, } } @@ -48,6 +49,10 @@ impl Mesh { self.flip_uv_y = flip; self } + + pub fn get_aabb(&self) -> Option<([f32; 3], [f32; 3])> { + self.mesh_data.as_ref().map(|d| (d.aabb_min, d.aabb_max)) + } } impl PillTypeMapKey for Mesh { @@ -62,26 +67,37 @@ impl Resource for Mesh { } fn initialize(&mut self, engine: &mut Engine) -> Result<()> { - let error_message = format!("Initializing {} {} failed", "Resource".gobj_style(), get_type_name::().sobj_style()); + let error_message = format!( + "Initializing {} {} failed", + "Resource".gobj_style(), + get_type_name::().sobj_style() + ); // Check if path to asset is correct let resource_file_path = engine.game_resources_directory_path.join(&self.path); - pill_core::validate_asset_path(&resource_file_path, &["obj"]).context(error_message.clone())?; + pill_core::validate_asset_path(&resource_file_path, &["obj"]) + .context(error_message.clone())?; // Create mesh data - let mesh_data = MeshData::new(&resource_file_path, self.flip_uv_y).context(error_message.clone()) - .context(format!("Failed to create mesh data from {} file", resource_file_path.file_name().unwrap().to_string_lossy()))?; + let mesh_data = MeshData::new(&resource_file_path, self.flip_uv_y) + .context(error_message.clone()) + .context(format!( + "Failed to create mesh data from {} file", + resource_file_path.file_name().unwrap().to_string_lossy() + ))?; self.mesh_data = Some(mesh_data); // Create new renderer mesh resource - let renderer_resource_handle = engine.renderer.create_mesh(&self.name, &self.mesh_data.as_ref().unwrap()).context(error_message.clone())?; + let renderer_resource_handle = engine + .renderer + .create_mesh(&self.name, &self.mesh_data.as_ref().unwrap()) + .context(error_message.clone())?; self.renderer_resource_handle = Some(renderer_resource_handle); Ok(()) } fn destroy(&mut self, engine: &mut Engine, self_handle: H) -> Result<()> { - // Destroy renderer resource if let Some(v) = self.renderer_resource_handle { engine.renderer.destroy_mesh(v).unwrap(); @@ -89,12 +105,16 @@ impl Resource for Mesh { // Find mesh rendering components that use this mesh and update them for (scene_handle, scene) in engine.scene_manager.scenes.iter_mut() { - for (entity_handle, mesh_rendering_component) in scene.get_one_component_iterator_mut::()? { + for (entity_handle, mesh_rendering_component) in + scene.get_one_component_iterator_mut::()? + { if let Some(mesh_handle) = mesh_rendering_component.mesh_handle { // If mesh rendering component has handle to this mesh if mesh_handle.data() == self_handle.data() { mesh_rendering_component.set_mesh_handle(Option::::None); - mesh_rendering_component.update_render_queue_key(&engine.resource_manager).unwrap(); + mesh_rendering_component + .update_render_queue_key(&engine.resource_manager) + .unwrap(); } } } @@ -119,6 +139,8 @@ pub struct MeshVertex { pub struct MeshData { pub vertices: Vec, pub indices: Vec, + pub aabb_min: [f32; 3], + pub aabb_max: [f32; 3], } impl MeshData { @@ -135,11 +157,15 @@ impl MeshData { // Check data validity if models.len() > 1 { - return Err(Error::new(EngineError::InvalidModelFileMultipleMeshes(path.clone().into_os_string().into_string().unwrap()))); + return Err(Error::new(EngineError::InvalidModelFileMultipleMeshes( + path.clone().into_os_string().into_string().unwrap(), + ))); } if models.len() < 1 { - return Err(Error::new(EngineError::InvalidModelFile(path.clone().into_os_string().into_string().unwrap()))); + return Err(Error::new(EngineError::InvalidModelFile( + path.clone().into_os_string().into_string().unwrap(), + ))); } // Load vertex data from model @@ -147,19 +173,28 @@ impl MeshData { // Read vertices let mut vertices = Vec::new(); + let mut min_v = cgmath::Vector3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY); + let mut max_v = + cgmath::Vector3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY); for i in 0..mesh.positions.len() / 3 { let uv_y = *mesh.texcoords.get(i * 2 + 1).unwrap_or(&0.0); let final_uv_y = if flip_uv_y { uv_y } else { 1.0 - uv_y }; + let pos = [ + mesh.positions[i * 3], + mesh.positions[i * 3 + 1], + mesh.positions[i * 3 + 2], + ]; + let p = cgmath::Vector3::new(pos[0], pos[1], pos[2]); + min_v = cgmath::Vector3::new(min_v.x.min(p.x), min_v.y.min(p.y), min_v.z.min(p.z)); + max_v = cgmath::Vector3::new(max_v.x.max(p.x), max_v.y.max(p.y), max_v.z.max(p.z)); + vertices.push(MeshVertex { - position: [ - mesh.positions[i * 3], - mesh.positions[i * 3 + 1], - mesh.positions[i * 3 + 2], - ], - texture_coordinates: [ // Blender uses V coordinate flipped + position: pos, + texture_coordinates: [ + // Blender uses V coordinate flipped *mesh.texcoords.get(i * 2).unwrap_or(&0.0), - final_uv_y + final_uv_y, ], normal: [ mesh.normals[i * 3], @@ -203,12 +238,18 @@ impl MeshData { let bitangent = (delta_pos2 * delta_uv1.x - delta_pos1 * delta_uv2.x) * r; // Assign same tangent/bitangent to each vertex in the triangle - vertices[c[0] as usize].tangent = (tangent + cgmath::Vector3::from(vertices[c[0] as usize].tangent)).into(); - vertices[c[1] as usize].tangent = (tangent + cgmath::Vector3::from(vertices[c[1] as usize].tangent)).into(); - vertices[c[2] as usize].tangent = (tangent + cgmath::Vector3::from(vertices[c[2] as usize].tangent)).into(); - vertices[c[0] as usize].bitangent = (bitangent + cgmath::Vector3::from(vertices[c[0] as usize].bitangent)).into(); - vertices[c[1] as usize].bitangent = (bitangent + cgmath::Vector3::from(vertices[c[1] as usize].bitangent)).into(); - vertices[c[2] as usize].bitangent = (bitangent + cgmath::Vector3::from(vertices[c[2] as usize].bitangent)).into(); + vertices[c[0] as usize].tangent = + (tangent + cgmath::Vector3::from(vertices[c[0] as usize].tangent)).into(); + vertices[c[1] as usize].tangent = + (tangent + cgmath::Vector3::from(vertices[c[1] as usize].tangent)).into(); + vertices[c[2] as usize].tangent = + (tangent + cgmath::Vector3::from(vertices[c[2] as usize].tangent)).into(); + vertices[c[0] as usize].bitangent = + (bitangent + cgmath::Vector3::from(vertices[c[0] as usize].bitangent)).into(); + vertices[c[1] as usize].bitangent = + (bitangent + cgmath::Vector3::from(vertices[c[1] as usize].bitangent)).into(); + vertices[c[2] as usize].bitangent = + (bitangent + cgmath::Vector3::from(vertices[c[2] as usize].bitangent)).into(); // Prepare data for averaging tangents and bitangents triangles_included[c[0] as usize] += 1; @@ -221,14 +262,18 @@ impl MeshData { let denom = 1.0 / n as f32; let vertex = &mut vertices[i]; vertex.tangent = (Vector3f::from(vertex.tangent) * denom).normalize().into(); - vertex.bitangent = (Vector3f::from(vertex.bitangent) * denom).normalize().into(); + vertex.bitangent = (Vector3f::from(vertex.bitangent) * denom) + .normalize() + .into(); } let mesh_data = MeshData { vertices: vertices, indices: mesh.indices.clone(), + aabb_min: [min_v.x, min_v.y, min_v.z], + aabb_max: [max_v.x, max_v.y, max_v.z], }; Ok(mesh_data) } -} \ No newline at end of file +} diff --git a/engine/pill_renderer/src/m1_bootstrap.rs b/engine/pill_renderer/src/m1_bootstrap.rs deleted file mode 100644 index 269a32fb..00000000 --- a/engine/pill_renderer/src/m1_bootstrap.rs +++ /dev/null @@ -1,181 +0,0 @@ -use anyhow::Result; - -#[derive(Clone, Debug)] -pub struct WindowComponent { - pub width: u32, - pub height: u32, - pub title: String, - pub vsync: bool, -} - -pub fn run_m1(window: &WindowComponent) -> Result<()> { - // winit event loop + wgpu init per DESIGN.md (M1) - use winit::{ - dpi::LogicalSize, - event::{Event, WindowEvent}, - event_loop::EventLoop, - window::WindowBuilder, - }; - - let event_loop = EventLoop::new().unwrap(); - let winit_window = WindowBuilder::new() - .with_title(window.title.clone()) - .with_inner_size(LogicalSize::new(window.width as f64, window.height as f64)) - .build(&event_loop)?; - let vsync = window.vsync; // NEW - let window = std::sync::Arc::new(winit_window); - - // Instance and surface - let backends = wgpu::util::backend_bits_from_env().unwrap_or_default(); - let dx12_shader_compiler = wgpu::util::dx12_shader_compiler_from_env().unwrap_or_default(); - let gles_minor_version = wgpu::util::gles_minor_version_from_env().unwrap_or_default(); - - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends, - flags: wgpu::InstanceFlags::from_build_config().with_env(), - dx12_shader_compiler, - gles_minor_version, - }); - let surface = instance.create_surface(window.clone()).unwrap(); - - // Adapter - let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::default(), - compatible_surface: Some(&surface), - force_fallback_adapter: false, - })) - .ok_or_else(|| anyhow::anyhow!("No compatible GPU adapter found"))?; - - // Device/queue - let (device, queue) = pollster::block_on(adapter.request_device( - &wgpu::DeviceDescriptor { - label: Some("m1_device"), - required_features: wgpu::Features::empty(), - required_limits: wgpu::Limits::default(), - }, - None, - ))?; - - // Surface configuration per capabilities - let surface_caps = surface.get_capabilities(&adapter); - let preferred_formats = [wgpu::TextureFormat::Rgba8UnormSrgb, wgpu::TextureFormat::Bgra8UnormSrgb]; - let format = preferred_formats - .into_iter() - .find(|f| surface_caps.formats.contains(f)) - .unwrap_or(surface_caps.formats[0]); - - let present_mode = if vsync { - wgpu::PresentMode::Fifo - } else if surface_caps.present_modes.contains(&wgpu::PresentMode::Mailbox) { - wgpu::PresentMode::Mailbox - } else if surface_caps.present_modes.contains(&wgpu::PresentMode::Immediate) { - wgpu::PresentMode::Immediate - } else { - wgpu::PresentMode::Fifo - }; - - let size = window.inner_size(); - let mut config = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format, - width: size.width, - height: size.height, - desired_maximum_frame_latency: 2, - present_mode, - alpha_mode: wgpu::CompositeAlphaMode::Auto, - view_formats: vec![format], - }; - surface.configure(&device, &config); - - // Depth texture created but unused yet; recreate on resize - let mut depth_view = create_depth_view(&device, &config); - - event_loop.run(move |event, elwt| { - match event { - Event::WindowEvent { event, window_id } if window_id == window.id() => match event { - WindowEvent::CloseRequested => elwt.exit(), - WindowEvent::Resized(new_size) => { - if new_size.width > 0 && new_size.height > 0 { - config.width = new_size.width; - config.height = new_size.height; - surface.configure(&device, &config); - depth_view = create_depth_view(&device, &config); - } - } - WindowEvent::ScaleFactorChanged { .. } => { - let new_size = window.inner_size(); - if new_size.width > 0 && new_size.height > 0 { - config.width = new_size.width; - config.height = new_size.height; - surface.configure(&device, &config); - depth_view = create_depth_view(&device, &config); - } - } - _ => {} - }, - Event::AboutToWait => { - // Render black frame - let frame = match surface.get_current_texture() { - Ok(frame) => frame, - Err(err) => match err { - wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated => { - surface.configure(&device, &config); - return; - } - wgpu::SurfaceError::Timeout => { - return; - } - wgpu::SurfaceError::OutOfMemory => { - elwt.exit(); - return; - } - }, - }; - let view = frame.texture.create_view(&wgpu::TextureViewDescriptor::default()); - let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("m1_encoder") }); - { - let _rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("m1_clear_black"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &depth_view, - depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: wgpu::StoreOp::Store }), - stencil_ops: None, - }), - timestamp_writes: None, - occlusion_query_set: None, - }); - } - queue.submit([encoder.finish()]); - frame.present(); - } - _ => {} - } - })?; - - Ok(()) -} - -fn create_depth_view(device: &wgpu::Device, config: &wgpu::SurfaceConfiguration) -> wgpu::TextureView { - let depth_format = wgpu::TextureFormat::Depth32Float; - let depth = device.create_texture(&wgpu::TextureDescriptor { - label: Some("m1_depth"), - size: wgpu::Extent3d { width: config.width, height: config.height, depth_or_array_layers: 1 }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: depth_format, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], - }); - depth.create_view(&wgpu::TextureViewDescriptor::default()) -} - - diff --git a/engine/pill_renderer/src/mesh_drawer.rs b/engine/pill_renderer/src/mesh_drawer.rs index ecd52ad9..9b6de538 100644 --- a/engine/pill_renderer/src/mesh_drawer.rs +++ b/engine/pill_renderer/src/mesh_drawer.rs @@ -1,20 +1,27 @@ use std::{num::NonZeroU32, ops::Range}; +use crate::{ + resources::RendererCamera, Instance, RendererResourceStorage, INITIAL_INSTANCE_VECTOR_CAPACITY, +}; use pill_core::{RendererError, Timer}; -use pill_engine::{ internal::{ RenderQueueItem, RendererMaterialHandle, RendererMeshHandle, RendererPipelineHandle, TransformComponent, RENDER_QUEUE_KEY_ORDER}, ComponentStorage}; -use crate::{resources::RendererCamera, Instance, RendererResourceStorage,INITIAL_INSTANCE_VECTOR_CAPACITY}; +use pill_engine::{ + internal::{ + RenderQueueItem, RendererMaterialHandle, RendererMeshHandle, RendererPipelineHandle, + TransformComponent, RENDER_QUEUE_KEY_ORDER, + }, + ComponentStorage, +}; use anyhow::{Error, Result}; pub struct MeshDrawer { max_instance_batch_size: u32, - instances: Vec::, + instances: Vec, instance_buffer: wgpu::Buffer, } impl MeshDrawer { pub fn new(device: &wgpu::Device, max_instance_batch_size: u32) -> Self { - // Create instance buffer let buffer_size = (size_of::() * max_instance_batch_size as usize) as u64; let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor { @@ -26,25 +33,25 @@ impl MeshDrawer { MeshDrawer { max_instance_batch_size, - instances: Vec::::with_capacity(INITIAL_INSTANCE_VECTOR_CAPACITY), + instances: Vec::::with_capacity(INITIAL_INSTANCE_VECTOR_CAPACITY), instance_buffer, } } pub fn record_draw_commands( - &mut self, + &mut self, // Resources - queue: &wgpu::Queue, + queue: &wgpu::Queue, device: &wgpu::Device, - renderer_resource_storage: &RendererResourceStorage, - color_attachment: wgpu::RenderPassColorAttachment, + renderer_resource_storage: &RendererResourceStorage, + color_attachment: wgpu::RenderPassColorAttachment, depth_stencil_attachment: wgpu::RenderPassDepthStencilAttachment, // Rendring data camera: &RendererCamera, - render_queue: &Vec::, + render_queue: &Vec, transform_component_storage: &ComponentStorage, - timer: &mut Timer - ) -> Result<()> { + timer: &mut Timer, + ) -> Result<()> { timer.record("Prepare render pass"); let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { @@ -53,14 +60,16 @@ impl MeshDrawer { let mut instance_batch_number = 0; let mut rendering_context_change_number = 0; - - for (i, instance_batch) in render_queue.chunks(self.max_instance_batch_size as usize).enumerate() { + for (i, instance_batch) in render_queue + .chunks(self.max_instance_batch_size as usize) + .enumerate() + { let batch_size = instance_batch.len(); instance_batch_number += 1; timer.begin_context(&format!("Prepare draw instance batch {}", i)); - + timer.record(&format!("Write instance buffer")); // Prepare instance data and load it to buffer @@ -68,7 +77,8 @@ impl MeshDrawer { self.instances.reserve(instance_batch.len()); // Pre-allocate exact capacity for render_queue_item in instance_batch { - let transform_slot = transform_component_storage.data + let transform_slot = transform_component_storage + .data .get(render_queue_item.entity_index as usize) .ok_or_else(|| Error::new(RendererError::Other))?; let transform_component = transform_slot @@ -78,61 +88,71 @@ impl MeshDrawer { self.instances.push(Instance::new(transform_component)); } + queue.write_buffer( + &self.instance_buffer, + 0, + bytemuck::cast_slice(&self.instances), + ); // Update instance buffer - - queue.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&self.instances)); // Update instance buffer - - - let mut current_rendering_order: u8 = 0; - let mut current_pipeline_handle: Option = None; - let mut current_material_handle: Option = None; - let mut current_mesh_handle: Option = None; - let mut current_mesh_index_count: u32 = 0; // Number of indices in the current mesh - + let mut current_rendering_order: u8 = 0; + let mut current_pipeline_handle: Option = None; + let mut current_material_handle: Option = None; + let mut current_mesh_handle: Option = None; + let mut current_mesh_index_count: u32 = 0; // Number of indices in the current mesh // Start encoding render pass let load_op = if i == 0 { - wgpu::LoadOp::Clear(wgpu::Color::BLACK) -} else { - wgpu::LoadOp::Load -}; - -let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("render_pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - ops: wgpu::Operations { - load: load_op, - store: wgpu::StoreOp::Store, - }, - ..color_attachment.clone() - })], - depth_stencil_attachment: Some(depth_stencil_attachment.clone()), - timestamp_writes: None, - occlusion_query_set: None, -}); - + wgpu::LoadOp::Clear(wgpu::Color::BLACK) + } else { + wgpu::LoadOp::Load + }; + + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("render_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + ops: wgpu::Operations { + load: load_op, + store: wgpu::StoreOp::Store, + }, + ..color_attachment.clone() + })], + depth_stencil_attachment: Some(depth_stencil_attachment.clone()), + timestamp_writes: None, + occlusion_query_set: None, + }); render_pass.set_vertex_buffer(1, self.instance_buffer.slice(..)); // Set instance buffer - + timer.record("Record draw instances commands"); // Reset instance range for each batch // Start inclusive, end exclusive (e.g. 0..3 means indices 0, 1, 2. e.g. 5..7 means indices 5, 6) let mut accumulated_instance_range: Range = 0..0; - let mut accumulated_instance_count: u32 = 0; + let mut accumulated_instance_count: u32 = 0; for (j, render_queue_item) in instance_batch.iter().enumerate() { - let render_queue_key_fields = pill_engine::internal::decompose_render_queue_key(render_queue_item.key); + let render_queue_key_fields = + pill_engine::internal::decompose_render_queue_key(render_queue_item.key); // Recreate resource handles - let renderer_material_handle = RendererMaterialHandle::new(render_queue_key_fields.material_index.into(), NonZeroU32::new(render_queue_key_fields.material_version.into()).unwrap()); - let renderer_mesh_handle = RendererMeshHandle::new(render_queue_key_fields.mesh_index.into(), NonZeroU32::new(render_queue_key_fields.mesh_version.into()).unwrap()); - + let renderer_material_handle = RendererMaterialHandle::new( + render_queue_key_fields.material_index.into(), + NonZeroU32::new(render_queue_key_fields.material_version.into()).unwrap(), + ); + let renderer_mesh_handle = RendererMeshHandle::new( + render_queue_key_fields.mesh_index.into(), + NonZeroU32::new(render_queue_key_fields.mesh_version.into()).unwrap(), + ); // Check rendering order if current_rendering_order > render_queue_key_fields.order { if accumulated_instance_count > 0 { - render_pass.draw_indexed(0..current_mesh_index_count, 0, accumulated_instance_range.clone()); - accumulated_instance_range = accumulated_instance_range.end..accumulated_instance_range.end; + render_pass.draw_indexed( + 0..current_mesh_index_count, + 0, + accumulated_instance_range.clone(), + ); + accumulated_instance_range = + accumulated_instance_range.end..accumulated_instance_range.end; } // Set new order @@ -144,17 +164,28 @@ let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { if current_material_handle != Some(renderer_material_handle) { // Render accumulated instances if accumulated_instance_count > 0 { - render_pass.draw_indexed(0..current_mesh_index_count, 0, accumulated_instance_range.clone()); - accumulated_instance_range = accumulated_instance_range.end..accumulated_instance_range.end; + render_pass.draw_indexed( + 0..current_mesh_index_count, + 0, + accumulated_instance_range.clone(), + ); + accumulated_instance_range = + accumulated_instance_range.end..accumulated_instance_range.end; } // Set new material current_material_handle = Some(renderer_material_handle); - let material = renderer_resource_storage.materials.get(current_material_handle.unwrap()).unwrap(); - + let material = renderer_resource_storage + .materials + .get(current_material_handle.unwrap()) + .unwrap(); + // Set pipeline if new material is using different one if current_pipeline_handle != Some(material.pipeline_handle) { current_pipeline_handle = Some(material.pipeline_handle); - let pipeline = renderer_resource_storage.pipelines.get( current_pipeline_handle.unwrap()).unwrap(); + let pipeline = renderer_resource_storage + .pipelines + .get(current_pipeline_handle.unwrap()) + .unwrap(); render_pass.set_pipeline(&pipeline.render_pipeline); } @@ -169,32 +200,47 @@ let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { if current_mesh_handle != Some(renderer_mesh_handle) { // Render accumulated instances if accumulated_instance_count > 0 { - render_pass.draw_indexed(0..current_mesh_index_count, 0, accumulated_instance_range.clone()); - accumulated_instance_range = accumulated_instance_range.end..accumulated_instance_range.end; + render_pass.draw_indexed( + 0..current_mesh_index_count, + 0, + accumulated_instance_range.clone(), + ); + accumulated_instance_range = + accumulated_instance_range.end..accumulated_instance_range.end; } // Set new mesh - current_mesh_handle = Some(renderer_mesh_handle); - let mesh = renderer_resource_storage.meshes.get(current_mesh_handle.unwrap()).unwrap(); + current_mesh_handle = Some(renderer_mesh_handle); + let mesh = renderer_resource_storage + .meshes + .get(current_mesh_handle.unwrap()) + .unwrap(); current_mesh_index_count = mesh.index_count; - render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); - render_pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32); + render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); + render_pass + .set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32); rendering_context_change_number += 1; } // Add new instance - accumulated_instance_range = accumulated_instance_range.start..accumulated_instance_range.end + 1; - accumulated_instance_count = accumulated_instance_range.end - accumulated_instance_range.start; + accumulated_instance_range = + accumulated_instance_range.start..accumulated_instance_range.end + 1; + accumulated_instance_count = + accumulated_instance_range.end - accumulated_instance_range.start; // If last in batch, render accumulated instances if j == batch_size - 1 && accumulated_instance_count > 0 { - render_pass.draw_indexed(0..current_mesh_index_count, 0, accumulated_instance_range.clone()); + render_pass.draw_indexed( + 0..current_mesh_index_count, + 0, + accumulated_instance_range.clone(), + ); } } timer.end_context()?; // End "Draw instance batch" context } -queue.submit(std::iter::once(encoder.finish())); + queue.submit(std::iter::once(encoder.finish())); Ok(()) } -} \ No newline at end of file +} diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index f1828ec2..82fd7423 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -3,8 +3,6 @@ // https://github.com/emilk/egui/discussions/3067 use crate::{ - instance::Instance, - mesh_drawer::MeshDrawer, renderer_resource_storage::RendererResourceStorage, resources::{ RendererCamera, RendererMaterial, RendererMesh, RendererPipeline, RendererTexture, Vertex, @@ -21,8 +19,9 @@ use pill_engine::internal::{ use pill_core::{PillSlotMapKey, PillSlotMapKeyData, PillStyle, RendererError, Timer}; -use std::{iter, mem::size_of, num::NonZeroU32, ops::Range, sync::Arc}; +use std::{num::NonZeroU32, sync::Arc}; +use cgmath::{Deg, InnerSpace}; use naga::back::wgsl; use naga::front::glsl; @@ -33,6 +32,7 @@ use crate::egui::EguiRenderer; pub const MAX_INSTANCE_BATCH_SIZE: usize = 10000; // Maximum number of instances that can be drawn in a single draw call pub const INITIAL_INSTANCE_VECTOR_CAPACITY: usize = 10000; +// M2 inline draw: no MeshDrawer/instance batching // Default resource handle - Master pipeline pub const MASTER_PIPELINE_HANDLE: RendererPipelineHandle = RendererPipelineHandle { @@ -80,28 +80,39 @@ impl PillRenderer for Renderer { fn set_master_pipeline( &mut self, - vertex_shader_bytes: &[u8], - fragment_shader_bytes: &[u8], + _vertex_shader_bytes: &[u8], + _fragment_shader_bytes: &[u8], ) -> Result<()> { - // Create shaders - // Convert bytes to string - let vertex_shader_source = std::str::from_utf8(vertex_shader_bytes) - .map_err(|e| anyhow::anyhow!("Invalid UTF-8 in vertex shader: {}", e))?; - let fragment_shader_source = std::str::from_utf8(fragment_shader_bytes) - .map_err(|e| anyhow::anyhow!("Invalid UTF-8 in fragment shader: {}", e))?; - - // Convert GLSL to WGSL - let vertex_wgsl = - compile_glsl_to_wgsl(vertex_shader_source, naga::ShaderStage::Vertex).unwrap(); - let fragment_wgsl = - compile_glsl_to_wgsl(fragment_shader_source, naga::ShaderStage::Fragment).unwrap(); + // M3: WGSL pipeline with per-draw MVP via dynamic offsets (set 3) + let vertex_wgsl = r#" +struct Camera { position: vec4, view_projection_matrix: mat4x4, }; +@group(2) @binding(0) var GCamera: Camera; + +struct PerDraw { mvp: mat4x4 }; +@group(3) @binding(0) var UPerDraw: PerDraw; + +struct VSIn { @location(0) pos: vec3, }; +struct VSOut { @builtin(position) pos: vec4, }; + +@vertex fn main(input: VSIn) -> VSOut { + var out: VSOut; + out.pos = UPerDraw.mvp * vec4(input.pos, 1.0); + return out; +} +"#; + + let fragment_wgsl = r#" +@fragment fn main() -> @location(0) vec4 { + return vec4(1.0, 0.7, 0.3, 1.0); +} +"#; // Create shader modules with WGSL let vertex_shader = self .state .device .create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("master_vertex_shader"), + label: Some("m2_vertex_shader"), source: wgpu::ShaderSource::Wgsl(vertex_wgsl.into()), }); @@ -109,7 +120,7 @@ impl PillRenderer for Renderer { self.state .device .create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("master_fragment_shader"), + label: Some("m2_fragment_shader"), source: wgpu::ShaderSource::Wgsl(fragment_wgsl.into()), }); @@ -120,10 +131,7 @@ impl PillRenderer for Renderer { fragment_shader, self.state.color_format, Some(self.state.depth_format), - &[ - RendererMesh::data_layout_descriptor(), - Instance::data_layout_descriptor(), - ], + &[RendererMesh::data_layout_descriptor()], ) .unwrap(); @@ -319,7 +327,6 @@ pub struct State { color_format: wgpu::TextureFormat, depth_format: wgpu::TextureFormat, depth_texture: RendererTexture, - mesh_drawer: MeshDrawer, // Other config: config::Config, egui_renderer: crate::egui::EguiRenderer, @@ -439,9 +446,6 @@ impl State { let color_format = surface_configuration.format; let depth_format = wgpu::TextureFormat::Depth32Float; - // Create drawing state - let mesh_drawer = MeshDrawer::new(&device, MAX_INSTANCE_BATCH_SIZE as u32); - let egui_renderer = EguiRenderer::new(&device, surface_configuration.format, None, 1, window_ref); @@ -458,7 +462,6 @@ impl State { color_format, depth_format, depth_texture, - mesh_drawer, // Other config, egui_renderer, @@ -490,66 +493,7 @@ impl State { egui_ui: Box, timer: &mut Timer, ) -> Result<()> { - // Get frame or return mapped error if failed - let frame = self.surface.get_current_texture(); - - let frame = match frame { - std::result::Result::Ok(frame) => frame, - std::result::Result::Err(error) => match error { - wgpu::SurfaceError::Lost => return Err(RendererError::SurfaceLost.into()), - wgpu::SurfaceError::OutOfMemory => { - return Err(RendererError::SurfaceOutOfMemory.into()) - } - _ => return Err(RendererError::SurfaceOther.into()), - }, - }; - - let view = frame - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - let mut encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("m1_encoder"), - }); - { - let _rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("m1_clear_black"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &self.depth_texture.texture_view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Clear(1.0), - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }), - timestamp_writes: None, - occlusion_query_set: None, - }); - } - self.queue.submit([encoder.finish()]); - frame.present(); - - Ok(()) - } - - fn render_old( - &mut self, - active_camera_entity_handle: EntityHandle, - render_queue: &Vec, - camera_component_storage: &ComponentStorage, - transform_component_storage: &ComponentStorage, - egui_ui: Box, - timer: &mut Timer, - ) -> Result<()> { + // M3: Hello Mesh + per-draw MVP (dynamic offsets); timer.record("Get frame"); // Get frame or return mapped error if failed @@ -604,67 +548,273 @@ impl State { .unwrap(); let clear_color = active_camera_component.clear_color; - // Build a command buffer that can be sent to the GPU + // Record inline draw pass (M2) let mut encoder = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("render_encoder"), + label: Some("m2_inline_encoder"), }); - { - // Additional scope to release mutable borrow of encoder done by begin_render_pass - - // Create color attachment - let color_attachment = wgpu::RenderPassColorAttachment { - view: &view, // Specifies what texture to save the colors to - resolve_target: None, // Specifies what texture will receive the resolved output - ops: wgpu::Operations { - // Specifies what to do with the colors on the screen - load: wgpu::LoadOp::Clear(wgpu::Color { - r: clear_color.x as f64, - g: clear_color.y as f64, - b: clear_color.z as f64, - a: 1.0, - }), // Specifies how to handle colors stored from the previous frame - store: wgpu::StoreOp::Store, - }, - }; + // Prepare per-draw dynamic uniform buffer and bind group BEFORE render pass (lifetime requirement) + let per_draw_stride: u64 = 256; // alignment + let per_draw_capacity = render_queue.len() as u64 + 1; + let per_draw_buffer_size = per_draw_stride * per_draw_capacity; + let per_draw_buffer = self.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("per_draw_dynamic_ubo"), + size: per_draw_buffer_size, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let pipeline = self + .renderer_resource_storage + .pipelines + .get(MASTER_PIPELINE_HANDLE) + .unwrap(); + let per_draw_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("per_draw_bind_group"), + layout: &pipeline.per_draw_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &per_draw_buffer, + offset: 0, + size: Some(std::num::NonZeroU64::new(64).unwrap()), + }), + }], + }); - // Create depth attachment - let depth_stencil_attachment = wgpu::RenderPassDepthStencilAttachment { - view: &self.depth_texture.texture_view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Clear(1.0), - store: wgpu::StoreOp::Store, + { + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("m2_inline_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: clear_color.x as f64, + g: clear_color.y as f64, + b: clear_color.z as f64, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &self.depth_texture.texture_view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, }), - stencil_ops: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + // Inline draw: iterate render_queue, set state, draw + let mut current_pipeline: Option = None; + let mut current_material: Option = None; + let mut current_mesh: Option = None; + + let mut draw_index: u64 = 0; + + // Extract frustum planes from camera view-projection for CPU culling + let vp_mat: cgmath::Matrix4 = + renderer_camera.uniform.view_projection_matrix.into(); + let m = vp_mat; + let row = |i: usize| -> cgmath::Vector4 { + // cgmath is column-major; construct row i + cgmath::Vector4::new(m.x[i], m.y[i], m.z[i], m.w[i]) }; + let make_plane = |v: cgmath::Vector4| -> (cgmath::Vector3, f32) { + let n = cgmath::Vector3::new(v.x, v.y, v.z); + let len = n.magnitude(); + let normal = if len > 0.0 { n / len } else { n }; + let d = v.w / if len > 0.0 { len } else { 1.0 }; + (normal, d) + }; + let planes = [ + make_plane(row(3) + row(0)), // left + make_plane(row(3) - row(0)), // right + make_plane(row(3) + row(1)), // bottom + make_plane(row(3) - row(1)), // top + make_plane(row(3) + row(2)), // near + make_plane(row(3) - row(2)), // far + ]; + + for render_queue_item in render_queue.iter() { + let key = pill_engine::internal::decompose_render_queue_key(render_queue_item.key); + + let mesh_handle = RendererMeshHandle::new( + key.mesh_index.into(), + NonZeroU32::new(key.mesh_version.into()).unwrap(), + ); + let material_handle = RendererMaterialHandle::new( + key.material_index.into(), + NonZeroU32::new(key.material_version.into()).unwrap(), + ); + + if current_material != Some(material_handle) { + current_material = Some(material_handle); + let material = self + .renderer_resource_storage + .materials + .get(material_handle) + .unwrap(); + + if current_pipeline != Some(material.pipeline_handle) { + current_pipeline = Some(material.pipeline_handle); + let pipeline = self + .renderer_resource_storage + .pipelines + .get(material.pipeline_handle) + .unwrap(); + rpass.set_pipeline(&pipeline.render_pipeline); + } + + rpass.set_bind_group(0, &material.texture_bind_group, &[]); + rpass.set_bind_group(1, &material.parameter_bind_group, &[]); + rpass.set_bind_group(2, &renderer_camera.bind_group, &[]); + } - timer.begin_context("Mesh Drawer"); + if current_mesh != Some(mesh_handle) { + current_mesh = Some(mesh_handle); + let mesh = self + .renderer_resource_storage + .meshes + .get(mesh_handle) + .unwrap(); + rpass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); + rpass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32); + } - self.mesh_drawer.record_draw_commands( - &self.queue, - &self.device, - &self.renderer_resource_storage, - color_attachment, - depth_stencil_attachment, - &renderer_camera, - &render_queue, - &transform_component_storage, - timer, - )?; - - timer.end_context()?; - } + // Compute model matrix and MVP, upload at aligned offset, bind with dynamic offset + let entity_index = render_queue_item.entity_index as usize; + let transform = transform_component_storage + .data + .get(entity_index) + .unwrap() + .as_ref() + .unwrap(); + + // Model matrix updated in system, fetch and convert + let model_arr = pill_engine::internal::get_model_matrix(transform); + let model: cgmath::Matrix4 = model_arr.into(); + + // Frustum culling against mesh world-space AABB + let mesh = self + .renderer_resource_storage + .meshes + .get(current_mesh.unwrap()) + .unwrap(); + + let local_min = + cgmath::Vector3::new(mesh.aabb_min[0], mesh.aabb_min[1], mesh.aabb_min[2]); + let local_max = + cgmath::Vector3::new(mesh.aabb_max[0], mesh.aabb_max[1], mesh.aabb_max[2]); + let corners = [ + cgmath::Vector3::new(local_min.x, local_min.y, local_min.z), + cgmath::Vector3::new(local_max.x, local_min.y, local_min.z), + cgmath::Vector3::new(local_min.x, local_max.y, local_min.z), + cgmath::Vector3::new(local_max.x, local_max.y, local_min.z), + cgmath::Vector3::new(local_min.x, local_min.y, local_max.z), + cgmath::Vector3::new(local_max.x, local_min.y, local_max.z), + cgmath::Vector3::new(local_min.x, local_max.y, local_max.z), + cgmath::Vector3::new(local_max.x, local_max.y, local_max.z), + ]; + let mut world_min = + cgmath::Vector3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY); + let mut world_max = + cgmath::Vector3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY); + for c in &corners { + let p4 = model * cgmath::Vector4::new(c.x, c.y, c.z, 1.0); + let p = cgmath::Vector3::new(p4.x, p4.y, p4.z); + world_min.x = world_min.x.min(p.x); + world_min.y = world_min.y.min(p.y); + world_min.z = world_min.z.min(p.z); + world_max.x = world_max.x.max(p.x); + world_max.y = world_max.y.max(p.y); + world_max.z = world_max.z.max(p.z); + } + + // Plane-AABB test; skip draw if outside any plane + let mut outside = false; + for (normal, d) in &planes { + let p = cgmath::Vector3::new( + if normal.x >= 0.0 { + world_max.x + } else { + world_min.x + }, + if normal.y >= 0.0 { + world_max.y + } else { + world_min.y + }, + if normal.z >= 0.0 { + world_max.z + } else { + world_min.z + }, + ); + let dist = normal.dot(p) + *d; + if dist < 0.0 { + outside = true; + break; + } + } + if outside { + continue; + } - timer.begin_context("Egui Draw"); + // Use camera view-projection from renderer_camera.uniform + let view_proj: cgmath::Matrix4 = + renderer_camera.uniform.view_projection_matrix.into(); + let mvp = view_proj * model; + + let offset_bytes = draw_index * per_draw_stride; + let offset_aligned_u32 = offset_bytes as u32; // guaranteed < 4GiB in this simple path + let mvp_arr: [[f32; 4]; 4] = mvp.into(); + self.queue.write_buffer( + &per_draw_buffer, + offset_bytes, + bytemuck::cast_slice(&[mvp_arr]), + ); + + rpass.set_bind_group(3, &per_draw_bind_group, &[offset_aligned_u32]); + rpass.draw_indexed(0..mesh.index_count, 0, 0..1); + draw_index += 1; + } + } - timer.end_context()?; // End Egui Draw context + self.queue.submit([encoder.finish()]); - timer.record("Submit commands and present frame"); + // Egui overlay (load over the rendered frame) + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("egui_encoder"), + }); + let screen_descriptor = egui_wgpu::ScreenDescriptor { + size_in_pixels: [self.window_size.width, self.window_size.height], + pixels_per_point: self.egui_renderer.window_scale_factor, + }; + self.egui_renderer.draw( + &self.device, + &self.queue, + &mut encoder, + &view, + screen_descriptor, + |ctx| egui_ui(ctx), + timer, + )?; + self.queue.submit([encoder.finish()]); + // Present frame frame.present(); Ok(()) } + + // old render path removed (M2 uses inline draws) } diff --git a/engine/pill_renderer/src/resources/renderer_mesh.rs b/engine/pill_renderer/src/resources/renderer_mesh.rs index 46828a91..68546df3 100644 --- a/engine/pill_renderer/src/resources/renderer_mesh.rs +++ b/engine/pill_renderer/src/resources/renderer_mesh.rs @@ -1,7 +1,7 @@ -use pill_engine::internal::{ MeshData, MeshVertex }; +use pill_engine::internal::{MeshData, MeshVertex}; -use wgpu::util::DeviceExt; use anyhow::*; +use wgpu::util::DeviceExt; // --- Vertex --- @@ -16,11 +16,12 @@ pub struct RendererMesh { pub vertex_buffer: wgpu::Buffer, pub index_buffer: wgpu::Buffer, pub index_count: u32, + pub aabb_min: [f32; 3], + pub aabb_max: [f32; 3], } impl RendererMesh { - pub fn new(device: &wgpu::Device, name: &str, mesh_data: &MeshData) -> Result { - + pub fn new(device: &wgpu::Device, name: &str, mesh_data: &MeshData) -> Result { let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some(&format!("{:?}_vertex_buffer", name)), contents: bytemuck::cast_slice(&mesh_data.vertices), @@ -37,6 +38,8 @@ impl RendererMesh { vertex_buffer, index_buffer, index_count: mesh_data.indices.len() as u32, + aabb_min: mesh_data.aabb_min, + aabb_max: mesh_data.aabb_max, }; Ok(renderer_mesh) @@ -50,27 +53,32 @@ impl Vertex for RendererMesh { array_stride: mem::size_of::() as wgpu::BufferAddress, step_mode: wgpu::VertexStepMode::Vertex, attributes: &[ - wgpu::VertexAttribute { // Vertex position - offset: 0, - shader_location: 0, - format: wgpu::VertexFormat::Float32x3, + wgpu::VertexAttribute { + // Vertex position + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float32x3, }, - wgpu::VertexAttribute { // Vertex texture coordinates + wgpu::VertexAttribute { + // Vertex texture coordinates offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, shader_location: 1, format: wgpu::VertexFormat::Float32x2, }, - wgpu::VertexAttribute { // Vertex normal + wgpu::VertexAttribute { + // Vertex normal offset: mem::size_of::<[f32; 5]>() as wgpu::BufferAddress, shader_location: 2, format: wgpu::VertexFormat::Float32x3, }, - wgpu::VertexAttribute { // Vertex tangent + wgpu::VertexAttribute { + // Vertex tangent offset: mem::size_of::<[f32; 8]>() as wgpu::BufferAddress, shader_location: 3, format: wgpu::VertexFormat::Float32x3, }, - wgpu::VertexAttribute { // Vertex bitangent + wgpu::VertexAttribute { + // Vertex bitangent offset: mem::size_of::<[f32; 11]>() as wgpu::BufferAddress, shader_location: 4, format: wgpu::VertexFormat::Float32x3, @@ -79,5 +87,3 @@ impl Vertex for RendererMesh { } } } - - diff --git a/engine/pill_renderer/src/resources/renderer_pipeline.rs b/engine/pill_renderer/src/resources/renderer_pipeline.rs index 10fa3c3b..35e14db5 100644 --- a/engine/pill_renderer/src/resources/renderer_pipeline.rs +++ b/engine/pill_renderer/src/resources/renderer_pipeline.rs @@ -1,4 +1,4 @@ -use anyhow::{ Result }; +use anyhow::Result; // --- Pipeline --- @@ -7,6 +7,7 @@ pub struct RendererPipeline { pub material_texture_bind_group_layout: wgpu::BindGroupLayout, pub material_parameter_bind_group_layout: wgpu::BindGroupLayout, pub camera_bind_group_layout: wgpu::BindGroupLayout, + pub per_draw_bind_group_layout: wgpu::BindGroupLayout, } impl RendererPipeline { @@ -18,50 +19,53 @@ impl RendererPipeline { depth_format: Option, vertex_layouts: &[wgpu::VertexBufferLayout], ) -> Result { - // Define material bind group layout (Describes a set of resources and how they can be accessed by a shader) - let material_texture_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("material_texture_bind_group_layout"), - entries: &[ - wgpu::BindGroupLayoutEntry { // Entry for the sampled texture at binding 0 - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, // Visible only to fragment shader - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, + let material_texture_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("material_texture_bind_group_layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + // Entry for the sampled texture at binding 0 + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, // Visible only to fragment shader + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, }, - count: None, - }, - wgpu::BindGroupLayoutEntry { // Entry for the sampler at binding 1 - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, // Visible only to fragment shader - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - wgpu::BindGroupLayoutEntry { // Normal map - binding: 2, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - view_dimension: wgpu::TextureViewDimension::D2, + wgpu::BindGroupLayoutEntry { + // Entry for the sampler at binding 1 + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, // Visible only to fragment shader + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 3, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], - }); + wgpu::BindGroupLayoutEntry { + // Normal map + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); - let material_parameter_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("material_parameter_bind_group_layout"), - entries: &[ - wgpu::BindGroupLayoutEntry { + let material_parameter_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("material_parameter_bind_group_layout"), + entries: &[wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { @@ -70,24 +74,40 @@ impl RendererPipeline { min_binding_size: None, }, count: None, - }, - ], - }); + }], + }); // Define camera bind group layout - let camera_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("camera_bind_group_layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, // Specifies if this buffer will be changing size or not - min_binding_size: None, - }, - count: None, - }] - }); + let camera_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("camera_bind_group_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, // Specifies if this buffer will be changing size or not + min_binding_size: None, + }, + count: None, + }], + }); + + // Define per-draw bind group layout (dynamic uniform buffer) + let per_draw_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("per_draw_bind_group_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: Some(std::num::NonZeroU64::new(64).unwrap()), + }, + count: None, + }], + }); // Create pipeline layout descriptor let pipeline_layout_descriptor = wgpu::PipelineLayoutDescriptor { @@ -96,6 +116,7 @@ impl RendererPipeline { &material_texture_bind_group_layout, &material_parameter_bind_group_layout, &camera_bind_group_layout, + &per_draw_bind_group_layout, ], push_constant_ranges: &[], }; @@ -104,7 +125,7 @@ impl RendererPipeline { let layout = device.create_pipeline_layout(&pipeline_layout_descriptor); // Create color target states that specifies what what color outputs wgpu should set up - let color_target_states = &[Some(wgpu::ColorTargetState { + let color_target_states = &[Some(wgpu::ColorTargetState { format: color_format, blend: Some(wgpu::BlendState { alpha: wgpu::BlendComponent::REPLACE, @@ -116,7 +137,7 @@ impl RendererPipeline { let render_pipeline_descriptor = wgpu::RenderPipelineDescriptor { label: Some("render_pipeline"), layout: Some(&layout), - vertex: wgpu::VertexState { + vertex: wgpu::VertexState { module: &vertex_shader, entry_point: "main", buffers: vertex_layouts, // Specifies structure of vertices that will be passed to the vertex shader @@ -128,12 +149,13 @@ impl RendererPipeline { targets: color_target_states, compilation_options: Default::default(), }), - primitive: wgpu::PrimitiveState { // Specifies how to interpret vertices when converting them into triangles + primitive: wgpu::PrimitiveState { + // Specifies how to interpret vertices when converting them into triangles topology: wgpu::PrimitiveTopology::TriangleList, // Each three vertices will correspond to one triangle strip_index_format: None, front_face: wgpu::FrontFace::Ccw, // Specifies how to determine whether a given triangle is facing forward or not (FrontFace::Ccw means that a triangle is facing forward if the vertices are arranged in a counter clockwise direction) - cull_mode: Some(wgpu::Face::Back), // Triangles that are not considered facing forward are culled (not included in the render) as specified by CullMode::Back - polygon_mode: wgpu::PolygonMode::Fill, // Setting this to anything other than Fill requires Features::NON_FILL_POLYGON_MODE + cull_mode: Some(wgpu::Face::Back), // Triangles that are not considered facing forward are culled (not included in the render) as specified by CullMode::Back + polygon_mode: wgpu::PolygonMode::Fill, // Setting this to anything other than Fill requires Features::NON_FILL_POLYGON_MODE conservative: false, // Requires Features::CONSERVATIVE_RASTERIZATION unclipped_depth: true, // Requires Features::DEPTH_CLAMPING }, @@ -155,11 +177,12 @@ impl RendererPipeline { let render_pipeline = device.create_render_pipeline(&render_pipeline_descriptor); - let pipeline = Self { + let pipeline = Self { render_pipeline, material_texture_bind_group_layout, material_parameter_bind_group_layout, camera_bind_group_layout, + per_draw_bind_group_layout, }; Ok(pipeline) From 73553f1aa7fcc15682d34cf2d7cf521f2532e580 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 01:07:28 +0200 Subject: [PATCH 08/69] feat: M3, refactor culling --- engine/pill_renderer/src/renderer.rs | 270 ++++++++++++++------------- 1 file changed, 142 insertions(+), 128 deletions(-) diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 82fd7423..f9ee007b 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -555,16 +555,148 @@ impl State { label: Some("m2_inline_encoder"), }); - // Prepare per-draw dynamic uniform buffer and bind group BEFORE render pass (lifetime requirement) + // Build view before pass: cull and prepare per-draw MVPs + let vp_mat: cgmath::Matrix4 = renderer_camera.uniform.view_projection_matrix.into(); + let m = vp_mat; + let row = |i: usize| -> cgmath::Vector4 { + // cgmath is column-major; construct row i + cgmath::Vector4::new(m.x[i], m.y[i], m.z[i], m.w[i]) + }; + let make_plane = |v: cgmath::Vector4| -> (cgmath::Vector3, f32) { + let n = cgmath::Vector3::new(v.x, v.y, v.z); + let len = n.magnitude(); + let normal = if len > 0.0 { n / len } else { n }; + let d = v.w / if len > 0.0 { len } else { 1.0 }; + (normal, d) + }; + let planes = [ + make_plane(row(3) + row(0)), // left + make_plane(row(3) - row(0)), // right + make_plane(row(3) + row(1)), // bottom + make_plane(row(3) - row(1)), // top + make_plane(row(3) + row(2)), // near + make_plane(row(3) - row(2)), // far + ]; + + struct VisiblePreDraw { + mesh_handle: RendererMeshHandle, + material_handle: RendererMaterialHandle, + mvp: [[f32; 4]; 4], + } + let mut visible: Vec = Vec::with_capacity(render_queue.len()); + + for render_queue_item in render_queue.iter() { + let key = pill_engine::internal::decompose_render_queue_key(render_queue_item.key); + let mesh_handle = RendererMeshHandle::new( + key.mesh_index.into(), + NonZeroU32::new(key.mesh_version.into()).unwrap(), + ); + let material_handle = RendererMaterialHandle::new( + key.material_index.into(), + NonZeroU32::new(key.material_version.into()).unwrap(), + ); + + // Transform and model + let entity_index = render_queue_item.entity_index as usize; + let transform = transform_component_storage + .data + .get(entity_index) + .unwrap() + .as_ref() + .unwrap(); + let model_arr = pill_engine::internal::get_model_matrix(transform); + let model: cgmath::Matrix4 = model_arr.into(); + + // World AABB from mesh local AABB + let mesh = self + .renderer_resource_storage + .meshes + .get(mesh_handle) + .unwrap(); + let local_min = + cgmath::Vector3::new(mesh.aabb_min[0], mesh.aabb_min[1], mesh.aabb_min[2]); + let local_max = + cgmath::Vector3::new(mesh.aabb_max[0], mesh.aabb_max[1], mesh.aabb_max[2]); + let corners = [ + cgmath::Vector3::new(local_min.x, local_min.y, local_min.z), + cgmath::Vector3::new(local_max.x, local_min.y, local_min.z), + cgmath::Vector3::new(local_min.x, local_max.y, local_min.z), + cgmath::Vector3::new(local_max.x, local_max.y, local_min.z), + cgmath::Vector3::new(local_min.x, local_min.y, local_max.z), + cgmath::Vector3::new(local_max.x, local_min.y, local_max.z), + cgmath::Vector3::new(local_min.x, local_max.y, local_max.z), + cgmath::Vector3::new(local_max.x, local_max.y, local_max.z), + ]; + let mut world_min = cgmath::Vector3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY); + let mut world_max = + cgmath::Vector3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY); + for c in &corners { + let p4 = model * cgmath::Vector4::new(c.x, c.y, c.z, 1.0); + let p = cgmath::Vector3::new(p4.x, p4.y, p4.z); + world_min.x = world_min.x.min(p.x); + world_min.y = world_min.y.min(p.y); + world_min.z = world_min.z.min(p.z); + world_max.x = world_max.x.max(p.x); + world_max.y = world_max.y.max(p.y); + world_max.z = world_max.z.max(p.z); + } + let mut outside = false; + for (normal, d) in &planes { + let p = cgmath::Vector3::new( + if normal.x >= 0.0 { + world_max.x + } else { + world_min.x + }, + if normal.y >= 0.0 { + world_max.y + } else { + world_min.y + }, + if normal.z >= 0.0 { + world_max.z + } else { + world_min.z + }, + ); + let dist = normal.dot(p) + *d; + if dist < 0.0 { + outside = true; + break; + } + } + if outside { + continue; + } + + let view_proj: cgmath::Matrix4 = + renderer_camera.uniform.view_projection_matrix.into(); + let mvp: [[f32; 4]; 4] = (view_proj * model).into(); + visible.push(VisiblePreDraw { + mesh_handle, + material_handle, + mvp, + }); + } + + // Prepare per-draw dynamic uniform buffer and bind group sized to visible set let per_draw_stride: u64 = 256; // alignment - let per_draw_capacity = render_queue.len() as u64 + 1; - let per_draw_buffer_size = per_draw_stride * per_draw_capacity; + let per_draw_capacity = visible.len() as u64; + let per_draw_buffer_size = per_draw_stride * per_draw_capacity.max(1); let per_draw_buffer = self.device.create_buffer(&wgpu::BufferDescriptor { label: Some("per_draw_dynamic_ubo"), size: per_draw_buffer_size, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); + for (i, v) in visible.iter().enumerate() { + let offset_bytes = (i as u64) * per_draw_stride; + self.queue.write_buffer( + &per_draw_buffer, + offset_bytes, + bytemuck::cast_slice(&[v.mvp]), + ); + } let pipeline = self .renderer_resource_storage .pipelines @@ -611,48 +743,15 @@ impl State { occlusion_query_set: None, }); - // Inline draw: iterate render_queue, set state, draw + // Inline draw: iterate visible, set state, draw let mut current_pipeline: Option = None; let mut current_material: Option = None; let mut current_mesh: Option = None; - let mut draw_index: u64 = 0; - // Extract frustum planes from camera view-projection for CPU culling - let vp_mat: cgmath::Matrix4 = - renderer_camera.uniform.view_projection_matrix.into(); - let m = vp_mat; - let row = |i: usize| -> cgmath::Vector4 { - // cgmath is column-major; construct row i - cgmath::Vector4::new(m.x[i], m.y[i], m.z[i], m.w[i]) - }; - let make_plane = |v: cgmath::Vector4| -> (cgmath::Vector3, f32) { - let n = cgmath::Vector3::new(v.x, v.y, v.z); - let len = n.magnitude(); - let normal = if len > 0.0 { n / len } else { n }; - let d = v.w / if len > 0.0 { len } else { 1.0 }; - (normal, d) - }; - let planes = [ - make_plane(row(3) + row(0)), // left - make_plane(row(3) - row(0)), // right - make_plane(row(3) + row(1)), // bottom - make_plane(row(3) - row(1)), // top - make_plane(row(3) + row(2)), // near - make_plane(row(3) - row(2)), // far - ]; - - for render_queue_item in render_queue.iter() { - let key = pill_engine::internal::decompose_render_queue_key(render_queue_item.key); - - let mesh_handle = RendererMeshHandle::new( - key.mesh_index.into(), - NonZeroU32::new(key.mesh_version.into()).unwrap(), - ); - let material_handle = RendererMaterialHandle::new( - key.material_index.into(), - NonZeroU32::new(key.material_version.into()).unwrap(), - ); + for v in visible.iter() { + let mesh_handle = v.mesh_handle; + let material_handle = v.material_handle; if current_material != Some(material_handle) { current_material = Some(material_handle); @@ -688,100 +787,15 @@ impl State { rpass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32); } - // Compute model matrix and MVP, upload at aligned offset, bind with dynamic offset - let entity_index = render_queue_item.entity_index as usize; - let transform = transform_component_storage - .data - .get(entity_index) - .unwrap() - .as_ref() - .unwrap(); - - // Model matrix updated in system, fetch and convert - let model_arr = pill_engine::internal::get_model_matrix(transform); - let model: cgmath::Matrix4 = model_arr.into(); + let offset_bytes = draw_index * per_draw_stride; + let offset_aligned_u32 = offset_bytes as u32; // guaranteed < 4GiB in this simple path - // Frustum culling against mesh world-space AABB + rpass.set_bind_group(3, &per_draw_bind_group, &[offset_aligned_u32]); let mesh = self .renderer_resource_storage .meshes .get(current_mesh.unwrap()) .unwrap(); - - let local_min = - cgmath::Vector3::new(mesh.aabb_min[0], mesh.aabb_min[1], mesh.aabb_min[2]); - let local_max = - cgmath::Vector3::new(mesh.aabb_max[0], mesh.aabb_max[1], mesh.aabb_max[2]); - let corners = [ - cgmath::Vector3::new(local_min.x, local_min.y, local_min.z), - cgmath::Vector3::new(local_max.x, local_min.y, local_min.z), - cgmath::Vector3::new(local_min.x, local_max.y, local_min.z), - cgmath::Vector3::new(local_max.x, local_max.y, local_min.z), - cgmath::Vector3::new(local_min.x, local_min.y, local_max.z), - cgmath::Vector3::new(local_max.x, local_min.y, local_max.z), - cgmath::Vector3::new(local_min.x, local_max.y, local_max.z), - cgmath::Vector3::new(local_max.x, local_max.y, local_max.z), - ]; - let mut world_min = - cgmath::Vector3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY); - let mut world_max = - cgmath::Vector3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY); - for c in &corners { - let p4 = model * cgmath::Vector4::new(c.x, c.y, c.z, 1.0); - let p = cgmath::Vector3::new(p4.x, p4.y, p4.z); - world_min.x = world_min.x.min(p.x); - world_min.y = world_min.y.min(p.y); - world_min.z = world_min.z.min(p.z); - world_max.x = world_max.x.max(p.x); - world_max.y = world_max.y.max(p.y); - world_max.z = world_max.z.max(p.z); - } - - // Plane-AABB test; skip draw if outside any plane - let mut outside = false; - for (normal, d) in &planes { - let p = cgmath::Vector3::new( - if normal.x >= 0.0 { - world_max.x - } else { - world_min.x - }, - if normal.y >= 0.0 { - world_max.y - } else { - world_min.y - }, - if normal.z >= 0.0 { - world_max.z - } else { - world_min.z - }, - ); - let dist = normal.dot(p) + *d; - if dist < 0.0 { - outside = true; - break; - } - } - if outside { - continue; - } - - // Use camera view-projection from renderer_camera.uniform - let view_proj: cgmath::Matrix4 = - renderer_camera.uniform.view_projection_matrix.into(); - let mvp = view_proj * model; - - let offset_bytes = draw_index * per_draw_stride; - let offset_aligned_u32 = offset_bytes as u32; // guaranteed < 4GiB in this simple path - let mvp_arr: [[f32; 4]; 4] = mvp.into(); - self.queue.write_buffer( - &per_draw_buffer, - offset_bytes, - bytemuck::cast_slice(&[mvp_arr]), - ); - - rpass.set_bind_group(3, &per_draw_bind_group, &[offset_aligned_u32]); rpass.draw_indexed(0..mesh.index_count, 0, 0..1); draw_index += 1; } From 212bee540d6c4b10f9fbfae3946f6cbdc261a361 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 01:21:32 +0200 Subject: [PATCH 09/69] feat: M3, group draw calls by meterial and mesh --- engine/pill_renderer/src/renderer.rs | 122 ++++++++++++++++----------- 1 file changed, 73 insertions(+), 49 deletions(-) diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index f9ee007b..19957077 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -579,8 +579,9 @@ impl State { ]; struct VisiblePreDraw { - mesh_handle: RendererMeshHandle, + pipeline_handle: RendererPipelineHandle, material_handle: RendererMaterialHandle, + mesh_handle: RendererMeshHandle, mvp: [[f32; 4]; 4], } let mut visible: Vec = Vec::with_capacity(render_queue.len()); @@ -595,6 +596,12 @@ impl State { key.material_index.into(), NonZeroU32::new(key.material_version.into()).unwrap(), ); + let material_for_pipeline = self + .renderer_resource_storage + .materials + .get(material_handle) + .unwrap(); + let pipeline_handle = material_for_pipeline.pipeline_handle; // Transform and model let entity_index = render_queue_item.entity_index as usize; @@ -673,12 +680,56 @@ impl State { renderer_camera.uniform.view_projection_matrix.into(); let mvp: [[f32; 4]; 4] = (view_proj * model).into(); visible.push(VisiblePreDraw { - mesh_handle, + pipeline_handle, material_handle, + mesh_handle, mvp, }); } + // Sort by pipeline -> material -> mesh to minimize state changes + visible.sort_by_key(|v| { + ( + v.pipeline_handle.data().index, + v.material_handle.data().index, + v.mesh_handle.data().index, + ) + }); + + // Build grouped command list by (pipeline, material) + struct DrawCmd { + mesh_handle: RendererMeshHandle, + offset_u32: u32, + } + struct GroupCmd { + pipeline_handle: RendererPipelineHandle, + material_handle: RendererMaterialHandle, + draws: Vec, + } + let mut groups: Vec = Vec::new(); + let mut next_offset_u32: u32 = 0; + for v in &visible { + if groups + .last() + .map(|g| { + g.pipeline_handle != v.pipeline_handle || g.material_handle != v.material_handle + }) + .unwrap_or(true) + { + groups.push(GroupCmd { + pipeline_handle: v.pipeline_handle, + material_handle: v.material_handle, + draws: Vec::new(), + }); + } + let g = groups.last_mut().unwrap(); + g.draws.push(DrawCmd { + mesh_handle: v.mesh_handle, + offset_u32: next_offset_u32, + }); + next_offset_u32 = next_offset_u32.wrapping_add(256); + } + // Prepare per-draw dynamic uniform buffer and bind group sized to visible set let per_draw_stride: u64 = 256; // alignment let per_draw_capacity = visible.len() as u64; @@ -742,62 +793,35 @@ impl State { timestamp_writes: None, occlusion_query_set: None, }); + // Iterate groups: bind pipeline/material once per group; mesh + offset per draw + for group in &groups { + let pipeline = self + .renderer_resource_storage + .pipelines + .get(group.pipeline_handle) + .unwrap(); + rpass.set_pipeline(&pipeline.render_pipeline); - // Inline draw: iterate visible, set state, draw - let mut current_pipeline: Option = None; - let mut current_material: Option = None; - let mut current_mesh: Option = None; - let mut draw_index: u64 = 0; - - for v in visible.iter() { - let mesh_handle = v.mesh_handle; - let material_handle = v.material_handle; - - if current_material != Some(material_handle) { - current_material = Some(material_handle); - let material = self - .renderer_resource_storage - .materials - .get(material_handle) - .unwrap(); - - if current_pipeline != Some(material.pipeline_handle) { - current_pipeline = Some(material.pipeline_handle); - let pipeline = self - .renderer_resource_storage - .pipelines - .get(material.pipeline_handle) - .unwrap(); - rpass.set_pipeline(&pipeline.render_pipeline); - } - - rpass.set_bind_group(0, &material.texture_bind_group, &[]); - rpass.set_bind_group(1, &material.parameter_bind_group, &[]); - rpass.set_bind_group(2, &renderer_camera.bind_group, &[]); - } + let material = self + .renderer_resource_storage + .materials + .get(group.material_handle) + .unwrap(); + rpass.set_bind_group(0, &material.texture_bind_group, &[]); + rpass.set_bind_group(1, &material.parameter_bind_group, &[]); + rpass.set_bind_group(2, &renderer_camera.bind_group, &[]); - if current_mesh != Some(mesh_handle) { - current_mesh = Some(mesh_handle); + for draw in &group.draws { let mesh = self .renderer_resource_storage .meshes - .get(mesh_handle) + .get(draw.mesh_handle) .unwrap(); rpass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); rpass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32); + rpass.set_bind_group(3, &per_draw_bind_group, &[draw.offset_u32]); + rpass.draw_indexed(0..mesh.index_count, 0, 0..1); } - - let offset_bytes = draw_index * per_draw_stride; - let offset_aligned_u32 = offset_bytes as u32; // guaranteed < 4GiB in this simple path - - rpass.set_bind_group(3, &per_draw_bind_group, &[offset_aligned_u32]); - let mesh = self - .renderer_resource_storage - .meshes - .get(current_mesh.unwrap()) - .unwrap(); - rpass.draw_indexed(0..mesh.index_count, 0, 0..1); - draw_index += 1; } } From 5bbbc977230a4c35349c82ad48a89d78acdce623 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 01:48:11 +0200 Subject: [PATCH 10/69] feat: M4 bring back the material --- engine/pill_renderer/DESIGN.md | 13 ++-- engine/pill_renderer/src/renderer.rs | 64 +++++++++++++++---- .../src/resources/renderer_pipeline.rs | 5 +- 3 files changed, 63 insertions(+), 19 deletions(-) diff --git a/engine/pill_renderer/DESIGN.md b/engine/pill_renderer/DESIGN.md index c740677c..44447a27 100644 --- a/engine/pill_renderer/DESIGN.md +++ b/engine/pill_renderer/DESIGN.md @@ -133,7 +133,7 @@ struct VSOut { @builtin(position) pos: vec4; }; ### Milestones (incremental, debuggable) with API-correctness notes -1) Bootstrap: Black Window +1) [Done] Bootstrap: Black Window - Expected result: Black frame; vsync honored; resizes OK. - Client API: `Renderer::new_from_window_component(&wc)?;` run loop; no passes. @@ -145,7 +145,7 @@ struct VSOut { @builtin(position) pos: vec4; }; - Use `surface.get_capabilities(&adapter)` for format/present mode. - Set `usage: RENDER_ATTACHMENT` and correct `alpha_mode`. -2) Hello Mesh: Single Pass, Inline Draw +2) [Done] Hello Mesh: Single Pass, Inline Draw - Expected result: Pill mesh or fallback triangle; fixed MVP (identity or simple ortho) hardcoded; depth test on. - Client API: Create mesh + pipeline; `Inline` pass records one draw. @@ -158,7 +158,7 @@ struct VSOut { @builtin(position) pos: vec4; }; - Pipeline layout: explicit `BindGroupLayout` for globals even if empty; avoid implicit layout. - Clear ops via `RenderPassColorAttachment` and depth attachment. -3) Camera + FromView Culling +3) [Done] Camera + FromView Culling - Expected result: Camera matrices applied; frustum culling removes offscreen entities. - Client API: Build `CameraView`; `get_view_from_entities`; pass uses `FromView`. @@ -169,7 +169,7 @@ struct VSOut { @builtin(position) pos: vec4; }; - Uniform buffer alignment (offset multiple of 256); `BindGroupLayoutEntry` for uniform with `has_dynamic_offset: true`. - Call `set_bind_group(group_index, &bg, &[offset])` matching the layout. -4) Materials + Textures (set 1) +4) [Done] Materials + Textures (set 1) - Expected result: Textured mesh with material BG; per-entity tint supported from UBO. - Client API: `create_texture`, `create_bind_group(material_desc)`; assign material handle. @@ -209,8 +209,9 @@ struct VSOut { @builtin(position) pos: vec4; }; - wgpu correctness: - Keep GPU handles alive until all pass encoders referencing them are submitted. -8) Draw List Optimization - +8) [Basic] Draw List Optimization + Done: sort by PSo/material/mesh + Remaining: optional client sort keys, per-pipeline bind group layout identity safety, and a minor mesh buffer rebind micro-optimization. - Expected result: Fewer state changes; sorted by PSO/material. - Client API: Optional sort keys; otherwise internal. - Implementables: diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 19957077..56dbda04 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -88,22 +88,50 @@ impl PillRenderer for Renderer { struct Camera { position: vec4, view_projection_matrix: mat4x4, }; @group(2) @binding(0) var GCamera: Camera; -struct PerDraw { mvp: mat4x4 }; +struct PerDraw { + mvp: mat4x4, + model: mat4x4, + tint: vec4, +}; @group(3) @binding(0) var UPerDraw: PerDraw; -struct VSIn { @location(0) pos: vec3, }; -struct VSOut { @builtin(position) pos: vec4, }; +struct VSIn { @location(0) pos: vec3, @location(1) uv: vec2, }; +struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; @vertex fn main(input: VSIn) -> VSOut { var out: VSOut; out.pos = UPerDraw.mvp * vec4(input.pos, 1.0); + out.uv = input.uv; return out; } "#; let fragment_wgsl = r#" -@fragment fn main() -> @location(0) vec4 { - return vec4(1.0, 0.7, 0.3, 1.0); +// Material set(0): +@group(0) @binding(0) var tex_diffuse: texture_2d; +@group(0) @binding(1) var smp_diffuse: sampler; +@group(0) @binding(2) var tex_normal: texture_2d; +@group(0) @binding(3) var smp_normal: sampler; + +// Material parameters set(1) – pack tint.rgb + specularity in a single vec4 +struct MaterialParams { tint_spec: vec4, } +@group(1) @binding(0) var UMaterial: MaterialParams; + +// Per-draw (set 3): supports per-entity tint for M4 +struct PerDraw { + mvp: mat4x4, + model: mat4x4, + tint: vec4, +}; +@group(3) @binding(0) var UPerDraw: PerDraw; + +@fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let albedo = textureSample(tex_diffuse, smp_diffuse, uv); + // Combine material tint and per-entity tint; modulate brightness by specularity for demo + let tinted = vec4(UMaterial.tint_spec.rgb, 1.0) * UPerDraw.tint; + let spec_boost = 0.5 + 0.5 * UMaterial.tint_spec.a; + let color = albedo * tinted * spec_boost; + return vec4(color.rgb, 1.0); } "#; @@ -742,11 +770,25 @@ impl State { }); for (i, v) in visible.iter().enumerate() { let offset_bytes = (i as u64) * per_draw_stride; - self.queue.write_buffer( - &per_draw_buffer, - offset_bytes, - bytemuck::cast_slice(&[v.mvp]), - ); + // Pack { mvp, model(=identity for now), tint(=1,1,1,1) } + let model: [[f32; 4]; 4] = cgmath::Matrix4::::from_scale(1.0).into(); + let tint: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; + #[repr(C)] + #[derive(Copy, Clone)] + struct PerDrawStd140 { + mvp: [[f32; 4]; 4], + model: [[f32; 4]; 4], + tint: [f32; 4], + } + unsafe impl bytemuck::Zeroable for PerDrawStd140 {} + unsafe impl bytemuck::Pod for PerDrawStd140 {} + let pd = PerDrawStd140 { + mvp: v.mvp, + model, + tint, + }; + self.queue + .write_buffer(&per_draw_buffer, offset_bytes, bytemuck::bytes_of(&pd)); } let pipeline = self .renderer_resource_storage @@ -761,7 +803,7 @@ impl State { resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &per_draw_buffer, offset: 0, - size: Some(std::num::NonZeroU64::new(64).unwrap()), + size: Some(std::num::NonZeroU64::new(144).unwrap()), }), }], }); diff --git a/engine/pill_renderer/src/resources/renderer_pipeline.rs b/engine/pill_renderer/src/resources/renderer_pipeline.rs index 35e14db5..62176065 100644 --- a/engine/pill_renderer/src/resources/renderer_pipeline.rs +++ b/engine/pill_renderer/src/resources/renderer_pipeline.rs @@ -99,11 +99,12 @@ impl RendererPipeline { label: Some("per_draw_bind_group_layout"), entries: &[wgpu::BindGroupLayoutEntry { binding: 0, - visibility: wgpu::ShaderStages::VERTEX, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: true, - min_binding_size: Some(std::num::NonZeroU64::new(64).unwrap()), + // mvp(64) + model(64) + tint(vec4=16) = 144 bytes minimum + min_binding_size: Some(std::num::NonZeroU64::new(144).unwrap()), }, count: None, }], From c0b57bcf8c635c1be1466dce2808a35d4b403a49 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 01:57:34 +0200 Subject: [PATCH 11/69] feat: M4 update time measure --- engine/pill_renderer/src/renderer.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 56dbda04..0722753c 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -521,8 +521,8 @@ impl State { egui_ui: Box, timer: &mut Timer, ) -> Result<()> { - // M3: Hello Mesh + per-draw MVP (dynamic offsets); - timer.record("Get frame"); + // M3: Hello Mesh + per-draw MVP (dynamic offsets) + timer.record("Frame: acquire"); // Get frame or return mapped error if failed let frame = self.surface.get_current_texture(); @@ -542,7 +542,7 @@ impl State { .texture .create_view(&wgpu::TextureViewDescriptor::default()); - timer.record("Get clear color and create render pass attachments"); + timer.record("Frame: setup view & camera"); // Get active camera and update it let camera_storage = camera_component_storage @@ -612,6 +612,7 @@ impl State { mesh_handle: RendererMeshHandle, mvp: [[f32; 4]; 4], } + timer.record("Culling & MVP build"); let mut visible: Vec = Vec::with_capacity(render_queue.len()); for render_queue_item in render_queue.iter() { @@ -729,6 +730,7 @@ impl State { mesh_handle: RendererMeshHandle, offset_u32: u32, } + timer.record("Sort & group draws"); struct GroupCmd { pipeline_handle: RendererPipelineHandle, material_handle: RendererMaterialHandle, @@ -782,7 +784,7 @@ impl State { } unsafe impl bytemuck::Zeroable for PerDrawStd140 {} unsafe impl bytemuck::Pod for PerDrawStd140 {} - let pd = PerDrawStd140 { + let pd: PerDrawStd140 = PerDrawStd140 { mvp: v.mvp, model, tint, @@ -790,6 +792,7 @@ impl State { self.queue .write_buffer(&per_draw_buffer, offset_bytes, bytemuck::bytes_of(&pd)); } + timer.record("Per-draw UBO writes"); let pipeline = self .renderer_resource_storage .pipelines @@ -865,9 +868,11 @@ impl State { rpass.draw_indexed(0..mesh.index_count, 0, 0..1); } } + timer.record("Encode render pass"); } self.queue.submit([encoder.finish()]); + timer.record("Submit main pass"); // Egui overlay (load over the rendered frame) let mut encoder = self @@ -889,9 +894,11 @@ impl State { timer, )?; self.queue.submit([encoder.finish()]); + timer.record("UI draw & submit"); // Present frame frame.present(); + timer.record("Present"); Ok(()) } From f943910b53393b360b8105de6011586ba7567c4c Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 02:02:10 +0200 Subject: [PATCH 12/69] fix: build warning --- engine/pill_core/src/pill_slotmap.rs | 47 +- engine/pill_core/src/pill_typemap.rs | 37 +- engine/pill_core/src/utils.rs | 62 +- .../src/ecs/systems/rendering_system.rs | 106 ++- engine/pill_engine/src/engine.rs | 743 +++++++++++++----- engine/pill_renderer/src/mesh_drawer.rs | 12 +- 6 files changed, 704 insertions(+), 303 deletions(-) diff --git a/engine/pill_core/src/pill_slotmap.rs b/engine/pill_core/src/pill_slotmap.rs index 5f39ac6a..66d24ec8 100644 --- a/engine/pill_core/src/pill_slotmap.rs +++ b/engine/pill_core/src/pill_slotmap.rs @@ -20,7 +20,6 @@ // 3. This notice may not be removed or altered from any source distribution. - // --- PillSlotMap // This is slotmap crate modified by changing names of types, adding public key variables, removing iterators and adding possibility to restrict version limit // Original crate: https://crates.io/crates/SlotMap @@ -30,14 +29,14 @@ //! Contains the slot map implementation. use core::fmt; +use core::fmt::Debug; use core::marker::PhantomData; #[allow(unused_imports)] // MaybeUninit is only used on nightly at the moment. use core::mem::{ManuallyDrop, MaybeUninit}; use core::ops::{Index, IndexMut}; use std::fmt::Formatter; -use core::fmt::Debug; use std::iter::Enumerate; -use std::num::{ NonZeroU32}; +use std::num::NonZeroU32; // Storage inside a slot or metadata for the freelist when vacant. union SlotUnion { @@ -73,7 +72,7 @@ impl Slot { self.version % 2 > 0 } - pub fn get(&self) -> SlotContent { + pub fn get(&self) -> SlotContent<'_, T> { unsafe { if self.occupied() { Occupied(&*self.u.value) @@ -83,7 +82,7 @@ impl Slot { } } - pub fn get_mut(&mut self) -> SlotContentMut { + pub fn get_mut(&mut self) -> SlotContentMut<'_, T> { unsafe { if self.occupied() { OccupiedMut(&mut *self.u.value) @@ -142,7 +141,6 @@ pub struct PillSlotMap { } impl PillSlotMap { - pub fn with_key() -> Self { Self::with_capacity_and_key(0) } @@ -152,11 +150,14 @@ impl PillSlotMap { } // Warning: Version limit has to be odd value - pub fn with_capacity_and_key_and_version_limit(capacity: usize, version_limit: u32) -> Result { + pub fn with_capacity_and_key_and_version_limit( + capacity: usize, + version_limit: u32, + ) -> Result { if version_limit % 2 == 0 { - return Err(()) + return Err(()); } - + let mut slots = Vec::with_capacity(capacity + 1); slots.push(Slot { u: SlotUnion { next_free: 0 }, @@ -170,7 +171,7 @@ impl PillSlotMap { version_limit, _k: PhantomData, }; - + Ok(slotmap) } @@ -201,7 +202,6 @@ impl PillSlotMap { } pub fn get_next_free_slot_handle(&self) -> K { - let new_num_elems = self.num_elems + 1; if new_num_elems == core::u32::MAX { panic!("PillSlotMap number of elements overflow"); @@ -290,8 +290,11 @@ impl PillSlotMap { self.free_head = index as u32; self.num_elems -= 1; - - slot.version = if slot.version >= self.version_limit { 0 } else { slot.version + 1 }; + slot.version = if slot.version >= self.version_limit { + 0 + } else { + slot.version + 1 + }; value } @@ -366,7 +369,7 @@ impl PillSlotMap { .value } - pub fn iter(&self) -> Iter { + pub fn iter(&self) -> Iter<'_, K, V> { let mut it = self.slots.iter().enumerate(); it.next(); // Skip sentinel. Iter { @@ -376,7 +379,7 @@ impl PillSlotMap { } } - pub fn iter_mut(&mut self) -> IterMut { + pub fn iter_mut(&mut self) -> IterMut<'_, K, V> { let len = self.len(); let mut it = self.slots.iter_mut().enumerate(); it.next(); // Skip sentinel. @@ -528,8 +531,18 @@ impl Default for PillSlotMapKeyData { } } -pub unsafe trait PillSlotMapKey: - From + Copy + Clone + Default + Eq + PartialEq + Ord + PartialOrd + core::hash::Hash + core::fmt::Debug { +pub unsafe trait PillSlotMapKey: + From + + Copy + + Clone + + Default + + Eq + + PartialEq + + Ord + + PartialOrd + + core::hash::Hash + + core::fmt::Debug +{ fn null() -> Self { PillSlotMapKeyData::null().into() } diff --git a/engine/pill_core/src/pill_typemap.rs b/engine/pill_core/src/pill_typemap.rs index f568f4dd..6a8cb381 100644 --- a/engine/pill_core/src/pill_typemap.rs +++ b/engine/pill_core/src/pill_typemap.rs @@ -1,18 +1,14 @@ - // --- PillTypeMap // This is typemap_rev crate modified by changing names of types // Original crate: https://crates.io/crates/typemap_rev //! A hashmap whose keys are defined by types. - use std::any::{Any, TypeId}; -use std::collections::HashMap; use std::collections::hash_map::{ - Entry as HashMapEntry, - OccupiedEntry as HashMapOccupiedEntry, - VacantEntry as HashMapVacantEntry, + Entry as HashMapEntry, OccupiedEntry as HashMapOccupiedEntry, VacantEntry as HashMapVacantEntry, }; +use std::collections::HashMap; use std::marker::PhantomData; /// PillTypeMapKey is used to declare key types that are eligible for use @@ -30,8 +26,7 @@ pub trait PillTypeMapKey: Any { /// /// [`HashMap`]: std::collections::HashMap //pub struct PillTypeMap(HashMap>); -pub struct PillTypeMap(HashMap>); - +pub struct PillTypeMap(HashMap>); impl PillTypeMap { /// Creates a new instance of `PillTypeMap`. @@ -59,7 +54,7 @@ impl PillTypeMap { #[inline] pub fn contains_key(&self) -> bool where - T: PillTypeMapKey + T: PillTypeMapKey, { self.0.contains_key(&TypeId::of::()) } @@ -87,7 +82,7 @@ impl PillTypeMap { #[inline] pub fn insert(&mut self, value: T::Storage) where - T: PillTypeMapKey + T: PillTypeMapKey, { self.0.insert(TypeId::of::(), Box::new(value)); } @@ -98,7 +93,7 @@ impl PillTypeMap { #[inline] pub fn entry(&mut self) -> Entry<'_, T> where - T: PillTypeMapKey + T: PillTypeMapKey, { match self.0.entry(TypeId::of::()) { HashMapEntry::Occupied(entry) => Entry::Occupied(OccupiedEntry { @@ -108,7 +103,7 @@ impl PillTypeMap { HashMapEntry::Vacant(entry) => Entry::Vacant(VacantEntry { entry, _marker: PhantomData, - }) + }), } } @@ -134,7 +129,7 @@ impl PillTypeMap { #[inline] pub fn get(&self) -> Option<&T::Storage> where - T: PillTypeMapKey + T: PillTypeMapKey, { self.0 .get(&TypeId::of::()) @@ -165,7 +160,7 @@ impl PillTypeMap { #[inline] pub fn get_mut(&mut self) -> Option<&mut T::Storage> where - T: PillTypeMapKey + T: PillTypeMapKey, { self.0 .get_mut(&TypeId::of::()) @@ -192,7 +187,7 @@ impl PillTypeMap { #[inline] pub fn remove(&mut self) -> Option where - T: PillTypeMapKey + T: PillTypeMapKey, { self.0 .remove(&TypeId::of::()) @@ -239,7 +234,7 @@ where #[inline] pub fn or_insert_with(self, f: F) -> &'a mut K::Storage where - F: FnOnce() -> K::Storage + F: FnOnce() -> K::Storage, { match self { Entry::Occupied(entry) => entry.into_mut(), @@ -250,13 +245,13 @@ where #[inline] pub fn and_modify(self, f: F) -> Self where - F: FnOnce(&mut K::Storage) + F: FnOnce(&mut K::Storage), { match self { Entry::Occupied(mut entry) => { f(entry.get_mut()); Entry::Occupied(entry) - }, + } Entry::Vacant(entry) => Entry::Vacant(entry), } } @@ -265,7 +260,7 @@ where impl<'a, K> Entry<'a, K> where K: PillTypeMapKey, - K::Storage: Default + K::Storage: Default, { #[inline] pub fn or_default(self) -> &'a mut K::Storage { @@ -278,7 +273,7 @@ where K: PillTypeMapKey, { //entry: HashMapOccupiedEntry<'a, TypeId, Box<(dyn Any + Send + Sync)>>, - entry: HashMapOccupiedEntry<'a, TypeId, Box<(dyn Any + Send)>>, + entry: HashMapOccupiedEntry<'a, TypeId, Box>, _marker: PhantomData<&'a K::Storage>, } @@ -317,7 +312,7 @@ where K: PillTypeMapKey, { //entry: HashMapVacantEntry<'a, TypeId, Box<(dyn Any + Send + Sync)>>, - entry: HashMapVacantEntry<'a, TypeId, Box<(dyn Any + Send)>>, + entry: HashMapVacantEntry<'a, TypeId, Box>, _marker: PhantomData<&'a K::Storage>, } diff --git a/engine/pill_core/src/utils.rs b/engine/pill_core/src/utils.rs index 50a910b7..15dcfb24 100644 --- a/engine/pill_core/src/utils.rs +++ b/engine/pill_core/src/utils.rs @@ -1,10 +1,9 @@ -use crate::{EngineError, define_new_pill_slotmap_key}; +use crate::{define_new_pill_slotmap_key, EngineError}; -use anyhow::{ Context, Result, Error }; -use boolinator::Boolinator; +use anyhow::{Context, Error, Result}; use colored::{ColoredString, Colorize}; -use std::{ any::type_name, collections::HashMap, hash::Hash, path::PathBuf }; use log::debug; +use std::{any::type_name, collections::HashMap, hash::Hash, path::PathBuf}; // --- Type to string utils --- @@ -52,7 +51,12 @@ impl PillStyle for &str { // To be used with large module objects (Engine, Renderer, Window, etc) - changes color and adds bold #[inline] fn mobj_style(self) -> ColoredString { - self.color(colored::Color::TrueColor { r: 180, g: 25, b: 100 }).bold() + self.color(colored::Color::TrueColor { + r: 180, + g: 25, + b: 100, + }) + .bold() } // To be used with general objects (Scene, Component, System, Resource, etc) - changes color and adds bold @@ -64,13 +68,21 @@ impl PillStyle for &str { // To be used with specific objects (CameraComponent, Texture, Mesh, etc) - changes color #[inline] fn sobj_style(self) -> ColoredString { - self.color(colored::Color::TrueColor { r: 95, g: 210, b: 90 }) + self.color(colored::Color::TrueColor { + r: 95, + g: 210, + b: 90, + }) } // To be used with names - changes color adds quotation marks #[inline] fn name_style(self) -> ColoredString { - format!("\"{}\"", self).color(colored::Color::TrueColor { r: 190, g: 220, b: 160 }) + format!("\"{}\"", self).color(colored::Color::TrueColor { + r: 190, + g: 220, + b: 160, + }) } // To be used with names - changes color adds bold @@ -93,30 +105,45 @@ pub trait MeshXImpl { // Check if path to asset is correct (exists and has supported format) pub fn validate_asset_path(path: &PathBuf, allowed_formats: &'static [&'static str]) -> Result<()> // Vec { - path.exists().ok_or(Error::new(EngineError::InvalidAssetPath(path.display().to_string())))?; + if !path.exists() { + return Err(Error::new(EngineError::InvalidAssetPath( + path.display().to_string(), + ))); + } match path.extension() { - Some(v) => match allowed_formats.contains(&v.to_str().unwrap()) { //} v.eq(allowed_format) { + Some(v) => match allowed_formats.contains(&v.to_str().unwrap()) { + //} v.eq(allowed_format) { true => return Ok(()), - false => return Err(Error::new(EngineError::InvalidAssetFormat(allowed_formats, v.to_str().unwrap().to_string()))), + false => { + return Err(Error::new(EngineError::InvalidAssetFormat( + allowed_formats, + v.to_str().unwrap().to_string(), + ))) + } }, - None => return Err(Error::new(EngineError::InvalidAssetPath(path.display().to_string()))), + None => { + return Err(Error::new(EngineError::InvalidAssetPath( + path.display().to_string(), + ))) + } } } // --- PillSlotMap utils --- -#[macro_export] macro_rules! define_component_handle { +#[macro_export] +macro_rules! define_component_handle { ( $(#[$outer:meta])* $vis:vis struct $name:ident; $($rest:tt)* ) => { - pill_core::define_new_pill_slotmap_key! { } - }; + pill_core::define_new_pill_slotmap_key! {} + }; } // --- Other --- #[inline] pub fn get_game_error_message(result: Result<()>) -> Option { - if result.is_err() { + if result.is_err() { let mut message = String::new(); for (i, error) in result.err().unwrap().chain().enumerate() { let message_part = match i == 0 { @@ -126,8 +153,7 @@ pub fn get_game_error_message(result: Result<()>) -> Option { message.push_str(message_part.as_str()); } Some(message) - } - else { + } else { None } } @@ -141,4 +167,4 @@ macro_rules! create_game { Box::into_raw(Box::new(game)) as *mut std::ffi::c_void } }; -} \ No newline at end of file +} diff --git a/engine/pill_engine/src/ecs/systems/rendering_system.rs b/engine/pill_engine/src/ecs/systems/rendering_system.rs index 5818d8c0..3e5e3ff3 100644 --- a/engine/pill_engine/src/ecs/systems/rendering_system.rs +++ b/engine/pill_engine/src/ecs/systems/rendering_system.rs @@ -1,17 +1,21 @@ use crate::{ - config::RENDERING_SYSTEM, - ecs::{ scene, update_transform_matrices, CameraAspectRatio, CameraComponent, Component, ComponentStorage, EguiManagerComponent, EntityHandle, MeshRenderingComponent, TransformComponent, UpdatePhase }, - engine::Engine, - graphics::{ compose_render_queue_key, RenderQueueItem, RenderQueueKey }, - resources::{ Material, MaterialHandle, Mesh, MeshHandle, ResourceManager } + config::RENDERING_SYSTEM, + ecs::{ + scene, update_transform_matrices, CameraAspectRatio, CameraComponent, Component, + ComponentStorage, EguiManagerComponent, EntityHandle, MeshRenderingComponent, + TransformComponent, UpdatePhase, + }, + engine::Engine, + graphics::{compose_render_queue_key, RenderQueueItem, RenderQueueKey}, + resources::{Material, MaterialHandle, Mesh, MeshHandle, ResourceManager}, }; -use pill_core::{ EngineError, PillSlotMapKey, PillStyle, RendererError, Timer }; +use pill_core::{EngineError, PillSlotMapKey, PillStyle, RendererError, Timer}; -use std::{ ops::Range, time::Instant }; -use anyhow::{ Result, Context, Error }; +use anyhow::{Context, Error, Result}; use boolinator::Boolinator; -use log::{ debug }; +use log::debug; +use std::{ops::Range, time::Instant}; pub fn rendering_system(engine: &mut Engine) -> Result<()> { let mut timer = Timer::new(); @@ -20,18 +24,21 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { let active_scene_handle = engine.scene_manager.get_active_scene_handle()?; let mut active_camera_entity_handle_result: Option = None; - + { let active_scene = engine.scene_manager.get_active_scene_mut()?; // - Find active camera and update its aspect ratio if needed // Find first enabled camera and use it as active - for (entity_handle, camera_component) in active_scene.get_one_component_iterator_mut::()? { + for (entity_handle, camera_component) in + active_scene.get_one_component_iterator_mut::()? + { if camera_component.enabled { // Update active camera aspect ratio if it is set to automatic if let CameraAspectRatio::Automatic(_) = camera_component.aspect { - let aspect_ratio = engine.window_size.width as f32 / engine.window_size.height as f32; + let aspect_ratio = + engine.window_size.width as f32 / engine.window_size.height as f32; camera_component.aspect = CameraAspectRatio::Automatic(aspect_ratio); } active_camera_entity_handle_result = Some(entity_handle); @@ -40,8 +47,9 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { } } - - let active_camera_entity_handle = active_camera_entity_handle_result.ok_or(Error::new(EngineError::NoActiveCamera))?.clone(); + let active_camera_entity_handle = active_camera_entity_handle_result + .ok_or(Error::new(EngineError::NoActiveCamera))? + .clone(); // - Prepare rendering data timer.record("Clear render queue"); @@ -52,13 +60,15 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { timer.record("Prepare render queue"); - let mut matrix_calculation_duration: f32 = 0.0; + let matrix_calculation_duration: f32 = 0.0; let mut add_to_render_queue_duration: f32 = 0.0; // Iterate mesh rendering components - for (entity_handle, transform_component, mesh_rendering_component) in - engine.scene_manager.get_two_component_iterator_mut::(active_scene_handle)? - { + for (entity_handle, transform_component, mesh_rendering_component) in engine + .scene_manager + .get_two_component_iterator_mut::( + active_scene_handle, + )? { // Update transform matrices if required // let update_transform_matrices_start_time = Instant::now(); @@ -80,11 +90,18 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { debug!("Invalid render queue key"); continue; } - add_to_render_queue_duration += add_to_render_queue_start_time.elapsed().as_secs_f32() * 1000.0; + add_to_render_queue_duration += + add_to_render_queue_start_time.elapsed().as_secs_f32() * 1000.0; } - timer.record(&format!("Matrix calculation {} ms", matrix_calculation_duration)); - timer.record(&format!("Add to render queue {} ms", add_to_render_queue_duration)); + timer.record(&format!( + "Matrix calculation {} ms", + matrix_calculation_duration + )); + timer.record(&format!( + "Add to render queue {} ms", + add_to_render_queue_duration + )); timer.record("Sort render queue"); @@ -93,44 +110,63 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { timer.record("Get component storages"); - let egui_ui = EguiManagerComponent::get_ui(engine);// egui_manager_component.get_ui(engine); + let egui_ui = EguiManagerComponent::get_ui(engine); // egui_manager_component.get_ui(engine); let active_scene = engine.scene_manager.get_active_scene_mut()?; // Get storages - let camera_component_storage = active_scene.get_component_storage::() - .context(format!("{}: Cannot get active {}", "rendering_system".sobj_style(), "Camera".gobj_style()))?; - let transform_component_storage = active_scene.get_component_storage::() - .context(format!("{}: Cannot get {}", "rendering_system".sobj_style(), "TransformComponents".sobj_style())).unwrap(); + let camera_component_storage = active_scene + .get_component_storage::() + .context(format!( + "{}: Cannot get active {}", + "rendering_system".sobj_style(), + "Camera".gobj_style() + ))?; + let transform_component_storage = active_scene + .get_component_storage::() + .context(format!( + "{}: Cannot get {}", + "rendering_system".sobj_style(), + "TransformComponents".sobj_style() + )) + .unwrap(); timer.begin_context("Render"); // Render match engine.renderer.render( - active_camera_entity_handle, - &engine.render_queue, + active_camera_entity_handle, + &engine.render_queue, camera_component_storage, transform_component_storage, egui_ui, - &mut timer + &mut timer, ) { Ok(_) => { timer.end_context()?; // End "Render" context - engine.system_manager.update_system_timer(RENDERING_SYSTEM.name, RENDERING_SYSTEM.update_phase, timer)?; + engine.system_manager.update_system_timer( + RENDERING_SYSTEM.name, + RENDERING_SYSTEM.update_phase, + timer, + )?; Ok(()) - } + } Err(e) => { match e.downcast_ref::() { Some(RendererError::SurfaceLost) => { // Recreate lost surface timer.end_context()?; // End "Render" context - engine.system_manager.update_system_timer(RENDERING_SYSTEM.name, RENDERING_SYSTEM.update_phase, timer)?; + engine.system_manager.update_system_timer( + RENDERING_SYSTEM.name, + RENDERING_SYSTEM.update_phase, + timer, + )?; Ok(engine.renderer.resize(engine.window_size)) - }, + } Some(RendererError::SurfaceOutOfMemory) => { panic!("Critical: Renderer error, system out of memory"); - }, + } _ => Err(e), } } } -} \ No newline at end of file +} diff --git a/engine/pill_engine/src/engine.rs b/engine/pill_engine/src/engine.rs index a7435dd4..37a26afe 100644 --- a/engine/pill_engine/src/engine.rs +++ b/engine/pill_engine/src/engine.rs @@ -1,28 +1,17 @@ -use crate::{ - resources::*, - ecs::*, - graphics::*, - config::*, -}; +use crate::{config::*, ecs::*, graphics::*, resources::*}; -use pill_core::{ - EngineError, - PillSlotMapKey, - PillStyle, - PillTypeMap, - get_type_name, - get_value_type_name, - get_enum_variant_type_name, - get_game_error_message, - Vector2f, - Timer +use pill_core::{ + get_enum_variant_type_name, get_game_error_message, get_type_name, get_value_type_name, + EngineError, PillSlotMapKey, PillStyle, PillTypeMap, Timer, Vector2f, }; -use std::{ any::type_name, any::Any, any::TypeId, collections::VecDeque, cell::RefCell, ops::RangeBounds }; -use anyhow::{Context, Result, Error}; +use anyhow::{Context, Error, Result}; use boolinator::Boolinator; -use log::{debug, info, error}; -use winit::{ dpi::PhysicalPosition, event::KeyEvent,}; +use log::{debug, error, info}; +use std::{ + any::type_name, any::Any, any::TypeId, cell::RefCell, collections::VecDeque, ops::RangeBounds, +}; +use winit::{dpi::PhysicalPosition, event::KeyEvent}; // ------------------------------------------------------------------------------- @@ -31,14 +20,14 @@ pub type KeyboardKey = winit::keyboard::KeyCode; pub type MouseButton = winit::event::MouseButton; /// Engine <-> Game interface -/// +/// /// Entry point of the game project. Mandatory to implement. -pub trait PillGame { +pub trait PillGame { fn start(&self, engine: &mut Engine) -> Result<()>; } /// Heart of Pill Engine -pub struct Engine { +pub struct Engine { pub(crate) config: config::Config, pub(crate) game: Option, pub(crate) renderer: Renderer, @@ -78,12 +67,14 @@ impl Engine { pub fn new( game: Box, game_resources_directory_path: std::path::PathBuf, - renderer: Box, - config: config::Config + renderer: Box, + config: config::Config, ) -> Self { - let max_entity_count = config.get_int("MAX_ENTITIES").unwrap_or(MAX_ENTITIES as i64) as usize; + let max_entity_count = config + .get_int("MAX_ENTITIES") + .unwrap_or(MAX_ENTITIES as i64) as usize; - Self { + Self { config, game: Some(game), renderer, @@ -101,10 +92,22 @@ impl Engine { fn create_default_resources(&mut self) -> Result<()> { // limits --------------------------------------------------------------- - let max_texture_count = self.config.get_int("MAX_TEXTURES").unwrap_or(MAX_TEXTURES as i64) as usize; - let max_mesh_count = self.config.get_int("MAX_MESHES").unwrap_or(MAX_MESHES as i64) as usize; - let max_material_count = self.config.get_int("MAX_MATERIALS").unwrap_or(MAX_MATERIALS as i64) as usize; - let max_sound_count = self.config.get_int("MAX_SOUNDS").unwrap_or(MAX_SOUNDS as i64) as usize; + let max_texture_count = self + .config + .get_int("MAX_TEXTURES") + .unwrap_or(MAX_TEXTURES as i64) as usize; + let max_mesh_count = self + .config + .get_int("MAX_MESHES") + .unwrap_or(MAX_MESHES as i64) as usize; + let max_material_count = self + .config + .get_int("MAX_MATERIALS") + .unwrap_or(MAX_MATERIALS as i64) as usize; + let max_sound_count = self + .config + .get_int("MAX_SOUNDS") + .unwrap_or(MAX_SOUNDS as i64) as usize; // register resource types -------------------------------------------- self.register_resource_type::(max_texture_count)?; @@ -115,9 +118,10 @@ impl Engine { // master shader & defaults ------------------------------------------- let master_vert = include_bytes!("../res/shaders/master.vert.glsl"); let master_frag = include_bytes!("../res/shaders/master.frag.glsl"); - self.renderer.set_master_pipeline(master_vert, master_frag)?; + self.renderer + .set_master_pipeline(master_vert, master_frag)?; - let default_color = Box::new(*include_bytes!("../res/textures/default_color.png")); + let default_color = Box::new(*include_bytes!("../res/textures/default_color.png")); let default_normal = Box::new(*include_bytes!("../res/textures/default_normal.png")); let mut color = Texture::new( @@ -146,24 +150,30 @@ impl Engine { fn start_game(&mut self) -> Result<()> { info!("Starting {}", "Game".mobj_style()); - let game = self.game.take().ok_or(EngineError::Other("Cannot get game".to_string()))?; - let stop_on_game_errors = self.config.get_bool("PANIC_ON_GAME_ERRORS").unwrap_or(PANIC_ON_GAME_ERRORS); + let game = self + .game + .take() + .ok_or(EngineError::Other("Cannot get game".to_string()))?; + let stop_on_game_errors = self + .config + .get_bool("PANIC_ON_GAME_ERRORS") + .unwrap_or(PANIC_ON_GAME_ERRORS); let result = game.start(self); match stop_on_game_errors { true => result.context(format!("{} error", "Game".mobj_style()))?, - false => { + false => { if let Some(message) = get_game_error_message(result) { error!("{}", message); - } - }, + } + } } self.game = Some(game); Ok(()) } - + /// Initializes Pill Engine - /// + /// /// Creates default global components, adds default systems, creates default resources, initializes game pub fn initialize(&mut self, window_size: winit::dpi::PhysicalSize) -> Result<()> { info!("Initializing {}", "Engine".mobj_style()); @@ -177,61 +187,98 @@ impl Engine { self.add_global_component(DeferredUpdateComponent::new())?; self.add_global_component(EguiManagerComponent::new())?; - let max_ambient_sink_count = self.config.get_int("MAX_CONCURRENT_2D_SOUNDS").unwrap_or(MAX_CONCURRENT_2D_SOUNDS as i64) as usize; - let max_spatial_sink_count = self.config.get_int("MAX_CONCURRENT_3D_SOUNDS").unwrap_or(MAX_CONCURRENT_3D_SOUNDS as i64) as usize; - self.add_global_component(AudioManagerComponent::new(max_ambient_sink_count, max_spatial_sink_count))?; + let max_ambient_sink_count = + self.config + .get_int("MAX_CONCURRENT_2D_SOUNDS") + .unwrap_or(MAX_CONCURRENT_2D_SOUNDS as i64) as usize; + let max_spatial_sink_count = + self.config + .get_int("MAX_CONCURRENT_3D_SOUNDS") + .unwrap_or(MAX_CONCURRENT_3D_SOUNDS as i64) as usize; + self.add_global_component(AudioManagerComponent::new( + max_ambient_sink_count, + max_spatial_sink_count, + ))?; // Add built-in systems - self.system_manager.add_system(INPUT_SYSTEM.name, INPUT_SYSTEM.system_function, INPUT_SYSTEM.update_phase)?; - self.system_manager.add_system(TIME_SYSTEM.name, TIME_SYSTEM.system_function, TIME_SYSTEM.update_phase)?; - self.system_manager.add_system(AUDIO_SYSTEM.name, AUDIO_SYSTEM.system_function, AUDIO_SYSTEM.update_phase)?; - self.system_manager.add_system(DEFERRED_UPDATE_SYSTEM.name, DEFERRED_UPDATE_SYSTEM.system_function, DEFERRED_UPDATE_SYSTEM.update_phase)?; - self.system_manager.add_system(RENDERING_SYSTEM.name, RENDERING_SYSTEM.system_function, RENDERING_SYSTEM.update_phase)?; + self.system_manager.add_system( + INPUT_SYSTEM.name, + INPUT_SYSTEM.system_function, + INPUT_SYSTEM.update_phase, + )?; + self.system_manager.add_system( + TIME_SYSTEM.name, + TIME_SYSTEM.system_function, + TIME_SYSTEM.update_phase, + )?; + self.system_manager.add_system( + AUDIO_SYSTEM.name, + AUDIO_SYSTEM.system_function, + AUDIO_SYSTEM.update_phase, + )?; + self.system_manager.add_system( + DEFERRED_UPDATE_SYSTEM.name, + DEFERRED_UPDATE_SYSTEM.system_function, + DEFERRED_UPDATE_SYSTEM.update_phase, + )?; + self.system_manager.add_system( + RENDERING_SYSTEM.name, + RENDERING_SYSTEM.system_function, + RENDERING_SYSTEM.update_phase, + )?; // Create default resources - self.create_default_resources().context("Failed to create default resources")?; + self.create_default_resources() + .context("Failed to create default resources")?; // Start game self.start_game()?; - Ok(()) } /// Main engine update function - /// - /// Runs all systems in order: PreGame -> Game -> PostGame + /// + /// Runs all systems in order: PreGame -> Game -> PostGame pub fn update(&mut self, delta_time: std::time::Duration) { - let stop_on_game_errors = self.config.get_bool("PANIC_ON_GAME_ERRORS").unwrap_or(PANIC_ON_GAME_ERRORS); + let stop_on_game_errors = self + .config + .get_bool("PANIC_ON_GAME_ERRORS") + .unwrap_or(PANIC_ON_GAME_ERRORS); // Run systems for update_phase_index in 0..self.system_manager.update_phases.len() { for system_index in 0..self.system_manager.update_phases[update_phase_index].len() { - let (system_name, update_phase, system_function); { - let system = &mut self.system_manager.update_phases[update_phase_index][system_index]; - if !system.enabled { continue; } + let system = + &mut self.system_manager.update_phases[update_phase_index][system_index]; + if !system.enabled { + continue; + } system_name = system.name.to_string(); update_phase = system.update_phase.clone(); system_function = system.system_function; } // Create new time and asign it to system so it can be accessed inside the system function - // For rendering system we can't clean its timer here, - // because it has to render its own timer data in the UI + // For rendering system we can't clean its timer here, + // because it has to render its own timer data in the UI // (and since the frame in which it renders is not yet finished when it renders UI, it has to use previous frame timer data) if system_name != RENDERING_SYSTEM.name { - let mut timer = Timer::new(); timer.begin_context(&format!("{} system update", system_name)); - self.system_manager.update_system_timer(system_name.as_str(), update_phase.clone(), timer).unwrap(); + self.system_manager + .update_system_timer(system_name.as_str(), update_phase.clone(), timer) + .unwrap(); } { // Run system update and handle errors based on configuration - let result = (system_function)(self) - .context(EngineError::SystemUpdateFailed(system_name.clone(), format!("{:?}", update_phase.clone()))); + let result = (system_function)(self).context(EngineError::SystemUpdateFailed( + system_name.clone(), + format!("{:?}", update_phase.clone()), + )); if update_phase == UpdatePhase::Game && stop_on_game_errors { result.unwrap(); // Panic on error if configured @@ -243,26 +290,43 @@ impl Engine { } // Update system timer with the final timer state - let mut timer = match self.system_manager.get_system_timer(system_name.as_str(), update_phase.clone()) { + let mut timer = match self + .system_manager + .get_system_timer(system_name.as_str(), update_phase.clone()) + { Ok(Some(timer)) => timer, Ok(None) => { - panic!("{}", Error::new(EngineError::NonReturnedSystemTimer(system_name.clone()))); + panic!( + "{}", + Error::new(EngineError::NonReturnedSystemTimer(system_name.clone())) + ); } Err(e) => { panic!("{}", Error::new(EngineError::Other(e.to_string()))); } }; - timer.end_context().context(format!("Failed to end timer context for {}", system_name.clone())).unwrap(); // End system update context - self.system_manager.update_system_timer(system_name.as_str(), update_phase.clone(), timer).unwrap(); + timer + .end_context() + .context(format!( + "Failed to end timer context for {}", + system_name.clone() + )) + .unwrap(); // End system update context + self.system_manager + .update_system_timer(system_name.as_str(), update_phase.clone(), timer) + .unwrap(); } } - + // Update FPS counter - let new_frame_time = delta_time.as_secs_f32() * 1000.0; + let new_frame_time = delta_time.as_secs_f32() * 1000.0; let fps = 1000.0 / new_frame_time; self.frame_delta_time = new_frame_time.into(); - println!("Frame finished (Time: {:.3}ms, FPS {:.0})", new_frame_time, fps); + println!( + "Frame finished (Time: {:.3}ms, FPS {:.0})", + new_frame_time, fps + ); } pub fn shutdown(&mut self) { @@ -270,7 +334,12 @@ impl Engine { } pub fn resize(&mut self, new_window_size: winit::dpi::PhysicalSize) { - debug!("{} resized to {}x{}", "Window".mobj_style(), new_window_size.width, new_window_size.height); + debug!( + "{} resized to {}x{}", + "Window".mobj_style(), + new_window_size.width, + new_window_size.height + ); self.window_size = new_window_size; self.renderer.resize(new_window_size); } @@ -279,7 +348,10 @@ impl Engine { let state: winit::event::ElementState = keyboard_input.state; match keyboard_input.physical_key { winit::keyboard::PhysicalKey::Code(key_code) => { - let input_event = InputEvent::KeyboardKey { key: key_code, state: state }; + let input_event = InputEvent::KeyboardKey { + key: key_code, + state: state, + }; self.input_queue.push_back(input_event); debug!("Got new keyboard key input: {:?} {:?}", key_code, state); } @@ -290,7 +362,10 @@ impl Engine { } pub fn pass_mouse_key_input(&mut self, key: &MouseButton, state: &winit::event::ElementState) { - let input_event = InputEvent::MouseButton { key: *key, state: *state }; + let input_event = InputEvent::MouseButton { + key: *key, + state: *state, + }; self.input_queue.push_back(input_event); debug!("Got new mouse key input"); } @@ -302,19 +377,23 @@ impl Engine { } pub fn pass_mouse_delta_input(&mut self, delta: &(f64, f64)) { - let input_event = InputEvent::MouseDelta { delta: Vector2f::new(delta.0 as f32, delta.1 as f32) }; + let input_event = InputEvent::MouseDelta { + delta: Vector2f::new(delta.0 as f32, delta.1 as f32), + }; self.input_queue.push_back(input_event); debug!("Got new mouse motion input"); } - + pub fn pass_mouse_position_input(&mut self, position: &PhysicalPosition) { - let input_event = InputEvent::MousePosition { position: Vector2f::new(position.x as f32, position.y as f32) }; + let input_event = InputEvent::MousePosition { + position: Vector2f::new(position.x as f32, position.y as f32), + }; self.input_queue.push_back(input_event); debug!("Got new mouse position input"); } pub fn pass_input_to_egui(&mut self, event: &winit::event::WindowEvent) { - self.renderer.pass_input_to_egui(event).unwrap(); + self.renderer.pass_input_to_egui(event).unwrap(); } pub fn get_input_queue(&self) -> &VecDeque { @@ -325,42 +404,84 @@ impl Engine { // --- API ------------------------------------------------------------------ /// Pill Engine game API -impl Engine { - +impl Engine { // --- System API --- /// Adds game-defined system to the game update phase - pub fn add_system(&mut self, name: &str, system_function: fn(engine: &mut Engine) -> Result<()>) -> Result<()> { - debug!("Adding {} {} to {} {}", "System".gobj_style(), name.name_style(), "UpdatePhase".sobj_style(), "Game".name_style()); + pub fn add_system( + &mut self, + name: &str, + system_function: fn(engine: &mut Engine) -> Result<()>, + ) -> Result<()> { + debug!( + "Adding {} {} to {} {}", + "System".gobj_style(), + name.name_style(), + "UpdatePhase".sobj_style(), + "Game".name_style() + ); - self.system_manager.add_system(name, system_function, UpdatePhase::Game).context(format!("Adding {} failed", "System".gobj_style())) + self.system_manager + .add_system(name, system_function, UpdatePhase::Game) + .context(format!("Adding {} failed", "System".gobj_style())) } /// Removes game-defined system pub fn remove_system(&mut self, name: &str) -> Result<()> { - debug!("Removing {} {} from {} {}", "System".gobj_style(), name.name_style(), "UpdatePhase".sobj_style(), "Game".name_style()); + debug!( + "Removing {} {} from {} {}", + "System".gobj_style(), + name.name_style(), + "UpdatePhase".sobj_style(), + "Game".name_style() + ); - self.system_manager.remove_system(name, UpdatePhase::Game).context(format!("Removing {} failed", "System".gobj_style())) + self.system_manager + .remove_system(name, UpdatePhase::Game) + .context(format!("Removing {} failed", "System".gobj_style())) } /// Toggles game-defined system - pub fn toggle_system(&mut self, name: &str, update_phase: UpdatePhase, enabled: bool) -> Result<()> { - debug!("Toggling {} {} from {} {} to {} state", "System".gobj_style(), name.name_style(), "UpdatePhase".sobj_style(), update_phase.to_string().name_style(), if enabled { "Enabled" } else { "Disabled" }); + pub fn toggle_system( + &mut self, + name: &str, + update_phase: UpdatePhase, + enabled: bool, + ) -> Result<()> { + debug!( + "Toggling {} {} from {} {} to {} state", + "System".gobj_style(), + name.name_style(), + "UpdatePhase".sobj_style(), + update_phase.to_string().name_style(), + if enabled { "Enabled" } else { "Disabled" } + ); - self.system_manager.toggle_system(name, update_phase, enabled).context(format!("Toggling {} failed", "System".gobj_style())) + self.system_manager + .toggle_system(name, update_phase, enabled) + .context(format!("Toggling {} failed", "System".gobj_style())) } /// Returns system timer. It has to be returned back using update_system_timer function, otherwise engine will panic. pub fn get_system_timer(&mut self, name: &str) -> Timer { - debug!("Getting {} {} timer from {} {}", "System".gobj_style(), name.name_style(), "UpdatePhase".sobj_style(), "Game".name_style()); + debug!( + "Getting {} {} timer from {} {}", + "System".gobj_style(), + name.name_style(), + "UpdatePhase".sobj_style(), + "Game".name_style() + ); - self.system_manager.get_system_timer(name, UpdatePhase::Game).unwrap().unwrap() + self.system_manager + .get_system_timer(name, UpdatePhase::Game) + .unwrap() + .unwrap() } - + // --- Entity API --- /// Returns EntityBuilder, allowing for handy entity creation - pub fn build_entity(&mut self, scene_handle: SceneHandle) -> EntityBuilder { + pub fn build_entity(&mut self, scene_handle: SceneHandle) -> EntityBuilder<'_> { let entity_handle = self.create_entity(scene_handle).unwrap(); EntityBuilder { engine: self, @@ -371,16 +492,43 @@ impl Engine { // Creates new entity to scene specified with scene handle pub fn create_entity(&mut self, scene_handle: SceneHandle) -> Result { - debug!("Creating {} in {} {}", "Entity".gobj_style(), "Scene".gobj_style(), self.scene_manager.get_scene(scene_handle).unwrap().name.name_style()); + debug!( + "Creating {} in {} {}", + "Entity".gobj_style(), + "Scene".gobj_style(), + self.scene_manager + .get_scene(scene_handle) + .unwrap() + .name + .name_style() + ); - self.scene_manager.create_entity(scene_handle).context(format!("Creating {} failed", "Entity".gobj_style())) + self.scene_manager + .create_entity(scene_handle) + .context(format!("Creating {} failed", "Entity".gobj_style())) } - // Removes entity specified with entity handle from scene specified with scene handle - pub fn remove_entity(&mut self, entity_handle: EntityHandle, scene_handle: SceneHandle) -> Result<()> { - debug!("Removing {} from {} {}", "Entity".gobj_style(), "Scene".gobj_style(), self.scene_manager.get_scene(scene_handle).unwrap().name.name_style()); + // Removes entity specified with entity handle from scene specified with scene handle + pub fn remove_entity( + &mut self, + entity_handle: EntityHandle, + scene_handle: SceneHandle, + ) -> Result<()> { + debug!( + "Removing {} from {} {}", + "Entity".gobj_style(), + "Scene".gobj_style(), + self.scene_manager + .get_scene(scene_handle) + .unwrap() + .name + .name_style() + ); - let component_destroyers = self.scene_manager.remove_entity(scene_handle, entity_handle).context(format!("Creating {} failed", "Entity".gobj_style()))?; + let component_destroyers = self + .scene_manager + .remove_entity(scene_handle, entity_handle) + .context(format!("Creating {} failed", "Entity".gobj_style()))?; // Destroy components using destroyers for mut component_destroyer in component_destroyers { @@ -393,33 +541,78 @@ impl Engine { // --- Component API --- /// Registers new component type in scene specified with scene handle - pub fn register_component(&mut self, scene_handle: SceneHandle) -> Result<()> - where T: Component> + pub fn register_component(&mut self, scene_handle: SceneHandle) -> Result<()> + where + T: Component>, { - debug!("Registering {} {} in {} {}", "Component".gobj_style(), get_type_name::().sobj_style(), "Scene".sobj_style(), self.scene_manager.get_scene(scene_handle).unwrap().name.name_style()); + debug!( + "Registering {} {} in {} {}", + "Component".gobj_style(), + get_type_name::().sobj_style(), + "Scene".sobj_style(), + self.scene_manager + .get_scene(scene_handle) + .unwrap() + .name + .name_style() + ); - self.scene_manager.register_component::(scene_handle).context(format!("Registering {} failed", "Component".gobj_style())) + self.scene_manager + .register_component::(scene_handle) + .context(format!("Registering {} failed", "Component".gobj_style())) } /// Adds new component to the entity specified with scene and entity handle - pub fn add_component_to_entity(&mut self, scene_handle: SceneHandle, entity_handle: EntityHandle, mut component: T) -> Result<()> - where T : Component> + pub fn add_component_to_entity( + &mut self, + scene_handle: SceneHandle, + entity_handle: EntityHandle, + mut component: T, + ) -> Result<()> + where + T: Component>, { - debug!("Adding {} {} to {} {} in {} {}", "Component".gobj_style(), get_type_name::().sobj_style(), "Entity".gobj_style(), entity_handle.data().index, "Scene".gobj_style(), self.scene_manager.get_scene(scene_handle).unwrap().name.name_style()); - + debug!( + "Adding {} {} to {} {} in {} {}", + "Component".gobj_style(), + get_type_name::().sobj_style(), + "Entity".gobj_style(), + entity_handle.data().index, + "Scene".gobj_style(), + self.scene_manager + .get_scene(scene_handle) + .unwrap() + .name + .name_style() + ); + // Check if already added let target_scene = self.scene_manager.get_scene(scene_handle)?; if target_scene.entity_has_component::(entity_handle)? { - return Err(Error::new(EngineError::ComponentAlreadyExists(get_type_name::()))) + return Err(Error::new(EngineError::ComponentAlreadyExists( + get_type_name::(), + ))); } // Initialize component - component.initialize(self).context(format!("Adding {} {} failed", "Component".gobj_style(), get_type_name::().sobj_style()))?; - + component.initialize(self).context(format!( + "Adding {} {} failed", + "Component".gobj_style(), + get_type_name::().sobj_style() + ))?; + // Add component - self.scene_manager.add_component_to_entity::(scene_handle, entity_handle, component).context(format!("Adding {} to {} failed", "Component".gobj_style(), "Entity".gobj_style()))?; - let component = self.scene_manager.get_entity_component::(entity_handle, scene_handle)?; + self.scene_manager + .add_component_to_entity::(scene_handle, entity_handle, component) + .context(format!( + "Adding {} to {} failed", + "Component".gobj_style(), + "Entity".gobj_style() + ))?; + let component = self + .scene_manager + .get_entity_component::(entity_handle, scene_handle)?; // Pass handles to entity and scene to this component so it can store it if needed component.pass_handles(scene_handle, entity_handle); @@ -428,12 +621,33 @@ impl Engine { } /// Removes component from the entity specified with scene and entity handle - pub fn remove_component_from_entity(&mut self, scene_handle: SceneHandle, entity_handle: EntityHandle) -> Result<()> - where T : Component> + pub fn remove_component_from_entity( + &mut self, + scene_handle: SceneHandle, + entity_handle: EntityHandle, + ) -> Result<()> + where + T: Component>, { - debug!("Removing {} {} from {} {} in {} {}", "Component".gobj_style(), get_type_name::().sobj_style(), "Entity".gobj_style(), entity_handle.data().index, "Scene".gobj_style(), self.scene_manager.get_scene(scene_handle).unwrap().name.name_style()); - - let mut component = self.scene_manager.remove_component_from_entity::(scene_handle, entity_handle).context("Removing component from entity failed").unwrap(); + debug!( + "Removing {} {} from {} {} in {} {}", + "Component".gobj_style(), + get_type_name::().sobj_style(), + "Entity".gobj_style(), + entity_handle.data().index, + "Scene".gobj_style(), + self.scene_manager + .get_scene(scene_handle) + .unwrap() + .name + .name_style() + ); + + let mut component = self + .scene_manager + .remove_component_from_entity::(scene_handle, entity_handle) + .context("Removing component from entity failed") + .unwrap(); // Destroy component component.destroy(self, scene_handle, entity_handle)?; @@ -444,140 +658,186 @@ impl Engine { // --- Global Component API --- /// Adds global component to engine - pub fn add_global_component(&mut self, mut component: T) -> Result<()> - where T: GlobalComponent> + pub fn add_global_component(&mut self, mut component: T) -> Result<()> + where + T: GlobalComponent>, { // Check if component of this type is not already added if self.global_components.contains_key::() { - return Err(Error::new(EngineError::GlobalComponentAlreadyExists(get_type_name::()))); + return Err(Error::new(EngineError::GlobalComponentAlreadyExists( + get_type_name::(), + ))); } // Initialize component component.initialize(self)?; // Add component - self.global_components.insert::(GlobalComponentStorage::::new(component)); + self.global_components + .insert::(GlobalComponentStorage::::new(component)); Ok(()) } /// Returns global component - pub fn get_global_component(&self) -> Result<&T> - where T: GlobalComponent> + pub fn get_global_component(&self) -> Result<&T> + where + T: GlobalComponent>, { // Get component - let component = self.global_components.get::().ok_or(Error::new(EngineError::GlobalComponentNotFound(get_type_name::())))?.data.as_ref().unwrap(); - + let component = self + .global_components + .get::() + .ok_or(Error::new(EngineError::GlobalComponentNotFound( + get_type_name::(), + )))? + .data + .as_ref() + .unwrap(); + Ok(component) } - /// Returns global mutable component - pub fn get_global_component_mut(&mut self) -> Result<&mut T> - where T: GlobalComponent> + /// Returns global mutable component + pub fn get_global_component_mut(&mut self) -> Result<&mut T> + where + T: GlobalComponent>, { // Get component - let component = self.global_components.get_mut::().ok_or(Error::new(EngineError::GlobalComponentNotFound(get_type_name::())))?.data.as_mut().unwrap(); + let component = self + .global_components + .get_mut::() + .ok_or(Error::new(EngineError::GlobalComponentNotFound( + get_type_name::(), + )))? + .data + .as_mut() + .unwrap(); Ok(component) } /// Removes global component from the engine - pub fn remove_global_component(&mut self) -> Result<()> - where T: GlobalComponent> + pub fn remove_global_component(&mut self) -> Result<()> + where + T: GlobalComponent>, { // Check if the type of the component is the same as of the ones, which cannot be removed if ENGINE_GLOBAL_COMPONENTS.contains(&TypeId::of::()) { - return Err(Error::new(EngineError::GlobalComponentCannotBeRemoved(get_type_name::()))); + return Err(Error::new(EngineError::GlobalComponentCannotBeRemoved( + get_type_name::(), + ))); } // Remove and destroy component - let global_component_storage = self.global_components.remove::().ok_or(EngineError::GlobalComponentNotFound(get_type_name::()))?; + let global_component_storage = self + .global_components + .remove::() + .ok_or(EngineError::GlobalComponentNotFound(get_type_name::()))?; let mut global_component = global_component_storage.data.unwrap(); global_component.destroy(self)?; - + Ok(()) } // --- Iterator API --- - + /// Returns iterator for specified component - /// + /// /// Additionally returns entity handle to matching entities - pub fn iterate_one_component(&self) -> Result> - where A: Component> + pub fn iterate_one_component(&self) -> Result> + where + A: Component>, { // Get scene handle and iterator let scene_handle = self.scene_manager.get_active_scene_handle()?; - self.scene_manager.get_one_component_iterator::(scene_handle) + self.scene_manager + .get_one_component_iterator::(scene_handle) } /// Returns iterator for specified component mutable - /// + /// /// Additionally returns entity handle to matching entities - pub fn iterate_one_component_mut(&mut self) -> Result> - where A: Component> + pub fn iterate_one_component_mut( + &mut self, + ) -> Result> + where + A: Component>, { // Get scene handle and iterator let scene_handle = self.scene_manager.get_active_scene_handle()?; - self.scene_manager.get_one_component_iterator_mut::(scene_handle) + self.scene_manager + .get_one_component_iterator_mut::(scene_handle) } - + /// Returns iterator for specified component pair - /// + /// /// Iterator fetches specified components only for those entities which have them all /// Additionally returns entity handle to matching entities - pub fn iterate_two_components(&self) -> Result> - where + pub fn iterate_two_components( + &self, + ) -> Result> + where A: Component>, - B: Component> + B: Component>, { // Get scene handle and iterator let scene_handle = self.scene_manager.get_active_scene_handle()?; - self.scene_manager.get_two_component_iterator::(scene_handle) + self.scene_manager + .get_two_component_iterator::(scene_handle) } /// Returns iterator for specified component pair mutable - /// + /// /// Iterator fetches specified components only for those entities which have them all /// Additionally returns entity handle to matching entities - pub fn iterate_two_components_mut(&mut self) -> Result> - where + pub fn iterate_two_components_mut( + &mut self, + ) -> Result> + where A: Component>, - B: Component> + B: Component>, { // Get scene handle and iterator let scene_handle = self.scene_manager.get_active_scene_handle()?; - self.scene_manager.get_two_component_iterator_mut::(scene_handle) + self.scene_manager + .get_two_component_iterator_mut::(scene_handle) } - /// Returns iterator for specified component triple - /// + /// Returns iterator for specified component triple + /// /// Iterator fetches specified components only for those entities which have them all /// Additionally returns entity handle to matching entities - pub fn iterate_three_components(&self) -> Result> - where + pub fn iterate_three_components( + &self, + ) -> Result> + where A: Component>, B: Component>, - C: Component> + C: Component>, { // Get scene handle and iterator let scene_handle = self.scene_manager.get_active_scene_handle()?; - self.scene_manager.get_three_component_iterator::(scene_handle) + self.scene_manager + .get_three_component_iterator::(scene_handle) } - + /// Returns iterator for specified component triple mutable - /// + /// /// Iterator fetches specified components only for those entities which have them all /// Additionally returns entity handle to matching entities - pub fn iterate_three_components_mut(&mut self) -> Result> - where + pub fn iterate_three_components_mut( + &mut self, + ) -> Result> + where A: Component>, B: Component>, - C: Component> + C: Component>, { // Get scene handle and iterator let scene_handle = self.scene_manager.get_active_scene_handle()?; - self.scene_manager.get_three_component_iterator_mut::(scene_handle) + self.scene_manager + .get_three_component_iterator_mut::(scene_handle) } // --- Scene API --- @@ -585,21 +845,33 @@ impl Engine { // Creates scene pub fn create_scene(&mut self, name: &str) -> Result { info!("Creating scene: {}", name); - self.scene_manager.create_scene(name).context(format!("Creating new {} failed", "Scene".gobj_style())) + self.scene_manager + .create_scene(name) + .context(format!("Creating new {} failed", "Scene".gobj_style())) } /// Returns handle to the scene specified by its name pub fn get_scene_handle(&self, name: &str) -> Result { - self.scene_manager.get_scene_handle(name).context(format!("Getting {} failed", "SceneHandle".sobj_style())) + self.scene_manager + .get_scene_handle(name) + .context(format!("Getting {} failed", "SceneHandle".sobj_style())) } pub fn set_active_scene(&mut self, scene_handle: SceneHandle) -> Result<()> { - self.scene_manager.set_active_scene(scene_handle).context(format!("Setting active {} failed", "Scene".gobj_style())) + self.scene_manager + .set_active_scene(scene_handle) + .context(format!("Setting active {} failed", "Scene".gobj_style())) } /// Returns handle to the active scene pub fn get_active_scene_handle(&self) -> Result { - self.scene_manager.get_active_scene_handle().context(format!("Getting {} of active {} failed", "SceneHandle".sobj_style(), "Scene".gobj_style())) + self.scene_manager + .get_active_scene_handle() + .context(format!( + "Getting {} of active {} failed", + "SceneHandle".sobj_style(), + "Scene".gobj_style() + )) } // Removes scene deleting all data in it @@ -619,7 +891,13 @@ impl Engine { } // Remove scene - self.scene_manager.remove_scene(scene_handle).context(format!("Removing {} with usage of {} failed", "Scene".sobj_style(), "SceneHandle".gobj_style()))?; + self.scene_manager + .remove_scene(scene_handle) + .context(format!( + "Removing {} with usage of {} failed", + "Scene".sobj_style(), + "SceneHandle".gobj_style() + ))?; Ok(()) } @@ -627,30 +905,44 @@ impl Engine { // --- Resource API --- // Registers new resource type in the engine - pub fn register_resource_type(&mut self, max_resource_count: usize) -> Result<()> - where T: Resource> + pub fn register_resource_type(&mut self, max_resource_count: usize) -> Result<()> + where + T: Resource>, { - self.resource_manager.register_resource_type::(max_resource_count) + self.resource_manager + .register_resource_type::(max_resource_count) } // Adds resource to the engine - pub fn add_resource(&mut self, mut resource: T) -> Result - where T: Resource> + pub fn add_resource(&mut self, mut resource: T) -> Result + where + T: Resource>, { - debug!("Adding {} {} {}", "Resource".gobj_style(), get_type_name::().sobj_style(), resource.get_name().name_style()); + debug!( + "Adding {} {} {}", + "Resource".gobj_style(), + get_type_name::().sobj_style(), + resource.get_name().name_style() + ); // Check if resource has proper name let resource_name = resource.get_name(); if resource_name.starts_with(DEFAULT_RESOURCE_PREFIX) { - return Err(Error::new(EngineError::WrongResourceName(resource_name.clone()))) + return Err(Error::new(EngineError::WrongResourceName( + resource_name.clone(), + ))); } // Initialize resource - resource.initialize(self).context(format!("Adding {} {} failed", "Resource".gobj_style(), get_type_name::().sobj_style()))?; - + resource.initialize(self).context(format!( + "Adding {} {} failed", + "Resource".gobj_style(), + get_type_name::().sobj_style() + ))?; + // Add resource and get it back let add_result = self.resource_manager.add_resource(resource)?; - let resource_handle = add_result.0; + let resource_handle = add_result.0; let resource = add_result.1; // Pass handle to this resource so it can store it if needed @@ -660,77 +952,116 @@ impl Engine { } // Returns resource associated with resource handle - pub fn get_resource<'a, T>(&'a self, resource_handle: &'a T::Handle) -> Result<&'a T> - where T: Resource> + pub fn get_resource<'a, T>(&'a self, resource_handle: &'a T::Handle) -> Result<&'a T> + where + T: Resource>, { Ok(self.resource_manager.get_resource::(resource_handle)?) } /// Returns resource specified by its name - pub fn get_resource_by_name(&self, name: &str) -> Result<&T> - where T: Resource> + pub fn get_resource_by_name(&self, name: &str) -> Result<&T> + where + T: Resource>, { Ok(self.resource_manager.get_resource_by_name::(name)?) } /// Returns handle to resource specified by the name of this resource - pub fn get_resource_handle(&self, name: &str) -> Result - where T: Resource> + pub fn get_resource_handle(&self, name: &str) -> Result + where + T: Resource>, { Ok(self.resource_manager.get_resource_handle::(name)?) } // Returns mutable resource associated with resource handle - pub fn get_resource_mut<'a, T>(&'a mut self, resource_handle: &'a T::Handle) -> Result<&'a mut T> - where T: Resource> + pub fn get_resource_mut<'a, T>( + &'a mut self, + resource_handle: &'a T::Handle, + ) -> Result<&'a mut T> + where + T: Resource>, { - Ok(self.resource_manager.get_resource_mut::(resource_handle)?) + Ok(self + .resource_manager + .get_resource_mut::(resource_handle)?) } /// Returns mutable resource specified by its name - pub fn get_resource_by_name_mut(&mut self, name: &str) -> Result<&mut T> - where T: Resource> + pub fn get_resource_by_name_mut(&mut self, name: &str) -> Result<&mut T> + where + T: Resource>, { Ok(self.resource_manager.get_resource_by_name_mut::(name)?) } - // Removes resource associated with resource handle from the engine - pub fn remove_resource(&mut self, resource_handle: &T::Handle) -> Result<()> - where T: Resource> + // Removes resource associated with resource handle from the engine + pub fn remove_resource(&mut self, resource_handle: &T::Handle) -> Result<()> + where + T: Resource>, { - let error_message = format!("Removing {} {} failed", "Resource".gobj_style(), get_type_name::().sobj_style()); - + let error_message = format!( + "Removing {} {} failed", + "Resource".gobj_style(), + get_type_name::().sobj_style() + ); + // Check if resource is not default - let resource_name = self.resource_manager.get_resource::(resource_handle).context(error_message.to_string())?.get_name(); + let resource_name = self + .resource_manager + .get_resource::(resource_handle) + .context(error_message.to_string())? + .get_name(); if resource_name.starts_with(DEFAULT_RESOURCE_PREFIX) { - return Err(Error::new(EngineError::RemoveDefaultResource(resource_name.clone()))).context(error_message.to_string()) + return Err(Error::new(EngineError::RemoveDefaultResource( + resource_name.clone(), + ))) + .context(error_message.to_string()); } // Remove and destroy resource - let mut remove_result = self.resource_manager.remove_resource::(resource_handle).context(error_message.to_string())?; + let mut remove_result = self + .resource_manager + .remove_resource::(resource_handle) + .context(error_message.to_string())?; remove_result.1.destroy(self, *resource_handle)?; Ok(()) } - // Removes resource specified with its name from the engine - pub fn remove_resource_by_name(&mut self, name: &str) -> Result<()> - where T: Resource> + // Removes resource specified with its name from the engine + pub fn remove_resource_by_name(&mut self, name: &str) -> Result<()> + where + T: Resource>, { - let error_message = format!("Removing {} {} {} failed", "Resource".gobj_style(), get_type_name::().sobj_style(), name.to_string().name_style()); + let error_message = format!( + "Removing {} {} {} failed", + "Resource".gobj_style(), + get_type_name::().sobj_style(), + name.to_string().name_style() + ); // Check if resource exists - self.resource_manager.get_resource_by_name::(name).context(error_message.to_string())?; + self.resource_manager + .get_resource_by_name::(name) + .context(error_message.to_string())?; // Check if resource is not default if name.starts_with(DEFAULT_RESOURCE_PREFIX) { - return Err(Error::new(EngineError::RemoveDefaultResource(name.to_string()))).context(error_message.to_string()) + return Err(Error::new(EngineError::RemoveDefaultResource( + name.to_string(), + ))) + .context(error_message.to_string()); } // Remove resource - let mut remove_result = self.resource_manager.remove_resource_by_name::(name).context(error_message.to_string())?; + let mut remove_result = self + .resource_manager + .remove_resource_by_name::(name) + .context(error_message.to_string())?; remove_result.1.destroy(self, remove_result.0)?; Ok(()) } -} \ No newline at end of file +} diff --git a/engine/pill_renderer/src/mesh_drawer.rs b/engine/pill_renderer/src/mesh_drawer.rs index 9b6de538..9d6c351f 100644 --- a/engine/pill_renderer/src/mesh_drawer.rs +++ b/engine/pill_renderer/src/mesh_drawer.rs @@ -58,15 +58,15 @@ impl MeshDrawer { label: Some("mesh_drawer_encoder"), }); - let mut instance_batch_number = 0; - let mut rendering_context_change_number = 0; + let mut _instance_batch_number = 0; + let mut _rendering_context_change_number = 0; for (i, instance_batch) in render_queue .chunks(self.max_instance_batch_size as usize) .enumerate() { let batch_size = instance_batch.len(); - instance_batch_number += 1; + _instance_batch_number += 1; timer.begin_context(&format!("Prepare draw instance batch {}", i)); @@ -157,7 +157,7 @@ impl MeshDrawer { // Set new order current_rendering_order = render_queue_key_fields.order; - rendering_context_change_number += 1; + _rendering_context_change_number += 1; } // Check material @@ -193,7 +193,7 @@ impl MeshDrawer { render_pass.set_bind_group(1, &material.parameter_bind_group, &[]); render_pass.set_bind_group(2, &camera.bind_group, &[]); - rendering_context_change_number += 1; + _rendering_context_change_number += 1; } // Check mesh @@ -219,7 +219,7 @@ impl MeshDrawer { render_pass .set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32); - rendering_context_change_number += 1; + _rendering_context_change_number += 1; } // Add new instance From ffbf7864eff3258e95ef6ed5c560fd54aece1c99 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 02:37:08 +0200 Subject: [PATCH 13/69] feat: inlining pipeline --- engine/pill_renderer/src/renderer.rs | 289 ++++++++++++++++----------- 1 file changed, 172 insertions(+), 117 deletions(-) diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 0722753c..c0611890 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -83,91 +83,7 @@ impl PillRenderer for Renderer { _vertex_shader_bytes: &[u8], _fragment_shader_bytes: &[u8], ) -> Result<()> { - // M3: WGSL pipeline with per-draw MVP via dynamic offsets (set 3) - let vertex_wgsl = r#" -struct Camera { position: vec4, view_projection_matrix: mat4x4, }; -@group(2) @binding(0) var GCamera: Camera; - -struct PerDraw { - mvp: mat4x4, - model: mat4x4, - tint: vec4, -}; -@group(3) @binding(0) var UPerDraw: PerDraw; - -struct VSIn { @location(0) pos: vec3, @location(1) uv: vec2, }; -struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; - -@vertex fn main(input: VSIn) -> VSOut { - var out: VSOut; - out.pos = UPerDraw.mvp * vec4(input.pos, 1.0); - out.uv = input.uv; - return out; -} -"#; - - let fragment_wgsl = r#" -// Material set(0): -@group(0) @binding(0) var tex_diffuse: texture_2d; -@group(0) @binding(1) var smp_diffuse: sampler; -@group(0) @binding(2) var tex_normal: texture_2d; -@group(0) @binding(3) var smp_normal: sampler; - -// Material parameters set(1) – pack tint.rgb + specularity in a single vec4 -struct MaterialParams { tint_spec: vec4, } -@group(1) @binding(0) var UMaterial: MaterialParams; - -// Per-draw (set 3): supports per-entity tint for M4 -struct PerDraw { - mvp: mat4x4, - model: mat4x4, - tint: vec4, -}; -@group(3) @binding(0) var UPerDraw: PerDraw; - -@fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { - let albedo = textureSample(tex_diffuse, smp_diffuse, uv); - // Combine material tint and per-entity tint; modulate brightness by specularity for demo - let tinted = vec4(UMaterial.tint_spec.rgb, 1.0) * UPerDraw.tint; - let spec_boost = 0.5 + 0.5 * UMaterial.tint_spec.a; - let color = albedo * tinted * spec_boost; - return vec4(color.rgb, 1.0); -} -"#; - - // Create shader modules with WGSL - let vertex_shader = self - .state - .device - .create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("m2_vertex_shader"), - source: wgpu::ShaderSource::Wgsl(vertex_wgsl.into()), - }); - - let fragment_shader = - self.state - .device - .create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("m2_fragment_shader"), - source: wgpu::ShaderSource::Wgsl(fragment_wgsl.into()), - }); - - // Create master pipeline - let master_pipeline = RendererPipeline::new( - &self.state.device, - vertex_shader, - fragment_shader, - self.state.color_format, - Some(self.state.depth_format), - &[RendererMesh::data_layout_descriptor()], - ) - .unwrap(); - - self.state - .renderer_resource_storage - .pipelines - .insert(master_pipeline); - + // No-op: pipeline created lazily in render Ok(()) } @@ -206,23 +122,73 @@ struct PerDraw { textures: &MaterialTextureMap, parameters: &MaterialParameterMap, ) -> Result { - let pipeline_handle = MASTER_PIPELINE_HANDLE; - let pipeline = self - .state - .renderer_resource_storage - .pipelines - .get(pipeline_handle) - .unwrap(); + // Create bind group layouts inline (avoid pipeline storage dependency) + let material_texture_bind_group_layout = + self.state + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("material_texture_bind_group_layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + let material_parameter_bind_group_layout = + self.state + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("material_parameter_bind_group_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); let material = RendererMaterial::new( &self.state.device, &self.state.queue, &self.state.renderer_resource_storage, name, - pipeline_handle, - &pipeline.material_texture_bind_group_layout, + MASTER_PIPELINE_HANDLE, + &material_texture_bind_group_layout, textures, - &pipeline.material_parameter_bind_group_layout, + &material_parameter_bind_group_layout, parameters, ) .unwrap(); @@ -237,15 +203,23 @@ struct PerDraw { } fn create_camera(&mut self) -> Result { - let pipeline_handle = MASTER_PIPELINE_HANDLE; - let pipeline = self - .state - .renderer_resource_storage - .pipelines - .get(pipeline_handle) - .unwrap(); - let camera_bind_group_layout = &pipeline.camera_bind_group_layout; - let camera = RendererCamera::new(&self.state.device, camera_bind_group_layout)?; + let camera_bind_group_layout = + self.state + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("camera_bind_group_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + let camera = RendererCamera::new(&self.state.device, &camera_bind_group_layout)?; let handle = self.state.renderer_resource_storage.cameras.insert(camera); Ok(handle) @@ -357,7 +331,7 @@ pub struct State { depth_texture: RendererTexture, // Other config: config::Config, - egui_renderer: crate::egui::EguiRenderer, + egui_renderer: crate::egui::EguiRenderer, // TODO: Separate system adding Pass } impl State { @@ -521,6 +495,81 @@ impl State { egui_ui: Box, timer: &mut Timer, ) -> Result<()> { + // Build default pipeline inline (no storage) + let vertex_wgsl = r#" +struct Camera { position: vec4, view_projection_matrix: mat4x4, }; +@group(2) @binding(0) var GCamera: Camera; + +struct PerDraw { + mvp: mat4x4, + model: mat4x4, + tint: vec4, +}; +@group(3) @binding(0) var UPerDraw: PerDraw; + +struct VSIn { @location(0) pos: vec3, @location(1) uv: vec2, }; +struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; + +@vertex fn main(input: VSIn) -> VSOut { + var out: VSOut; + out.pos = UPerDraw.mvp * vec4(input.pos, 1.0); + out.uv = input.uv; + return out; +} +"#; + + let fragment_wgsl = r#" +// Material set(0): +@group(0) @binding(0) var tex_diffuse: texture_2d; +@group(0) @binding(1) var smp_diffuse: sampler; +@group(0) @binding(2) var tex_normal: texture_2d; +@group(0) @binding(3) var smp_normal: sampler; + +// Material parameters set(1) – pack tint.rgb + specularity in a single vec4 +struct MaterialParams { tint_spec: vec4, } +@group(1) @binding(0) var UMaterial: MaterialParams; + +// Per-draw (set 3): supports per-entity tint for M4 +struct PerDraw { + mvp: mat4x4, + model: mat4x4, + tint: vec4, +}; +@group(3) @binding(0) var UPerDraw: PerDraw; + +@fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let albedo = textureSample(tex_diffuse, smp_diffuse, uv); + let tinted = vec4(UMaterial.tint_spec.rgb, 1.0) * UPerDraw.tint; + let spec_boost = 0.5 + 0.5 * UMaterial.tint_spec.a; + let color = albedo * tinted * spec_boost; + return vec4(color.rgb, 1.0); +} +"#; + + let vertex_shader = self + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("m2_vertex_shader"), + source: wgpu::ShaderSource::Wgsl(vertex_wgsl.into()), + }); + let fragment_shader = self + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("m2_fragment_shader"), + source: wgpu::ShaderSource::Wgsl(fragment_wgsl.into()), + }); + + let default_pipeline = RendererPipeline::new( + &self.device, + vertex_shader, + fragment_shader, + self.color_format, + Some(self.depth_format), + &[RendererMesh::data_layout_descriptor()], + ) + .unwrap(); + // Use default_pipeline directly below for bind group layouts and pipeline + let _per_draw_bind_group_layout = &default_pipeline.per_draw_bind_group_layout; // M3: Hello Mesh + per-draw MVP (dynamic offsets) timer.record("Frame: acquire"); @@ -793,14 +842,25 @@ impl State { .write_buffer(&per_draw_buffer, offset_bytes, bytemuck::bytes_of(&pd)); } timer.record("Per-draw UBO writes"); - let pipeline = self - .renderer_resource_storage - .pipelines - .get(MASTER_PIPELINE_HANDLE) - .unwrap(); + // Create per-draw bind group layout raw (avoid pipeline storage) + let per_draw_bind_group_layout = + self.device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("per_draw_bind_group_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: Some(std::num::NonZeroU64::new(144).unwrap()), + }, + count: None, + }], + }); let per_draw_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("per_draw_bind_group"), - layout: &pipeline.per_draw_bind_group_layout, + layout: &per_draw_bind_group_layout, entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { @@ -840,12 +900,7 @@ impl State { }); // Iterate groups: bind pipeline/material once per group; mesh + offset per draw for group in &groups { - let pipeline = self - .renderer_resource_storage - .pipelines - .get(group.pipeline_handle) - .unwrap(); - rpass.set_pipeline(&pipeline.render_pipeline); + rpass.set_pipeline(&default_pipeline.render_pipeline); let material = self .renderer_resource_storage From 2d159e5b9e6fa597e4bb1ac5218d1e7ad3a1a5e1 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 02:56:10 +0200 Subject: [PATCH 14/69] feat: M4 prebuilt PSO --- engine/pill_renderer/src/renderer.rs | 145 +++++++++++++++------------ 1 file changed, 79 insertions(+), 66 deletions(-) diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index c0611890..3c4676e9 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -329,6 +329,8 @@ pub struct State { color_format: wgpu::TextureFormat, depth_format: wgpu::TextureFormat, depth_texture: RendererTexture, + // Prebuilt PSO (static pipeline) + default_pipeline: RendererPipeline, // Other config: config::Config, egui_renderer: crate::egui::EguiRenderer, // TODO: Separate system adding Pass @@ -451,51 +453,7 @@ impl State { let egui_renderer = EguiRenderer::new(&device, surface_configuration.format, None, 1, window_ref); - // Create state - Self { - // Resources - renderer_resource_storage, - // Renderer variables - surface, - device, - queue, - surface_configuration, - window_size, - color_format, - depth_format, - depth_texture, - // Other - config, - egui_renderer, - } - } - - fn resize(&mut self, new_window_size: winit::dpi::PhysicalSize) { - if new_window_size.width > 0 && new_window_size.height > 0 { - self.window_size = new_window_size; - self.surface_configuration.width = new_window_size.width; - self.surface_configuration.height = new_window_size.height; - self.surface - .configure(&self.device, &self.surface_configuration); - self.depth_texture = RendererTexture::new_depth_texture( - &self.device, - &self.surface_configuration, - "depth_texture", - ) - .unwrap(); - } - } - - fn render( - &mut self, - active_camera_entity_handle: EntityHandle, - render_queue: &Vec, - camera_component_storage: &ComponentStorage, - transform_component_storage: &ComponentStorage, - egui_ui: Box, - timer: &mut Timer, - ) -> Result<()> { - // Build default pipeline inline (no storage) + // Build static shader modules and pipeline once ([SIMILAR] prebuilt PSO per TALK) let vertex_wgsl = r#" struct Camera { position: vec4, view_projection_matrix: mat4x4, }; @group(2) @binding(0) var GCamera: Camera; @@ -517,7 +475,6 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; return out; } "#; - let fragment_wgsl = r#" // Material set(0): @group(0) @binding(0) var tex_diffuse: texture_2d; @@ -545,31 +502,73 @@ struct PerDraw { return vec4(color.rgb, 1.0); } "#; - - let vertex_shader = self - .device - .create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("m2_vertex_shader"), - source: wgpu::ShaderSource::Wgsl(vertex_wgsl.into()), - }); - let fragment_shader = self - .device - .create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("m2_fragment_shader"), - source: wgpu::ShaderSource::Wgsl(fragment_wgsl.into()), - }); - + let vertex_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("m2_vertex_shader"), + source: wgpu::ShaderSource::Wgsl(vertex_wgsl.into()), + }); + let fragment_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("m2_fragment_shader"), + source: wgpu::ShaderSource::Wgsl(fragment_wgsl.into()), + }); let default_pipeline = RendererPipeline::new( - &self.device, + &device, vertex_shader, fragment_shader, - self.color_format, - Some(self.depth_format), + color_format, + Some(depth_format), &[RendererMesh::data_layout_descriptor()], ) .unwrap(); - // Use default_pipeline directly below for bind group layouts and pipeline - let _per_draw_bind_group_layout = &default_pipeline.per_draw_bind_group_layout; + + // Create state + Self { + // Resources + renderer_resource_storage, + // Renderer variables + surface, + device, + queue, + surface_configuration, + window_size, + color_format, + depth_format, + depth_texture, + // Prebuilt PSO + default_pipeline, + // Other + config, + egui_renderer, + } + } + + fn resize(&mut self, new_window_size: winit::dpi::PhysicalSize) { + if new_window_size.width > 0 && new_window_size.height > 0 { + self.window_size = new_window_size; + self.surface_configuration.width = new_window_size.width; + self.surface_configuration.height = new_window_size.height; + self.surface + .configure(&self.device, &self.surface_configuration); + self.depth_texture = RendererTexture::new_depth_texture( + &self.device, + &self.surface_configuration, + "depth_texture", + ) + .unwrap(); + } + } + + fn render( + &mut self, + active_camera_entity_handle: EntityHandle, + render_queue: &Vec, + camera_component_storage: &ComponentStorage, + transform_component_storage: &ComponentStorage, + egui_ui: Box, + timer: &mut Timer, + ) -> Result<()> { + // [SIMILAR] Prebuilt PSO used; avoid hot-path pipeline creation per TALK + // Use prebuilt default_pipeline for bind group layouts and pipeline + let _per_draw_bind_group_layout = &self.default_pipeline.per_draw_bind_group_layout; // M3: Hello Mesh + per-draw MVP (dynamic offsets) timer.record("Frame: acquire"); @@ -593,6 +592,8 @@ struct PerDraw { timer.record("Frame: setup view & camera"); + // [SIMILAR] Modify resources (camera UBO) before draw; separates data mods from drawing as per TALK + // [DIFF] Bind group slot indices differ from TALK’s convention (0=globals,1=material,2=shader,3=dynamic); here camera is @group(2) // Get active camera and update it let camera_storage = camera_component_storage .data @@ -632,6 +633,9 @@ struct PerDraw { label: Some("m2_inline_encoder"), }); + // [SIMILAR] CPU frustum culling and precomputing per-draw transforms before starting the pass matches TALK’s advice + // [DIFF] Only frustum culling here; TALK discusses 2-pass occlusion and broader CPU-driven pipelines + // [API->CLIENT] Culling, model matrix building, and visible set construction should live in client/high-level renderer // Build view before pass: cull and prepare per-draw MVPs let vp_mat: cgmath::Matrix4 = renderer_camera.uniform.view_projection_matrix.into(); let m = vp_mat; @@ -765,6 +769,8 @@ struct PerDraw { }); } + // [SIMILAR] Sort by pipeline/material/mesh to minimize state changes as in TALK + // [API->CLIENT] Sorting/grouping is client responsibility; low-level should accept an already ordered draw stream // Sort by pipeline -> material -> mesh to minimize state changes visible.sort_by_key(|v| { ( @@ -809,6 +815,9 @@ struct PerDraw { next_offset_u32 = next_offset_u32.wrapping_add(256); } + // [SIMILAR] Per-pass batch upload of dynamic UBOs with dynamic offsets; avoids per-draw map/unmap + // [DIFF] Recreates buffer and issues one write per visible; TALK prefers a reusable temp bump allocator/ring buffer + // [API->CLIENT] Data layout/packing and offset management are client-owned; low-level only binds buffer+offsets // Prepare per-draw dynamic uniform buffer and bind group sized to visible set let per_draw_stride: u64 = 256; // alignment let per_draw_capacity = visible.len() as u64; @@ -872,6 +881,7 @@ struct PerDraw { }); { + // [DIFF] TALK advocates explicit per-pass transitions; wgpu hides resource states implicitly here let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("m2_inline_pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { @@ -898,9 +908,10 @@ struct PerDraw { timestamp_writes: None, occlusion_query_set: None, }); + // [SIMILAR] Bind pipeline/material once per group; change only minimal per-draw state (offsets) // Iterate groups: bind pipeline/material once per group; mesh + offset per draw for group in &groups { - rpass.set_pipeline(&default_pipeline.render_pipeline); + rpass.set_pipeline(&self.default_pipeline.render_pipeline); let material = self .renderer_resource_storage @@ -917,6 +928,8 @@ struct PerDraw { .meshes .get(draw.mesh_handle) .unwrap(); + // [DIFF] VB/IB are rebound each draw; TALK suggests packing meshes in large buffers and using base vertex/index to avoid rebinding + // [API->CLIENT] Provide packed mesh atlas + per-draw base offsets so low-level can draw without re-binding buffers rpass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); rpass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32); rpass.set_bind_group(3, &per_draw_bind_group, &[draw.offset_u32]); From 9cb7de5cd45e8d784384d27ae86eecf98b9e6562 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 03:00:12 +0200 Subject: [PATCH 15/69] feat: timers --- engine/pill_renderer/src/renderer.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 3c4676e9..c554e316 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -482,7 +482,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; @group(0) @binding(2) var tex_normal: texture_2d; @group(0) @binding(3) var smp_normal: sampler; -// Material parameters set(1) – pack tint.rgb + specularity in a single vec4 +// Material parameters set(1) - pack tint.rgb + specularity in a single vec4 struct MaterialParams { tint_spec: vec4, } @group(1) @binding(0) var UMaterial: MaterialParams; @@ -818,6 +818,7 @@ struct PerDraw { // [SIMILAR] Per-pass batch upload of dynamic UBOs with dynamic offsets; avoids per-draw map/unmap // [DIFF] Recreates buffer and issues one write per visible; TALK prefers a reusable temp bump allocator/ring buffer // [API->CLIENT] Data layout/packing and offset management are client-owned; low-level only binds buffer+offsets + timer.begin_context("Per-draw UBO setup"); // Prepare per-draw dynamic uniform buffer and bind group sized to visible set let per_draw_stride: u64 = 256; // alignment let per_draw_capacity = visible.len() as u64; @@ -879,8 +880,10 @@ struct PerDraw { }), }], }); + timer.end_context()?; // End "Per-draw UBO setup" { + timer.begin_context("Main pass"); // [DIFF] TALK advocates explicit per-pass transitions; wgpu hides resource states implicitly here let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("m2_inline_pass"), @@ -911,6 +914,11 @@ struct PerDraw { // [SIMILAR] Bind pipeline/material once per group; change only minimal per-draw state (offsets) // Iterate groups: bind pipeline/material once per group; mesh + offset per draw for group in &groups { + timer.begin_context(&format!( + "Pipeline {} / Material {}", + group.pipeline_handle.data().index, + group.material_handle.data().index + )); rpass.set_pipeline(&self.default_pipeline.render_pipeline); let material = self @@ -935,14 +943,17 @@ struct PerDraw { rpass.set_bind_group(3, &per_draw_bind_group, &[draw.offset_u32]); rpass.draw_indexed(0..mesh.index_count, 0, 0..1); } + timer.end_context()?; // End pipeline/material group } timer.record("Encode render pass"); } + timer.end_context()?; // End "Main pass" self.queue.submit([encoder.finish()]); timer.record("Submit main pass"); // Egui overlay (load over the rendered frame) + timer.begin_context("UI pass"); let mut encoder = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { @@ -963,6 +974,7 @@ struct PerDraw { )?; self.queue.submit([encoder.finish()]); timer.record("UI draw & submit"); + timer.end_context()?; // End "UI pass" // Present frame frame.present(); From c36fd74562316ebf07b58efce102e5a7cf6f630f Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 03:30:58 +0200 Subject: [PATCH 16/69] feat: marix update 15ms -> 0.5ms with SIMD --- engine/Cargo.toml | 9 ++ engine/pill_engine/Cargo.toml | 1 + .../src/ecs/components/transform_component.rs | 111 ++++++++++++------ .../src/ecs/systems/rendering_system.rs | 63 ++++++---- 4 files changed, 127 insertions(+), 57 deletions(-) diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 85551ef7..106db231 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -11,3 +11,12 @@ members = [ exclude = [ "pill_launcher", ] + +[profile.dev] +opt-level = 3 +incremental = true + +[profile.release] +opt-level = 3 +lto = "thin" +codegen-units = 1 diff --git a/engine/pill_engine/Cargo.toml b/engine/pill_engine/Cargo.toml index 453d496b..f2a801f9 100644 --- a/engine/pill_engine/Cargo.toml +++ b/engine/pill_engine/Cargo.toml @@ -40,6 +40,7 @@ rand = "0.9.2" bincode = "1.0" gilrs = "0.11.0" once_cell = "1.21.3" +glam = { version = "0.27" } [build-dependencies] anyhow = "1.0" diff --git a/engine/pill_engine/src/ecs/components/transform_component.rs b/engine/pill_engine/src/ecs/components/transform_component.rs index 809639bf..b0c1940c 100644 --- a/engine/pill_engine/src/ecs/components/transform_component.rs +++ b/engine/pill_engine/src/ecs/components/transform_component.rs @@ -1,14 +1,15 @@ use crate::{ ecs::{ - Component, ComponentStorage, DeferredUpdateComponent, DeferredUpdateComponentRequest, DeferredUpdateManagerPointer, EntityHandle, SceneHandle - }, engine::Engine -}; -use pill_core::{ - get_type_name, Direction, PillStyle, PillTypeMap, PillTypeMapKey, Vector3f + Component, ComponentStorage, DeferredUpdateComponent, DeferredUpdateComponentRequest, + DeferredUpdateManagerPointer, EntityHandle, SceneHandle, + }, + engine::Engine, }; +use anyhow::{Context, Error, Result}; use cgmath::{Deg, Matrix3, SquareMatrix, Zero}; -use anyhow::{ Result, Context, Error }; -use serde::{ Serialize, Deserialize }; +use glam::{Mat3, Mat4, Quat, Vec3}; +use pill_core::{get_type_name, Direction, PillStyle, PillTypeMap, PillTypeMapKey, Vector3f}; +use serde::{Deserialize, Serialize}; // Coordinate system: // @@ -114,7 +115,7 @@ impl TransformComponent { Direction::WorldRight => self.position.x += delta, Direction::WorldLeft => self.position.x -= delta, Direction::WorldUp => self.position.y += delta, - Direction::WorldDown => self.position.y -= delta + Direction::WorldDown => self.position.y -= delta, } self.matrix_update_required = true; } @@ -125,9 +126,9 @@ impl TransformComponent { } pub fn translate_local(&mut self, delta: Vector3f) { - self.position += self.get_forward_direction() * delta.z + - self.get_right_direction() * delta.x + - self.get_up_direction() * delta.y; + self.position += self.get_forward_direction() * delta.z + + self.get_right_direction() * delta.x + + self.get_up_direction() * delta.y; self.matrix_update_required = true; } @@ -158,10 +159,21 @@ impl TransformComponent { } fn get_rotation_matrix(&self) -> Matrix3 { - let roll = Matrix3::from_angle_z(Deg(self.rotation.z)); - let yaw = Matrix3::from_angle_y(Deg(self.rotation.y)); - let pitch = Matrix3::from_angle_x(Deg(self.rotation.x)); - yaw * pitch * roll + // SIMD via glam: build quaternion then extract 3x3 + let rot = Vec3::new( + self.rotation.x.to_radians(), + self.rotation.y.to_radians(), + self.rotation.z.to_radians(), + ); + let qx = Quat::from_rotation_x(rot.x); + let qy = Quat::from_rotation_y(rot.y); + let qz = Quat::from_rotation_z(rot.z); + let q = qz * qy * qx; // Rz * Ry * Rx + let m = Mat3::from_quat(q); + let c = m.to_cols_array_2d(); + Matrix3::new( + c[0][0], c[0][1], c[0][2], c[1][0], c[1][1], c[1][2], c[2][0], c[2][1], c[2][2], + ) } // --- Rotation --- @@ -183,12 +195,39 @@ impl TransformComponent { self.scale = scale; self.matrix_update_required = true; } - } pub fn update_transform_matrices(transform_component: &mut TransformComponent) { - transform_component.model_matrix = cgmath::Matrix4::model(transform_component.position, transform_component.rotation, transform_component.scale).into(); - transform_component.normal_matrix = cgmath::Matrix3::from_euler_angles(transform_component.rotation).into(); + // SIMD path via glam: quat + SRT + let pos = Vec3::new( + transform_component.position.x, + transform_component.position.y, + transform_component.position.z, + ); + let rot = Vec3::new( + transform_component.rotation.x.to_radians(), + transform_component.rotation.y.to_radians(), + transform_component.rotation.z.to_radians(), + ); + let scl = Vec3::new( + transform_component.scale.x, + transform_component.scale.y, + transform_component.scale.z, + ); + + // Convert Euler XYZ -> quat (pitch X, yaw Y, roll Z) + let qx = Quat::from_rotation_x(rot.x); + let qy = Quat::from_rotation_y(rot.y); + let qz = Quat::from_rotation_z(rot.z); + let q = qz * qy * qx; // Match Rz * Ry * Rx ordering + + let m = Mat4::from_scale_rotation_translation(scl, q, pos); + // Convert to cgmath array layout via to_cols_array_2d (column-major 4x4) + transform_component.model_matrix = m.to_cols_array_2d(); + + // Normal matrix: rotation only + let n = Mat3::from_quat(q); + transform_component.normal_matrix = n.to_cols_array_2d(); } pub fn get_model_matrix(transform_component: &TransformComponent) -> [[f32; 4]; 4] { @@ -203,14 +242,12 @@ impl PillTypeMapKey for TransformComponent { type Storage = ComponentStorage; } -impl Component for TransformComponent { - -} +impl Component for TransformComponent {} impl Default for TransformComponent { - fn default() -> Self { - Self::new() - } + fn default() -> Self { + Self::new() + } } pub trait MatrixAngleExt { @@ -218,7 +255,11 @@ pub trait MatrixAngleExt { } pub trait MatrixModelExt { - fn model(position: cgmath::Vector3, rotation: cgmath::Vector3, scale: cgmath::Vector3) -> Self; + fn model( + position: cgmath::Vector3, + rotation: cgmath::Vector3, + scale: cgmath::Vector3, + ) -> Self; } impl MatrixAngleExt for cgmath::Matrix4 { @@ -232,17 +273,21 @@ impl MatrixAngleExt for cgmath::Matrix4 { } impl MatrixModelExt for cgmath::Matrix4 { - fn model(position: cgmath::Vector3, rotation: cgmath::Vector3, scale: cgmath::Vector3) -> Self { - cgmath::Matrix4::from_translation(position) * - cgmath::Matrix4::from_euler_angles(rotation) * - cgmath::Matrix4::from_nonuniform_scale(scale.x, scale.y, scale.z) + fn model( + position: cgmath::Vector3, + rotation: cgmath::Vector3, + scale: cgmath::Vector3, + ) -> Self { + cgmath::Matrix4::from_translation(position) + * cgmath::Matrix4::from_euler_angles(rotation) + * cgmath::Matrix4::from_nonuniform_scale(scale.x, scale.y, scale.z) } } impl MatrixAngleExt for cgmath::Matrix3 { fn from_euler_angles(v: cgmath::Vector3) -> Self { - cgmath::Matrix3::from_angle_z(cgmath::Deg(v.z)) * - cgmath::Matrix3::from_angle_y(cgmath::Deg(v.y)) * - cgmath::Matrix3::from_angle_x(cgmath::Deg(v.x)) + cgmath::Matrix3::from_angle_z(cgmath::Deg(v.z)) + * cgmath::Matrix3::from_angle_y(cgmath::Deg(v.y)) + * cgmath::Matrix3::from_angle_x(cgmath::Deg(v.x)) } -} \ No newline at end of file +} diff --git a/engine/pill_engine/src/ecs/systems/rendering_system.rs b/engine/pill_engine/src/ecs/systems/rendering_system.rs index 3e5e3ff3..968b232b 100644 --- a/engine/pill_engine/src/ecs/systems/rendering_system.rs +++ b/engine/pill_engine/src/ecs/systems/rendering_system.rs @@ -60,48 +60,63 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { timer.record("Prepare render queue"); - let matrix_calculation_duration: f32 = 0.0; - let mut add_to_render_queue_duration: f32 = 0.0; + let mut dirty_entities: Vec = Vec::new(); - // Iterate mesh rendering components + // Phase 1: Sweep components; route transforms needing matrix update to a batch, push clean directly for (entity_handle, transform_component, mesh_rendering_component) in engine .scene_manager .get_two_component_iterator_mut::( active_scene_handle, )? { - // Update transform matrices if required - - // let update_transform_matrices_start_time = Instant::now(); if transform_component.matrix_update_required { - update_transform_matrices(transform_component); - transform_component.matrix_update_required = false; + // defer update to batch + dirty_entities.push(entity_handle); + continue; } - // matrix_calculation_duration += update_transform_matrices_start_time.elapsed().as_secs_f32() * 1000.0; - // Add valid mesh rendering components to render queue - let add_to_render_queue_start_time = Instant::now(); + // Push clean (non-dirty) items directly into the render queue if let Some(render_queue_key) = mesh_rendering_component.render_queue_key { let render_queue_item = RenderQueueItem { key: render_queue_key, entity_index: entity_handle.data().index as u32, }; engine.render_queue.push(render_queue_item); - } else { - debug!("Invalid render queue key"); - continue; } - add_to_render_queue_duration += - add_to_render_queue_start_time.elapsed().as_secs_f32() * 1000.0; } - timer.record(&format!( - "Matrix calculation {} ms", - matrix_calculation_duration - )); - timer.record(&format!( - "Add to render queue {} ms", - add_to_render_queue_duration - )); + // Phase 2: Batch update transforms with matrix_update_required + if !dirty_entities.is_empty() { + timer.begin_context(&format!("Batch update {} transforms", dirty_entities.len())); + for entity_handle in &dirty_entities { + // Pull the transform component mutably and update matrices + if let Ok(transform_component) = engine + .scene_manager + .get_entity_component::(*entity_handle, active_scene_handle) + { + if transform_component.matrix_update_required { + update_transform_matrices(transform_component); + transform_component.matrix_update_required = false; + } + } + } + timer.end_context()?; + } + + // Phase 3: Add updated (previously dirty) items to the render queue + for entity_handle in dirty_entities { + if let Ok(mesh_rendering_component) = engine + .scene_manager + .get_entity_component::(entity_handle, active_scene_handle) + { + if let Some(render_queue_key) = mesh_rendering_component.render_queue_key { + let render_queue_item = RenderQueueItem { + key: render_queue_key, + entity_index: entity_handle.data().index as u32, + }; + engine.render_queue.push(render_queue_item); + } + } + } timer.record("Sort render queue"); From fb6de9e72fd6a6732d6eac47e1f60307e44c3669 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 03:38:54 +0200 Subject: [PATCH 17/69] feat: M5 per frame ring buffer to avoid dynamic uniforms per frame allocation, 15ms -> 0.4ms, 5k capsules --- engine/pill_renderer/DESIGN.md | 2 +- engine/pill_renderer/src/renderer.rs | 140 +++++++++++++++------------ 2 files changed, 80 insertions(+), 62 deletions(-) diff --git a/engine/pill_renderer/DESIGN.md b/engine/pill_renderer/DESIGN.md index 44447a27..c34b7dd5 100644 --- a/engine/pill_renderer/DESIGN.md +++ b/engine/pill_renderer/DESIGN.md @@ -180,7 +180,7 @@ struct VSOut { @builtin(position) pos: vec4; }; - wgpu correctness: - Use sRGB surface format and sRGB textures for correct gamma; or convert in shader. -5) Per-frame Ring Buffer + Dynamic Offsets +5) [Done] Per-frame Ring Buffer + Dynamic Offsets - Expected result: No per-draw map/unmap; dynamic uniform offsets. - Client API: `push_per_draw` returns aligned offset; pass offsets via `set_bind_groups_with_offsets`. diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index c554e316..1634f408 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -329,6 +329,12 @@ pub struct State { color_format: wgpu::TextureFormat, depth_format: wgpu::TextureFormat, depth_texture: RendererTexture, + // Per-frame dynamic UBO ring (Milestone 5) + per_draw_stride: u64, + per_draw_capacity: u64, // in elements + per_draw_buffer: Option, + per_draw_bind_group_layout: wgpu::BindGroupLayout, + per_draw_bind_group: Option, // Prebuilt PSO (static pipeline) default_pipeline: RendererPipeline, // Other @@ -521,6 +527,20 @@ struct PerDraw { .unwrap(); // Create state + let per_draw_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("per_draw_bind_group_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: Some(std::num::NonZeroU64::new(144).unwrap()), + }, + count: None, + }], + }); Self { // Resources renderer_resource_storage, @@ -533,6 +553,13 @@ struct PerDraw { color_format, depth_format, depth_texture, + // Per-frame UBO ring init + per_draw_stride: 256, + per_draw_capacity: 0, + per_draw_buffer: None, + // [SIMILAR] Per-draw dynamic UBO layout with has_dynamic_offset=true per TALK (Milestone 5) + per_draw_bind_group_layout, + per_draw_bind_group: None, // Prebuilt PSO default_pipeline, // Other @@ -815,72 +842,59 @@ struct PerDraw { next_offset_u32 = next_offset_u32.wrapping_add(256); } - // [SIMILAR] Per-pass batch upload of dynamic UBOs with dynamic offsets; avoids per-draw map/unmap - // [DIFF] Recreates buffer and issues one write per visible; TALK prefers a reusable temp bump allocator/ring buffer - // [API->CLIENT] Data layout/packing and offset management are client-owned; low-level only binds buffer+offsets + // Milestone 5: Per-frame ring buffer timer.begin_context("Per-draw UBO setup"); - // Prepare per-draw dynamic uniform buffer and bind group sized to visible set - let per_draw_stride: u64 = 256; // alignment - let per_draw_capacity = visible.len() as u64; - let per_draw_buffer_size = per_draw_stride * per_draw_capacity.max(1); - let per_draw_buffer = self.device.create_buffer(&wgpu::BufferDescriptor { - label: Some("per_draw_dynamic_ubo"), - size: per_draw_buffer_size, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - for (i, v) in visible.iter().enumerate() { - let offset_bytes = (i as u64) * per_draw_stride; - // Pack { mvp, model(=identity for now), tint(=1,1,1,1) } - let model: [[f32; 4]; 4] = cgmath::Matrix4::::from_scale(1.0).into(); - let tint: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; - #[repr(C)] - #[derive(Copy, Clone)] - struct PerDrawStd140 { - mvp: [[f32; 4]; 4], - model: [[f32; 4]; 4], - tint: [f32; 4], - } - unsafe impl bytemuck::Zeroable for PerDrawStd140 {} - unsafe impl bytemuck::Pod for PerDrawStd140 {} - let pd: PerDrawStd140 = PerDrawStd140 { + let needed = visible.len() as u64; + if self.per_draw_capacity < needed { + self.per_draw_capacity = needed.next_power_of_two().max(1); + let size = self.per_draw_stride * self.per_draw_capacity; + self.per_draw_buffer = Some(self.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("per_draw_dynamic_ubo_ring"), + size, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + })); + self.per_draw_bind_group = + Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("per_draw_bind_group"), + layout: &self.per_draw_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: self.per_draw_buffer.as_ref().unwrap(), + offset: 0, + size: Some(std::num::NonZeroU64::new(144).unwrap()), + }), + }], + })); + } + #[repr(C)] + #[derive(Copy, Clone)] + struct PerDrawStd140 { + mvp: [[f32; 4]; 4], + model: [[f32; 4]; 4], + tint: [f32; 4], + } + unsafe impl bytemuck::Zeroable for PerDrawStd140 {} + unsafe impl bytemuck::Pod for PerDrawStd140 {} + let model: [[f32; 4]; 4] = cgmath::Matrix4::::from_scale(1.0).into(); + let tint: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; + let mut staging: Vec = Vec::with_capacity((self.per_draw_stride * needed) as usize); + for v in &visible { + let pd = PerDrawStd140 { mvp: v.mvp, model, tint, }; - self.queue - .write_buffer(&per_draw_buffer, offset_bytes, bytemuck::bytes_of(&pd)); + staging.extend_from_slice(bytemuck::bytes_of(&pd)); + let pad = (self.per_draw_stride as usize) - std::mem::size_of::(); + staging.extend(std::iter::repeat(0u8).take(pad)); } - timer.record("Per-draw UBO writes"); - // Create per-draw bind group layout raw (avoid pipeline storage) - let per_draw_bind_group_layout = - self.device - .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("per_draw_bind_group_layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: true, - min_binding_size: Some(std::num::NonZeroU64::new(144).unwrap()), - }, - count: None, - }], - }); - let per_draw_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("per_draw_bind_group"), - layout: &per_draw_bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &per_draw_buffer, - offset: 0, - size: Some(std::num::NonZeroU64::new(144).unwrap()), - }), - }], - }); - timer.end_context()?; // End "Per-draw UBO setup" + if let Some(buf) = &self.per_draw_buffer { + self.queue.write_buffer(buf, 0, &staging); + } + timer.record("Per-draw UBO write (ring)"); + timer.end_context()?; { timer.begin_context("Main pass"); @@ -940,7 +954,11 @@ struct PerDraw { // [API->CLIENT] Provide packed mesh atlas + per-draw base offsets so low-level can draw without re-binding buffers rpass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); rpass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32); - rpass.set_bind_group(3, &per_draw_bind_group, &[draw.offset_u32]); + rpass.set_bind_group( + 3, + self.per_draw_bind_group.as_ref().unwrap(), + &[draw.offset_u32], + ); rpass.draw_indexed(0..mesh.index_count, 0, 0..1); } timer.end_context()?; // End pipeline/material group From c5a1dfc6ca042e7e009d560ae6679eb96c97fac6 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 03:44:49 +0200 Subject: [PATCH 18/69] feat: mac retina preferred window setup --- engine/pill_renderer/src/renderer.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 1634f408..5e9bdaef 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -431,6 +431,16 @@ impl State { surface_caps.formats[0] // Use first available format }; + // macOS (Retina) note: use physical size (inner_size) and prefer premultiplied alpha if supported + let alpha_mode = if surface_caps + .alpha_modes + .contains(&wgpu::CompositeAlphaMode::PreMultiplied) + { + wgpu::CompositeAlphaMode::PreMultiplied + } else { + wgpu::CompositeAlphaMode::Auto + }; + let surface_configuration = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, // Defines how the swap_chain's underlying textures will be used format: format, // Defines how the swap_chain's textures will be stored on the gpu @@ -438,7 +448,7 @@ impl State { height: window_size.height, desired_maximum_frame_latency: 2, present_mode: present_mode, // Defines how to sync the surface with the display - alpha_mode: wgpu::CompositeAlphaMode::Auto, + alpha_mode, view_formats: vec![format], }; From 8f29d3362ebc487ff40cfe373309b60b8b05f04b Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 03:45:01 +0200 Subject: [PATCH 19/69] feat: stop log spam --- engine/pill_engine/src/engine.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/engine/pill_engine/src/engine.rs b/engine/pill_engine/src/engine.rs index 37a26afe..259aaa98 100644 --- a/engine/pill_engine/src/engine.rs +++ b/engine/pill_engine/src/engine.rs @@ -321,12 +321,7 @@ impl Engine { // Update FPS counter let new_frame_time = delta_time.as_secs_f32() * 1000.0; - let fps = 1000.0 / new_frame_time; self.frame_delta_time = new_frame_time.into(); - println!( - "Frame finished (Time: {:.3}ms, FPS {:.0})", - new_frame_time, fps - ); } pub fn shutdown(&mut self) { From 2a3ca06ef6f8c1fb00fa3a8d6e5e6e6232d64c8b Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 03:54:04 +0200 Subject: [PATCH 20/69] feat: egui, show resolution --- .../ecs/components/egui_manager_component.rs | 176 +++++++++++------- 1 file changed, 111 insertions(+), 65 deletions(-) diff --git a/engine/pill_engine/src/ecs/components/egui_manager_component.rs b/engine/pill_engine/src/ecs/components/egui_manager_component.rs index d688cdf2..4410c823 100644 --- a/engine/pill_engine/src/ecs/components/egui_manager_component.rs +++ b/engine/pill_engine/src/ecs/components/egui_manager_component.rs @@ -1,14 +1,18 @@ use std::collections::HashMap; use crate::{ - ecs::{components::{ Component, GlobalComponent, GlobalComponentStorage }, systems, UpdatePhase}, engine::Engine + ecs::{ + components::{Component, GlobalComponent, GlobalComponentStorage}, + systems, UpdatePhase, + }, + engine::Engine, }; use egui::Ui; use indexmap::IndexMap; use pill_core::{PillTypeMapKey, Timer, TimerRecord}; -use anyhow::{Result, Error, Context}; +use anyhow::{Context, Error, Result}; pub struct EguiManagerComponent { collapsing_state: HashMap, @@ -16,26 +20,55 @@ pub struct EguiManagerComponent { impl EguiManagerComponent { pub fn new() -> Self { - Self { - collapsing_state: HashMap::new(), + Self { + collapsing_state: HashMap::new(), } } pub fn get_ui(engine: &mut Engine) -> Box { + let entity_count = engine + .scene_manager + .get_active_scene() + .unwrap() + .entities + .len(); + let system_count = engine + .system_manager + .update_phases + .iter() + .map(|(_, systems)| systems.len()) + .sum::(); - let entity_count = engine.scene_manager.get_active_scene().unwrap().entities.len(); - let system_count = engine.system_manager.update_phases.iter().map(|(_, systems)| systems.len()).sum::(); - - let system_timers: Vec<(UpdatePhase, Vec<(String, Timer)>)> = engine.system_manager.update_phases + let system_timers: Vec<(UpdatePhase, Vec<(String, Timer)>)> = engine + .system_manager + .update_phases .iter() .map(|(update_phase, systems)| { - let system_timers = systems.iter().map(|(_, system)| (system.name.clone(), system.timer.clone().context(system.name.clone()).unwrap())).collect(); + let system_timers = systems + .iter() + .map(|(_, system)| { + ( + system.name.clone(), + system.timer.clone().context(system.name.clone()).unwrap(), + ) + }) + .collect(); (update_phase.clone(), system_timers) }) .collect::>(); - let total_systems_delta_time = system_timers.iter().map(|(_, timers)| timers.iter().map(|(_, timer)| timer.total_duration()).sum::()).sum::(); + let total_systems_delta_time = system_timers + .iter() + .map(|(_, timers)| { + timers + .iter() + .map(|(_, timer)| timer.total_duration()) + .sum::() + }) + .sum::(); let frame_delta_time = engine.frame_delta_time; + let window_w = engine.window_size.width; + let window_h = engine.window_size.height; let ui = Box::new(move |ui: &egui::Context| { egui::Window::new("PillEngine") @@ -44,69 +77,86 @@ impl EguiManagerComponent { .anchor(egui::Align2::LEFT_TOP, [0.0, 0.0]) .show(ui, |ui| { egui::ScrollArea::vertical() - .auto_shrink([false; 2]) // optional: prevent auto shrink - .show(ui, |ui| { - if ui.add(egui::Button::new("Click me")).clicked() { - println!("PRESSED"); - } - ui.add(egui::Label::new(format!("FPS {}", 1000.0 / frame_delta_time) )); - ui.add(egui::Label::new(format!("Frame Delta Time: {:.4} ms", frame_delta_time))); - ui.add(egui::Label::new(format!("Entities: {}", entity_count))); - ui.separator(); - ui.add(egui::Label::new(format!("Systems: {}, Total delta time: {:.3} ms", system_count, total_systems_delta_time))); - let mut phase_state = HashMap::new(); - for (update_phase, system_timers) in system_timers.iter() { - let phase_duration = system_timers - .iter() - .map(|(_, timer)| timer.total_duration()) - .sum::(); - - let phase_id = format!("phase_{}", update_phase); - let is_phase_open = *phase_state.get(&phase_id).unwrap_or(&true); - - let header = egui::CollapsingHeader::new(format!( - "Update Phase: {} {:.4} ms", - update_phase, phase_duration - )) - .id_source(&phase_id) - .default_open(is_phase_open) - .show(ui, |ui| { - for (system_name, timer) in system_timers { - let mut state = HashMap::new(); - for record in &timer.records { - Self::render_timer_tree_with_state(ui, record, &mut state); - } + .auto_shrink([false; 2]) // optional: prevent auto shrink + .show(ui, |ui| { + ui.horizontal(|ui| { + if ui.add(egui::Button::new("Click me")).clicked() { + println!("PRESSED"); } + ui.label(format!("FPS {:.1}", 1000.0 / frame_delta_time)); + ui.label(format!("{}x{}", window_w, window_h)); }); - - if header.header_response.clicked() { - phase_state.insert(phase_id, !is_phase_open); + ui.add(egui::Label::new(format!( + "Frame Delta Time: {:.4} ms", + frame_delta_time + ))); + ui.add(egui::Label::new(format!("Entities: {}", entity_count))); + ui.separator(); + ui.add(egui::Label::new(format!( + "Systems: {}, Total delta time: {:.3} ms", + system_count, total_systems_delta_time + ))); + let mut phase_state = HashMap::new(); + for (update_phase, system_timers) in system_timers.iter() { + let phase_duration = system_timers + .iter() + .map(|(_, timer)| timer.total_duration()) + .sum::(); + + let phase_id = format!("phase_{}", update_phase); + let is_phase_open = *phase_state.get(&phase_id).unwrap_or(&true); + + let header = egui::CollapsingHeader::new(format!( + "Update Phase: {} {:.4} ms", + update_phase, phase_duration + )) + .id_source(&phase_id) + .default_open(is_phase_open) + .show(ui, |ui| { + for (system_name, timer) in system_timers { + let mut state = HashMap::new(); + for record in &timer.records { + Self::render_timer_tree_with_state( + ui, record, &mut state, + ); + } + } + }); + + if header.header_response.clicked() { + phase_state.insert(phase_id, !is_phase_open); + } } - } - }); + }); }); - }); + }); ui } - pub fn render_timer_tree_with_state(ui: &mut Ui, record: &TimerRecord, state: &mut HashMap) { + pub fn render_timer_tree_with_state( + ui: &mut Ui, + record: &TimerRecord, + state: &mut HashMap, + ) { let summary = format!("{:.3} ms - {}", record.duration, record.label); if record.subrecords.is_empty() { ui.label(summary); } else { let id = format!("_{}", record.label); let is_open = state.get(&id).copied().unwrap_or(false); - let response = egui::CollapsingHeader::new(egui::RichText::new(summary) - .text_style(egui::TextStyle::Body) - .color(ui.visuals().text_color())) - .id_source(&id) - .default_open(is_open) - .show(ui, |ui| { - for sub in &record.subrecords { - Self::render_timer_tree_with_state(ui, sub, state); - } - }); + let response = egui::CollapsingHeader::new( + egui::RichText::new(summary) + .text_style(egui::TextStyle::Body) + .color(ui.visuals().text_color()), + ) + .id_source(&id) + .default_open(is_open) + .show(ui, |ui| { + for sub in &record.subrecords { + Self::render_timer_tree_with_state(ui, sub, state); + } + }); let header_response = response.header_response; if header_response.clicked() { state.insert(id, !is_open); @@ -115,16 +165,12 @@ impl EguiManagerComponent { } pub(crate) fn update(&mut self, delta_time: f32) -> Result<()> { - - Ok(()) } } impl PillTypeMapKey for EguiManagerComponent { - type Storage = GlobalComponentStorage; + type Storage = GlobalComponentStorage; } -impl GlobalComponent for EguiManagerComponent { - -} \ No newline at end of file +impl GlobalComponent for EguiManagerComponent {} From 344b94e0996fe3de944db43dd90ac6c980f23c98 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 04:07:22 +0200 Subject: [PATCH 21/69] feat: demo project --- examples/pill_demo/res/config.ini | 24 + examples/pill_demo/res/models/pill.obj | 2170 +++++++++++++++++ .../pill_demo/res/textures/pill_color.png | Bin 0 -> 99341 bytes .../pill_demo/res/textures/pill_normal.png | Bin 0 -> 117259 bytes examples/pill_demo/src/game.rs | 121 + examples/pill_demo/src/lib.rs | 4 + 6 files changed, 2319 insertions(+) create mode 100644 examples/pill_demo/res/config.ini create mode 100644 examples/pill_demo/res/models/pill.obj create mode 100644 examples/pill_demo/res/textures/pill_color.png create mode 100644 examples/pill_demo/res/textures/pill_normal.png create mode 100644 examples/pill_demo/src/game.rs create mode 100644 examples/pill_demo/src/lib.rs diff --git a/examples/pill_demo/res/config.ini b/examples/pill_demo/res/config.ini new file mode 100644 index 00000000..02a80df0 --- /dev/null +++ b/examples/pill_demo/res/config.ini @@ -0,0 +1,24 @@ +# GAME +TITLE=Pill-Demo + +# WINDOW +WINDOW_TITLE=Pill-Demo +WINDOW_WIDTH=1920 +WINDOW_HEIGHT=1200 +WINDOW_FULLSCREEN=false + +# ENGINE GENERAL +MAX_ENTITIES=10000000 +MAX_CAMERAS=1 +MAX_CONCURRENT_2D_SOUNDS=10 +MAX_CONCURRENT_3D_SOUNDS=10 + +# ENGINE RESOURCES +MAX_TEXTURES=10 +MAX_MATERIALS=32 +MAX_MESHES=10 +MAX_SOUNDS=10 + +# OTHER +LOG_LEVEL=Info +PANIC_ON_GAME_ERRORS=true diff --git a/examples/pill_demo/res/models/pill.obj b/examples/pill_demo/res/models/pill.obj new file mode 100644 index 00000000..e66d03e9 --- /dev/null +++ b/examples/pill_demo/res/models/pill.obj @@ -0,0 +1,2170 @@ +# Blender v3.0.0 OBJ File: 'PillEngine.blend' +# www.blender.org +mtllib Pill.mtl +o Pill_Sphere.007 +v 0.000000 2.396242 -0.195090 +v 0.000000 2.339336 -0.382683 +v 0.000000 2.246927 -0.555570 +v 0.000000 2.122564 -0.707107 +v 0.000000 1.971027 -0.831470 +v 0.000000 1.798140 -0.923880 +v 0.000000 1.610547 -0.980785 +v 0.000000 -1.970790 -0.831470 +v 0.000000 -2.122327 -0.707107 +v 0.000000 -2.246690 -0.555570 +v 0.038060 2.396242 -0.191342 +v 0.074658 2.339336 -0.375330 +v 0.108386 2.246927 -0.544895 +v 0.137950 2.122564 -0.693520 +v 0.162212 1.971027 -0.815493 +v 0.180240 1.798140 -0.906127 +v 0.191342 1.610547 -0.961940 +v 0.191342 -1.610310 -0.961940 +v 0.180240 -1.797903 -0.906127 +v 0.162212 -1.970790 -0.815493 +v 0.137950 -2.122327 -0.693520 +v 0.108386 -2.246690 -0.544895 +v 0.074658 -2.339100 -0.375330 +v 0.038060 -2.396006 -0.191342 +v 0.074658 2.396242 -0.180240 +v 0.146447 2.339336 -0.353553 +v 0.212608 2.246927 -0.513280 +v 0.270598 2.122564 -0.653281 +v 0.318190 1.971027 -0.768178 +v 0.353553 1.798140 -0.853553 +v 0.375330 1.610547 -0.906127 +v 0.375330 -1.610310 -0.906127 +v 0.353553 -1.797903 -0.853553 +v 0.318190 -1.970790 -0.768178 +v 0.270598 -2.122327 -0.653281 +v 0.212608 -2.246690 -0.513280 +v 0.146447 -2.339100 -0.353553 +v 0.074658 -2.396006 -0.180240 +v 0.108386 2.396242 -0.162212 +v 0.212608 2.339336 -0.318190 +v 0.308658 2.246927 -0.461940 +v 0.392848 2.122564 -0.587938 +v 0.461940 1.971027 -0.691342 +v 0.513280 1.798140 -0.768178 +v 0.544895 1.610547 -0.815493 +v 0.544895 -1.610310 -0.815493 +v 0.513280 -1.797903 -0.768178 +v 0.461940 -1.970790 -0.691342 +v 0.392848 -2.122327 -0.587938 +v 0.308658 -2.246690 -0.461940 +v 0.212608 -2.339100 -0.318190 +v 0.108386 -2.396006 -0.162212 +v 0.137950 2.396242 -0.137950 +v 0.270598 2.339336 -0.270598 +v 0.392848 2.246927 -0.392847 +v 0.500000 2.122564 -0.500000 +v 0.587938 1.971027 -0.587938 +v 0.653281 1.798140 -0.653281 +v 0.693520 1.610547 -0.693520 +v 0.693520 -1.610310 -0.693520 +v 0.653282 -1.797903 -0.653281 +v 0.587938 -1.970790 -0.587938 +v 0.500000 -2.122327 -0.500000 +v 0.392848 -2.246690 -0.392847 +v 0.270598 -2.339100 -0.270598 +v 0.137950 -2.396006 -0.137950 +v 0.162212 2.396242 -0.108386 +v 0.318190 2.339336 -0.212607 +v 0.461940 2.246927 -0.308658 +v 0.587938 2.122564 -0.392847 +v 0.691342 1.971027 -0.461940 +v 0.768178 1.798140 -0.513280 +v 0.815493 1.610547 -0.544895 +v 0.815493 -1.610310 -0.544895 +v 0.768178 -1.797903 -0.513280 +v 0.691342 -1.970790 -0.461940 +v 0.587938 -2.122327 -0.392847 +v 0.461940 -2.246690 -0.308658 +v 0.318190 -2.339100 -0.212608 +v 0.162212 -2.396006 -0.108386 +v 0.180240 2.396242 -0.074658 +v 0.353554 2.339336 -0.146447 +v 0.513280 2.246927 -0.212607 +v 0.653281 2.122564 -0.270598 +v 0.768178 1.971027 -0.318190 +v 0.853553 1.798140 -0.353553 +v 0.906127 1.610547 -0.375330 +v 0.906127 -1.610310 -0.375330 +v 0.853553 -1.797903 -0.353553 +v 0.768178 -1.970790 -0.318190 +v 0.653281 -2.122327 -0.270598 +v 0.513280 -2.246690 -0.212607 +v 0.353554 -2.339100 -0.146447 +v 0.180240 -2.396006 -0.074658 +v 0.191342 2.396242 -0.038060 +v 0.375330 2.339336 -0.074658 +v 0.544895 2.246927 -0.108386 +v 0.693520 2.122564 -0.137950 +v 0.815493 1.971027 -0.162212 +v 0.906127 1.798140 -0.180240 +v 0.961940 1.610547 -0.191342 +v 0.961940 -1.610310 -0.191342 +v 0.906128 -1.797903 -0.180240 +v 0.815493 -1.970790 -0.162212 +v 0.693520 -2.122327 -0.137950 +v 0.544895 -2.246690 -0.108386 +v 0.375330 -2.339100 -0.074658 +v 0.191342 -2.396006 -0.038060 +v 0.195090 2.396242 0.000000 +v 0.382684 2.339336 0.000000 +v 0.555570 2.246927 0.000000 +v 0.707107 2.122564 0.000000 +v 0.831469 1.971027 0.000000 +v 0.923879 1.798140 0.000000 +v 0.980785 1.610547 0.000000 +v 0.980785 -1.610310 0.000000 +v 0.923880 -1.797903 0.000000 +v 0.831469 -1.970790 0.000000 +v 0.707107 -2.122327 0.000000 +v 0.555570 -2.246690 0.000000 +v 0.382684 -2.339100 0.000000 +v 0.195090 -2.396006 0.000000 +v 0.191342 2.396242 0.038060 +v 0.375330 2.339336 0.074658 +v 0.544895 2.246927 0.108386 +v 0.693520 2.122564 0.137950 +v 0.815493 1.971027 0.162212 +v 0.906127 1.798140 0.180240 +v 0.961940 1.610547 0.191342 +v 0.961940 -1.610310 0.191342 +v 0.906127 -1.797903 0.180240 +v 0.815493 -1.970790 0.162212 +v 0.693520 -2.122327 0.137950 +v 0.544895 -2.246690 0.108386 +v 0.375330 -2.339100 0.074658 +v 0.191342 -2.396006 0.038060 +v 0.180240 2.396242 0.074658 +v 0.353553 2.339336 0.146447 +v 0.513280 2.246927 0.212608 +v 0.653281 2.122564 0.270598 +v 0.768178 1.971027 0.318190 +v 0.853553 1.798140 0.353553 +v 0.906127 1.610547 0.375330 +v 0.906127 -1.610310 0.375330 +v 0.853553 -1.797903 0.353554 +v 0.768178 -1.970790 0.318190 +v 0.653281 -2.122327 0.270598 +v 0.513280 -2.246690 0.212608 +v 0.353554 -2.339100 0.146447 +v 0.180240 -2.396006 0.074658 +v 0.162212 2.396242 0.108386 +v 0.318190 2.339336 0.212608 +v 0.461940 2.246927 0.308658 +v 0.587938 2.122564 0.392847 +v 0.691341 1.971027 0.461940 +v 0.768178 1.798140 0.513280 +v 0.815493 1.610547 0.544895 +v 0.815493 -1.610310 0.544895 +v 0.768178 -1.797903 0.513280 +v 0.691342 -1.970790 0.461940 +v 0.587938 -2.122327 0.392847 +v 0.461940 -2.246690 0.308658 +v 0.318190 -2.339100 0.212608 +v 0.162212 -2.396006 0.108386 +v 0.137950 2.396242 0.137950 +v 0.270598 2.339336 0.270598 +v 0.392847 2.246927 0.392847 +v 0.500000 2.122564 0.500000 +v 0.587938 1.971027 0.587938 +v 0.653281 1.798140 0.653281 +v 0.693520 1.610547 0.693520 +v 0.693520 -1.610310 0.693520 +v 0.653281 -1.797903 0.653282 +v 0.587938 -1.970790 0.587938 +v 0.500000 -2.122327 0.500000 +v 0.392847 -2.246690 0.392847 +v 0.270598 -2.339100 0.270598 +v 0.137950 -2.396006 0.137950 +v 0.108386 2.396242 0.162212 +v 0.212608 2.339336 0.318190 +v 0.308658 2.246927 0.461940 +v 0.392847 2.122564 0.587938 +v 0.461940 1.971027 0.691342 +v 0.513280 1.798140 0.768178 +v 0.544895 1.610547 0.815493 +v 0.544895 -1.610310 0.815493 +v 0.513280 -1.797903 0.768178 +v 0.461940 -1.970790 0.691342 +v 0.392847 -2.122327 0.587938 +v 0.308658 -2.246690 0.461940 +v 0.212608 -2.339100 0.318190 +v 0.108386 -2.396006 0.162212 +v 0.074658 2.396242 0.180240 +v 0.146447 2.339336 0.353554 +v 0.212607 2.246927 0.513280 +v 0.270598 2.122564 0.653281 +v 0.318190 1.971027 0.768178 +v 0.353553 1.798140 0.853553 +v 0.375330 1.610547 0.906127 +v 0.375330 -1.610310 0.906127 +v 0.353553 -1.797903 0.853553 +v 0.318190 -1.970790 0.768178 +v 0.270598 -2.122327 0.653281 +v 0.212607 -2.246690 0.513280 +v 0.146447 -2.339100 0.353554 +v 0.074658 -2.396006 0.180240 +v 0.038060 2.396242 0.191342 +v 0.074658 2.339336 0.375330 +v 0.108386 2.246927 0.544895 +v 0.137950 2.122564 0.693520 +v 0.162212 1.971027 0.815493 +v 0.180240 1.798140 0.906127 +v 0.191342 1.610547 0.961939 +v 0.191342 -1.610310 0.961939 +v 0.180240 -1.797903 0.906127 +v 0.162212 -1.970790 0.815493 +v 0.137950 -2.122327 0.693520 +v 0.108386 -2.246690 0.544895 +v 0.074658 -2.339100 0.375330 +v 0.038060 -2.396006 0.191342 +v 0.000000 2.396242 0.195090 +v -0.000000 2.339336 0.382684 +v -0.000000 2.246927 0.555570 +v 0.000000 2.122564 0.707107 +v -0.000000 1.971027 0.831469 +v 0.000000 1.798140 0.923879 +v -0.000000 1.610547 0.980785 +v -0.000000 -1.610310 0.980785 +v -0.000000 -1.797903 0.923880 +v -0.000000 -1.970790 0.831469 +v 0.000000 -2.122327 0.707107 +v -0.000000 -2.246690 0.555570 +v 0.000000 -2.339100 0.382684 +v -0.000000 -2.396006 0.195090 +v -0.038060 2.396242 0.191342 +v -0.074658 2.339336 0.375330 +v -0.108386 2.246927 0.544895 +v -0.137950 2.122564 0.693520 +v -0.162212 1.971027 0.815493 +v -0.180240 1.798140 0.906127 +v -0.191342 1.610547 0.961939 +v -0.191342 -1.610310 0.961939 +v -0.180240 -1.797903 0.906127 +v -0.162212 -1.970790 0.815493 +v -0.137950 -2.122327 0.693520 +v -0.108386 -2.246690 0.544895 +v -0.074658 -2.339100 0.375330 +v -0.038060 -2.396006 0.191342 +v -0.074658 2.396242 0.180240 +v -0.146447 2.339336 0.353553 +v -0.212608 2.246927 0.513280 +v -0.270598 2.122564 0.653281 +v -0.318189 1.971027 0.768177 +v -0.353553 1.798140 0.853553 +v -0.375330 1.610547 0.906127 +v -0.375330 -1.610310 0.906127 +v -0.353553 -1.797903 0.853553 +v -0.318190 -1.970790 0.768178 +v -0.270598 -2.122327 0.653281 +v -0.212608 -2.246690 0.513280 +v -0.146447 -2.339100 0.353553 +v -0.074658 -2.396006 0.180240 +v -0.108386 2.396242 0.162212 +v -0.212608 2.339336 0.318190 +v -0.308658 2.246927 0.461940 +v -0.392847 2.122564 0.587938 +v -0.461940 1.971027 0.691341 +v -0.513280 1.798140 0.768178 +v -0.544895 1.610547 0.815493 +v -0.544895 -1.610310 0.815493 +v -0.513280 -1.797903 0.768178 +v -0.461940 -1.970790 0.691342 +v -0.392847 -2.122327 0.587938 +v -0.308658 -2.246690 0.461940 +v -0.212608 -2.339100 0.318190 +v -0.108386 -2.396006 0.162212 +v -0.137950 2.396242 0.137950 +v -0.270598 2.339336 0.270598 +v -0.392847 2.246927 0.392847 +v -0.500000 2.122564 0.500000 +v -0.587937 1.971027 0.587938 +v -0.653281 1.798140 0.653281 +v -0.693520 1.610547 0.693520 +v -0.693520 -1.610310 0.693520 +v -0.653281 -1.797903 0.653281 +v -0.587938 -1.970790 0.587938 +v -0.500000 -2.122327 0.500000 +v -0.392847 -2.246690 0.392847 +v -0.270598 -2.339100 0.270598 +v -0.137950 -2.396006 0.137950 +v -0.162212 2.396242 0.108386 +v -0.318190 2.339336 0.212608 +v -0.461940 2.246927 0.308658 +v -0.587938 2.122564 0.392847 +v -0.691341 1.971027 0.461940 +v -0.768178 1.798140 0.513280 +v -0.815493 1.610547 0.544895 +v -0.815493 -1.610310 0.544895 +v -0.768178 -1.797903 0.513280 +v -0.691341 -1.970790 0.461940 +v -0.587938 -2.122327 0.392847 +v -0.461940 -2.246690 0.308658 +v -0.318190 -2.339100 0.212608 +v -0.162212 -2.396006 0.108386 +v -0.000000 2.415457 0.000000 +v -0.180240 2.396242 0.074658 +v -0.353553 2.339336 0.146447 +v -0.513280 2.246927 0.212607 +v -0.653281 2.122564 0.270598 +v -0.768177 1.971027 0.318190 +v -0.853553 1.798140 0.353553 +v -0.906127 1.610547 0.375330 +v -0.906127 -1.610310 0.375330 +v -0.853553 -1.797903 0.353553 +v -0.768177 -1.970790 0.318190 +v -0.653281 -2.122327 0.270598 +v -0.513280 -2.246690 0.212607 +v -0.353553 -2.339100 0.146447 +v -0.180240 -2.396006 0.074658 +v -0.191342 2.396242 0.038060 +v -0.375330 2.339336 0.074658 +v -0.544895 2.246927 0.108386 +v -0.693520 2.122564 0.137950 +v -0.815493 1.971027 0.162212 +v -0.906127 1.798140 0.180240 +v -0.961939 1.610547 0.191342 +v -0.961939 -1.610310 0.191342 +v -0.906127 -1.797903 0.180240 +v -0.815493 -1.970790 0.162212 +v -0.693520 -2.122327 0.137950 +v -0.544895 -2.246690 0.108386 +v -0.375330 -2.339100 0.074658 +v -0.191342 -2.396006 0.038060 +v -0.195090 2.396242 0.000000 +v -0.382683 2.339336 0.000000 +v -0.555570 2.246927 -0.000000 +v -0.707107 2.122564 0.000000 +v -0.831469 1.971027 0.000000 +v -0.923879 1.798140 0.000000 +v -0.980785 1.610547 -0.000000 +v -0.980785 -1.610310 -0.000000 +v -0.923879 -1.797903 -0.000000 +v -0.831469 -1.970790 0.000000 +v -0.707107 -2.122327 0.000000 +v -0.555570 -2.246690 -0.000000 +v -0.382683 -2.339100 0.000000 +v -0.195090 -2.396006 0.000000 +v -0.191342 2.396242 -0.038060 +v -0.375330 2.339336 -0.074658 +v -0.544895 2.246927 -0.108386 +v -0.693520 2.122564 -0.137950 +v -0.815493 1.971027 -0.162212 +v -0.906127 1.798140 -0.180240 +v -0.961939 1.610547 -0.191342 +v -0.961939 -1.610310 -0.191342 +v -0.906127 -1.797903 -0.180240 +v -0.815493 -1.970790 -0.162212 +v -0.693520 -2.122327 -0.137950 +v -0.544895 -2.246690 -0.108386 +v -0.375330 -2.339100 -0.074658 +v -0.191342 -2.396006 -0.038060 +v -0.180240 2.396242 -0.074658 +v -0.353553 2.339336 -0.146447 +v -0.513280 2.246927 -0.212607 +v -0.653281 2.122564 -0.270598 +v -0.768177 1.971027 -0.318189 +v -0.853553 1.798140 -0.353553 +v -0.906127 1.610547 -0.375330 +v -0.906127 -1.610310 -0.375330 +v -0.853553 -1.797903 -0.353553 +v -0.768177 -1.970790 -0.318189 +v -0.653281 -2.122327 -0.270598 +v -0.513280 -2.246690 -0.212607 +v -0.353553 -2.339100 -0.146446 +v -0.180240 -2.396006 -0.074658 +v -0.162212 2.396242 -0.108386 +v -0.318189 2.339336 -0.212607 +v -0.461939 2.246927 -0.308658 +v -0.587938 2.122564 -0.392847 +v -0.691341 1.971027 -0.461939 +v -0.768177 1.798140 -0.513280 +v -0.815493 1.610547 -0.544895 +v -0.815493 -1.610310 -0.544895 +v -0.768177 -1.797903 -0.513280 +v -0.691341 -1.970790 -0.461939 +v -0.587938 -2.122327 -0.392847 +v -0.461939 -2.246690 -0.308658 +v -0.318189 -2.339100 -0.212607 +v -0.162212 -2.396006 -0.108386 +v -0.137950 2.396242 -0.137950 +v -0.270598 2.339336 -0.270598 +v -0.392847 2.246927 -0.392847 +v -0.500000 2.122564 -0.500000 +v -0.587937 1.971027 -0.587937 +v -0.653281 1.798140 -0.653281 +v -0.693519 1.610547 -0.693520 +v -0.693519 -1.610310 -0.693520 +v -0.653281 -1.797903 -0.653281 +v -0.587937 -1.970790 -0.587937 +v -0.500000 -2.122327 -0.500000 +v -0.392847 -2.246690 -0.392847 +v -0.270598 -2.339100 -0.270598 +v -0.137950 -2.396006 -0.137950 +v -0.108386 2.396242 -0.162212 +v -0.212607 2.339336 -0.318189 +v -0.308658 2.246927 -0.461939 +v -0.392847 2.122564 -0.587937 +v -0.461939 1.971027 -0.691341 +v -0.513280 1.798140 -0.768177 +v -0.544895 1.610547 -0.815493 +v -0.544895 -1.610310 -0.815493 +v -0.513280 -1.797903 -0.768178 +v -0.461939 -1.970790 -0.691341 +v -0.392847 -2.122327 -0.587937 +v -0.308658 -2.246690 -0.461939 +v -0.212607 -2.339100 -0.318189 +v -0.108386 -2.396006 -0.162212 +v -0.074658 2.396242 -0.180240 +v -0.146446 2.339336 -0.353553 +v -0.212607 2.246927 -0.513279 +v -0.270598 2.122564 -0.653281 +v -0.318189 1.971027 -0.768177 +v -0.353553 1.798140 -0.853553 +v -0.375330 1.610547 -0.906127 +v -0.375330 -1.610310 -0.906127 +v -0.353553 -1.797903 -0.853553 +v -0.318189 -1.970790 -0.768177 +v -0.270598 -2.122327 -0.653281 +v -0.212607 -2.246690 -0.513279 +v -0.146446 -2.339100 -0.353553 +v -0.074658 -2.396006 -0.180240 +v -0.038060 2.396242 -0.191342 +v -0.074658 2.339336 -0.375330 +v -0.108386 2.246927 -0.544895 +v -0.137950 2.122564 -0.693520 +v -0.162212 1.971027 -0.815493 +v -0.180240 1.798140 -0.906127 +v -0.191341 1.610547 -0.961939 +v -0.191341 -1.610310 -0.961939 +v -0.180240 -1.797903 -0.906127 +v -0.162212 -1.970790 -0.815493 +v -0.137950 -2.122327 -0.693520 +v -0.108386 -2.246690 -0.544895 +v -0.074658 -2.339100 -0.375330 +v -0.038060 -2.396006 -0.191342 +v 0.000000 -1.610310 -0.980785 +v 0.000000 -1.797903 -0.923879 +v 0.000000 -2.339100 -0.382683 +v 0.000000 -2.396006 -0.195090 +v 0.000000 -2.415220 0.000000 +v 0.194760 1.432671 -0.979122 +v 0.382035 1.432671 -0.922313 +v 0.554628 1.432671 -0.830060 +v 0.705908 1.432671 -0.705908 +v 0.830060 1.432671 -0.554628 +v 0.922313 1.432671 -0.382034 +v 0.979122 1.432671 -0.194759 +v 0.998304 1.432671 0.000000 +v 0.979122 1.432671 0.194760 +v 0.922313 1.432671 0.382035 +v 0.830059 1.432671 0.554628 +v 0.705907 1.432671 0.705908 +v 0.554628 1.432671 0.830060 +v 0.382034 1.432671 0.922313 +v 0.194759 1.432671 0.979122 +v -0.000000 1.432671 0.998304 +v -0.194760 1.432671 0.979122 +v -0.382035 1.432671 0.922312 +v -0.554628 1.432671 0.830059 +v -0.705908 1.432671 0.705907 +v -0.830059 1.432671 0.554628 +v -0.922312 1.432671 0.382034 +v -0.979122 1.432671 0.194759 +v -0.998304 1.432671 -0.000000 +v -0.979121 1.432671 -0.194760 +v -0.922312 1.432671 -0.382035 +v -0.830059 1.432671 -0.554628 +v -0.705907 1.432671 -0.705908 +v -0.554627 1.432671 -0.830059 +v -0.382034 1.432671 -0.922312 +v -0.194759 1.432671 -0.979121 +v 0.000000 1.432671 -0.998303 +v 0.194723 -1.434348 -0.978937 +v 0.381963 -1.434348 -0.922139 +v 0.554524 -1.434348 -0.829903 +v 0.705775 -1.434348 -0.705774 +v 0.829903 -1.434348 -0.554523 +v 0.922139 -1.434348 -0.381962 +v 0.978937 -1.434348 -0.194723 +v 0.998116 -1.434348 0.000000 +v 0.978937 -1.434348 0.194723 +v 0.922139 -1.434348 0.381962 +v 0.829903 -1.434348 0.554524 +v 0.705774 -1.434348 0.705774 +v 0.554523 -1.434348 0.829903 +v 0.381962 -1.434348 0.922139 +v 0.194722 -1.434348 0.978937 +v -0.000000 -1.434348 0.998115 +v -0.194723 -1.434348 0.978937 +v -0.381962 -1.434348 0.922138 +v -0.554523 -1.434348 0.829902 +v -0.705774 -1.434348 0.705774 +v -0.829903 -1.434348 0.554523 +v -0.922138 -1.434348 0.381962 +v -0.978937 -1.434348 0.194722 +v -0.998115 -1.434348 -0.000000 +v -0.978936 -1.434348 -0.194723 +v -0.922138 -1.434348 -0.381962 +v -0.829902 -1.434348 -0.554523 +v -0.705774 -1.434348 -0.705774 +v -0.554523 -1.434348 -0.829902 +v -0.381962 -1.434348 -0.922138 +v -0.194722 -1.434348 -0.978936 +v 0.000001 -1.434348 -0.998115 +vt 0.734354 0.136501 +vt 0.734354 0.167718 +vt 0.705722 0.167718 +vt 0.705722 0.136501 +vt 0.734354 0.829859 +vt 0.734354 0.861076 +vt 0.705722 0.861076 +vt 0.705722 0.829859 +vt 0.734354 0.105284 +vt 0.705722 0.105284 +vt 0.734354 0.798642 +vt 0.705722 0.798642 +vt 0.734354 0.074067 +vt 0.705722 0.074067 +vt 0.734354 0.767425 +vt 0.705722 0.767425 +vt 0.734354 0.042850 +vt 0.705722 0.042850 +vt 0.734354 0.738963 +vt 0.705722 0.738963 +vt 0.734354 0.954727 +vt 0.720038 0.985944 +vt 0.705722 0.954727 +vt 0.720038 0.011633 +vt 0.734354 0.923510 +vt 0.705722 0.923510 +vt 0.734354 0.198935 +vt 0.734354 0.230152 +vt 0.705722 0.230152 +vt 0.705722 0.198935 +vt 0.734354 0.892293 +vt 0.705722 0.892293 +vt 0.677090 0.198935 +vt 0.677090 0.167718 +vt 0.677090 0.892293 +vt 0.677090 0.861076 +vt 0.677090 0.136501 +vt 0.677090 0.829859 +vt 0.677090 0.105284 +vt 0.677090 0.798642 +vt 0.677090 0.074067 +vt 0.677090 0.767425 +vt 0.677090 0.042850 +vt 0.677090 0.738963 +vt 0.691406 0.985944 +vt 0.677090 0.954727 +vt 0.691406 0.011633 +vt 0.677090 0.923510 +vt 0.677090 0.230152 +vt 0.648457 0.074067 +vt 0.648457 0.042850 +vt 0.648457 0.767425 +vt 0.648458 0.738963 +vt 0.662774 0.985944 +vt 0.648457 0.954727 +vt 0.662774 0.011633 +vt 0.648457 0.923510 +vt 0.648457 0.230152 +vt 0.648457 0.198935 +vt 0.648457 0.892293 +vt 0.648457 0.167718 +vt 0.648457 0.861076 +vt 0.648457 0.136501 +vt 0.648457 0.829859 +vt 0.648457 0.105284 +vt 0.648457 0.798642 +vt 0.619825 0.892293 +vt 0.619826 0.861076 +vt 0.619826 0.167718 +vt 0.619826 0.136501 +vt 0.619826 0.829859 +vt 0.619826 0.105284 +vt 0.619826 0.798642 +vt 0.619826 0.074067 +vt 0.619826 0.767425 +vt 0.619825 0.042850 +vt 0.619826 0.738963 +vt 0.634142 0.985944 +vt 0.619825 0.954727 +vt 0.634142 0.011633 +vt 0.619825 0.923510 +vt 0.619826 0.230152 +vt 0.619826 0.198935 +vt 0.591194 0.767425 +vt 0.591194 0.738963 +vt 0.605509 0.985944 +vt 0.591193 0.954727 +vt 0.605509 0.011633 +vt 0.591193 0.042850 +vt 0.591193 0.923510 +vt 0.591194 0.230152 +vt 0.591194 0.198935 +vt 0.591194 0.892293 +vt 0.591194 0.167718 +vt 0.591194 0.861076 +vt 0.591194 0.136501 +vt 0.591194 0.829859 +vt 0.591194 0.105284 +vt 0.591194 0.798642 +vt 0.591194 0.074067 +vt 0.562562 0.167718 +vt 0.562562 0.136501 +vt 0.562562 0.861076 +vt 0.562562 0.829859 +vt 0.562562 0.105284 +vt 0.562562 0.798642 +vt 0.562562 0.074067 +vt 0.562562 0.767425 +vt 0.562562 0.042850 +vt 0.562562 0.738963 +vt 0.576877 0.985944 +vt 0.562562 0.954727 +vt 0.576877 0.011633 +vt 0.562562 0.923510 +vt 0.562562 0.230152 +vt 0.562562 0.198935 +vt 0.562562 0.892293 +vt 0.548245 0.011633 +vt 0.533929 0.042850 +vt 0.533929 0.258309 +vt 0.533929 0.738963 +vt 0.505298 0.738963 +vt 0.505297 0.258309 +vt 0.533929 0.954727 +vt 0.533929 0.923510 +vt 0.533929 0.230152 +vt 0.533929 0.198935 +vt 0.533929 0.892293 +vt 0.533929 0.167718 +vt 0.533929 0.861076 +vt 0.533929 0.136501 +vt 0.533929 0.829859 +vt 0.533929 0.105284 +vt 0.533929 0.798642 +vt 0.533929 0.074067 +vt 0.533929 0.767425 +vt 0.548245 0.985944 +vt 0.505298 0.136501 +vt 0.505297 0.105284 +vt 0.505297 0.829859 +vt 0.505298 0.798642 +vt 0.505297 0.074067 +vt 0.505298 0.767425 +vt 0.505297 0.042850 +vt 0.519613 0.985944 +vt 0.505297 0.954727 +vt 0.519613 0.011633 +vt 0.505297 0.923510 +vt 0.505298 0.230152 +vt 0.505297 0.198935 +vt 0.505297 0.892293 +vt 0.505297 0.167718 +vt 0.505298 0.861076 +vt 0.476665 0.954727 +vt 0.476665 0.923510 +vt 0.476665 0.230152 +vt 0.476665 0.198935 +vt 0.476665 0.892293 +vt 0.476665 0.167718 +vt 0.476665 0.861076 +vt 0.476665 0.136501 +vt 0.476665 0.829859 +vt 0.476665 0.105284 +vt 0.476665 0.798642 +vt 0.476665 0.074067 +vt 0.476665 0.767425 +vt 0.476665 0.042850 +vt 0.476665 0.738963 +vt 0.490981 0.985944 +vt 0.490981 0.011633 +vt 0.448033 0.829859 +vt 0.448033 0.798642 +vt 0.448033 0.105284 +vt 0.448033 0.074067 +vt 0.448033 0.767425 +vt 0.448033 0.042850 +vt 0.448033 0.738963 +vt 0.462349 0.985944 +vt 0.448033 0.954727 +vt 0.462349 0.011633 +vt 0.448033 0.923510 +vt 0.448033 0.230152 +vt 0.448033 0.198935 +vt 0.448033 0.892293 +vt 0.448033 0.167718 +vt 0.448033 0.861076 +vt 0.448033 0.136501 +vt 0.419401 0.230152 +vt 0.419401 0.198935 +vt 0.419401 0.923510 +vt 0.419401 0.892293 +vt 0.419401 0.167718 +vt 0.419401 0.861076 +vt 0.419401 0.136501 +vt 0.419401 0.829859 +vt 0.419401 0.105284 +vt 0.419401 0.798642 +vt 0.419401 0.074067 +vt 0.419401 0.767425 +vt 0.419401 0.042850 +vt 0.419401 0.738963 +vt 0.433717 0.985944 +vt 0.419401 0.954727 +vt 0.433717 0.011633 +vt 0.390769 0.105284 +vt 0.390769 0.074067 +vt 0.390769 0.798642 +vt 0.390769 0.767425 +vt 0.390769 0.042850 +vt 0.390769 0.738963 +vt 0.405085 0.985944 +vt 0.390769 0.954727 +vt 0.405085 0.011633 +vt 0.390769 0.923510 +vt 0.390769 0.230152 +vt 0.390769 0.198935 +vt 0.390769 0.892293 +vt 0.390769 0.167718 +vt 0.390769 0.861076 +vt 0.390769 0.136501 +vt 0.390769 0.829859 +vt 0.362137 0.923510 +vt 0.362137 0.892293 +vt 0.362137 0.198935 +vt 0.362137 0.167718 +vt 0.362137 0.861076 +vt 0.362137 0.136501 +vt 0.362137 0.829859 +vt 0.362137 0.105284 +vt 0.362137 0.798642 +vt 0.362137 0.074067 +vt 0.362137 0.767425 +vt 0.362137 0.042850 +vt 0.362137 0.738963 +vt 0.376453 0.985944 +vt 0.362137 0.954727 +vt 0.376453 0.011633 +vt 0.362137 0.230152 +vt 0.333505 0.798642 +vt 0.333505 0.767425 +vt 0.333505 0.074067 +vt 0.333505 0.042850 +vt 0.333505 0.738963 +vt 0.347821 0.985944 +vt 0.333505 0.954727 +vt 0.347821 0.011633 +vt 0.333505 0.923510 +vt 0.333505 0.230152 +vt 0.333505 0.198935 +vt 0.333505 0.892293 +vt 0.333505 0.167718 +vt 0.333505 0.861076 +vt 0.333505 0.136501 +vt 0.333505 0.829859 +vt 0.333505 0.105284 +vt 0.304873 0.198935 +vt 0.304873 0.167718 +vt 0.304873 0.892293 +vt 0.304873 0.861076 +vt 0.304873 0.136501 +vt 0.304873 0.829859 +vt 0.304873 0.105284 +vt 0.304873 0.798642 +vt 0.304873 0.074067 +vt 0.304873 0.767425 +vt 0.304873 0.042850 +vt 0.304873 0.738963 +vt 0.319189 0.985944 +vt 0.304873 0.954727 +vt 0.319189 0.011633 +vt 0.304873 0.923510 +vt 0.304873 0.230152 +vt 0.276241 0.074067 +vt 0.276241 0.042850 +vt 0.276241 0.767425 +vt 0.276241 0.738963 +vt 0.290557 0.985944 +vt 0.276241 0.954727 +vt 0.290557 0.011633 +vt 0.276241 0.923510 +vt 0.276241 0.230152 +vt 0.276241 0.198935 +vt 0.276241 0.892293 +vt 0.276241 0.167718 +vt 0.276241 0.861076 +vt 0.276241 0.136501 +vt 0.276241 0.829859 +vt 0.276241 0.105284 +vt 0.276241 0.798642 +vt 0.247609 0.167718 +vt 0.247609 0.136501 +vt 0.247609 0.861076 +vt 0.247609 0.829859 +vt 0.247609 0.105284 +vt 0.247609 0.798642 +vt 0.247609 0.074067 +vt 0.247609 0.767425 +vt 0.247609 0.042850 +vt 0.247609 0.738963 +vt 0.261925 0.985944 +vt 0.247609 0.954727 +vt 0.261925 0.011633 +vt 0.247609 0.923510 +vt 0.247609 0.230152 +vt 0.247609 0.198935 +vt 0.247609 0.892293 +vt 0.233293 0.985944 +vt 0.218977 0.954727 +vt 0.233293 0.011633 +vt 0.218977 0.042850 +vt 0.218977 0.258309 +vt 0.218977 0.738963 +vt 0.190345 0.738963 +vt 0.190345 0.258309 +vt 0.218977 0.923510 +vt 0.218977 0.230152 +vt 0.218977 0.198935 +vt 0.218977 0.892293 +vt 0.218977 0.167718 +vt 0.218977 0.861076 +vt 0.218977 0.136501 +vt 0.218977 0.829859 +vt 0.218977 0.105284 +vt 0.218977 0.798642 +vt 0.218977 0.074067 +vt 0.218977 0.767425 +vt 0.190345 0.861076 +vt 0.190345 0.829859 +vt 0.190345 0.136501 +vt 0.190345 0.105284 +vt 0.190345 0.798642 +vt 0.190345 0.074067 +vt 0.190345 0.767425 +vt 0.190345 0.042850 +vt 0.204661 0.985944 +vt 0.190345 0.954727 +vt 0.204661 0.011633 +vt 0.190345 0.923510 +vt 0.190345 0.230152 +vt 0.190345 0.198935 +vt 0.190345 0.892293 +vt 0.190345 0.167718 +vt 0.161713 0.258309 +vt 0.161713 0.738963 +vt 0.133081 0.738963 +vt 0.133081 0.258309 +vt 0.161713 0.954727 +vt 0.161713 0.923510 +vt 0.161713 0.230152 +vt 0.161713 0.198935 +vt 0.161713 0.892293 +vt 0.161713 0.167718 +vt 0.161713 0.861076 +vt 0.161713 0.136501 +vt 0.161713 0.829859 +vt 0.161713 0.105284 +vt 0.161713 0.798642 +vt 0.161713 0.074067 +vt 0.161713 0.767425 +vt 0.161713 0.042850 +vt 0.176029 0.985944 +vt 0.176029 0.011633 +vt 0.133081 0.136501 +vt 0.133081 0.105284 +vt 0.133081 0.829859 +vt 0.133081 0.798642 +vt 0.133081 0.074067 +vt 0.133081 0.767425 +vt 0.133081 0.042850 +vt 0.147397 0.985944 +vt 0.133081 0.954727 +vt 0.147397 0.011633 +vt 0.133081 0.923510 +vt 0.133081 0.230152 +vt 0.133081 0.198935 +vt 0.133081 0.892293 +vt 0.133081 0.167718 +vt 0.133081 0.861076 +vt 0.104449 0.954727 +vt 0.104449 0.923510 +vt 0.104449 0.230152 +vt 0.104449 0.198935 +vt 0.104449 0.892293 +vt 0.104449 0.167718 +vt 0.104449 0.861076 +vt 0.104449 0.136501 +vt 0.104449 0.829859 +vt 0.104449 0.105284 +vt 0.104449 0.798642 +vt 0.104449 0.074067 +vt 0.104449 0.767425 +vt 0.104449 0.042850 +vt 0.104449 0.738963 +vt 0.118765 0.985944 +vt 0.118765 0.011633 +vt 0.075817 0.829859 +vt 0.075817 0.798642 +vt 0.075817 0.105284 +vt 0.075817 0.074067 +vt 0.075817 0.767425 +vt 0.075817 0.042850 +vt 0.075817 0.738963 +vt 0.090133 0.985944 +vt 0.075817 0.954727 +vt 0.090133 0.011633 +vt 0.075817 0.923510 +vt 0.075817 0.230152 +vt 0.075817 0.198935 +vt 0.075817 0.892293 +vt 0.075817 0.167718 +vt 0.075817 0.861076 +vt 0.075817 0.136501 +vt 0.047185 0.230152 +vt 0.047185 0.198935 +vt 0.047185 0.923510 +vt 0.047185 0.892293 +vt 0.047185 0.167718 +vt 0.047185 0.861076 +vt 0.047185 0.136501 +vt 0.047185 0.829859 +vt 0.047185 0.105284 +vt 0.047185 0.798642 +vt 0.047185 0.074067 +vt 0.047185 0.767425 +vt 0.047185 0.042850 +vt 0.047185 0.738963 +vt 0.061501 0.985944 +vt 0.047185 0.954727 +vt 0.061501 0.011633 +vt 0.963410 0.074067 +vt 0.963410 0.105284 +vt 0.934778 0.105284 +vt 0.934778 0.074067 +vt 0.963410 0.767425 +vt 0.963410 0.798642 +vt 0.934778 0.798642 +vt 0.934778 0.767425 +vt 0.963410 0.042850 +vt 0.934778 0.042850 +vt 0.963410 0.738963 +vt 0.934778 0.738963 +vt 0.963410 0.954727 +vt 0.949094 0.985944 +vt 0.934778 0.954727 +vt 0.949094 0.011633 +vt 0.963410 0.923510 +vt 0.934778 0.923510 +vt 0.963410 0.198935 +vt 0.963410 0.230152 +vt 0.934778 0.230152 +vt 0.934778 0.198935 +vt 0.963410 0.892293 +vt 0.934778 0.892293 +vt 0.963410 0.167718 +vt 0.934778 0.167718 +vt 0.963410 0.861076 +vt 0.934778 0.861076 +vt 0.963410 0.136501 +vt 0.934778 0.136501 +vt 0.963410 0.829859 +vt 0.934778 0.829859 +vt 0.906146 0.198935 +vt 0.906146 0.167718 +vt 0.906146 0.892293 +vt 0.906146 0.861076 +vt 0.906146 0.136501 +vt 0.906146 0.829859 +vt 0.906146 0.105284 +vt 0.906146 0.798642 +vt 0.906146 0.074067 +vt 0.906146 0.767425 +vt 0.906146 0.042850 +vt 0.906146 0.738963 +vt 0.920462 0.985944 +vt 0.906146 0.954727 +vt 0.920462 0.011633 +vt 0.906146 0.923510 +vt 0.906146 0.230152 +vt 0.877514 0.074067 +vt 0.877514 0.042850 +vt 0.877514 0.767425 +vt 0.877514 0.738963 +vt 0.891830 0.985944 +vt 0.877514 0.954727 +vt 0.891830 0.011633 +vt 0.877514 0.923510 +vt 0.877514 0.230152 +vt 0.877514 0.198935 +vt 0.877514 0.892293 +vt 0.877514 0.167718 +vt 0.877514 0.861076 +vt 0.877514 0.136501 +vt 0.877514 0.829859 +vt 0.877514 0.105284 +vt 0.877514 0.798642 +vt 0.848882 0.892293 +vt 0.848882 0.861076 +vt 0.848882 0.167718 +vt 0.848882 0.136501 +vt 0.848882 0.829859 +vt 0.848882 0.105284 +vt 0.848882 0.798642 +vt 0.848882 0.074067 +vt 0.848882 0.767425 +vt 0.848882 0.042850 +vt 0.848882 0.738963 +vt 0.863198 0.985944 +vt 0.848882 0.954727 +vt 0.863198 0.011633 +vt 0.848882 0.923510 +vt 0.848882 0.230152 +vt 0.848882 0.198935 +vt 0.820250 0.767425 +vt 0.820250 0.738963 +vt 0.834566 0.985944 +vt 0.820250 0.954727 +vt 0.834566 0.011633 +vt 0.820250 0.042850 +vt 0.820250 0.923510 +vt 0.820250 0.230152 +vt 0.820250 0.198935 +vt 0.820250 0.892293 +vt 0.820250 0.167718 +vt 0.820250 0.861076 +vt 0.820250 0.136501 +vt 0.820250 0.829859 +vt 0.820250 0.105284 +vt 0.820250 0.798642 +vt 0.820250 0.074067 +vt 0.791618 0.167718 +vt 0.791618 0.136501 +vt 0.791618 0.861076 +vt 0.791618 0.829859 +vt 0.791618 0.105284 +vt 0.791618 0.798642 +vt 0.791618 0.074067 +vt 0.791618 0.767425 +vt 0.791618 0.042850 +vt 0.791617 0.738963 +vt 0.805934 0.985944 +vt 0.791618 0.954727 +vt 0.805934 0.011633 +vt 0.791618 0.923510 +vt 0.791618 0.230152 +vt 0.791618 0.198935 +vt 0.791618 0.892293 +vt 0.777302 0.985944 +vt 0.762986 0.954727 +vt 0.777302 0.011633 +vt 0.762986 0.042850 +vt 0.762986 0.258309 +vt 0.762986 0.738963 +vt 0.734353 0.258309 +vt 0.762986 0.923510 +vt 0.762986 0.230152 +vt 0.762986 0.198935 +vt 0.762986 0.892293 +vt 0.762986 0.167718 +vt 0.762986 0.861076 +vt 0.762986 0.136501 +vt 0.762986 0.829859 +vt 0.762986 0.105284 +vt 0.762986 0.798642 +vt 0.762986 0.074067 +vt 0.762986 0.767425 +vt 0.748670 0.985944 +vt 0.748670 0.011633 +vt 0.705722 0.258309 +vt 0.791617 0.258309 +vt 0.820250 0.258309 +vt 0.848882 0.258309 +vt 0.877514 0.258309 +vt 0.906146 0.258309 +vt 0.934778 0.258309 +vt 0.963410 0.258309 +vt 0.075817 0.258309 +vt 0.047185 0.258309 +vt 0.104449 0.258309 +vt 0.247609 0.258309 +vt 0.276241 0.258309 +vt 0.304873 0.258309 +vt 0.333505 0.258309 +vt 0.362137 0.258309 +vt 0.390769 0.258309 +vt 0.419401 0.258309 +vt 0.448033 0.258309 +vt 0.476665 0.258309 +vt 0.562562 0.258309 +vt 0.591194 0.258309 +vt 0.619826 0.258309 +vt 0.648457 0.258309 +vt 0.677090 0.258309 +vn 0.0000 -0.7040 -0.7101 +vn 0.0000 -0.5528 -0.8333 +vn 0.1626 -0.5528 -0.8173 +vn 0.1385 -0.7040 -0.6965 +vn 0.0000 0.5528 -0.8333 +vn 0.0000 0.7040 -0.7101 +vn 0.1385 0.7040 -0.6965 +vn 0.1626 0.5528 -0.8173 +vn 0.0000 -0.8286 -0.5598 +vn 0.1092 -0.8286 -0.5490 +vn 0.0000 0.3805 -0.9247 +vn 0.1804 0.3805 -0.9070 +vn 0.0000 -0.9217 -0.3879 +vn 0.0757 -0.9217 -0.3804 +vn 0.0000 0.1939 -0.9810 +vn 0.1914 0.1939 -0.9622 +vn 0.0000 -0.9796 -0.2010 +vn 0.0392 -0.9796 -0.1971 +vn 0.0000 0.0489 -0.9988 +vn 0.1948 0.0489 -0.9796 +vn 0.0000 0.9796 -0.2010 +vn 0.0000 1.0000 0.0000 +vn 0.0392 0.9796 -0.1971 +vn 0.0000 -1.0000 0.0000 +vn 0.0000 0.9217 -0.3879 +vn 0.0757 0.9217 -0.3804 +vn 0.0000 -0.3805 -0.9247 +vn 0.0000 -0.1939 -0.9810 +vn 0.1914 -0.1939 -0.9622 +vn 0.1804 -0.3805 -0.9070 +vn 0.0000 0.8286 -0.5598 +vn 0.1092 0.8286 -0.5490 +vn 0.3539 -0.3805 -0.8544 +vn 0.3189 -0.5528 -0.7699 +vn 0.2142 0.8286 -0.5171 +vn 0.2717 0.7040 -0.6561 +vn 0.2717 -0.7040 -0.6561 +vn 0.3189 0.5528 -0.7699 +vn 0.2142 -0.8286 -0.5171 +vn 0.3539 0.3805 -0.8544 +vn 0.1484 -0.9217 -0.3583 +vn 0.3754 0.1939 -0.9063 +vn 0.0769 -0.9796 -0.1856 +vn 0.3822 0.0489 -0.9228 +vn 0.0769 0.9796 -0.1856 +vn 0.1484 0.9217 -0.3583 +vn 0.3754 -0.1939 -0.9063 +vn 0.2155 -0.9217 -0.3225 +vn 0.1116 -0.9796 -0.1671 +vn 0.5450 0.1939 -0.8157 +vn 0.5549 0.0489 -0.8305 +vn 0.1116 0.9796 -0.1671 +vn 0.2155 0.9217 -0.3225 +vn 0.5450 -0.1939 -0.8157 +vn 0.5137 -0.3805 -0.7689 +vn 0.3110 0.8286 -0.4654 +vn 0.4630 -0.5528 -0.6929 +vn 0.3945 0.7040 -0.5904 +vn 0.3945 -0.7040 -0.5904 +vn 0.4630 0.5528 -0.6929 +vn 0.3110 -0.8286 -0.4654 +vn 0.5137 0.3805 -0.7689 +vn 0.3958 0.8286 -0.3958 +vn 0.5021 0.7040 -0.5021 +vn 0.5893 -0.5528 -0.5893 +vn 0.5021 -0.7040 -0.5021 +vn 0.5893 0.5528 -0.5893 +vn 0.3958 -0.8286 -0.3958 +vn 0.6539 0.3805 -0.6539 +vn 0.2743 -0.9217 -0.2743 +vn 0.6937 0.1939 -0.6937 +vn 0.1421 -0.9796 -0.1421 +vn 0.7063 0.0489 -0.7063 +vn 0.1421 0.9796 -0.1421 +vn 0.2743 0.9217 -0.2743 +vn 0.6937 -0.1939 -0.6937 +vn 0.6539 -0.3805 -0.6539 +vn 0.8157 0.1939 -0.5450 +vn 0.8305 0.0489 -0.5549 +vn 0.1671 0.9796 -0.1116 +vn 0.1671 -0.9796 -0.1116 +vn 0.3225 0.9217 -0.2155 +vn 0.8157 -0.1939 -0.5450 +vn 0.7689 -0.3805 -0.5137 +vn 0.4654 0.8286 -0.3110 +vn 0.6929 -0.5528 -0.4630 +vn 0.5904 0.7040 -0.3945 +vn 0.5904 -0.7040 -0.3945 +vn 0.6929 0.5528 -0.4630 +vn 0.4654 -0.8286 -0.3110 +vn 0.7689 0.3805 -0.5137 +vn 0.3225 -0.9217 -0.2155 +vn 0.7699 -0.5528 -0.3189 +vn 0.6561 -0.7040 -0.2717 +vn 0.6561 0.7040 -0.2717 +vn 0.7699 0.5528 -0.3189 +vn 0.5171 -0.8286 -0.2142 +vn 0.8544 0.3805 -0.3539 +vn 0.3583 -0.9217 -0.1484 +vn 0.9063 0.1939 -0.3754 +vn 0.1856 -0.9796 -0.0769 +vn 0.9228 0.0489 -0.3822 +vn 0.1856 0.9796 -0.0769 +vn 0.3583 0.9217 -0.1484 +vn 0.9063 -0.1939 -0.3754 +vn 0.8544 -0.3805 -0.3539 +vn 0.5171 0.8286 -0.2142 +vn 0.1971 -0.9796 -0.0392 +vn 0.9796 -0.0489 -0.1948 +vn 0.9796 0.0489 -0.1948 +vn 0.9988 0.0489 0.0000 +vn 0.9988 -0.0489 0.0000 +vn 0.1971 0.9796 -0.0392 +vn 0.3804 0.9217 -0.0757 +vn 0.9622 -0.1939 -0.1914 +vn 0.9070 -0.3805 -0.1804 +vn 0.5490 0.8286 -0.1092 +vn 0.8173 -0.5528 -0.1626 +vn 0.6965 0.7040 -0.1385 +vn 0.6965 -0.7040 -0.1385 +vn 0.8173 0.5528 -0.1626 +vn 0.5490 -0.8286 -0.1092 +vn 0.9070 0.3805 -0.1804 +vn 0.3804 -0.9217 -0.0757 +vn 0.9622 0.1939 -0.1914 +vn 0.7101 -0.7040 0.0000 +vn 0.5598 -0.8286 0.0000 +vn 0.8333 0.5528 0.0000 +vn 0.9247 0.3805 0.0000 +vn 0.3879 -0.9217 0.0000 +vn 0.9810 0.1939 0.0000 +vn 0.2010 -0.9796 0.0000 +vn 0.2010 0.9796 0.0000 +vn 0.3879 0.9217 0.0000 +vn 0.9810 -0.1939 0.0000 +vn 0.9247 -0.3805 0.0000 +vn 0.5598 0.8286 0.0000 +vn 0.8333 -0.5528 0.0000 +vn 0.7101 0.7040 0.0000 +vn 0.1971 0.9796 0.0392 +vn 0.3804 0.9217 0.0757 +vn 0.9622 -0.1939 0.1914 +vn 0.9070 -0.3805 0.1804 +vn 0.5490 0.8286 0.1092 +vn 0.8173 -0.5528 0.1626 +vn 0.6965 0.7040 0.1385 +vn 0.6965 -0.7040 0.1385 +vn 0.8173 0.5528 0.1626 +vn 0.5490 -0.8286 0.1092 +vn 0.9070 0.3805 0.1804 +vn 0.3804 -0.9217 0.0757 +vn 0.9622 0.1939 0.1914 +vn 0.1971 -0.9796 0.0392 +vn 0.9796 0.0489 0.1948 +vn 0.7699 0.5528 0.3189 +vn 0.8544 0.3805 0.3539 +vn 0.5171 -0.8286 0.2142 +vn 0.3583 -0.9217 0.1484 +vn 0.9063 0.1939 0.3754 +vn 0.1856 -0.9796 0.0769 +vn 0.9228 0.0489 0.3822 +vn 0.1856 0.9796 0.0769 +vn 0.3583 0.9217 0.1484 +vn 0.9063 -0.1939 0.3754 +vn 0.8544 -0.3805 0.3539 +vn 0.5171 0.8286 0.2142 +vn 0.7699 -0.5528 0.3189 +vn 0.6561 0.7040 0.2717 +vn 0.6561 -0.7040 0.2717 +vn 0.8157 -0.1939 0.5450 +vn 0.7689 -0.3805 0.5137 +vn 0.3225 0.9217 0.2155 +vn 0.4654 0.8286 0.3110 +vn 0.6929 -0.5528 0.4630 +vn 0.5904 0.7040 0.3945 +vn 0.5904 -0.7040 0.3945 +vn 0.6929 0.5528 0.4630 +vn 0.4654 -0.8286 0.3110 +vn 0.7689 0.3805 0.5137 +vn 0.3225 -0.9217 0.2155 +vn 0.8157 0.1939 0.5450 +vn 0.1671 -0.9796 0.1116 +vn 0.8305 0.0489 0.5549 +vn 0.1671 0.9796 0.1116 +vn 0.3958 -0.8286 0.3958 +vn 0.2743 -0.9217 0.2743 +vn 0.6539 0.3805 0.6539 +vn 0.6937 0.1939 0.6937 +vn 0.1421 -0.9796 0.1421 +vn 0.7063 0.0489 0.7063 +vn 0.1421 0.9796 0.1421 +vn 0.2743 0.9217 0.2743 +vn 0.6937 -0.1939 0.6937 +vn 0.6539 -0.3805 0.6539 +vn 0.3958 0.8286 0.3958 +vn 0.5893 -0.5528 0.5893 +vn 0.5021 0.7040 0.5021 +vn 0.5021 -0.7040 0.5021 +vn 0.5893 0.5528 0.5893 +vn 0.2155 0.9217 0.3225 +vn 0.3110 0.8286 0.4654 +vn 0.5137 -0.3805 0.7689 +vn 0.4630 -0.5528 0.6929 +vn 0.3945 0.7040 0.5904 +vn 0.3945 -0.7040 0.5904 +vn 0.4630 0.5528 0.6929 +vn 0.3110 -0.8286 0.4654 +vn 0.5137 0.3805 0.7689 +vn 0.2155 -0.9217 0.3225 +vn 0.5450 0.1939 0.8157 +vn 0.1116 -0.9796 0.1671 +vn 0.5549 0.0489 0.8305 +vn 0.1116 0.9796 0.1671 +vn 0.5450 -0.1939 0.8157 +vn 0.3539 0.3805 0.8544 +vn 0.3754 0.1939 0.9063 +vn 0.1484 -0.9217 0.3583 +vn 0.0769 -0.9796 0.1856 +vn 0.3822 0.0489 0.9228 +vn 0.0769 0.9796 0.1856 +vn 0.1484 0.9217 0.3583 +vn 0.3754 -0.1939 0.9063 +vn 0.3539 -0.3805 0.8544 +vn 0.2142 0.8286 0.5171 +vn 0.3189 -0.5528 0.7699 +vn 0.2717 0.7040 0.6561 +vn 0.2717 -0.7040 0.6561 +vn 0.3189 0.5528 0.7699 +vn 0.2142 -0.8286 0.5171 +vn 0.1804 -0.3805 0.9070 +vn 0.1626 -0.5528 0.8173 +vn 0.1092 0.8286 0.5490 +vn 0.1385 0.7040 0.6965 +vn 0.1385 -0.7040 0.6965 +vn 0.1626 0.5528 0.8173 +vn 0.1092 -0.8286 0.5490 +vn 0.1804 0.3805 0.9070 +vn 0.0757 -0.9217 0.3804 +vn 0.1914 0.1939 0.9622 +vn 0.0392 -0.9796 0.1971 +vn 0.1948 0.0489 0.9796 +vn 0.0392 0.9796 0.1971 +vn 0.0757 0.9217 0.3804 +vn 0.1914 -0.1939 0.9622 +vn 0.0000 -0.9217 0.3879 +vn 0.0000 -0.9796 0.2010 +vn 0.0000 0.1939 0.9810 +vn 0.0000 0.0489 0.9988 +vn 0.0000 0.9796 0.2010 +vn 0.0000 0.9217 0.3879 +vn 0.0000 -0.1939 0.9810 +vn 0.0000 -0.3805 0.9247 +vn 0.0000 0.8286 0.5598 +vn 0.0000 -0.5528 0.8333 +vn 0.0000 0.7040 0.7101 +vn 0.0000 -0.7040 0.7101 +vn 0.0000 0.5528 0.8333 +vn 0.0000 -0.8286 0.5598 +vn 0.0000 0.3805 0.9247 +vn -0.1626 -0.5528 0.8173 +vn -0.1385 -0.7040 0.6965 +vn -0.1385 0.7040 0.6965 +vn -0.1626 0.5528 0.8173 +vn -0.1092 -0.8286 0.5490 +vn -0.1804 0.3805 0.9070 +vn -0.0757 -0.9217 0.3804 +vn -0.1914 0.1939 0.9622 +vn -0.0392 -0.9796 0.1971 +vn -0.1948 0.0489 0.9796 +vn -0.0392 0.9796 0.1971 +vn -0.0757 0.9217 0.3804 +vn -0.1914 -0.1939 0.9622 +vn -0.1804 -0.3805 0.9070 +vn -0.1092 0.8286 0.5490 +vn -0.0769 0.9796 0.1856 +vn -0.0769 -0.9796 0.1856 +vn -0.3822 -0.0489 0.9228 +vn -0.3822 0.0489 0.9228 +vn -0.5549 0.0489 0.8305 +vn -0.5549 -0.0489 0.8305 +vn -0.1484 0.9217 0.3583 +vn -0.3754 -0.1939 0.9063 +vn -0.3539 -0.3805 0.8544 +vn -0.2142 0.8286 0.5171 +vn -0.3189 -0.5528 0.7699 +vn -0.2717 0.7040 0.6561 +vn -0.2717 -0.7040 0.6561 +vn -0.3189 0.5528 0.7699 +vn -0.2142 -0.8286 0.5171 +vn -0.3539 0.3805 0.8544 +vn -0.1484 -0.9217 0.3583 +vn -0.3754 0.1939 0.9063 +vn -0.3945 0.7040 0.5904 +vn -0.4630 0.5528 0.6929 +vn -0.3945 -0.7040 0.5904 +vn -0.3110 -0.8286 0.4654 +vn -0.5137 0.3805 0.7689 +vn -0.2155 -0.9217 0.3225 +vn -0.5450 0.1939 0.8157 +vn -0.1116 -0.9796 0.1671 +vn -0.1116 0.9796 0.1671 +vn -0.2155 0.9217 0.3225 +vn -0.5450 -0.1939 0.8157 +vn -0.5137 -0.3805 0.7689 +vn -0.3110 0.8286 0.4654 +vn -0.4630 -0.5528 0.6929 +vn -0.7063 -0.0489 0.7062 +vn -0.7063 0.0489 0.7063 +vn -0.8305 0.0489 0.5549 +vn -0.8305 -0.0489 0.5549 +vn -0.1421 0.9796 0.1421 +vn -0.2743 0.9217 0.2743 +vn -0.6937 -0.1939 0.6937 +vn -0.6539 -0.3805 0.6539 +vn -0.3958 0.8286 0.3958 +vn -0.5893 -0.5528 0.5893 +vn -0.5021 0.7040 0.5021 +vn -0.5021 -0.7040 0.5021 +vn -0.5893 0.5528 0.5893 +vn -0.3958 -0.8286 0.3958 +vn -0.6539 0.3805 0.6539 +vn -0.2743 -0.9217 0.2743 +vn -0.6937 0.1939 0.6937 +vn -0.1421 -0.9796 0.1421 +vn -0.5904 -0.7040 0.3945 +vn -0.4654 -0.8286 0.3110 +vn -0.6929 0.5528 0.4630 +vn -0.7689 0.3805 0.5137 +vn -0.3225 -0.9217 0.2155 +vn -0.8157 0.1939 0.5450 +vn -0.1671 -0.9796 0.1116 +vn -0.1671 0.9796 0.1116 +vn -0.3225 0.9217 0.2155 +vn -0.8157 -0.1939 0.5450 +vn -0.7689 -0.3805 0.5137 +vn -0.4654 0.8286 0.3110 +vn -0.6929 -0.5528 0.4630 +vn -0.5904 0.7040 0.3945 +vn -0.1856 0.9796 0.0769 +vn -0.3583 0.9217 0.1484 +vn -0.9063 -0.1939 0.3754 +vn -0.8544 -0.3805 0.3539 +vn -0.5171 0.8286 0.2142 +vn -0.7699 -0.5528 0.3189 +vn -0.6561 0.7040 0.2717 +vn -0.6561 -0.7040 0.2717 +vn -0.7699 0.5528 0.3189 +vn -0.5171 -0.8286 0.2142 +vn -0.8544 0.3805 0.3539 +vn -0.3583 -0.9217 0.1484 +vn -0.9063 0.1939 0.3754 +vn -0.1856 -0.9796 0.0769 +vn -0.9228 0.0489 0.3822 +vn -0.8173 0.5528 0.1626 +vn -0.9070 0.3805 0.1804 +vn -0.5490 -0.8286 0.1092 +vn -0.3804 -0.9217 0.0757 +vn -0.9622 0.1939 0.1914 +vn -0.1971 -0.9796 0.0392 +vn -0.9796 0.0489 0.1948 +vn -0.1971 0.9796 0.0392 +vn -0.3804 0.9217 0.0757 +vn -0.9622 -0.1939 0.1914 +vn -0.9070 -0.3805 0.1804 +vn -0.5490 0.8286 0.1092 +vn -0.8173 -0.5528 0.1626 +vn -0.6965 0.7040 0.1385 +vn -0.6965 -0.7040 0.1385 +vn -0.9810 -0.1939 0.0000 +vn -0.9247 -0.3805 0.0000 +vn -0.3879 0.9217 0.0000 +vn -0.5598 0.8286 0.0000 +vn -0.8333 -0.5528 0.0000 +vn -0.7101 0.7040 0.0000 +vn -0.7101 -0.7040 0.0000 +vn -0.8333 0.5528 0.0000 +vn -0.5598 -0.8286 0.0000 +vn -0.9247 0.3805 0.0000 +vn -0.3879 -0.9217 0.0000 +vn -0.9810 0.1939 0.0000 +vn -0.2010 -0.9796 0.0000 +vn -0.9988 0.0489 0.0000 +vn -0.2010 0.9796 0.0000 +vn -0.5490 -0.8286 -0.1092 +vn -0.3804 -0.9217 -0.0757 +vn -0.9070 0.3805 -0.1804 +vn -0.9622 0.1939 -0.1914 +vn -0.1971 -0.9796 -0.0392 +vn -0.9796 0.0489 -0.1948 +vn -0.1971 0.9796 -0.0392 +vn -0.3804 0.9217 -0.0757 +vn -0.9622 -0.1939 -0.1914 +vn -0.9070 -0.3805 -0.1804 +vn -0.5490 0.8286 -0.1092 +vn -0.8173 -0.5528 -0.1626 +vn -0.6965 0.7040 -0.1385 +vn -0.6965 -0.7040 -0.1385 +vn -0.8173 0.5528 -0.1626 +vn -0.8544 -0.3805 -0.3539 +vn -0.7699 -0.5528 -0.3189 +vn -0.5171 0.8286 -0.2142 +vn -0.6561 0.7040 -0.2717 +vn -0.6561 -0.7040 -0.2717 +vn -0.7699 0.5528 -0.3189 +vn -0.5171 -0.8286 -0.2142 +vn -0.8544 0.3805 -0.3539 +vn -0.3583 -0.9217 -0.1484 +vn -0.9063 0.1939 -0.3754 +vn -0.1856 -0.9796 -0.0769 +vn -0.9228 0.0489 -0.3822 +vn -0.1856 0.9796 -0.0769 +vn -0.3583 0.9217 -0.1484 +vn -0.9063 -0.1939 -0.3754 +vn -0.3225 -0.9217 -0.2155 +vn -0.1671 -0.9796 -0.1116 +vn -0.8157 0.1939 -0.5450 +vn -0.8305 0.0489 -0.5549 +vn -0.1671 0.9796 -0.1116 +vn -0.3225 0.9217 -0.2155 +vn -0.8157 -0.1939 -0.5450 +vn -0.7689 -0.3805 -0.5137 +vn -0.4654 0.8286 -0.3110 +vn -0.6929 -0.5528 -0.4630 +vn -0.5904 0.7040 -0.3945 +vn -0.5904 -0.7040 -0.3945 +vn -0.6929 0.5528 -0.4630 +vn -0.4654 -0.8286 -0.3110 +vn -0.7689 0.3805 -0.5137 +vn -0.3958 0.8286 -0.3958 +vn -0.5021 0.7040 -0.5021 +vn -0.5893 -0.5528 -0.5893 +vn -0.5021 -0.7040 -0.5021 +vn -0.5893 0.5528 -0.5893 +vn -0.3958 -0.8286 -0.3958 +vn -0.6539 0.3805 -0.6539 +vn -0.2743 -0.9217 -0.2743 +vn -0.6937 0.1939 -0.6937 +vn -0.1421 -0.9796 -0.1421 +vn -0.7063 0.0489 -0.7063 +vn -0.1421 0.9796 -0.1421 +vn -0.2743 0.9217 -0.2743 +vn -0.6937 -0.1939 -0.6937 +vn -0.6539 -0.3805 -0.6539 +vn -0.5450 0.1939 -0.8157 +vn -0.5549 0.0489 -0.8305 +vn -0.1116 0.9796 -0.1671 +vn -0.1116 -0.9796 -0.1671 +vn -0.2155 0.9217 -0.3225 +vn -0.5450 -0.1939 -0.8157 +vn -0.5137 -0.3805 -0.7689 +vn -0.3110 0.8286 -0.4654 +vn -0.4630 -0.5528 -0.6929 +vn -0.3945 0.7040 -0.5904 +vn -0.3945 -0.7040 -0.5904 +vn -0.4630 0.5528 -0.6929 +vn -0.3110 -0.8286 -0.4654 +vn -0.5137 0.3805 -0.7689 +vn -0.2155 -0.9217 -0.3225 +vn -0.3189 -0.5528 -0.7699 +vn -0.2717 -0.7040 -0.6561 +vn -0.2717 0.7040 -0.6561 +vn -0.3189 0.5528 -0.7699 +vn -0.2142 -0.8286 -0.5171 +vn -0.3539 0.3805 -0.8544 +vn -0.1484 -0.9217 -0.3583 +vn -0.3754 0.1939 -0.9063 +vn -0.0769 -0.9796 -0.1856 +vn -0.3822 0.0489 -0.9228 +vn -0.0769 0.9796 -0.1856 +vn -0.1484 0.9217 -0.3583 +vn -0.3754 -0.1939 -0.9063 +vn -0.3539 -0.3805 -0.8544 +vn -0.2142 0.8286 -0.5171 +vn -0.0392 0.9796 -0.1971 +vn -0.0392 -0.9796 -0.1971 +vn -0.1948 -0.0489 -0.9796 +vn -0.1948 0.0489 -0.9796 +vn 0.0000 -0.0489 -0.9988 +vn -0.0757 0.9217 -0.3804 +vn -0.1914 -0.1939 -0.9622 +vn -0.1804 -0.3805 -0.9070 +vn -0.1092 0.8286 -0.5490 +vn -0.1626 -0.5528 -0.8173 +vn -0.1385 0.7040 -0.6965 +vn -0.1385 -0.7040 -0.6965 +vn -0.1626 0.5528 -0.8173 +vn -0.1092 -0.8286 -0.5490 +vn -0.1804 0.3805 -0.9070 +vn -0.0757 -0.9217 -0.3804 +vn -0.1914 0.1939 -0.9622 +vn 0.1948 -0.0489 -0.9796 +vn -0.3822 -0.0489 -0.9228 +vn -0.5549 -0.0489 -0.8305 +vn -0.7062 -0.0489 -0.7063 +vn -0.8305 -0.0489 -0.5549 +vn -0.9228 -0.0489 -0.3822 +vn -0.9796 -0.0489 -0.1948 +vn -0.9988 -0.0489 0.0000 +vn -0.9796 -0.0489 0.1948 +vn -0.9228 -0.0489 0.3822 +vn -0.1948 -0.0489 0.9796 +vn 0.0000 -0.0489 0.9988 +vn 0.1948 -0.0489 0.9796 +vn 0.3822 -0.0489 0.9228 +vn 0.5549 -0.0489 0.8305 +vn 0.7062 -0.0489 0.7063 +vn 0.8305 -0.0489 0.5549 +vn 0.9228 -0.0489 0.3822 +vn 0.9796 -0.0489 0.1948 +vn 0.9228 -0.0489 -0.3822 +vn 0.8305 -0.0489 -0.5549 +vn 0.7062 -0.0489 -0.7062 +vn 0.5549 -0.0489 -0.8305 +vn 0.3822 -0.0489 -0.9228 +usemtl Material +s 1 +f 9/1/1 8/2/2 20/3/3 21/4/4 +f 5/5/5 4/6/6 14/7/7 15/8/8 +f 10/9/9 9/1/1 21/4/4 22/10/10 +f 6/11/11 5/5/5 15/8/8 16/12/12 +f 448/13/13 10/9/9 22/10/10 23/14/14 +f 7/15/15 6/11/11 16/12/12 17/16/16 +f 449/17/17 448/13/13 23/14/14 24/18/18 +f 482/19/19 7/15/15 17/16/16 451/20/20 +f 1/21/21 305/22/22 11/23/23 +f 450/24/24 449/17/17 24/18/18 +f 2/25/25 1/21/21 11/23/23 12/26/26 +f 447/27/27 446/28/28 18/29/29 19/30/30 +f 3/31/31 2/25/25 12/26/26 13/32/32 +f 8/2/2 447/27/27 19/30/30 20/3/3 +f 4/6/6 3/31/31 13/32/32 14/7/7 +f 20/3/3 19/30/30 33/33/33 34/34/34 +f 14/7/7 13/32/32 27/35/35 28/36/36 +f 21/4/4 20/3/3 34/34/34 35/37/37 +f 15/8/8 14/7/7 28/36/36 29/38/38 +f 22/10/10 21/4/4 35/37/37 36/39/39 +f 16/12/12 15/8/8 29/38/38 30/40/40 +f 23/14/14 22/10/10 36/39/39 37/41/41 +f 17/16/16 16/12/12 30/40/40 31/42/42 +f 24/18/18 23/14/14 37/41/41 38/43/43 +f 451/20/20 17/16/16 31/42/42 452/44/44 +f 11/23/23 305/45/22 25/46/45 +f 450/47/24 24/18/18 38/43/43 +f 12/26/26 11/23/23 25/46/45 26/48/46 +f 19/30/30 18/29/29 32/49/47 33/33/33 +f 13/32/32 12/26/26 26/48/46 27/35/35 +f 38/43/43 37/41/41 51/50/48 52/51/49 +f 452/44/44 31/42/42 45/52/50 453/53/51 +f 25/46/45 305/54/22 39/55/52 +f 450/56/24 38/43/43 52/51/49 +f 26/48/46 25/46/45 39/55/52 40/57/53 +f 33/33/33 32/49/47 46/58/54 47/59/55 +f 27/35/35 26/48/46 40/57/53 41/60/56 +f 34/34/34 33/33/33 47/59/55 48/61/57 +f 28/36/36 27/35/35 41/60/56 42/62/58 +f 35/37/37 34/34/34 48/61/57 49/63/59 +f 29/38/38 28/36/36 42/62/58 43/64/60 +f 36/39/39 35/37/37 49/63/59 50/65/61 +f 30/40/40 29/38/38 43/64/60 44/66/62 +f 37/41/41 36/39/39 50/65/61 51/50/48 +f 31/42/42 30/40/40 44/66/62 45/52/50 +f 42/62/58 41/60/56 55/67/63 56/68/64 +f 49/63/59 48/61/57 62/69/65 63/70/66 +f 43/64/60 42/62/58 56/68/64 57/71/67 +f 50/65/61 49/63/59 63/70/66 64/72/68 +f 44/66/62 43/64/60 57/71/67 58/73/69 +f 51/50/48 50/65/61 64/72/68 65/74/70 +f 45/52/50 44/66/62 58/73/69 59/75/71 +f 52/51/49 51/50/48 65/74/70 66/76/72 +f 453/53/51 45/52/50 59/75/71 454/77/73 +f 39/55/52 305/78/22 53/79/74 +f 450/80/24 52/51/49 66/76/72 +f 40/57/53 39/55/52 53/79/74 54/81/75 +f 47/59/55 46/58/54 60/82/76 61/83/77 +f 41/60/56 40/57/53 54/81/75 55/67/63 +f 48/61/57 47/59/55 61/83/77 62/69/65 +f 454/77/73 59/75/71 73/84/78 455/85/79 +f 53/79/74 305/86/22 67/87/80 +f 450/88/24 66/76/72 80/89/81 +f 54/81/75 53/79/74 67/87/80 68/90/82 +f 61/83/77 60/82/76 74/91/83 75/92/84 +f 55/67/63 54/81/75 68/90/82 69/93/85 +f 62/69/65 61/83/77 75/92/84 76/94/86 +f 56/68/64 55/67/63 69/93/85 70/95/87 +f 63/70/66 62/69/65 76/94/86 77/96/88 +f 57/71/67 56/68/64 70/95/87 71/97/89 +f 64/72/68 63/70/66 77/96/88 78/98/90 +f 58/73/69 57/71/67 71/97/89 72/99/91 +f 65/74/70 64/72/68 78/98/90 79/100/92 +f 59/75/71 58/73/69 72/99/91 73/84/78 +f 66/76/72 65/74/70 79/100/92 80/89/81 +f 77/96/88 76/94/86 90/101/93 91/102/94 +f 71/97/89 70/95/87 84/103/95 85/104/96 +f 78/98/90 77/96/88 91/102/94 92/105/97 +f 72/99/91 71/97/89 85/104/96 86/106/98 +f 79/100/92 78/98/90 92/105/97 93/107/99 +f 73/84/78 72/99/91 86/106/98 87/108/100 +f 80/89/81 79/100/92 93/107/99 94/109/101 +f 455/85/79 73/84/78 87/108/100 456/110/102 +f 67/87/80 305/111/22 81/112/103 +f 450/113/24 80/89/81 94/109/101 +f 68/90/82 67/87/80 81/112/103 82/114/104 +f 75/92/84 74/91/83 88/115/105 89/116/106 +f 69/93/85 68/90/82 82/114/104 83/117/107 +f 76/94/86 75/92/84 89/116/106 90/101/93 +f 70/95/87 69/93/85 83/117/107 84/103/95 +f 450/118/24 94/109/101 108/119/108 +f 489/120/109 457/121/110 458/122/111 490/123/112 +f 82/114/104 81/112/103 95/124/113 96/125/114 +f 89/116/106 88/115/105 102/126/115 103/127/116 +f 83/117/107 82/114/104 96/125/114 97/128/117 +f 90/101/93 89/116/106 103/127/116 104/129/118 +f 84/103/95 83/117/107 97/128/117 98/130/119 +f 91/102/94 90/101/93 104/129/118 105/131/120 +f 85/104/96 84/103/95 98/130/119 99/132/121 +f 92/105/97 91/102/94 105/131/120 106/133/122 +f 86/106/98 85/104/96 99/132/121 100/134/123 +f 93/107/99 92/105/97 106/133/122 107/135/124 +f 87/108/100 86/106/98 100/134/123 101/136/125 +f 94/109/101 93/107/99 107/135/124 108/119/108 +f 456/110/102 87/108/100 101/136/125 457/121/110 +f 81/112/103 305/137/22 95/124/113 +f 106/133/122 105/131/120 119/138/126 120/139/127 +f 100/134/123 99/132/121 113/140/128 114/141/129 +f 107/135/124 106/133/122 120/139/127 121/142/130 +f 101/136/125 100/134/123 114/141/129 115/143/131 +f 108/119/108 107/135/124 121/142/130 122/144/132 +f 457/121/110 101/136/125 115/143/131 458/122/111 +f 95/124/113 305/145/22 109/146/133 +f 450/147/24 108/119/108 122/144/132 +f 96/125/114 95/124/113 109/146/133 110/148/134 +f 103/127/116 102/126/115 116/149/135 117/150/136 +f 97/128/117 96/125/114 110/148/134 111/151/137 +f 104/129/118 103/127/116 117/150/136 118/152/138 +f 98/130/119 97/128/117 111/151/137 112/153/139 +f 105/131/120 104/129/118 118/152/138 119/138/126 +f 99/132/121 98/130/119 112/153/139 113/140/128 +f 110/148/134 109/146/133 123/154/140 124/155/141 +f 117/150/136 116/149/135 130/156/142 131/157/143 +f 111/151/137 110/148/134 124/155/141 125/158/144 +f 118/152/138 117/150/136 131/157/143 132/159/145 +f 112/153/139 111/151/137 125/158/144 126/160/146 +f 119/138/126 118/152/138 132/159/145 133/161/147 +f 113/140/128 112/153/139 126/160/146 127/162/148 +f 120/139/127 119/138/126 133/161/147 134/163/149 +f 114/141/129 113/140/128 127/162/148 128/164/150 +f 121/142/130 120/139/127 134/163/149 135/165/151 +f 115/143/131 114/141/129 128/164/150 129/166/152 +f 122/144/132 121/142/130 135/165/151 136/167/153 +f 458/122/111 115/143/131 129/166/152 459/168/154 +f 109/146/133 305/169/22 123/154/140 +f 450/170/24 122/144/132 136/167/153 +f 128/164/150 127/162/148 141/171/155 142/172/156 +f 135/165/151 134/163/149 148/173/157 149/174/158 +f 129/166/152 128/164/150 142/172/156 143/175/159 +f 136/167/153 135/165/151 149/174/158 150/176/160 +f 459/168/154 129/166/152 143/175/159 460/177/161 +f 123/154/140 305/178/22 137/179/162 +f 450/180/24 136/167/153 150/176/160 +f 124/155/141 123/154/140 137/179/162 138/181/163 +f 131/157/143 130/156/142 144/182/164 145/183/165 +f 125/158/144 124/155/141 138/181/163 139/184/166 +f 132/159/145 131/157/143 145/183/165 146/185/167 +f 126/160/146 125/158/144 139/184/166 140/186/168 +f 133/161/147 132/159/145 146/185/167 147/187/169 +f 127/162/148 126/160/146 140/186/168 141/171/155 +f 134/163/149 133/161/147 147/187/169 148/173/157 +f 145/183/165 144/182/164 158/188/170 159/189/171 +f 139/184/166 138/181/163 152/190/172 153/191/173 +f 146/185/167 145/183/165 159/189/171 160/192/174 +f 140/186/168 139/184/166 153/191/173 154/193/175 +f 147/187/169 146/185/167 160/192/174 161/194/176 +f 141/171/155 140/186/168 154/193/175 155/195/177 +f 148/173/157 147/187/169 161/194/176 162/196/178 +f 142/172/156 141/171/155 155/195/177 156/197/179 +f 149/174/158 148/173/157 162/196/178 163/198/180 +f 143/175/159 142/172/156 156/197/179 157/199/181 +f 150/176/160 149/174/158 163/198/180 164/200/182 +f 460/177/161 143/175/159 157/199/181 461/201/183 +f 137/179/162 305/202/22 151/203/184 +f 450/204/24 150/176/160 164/200/182 +f 138/181/163 137/179/162 151/203/184 152/190/172 +f 163/198/180 162/196/178 176/205/185 177/206/186 +f 157/199/181 156/197/179 170/207/187 171/208/188 +f 164/200/182 163/198/180 177/206/186 178/209/189 +f 461/201/183 157/199/181 171/208/188 462/210/190 +f 151/203/184 305/211/22 165/212/191 +f 450/213/24 164/200/182 178/209/189 +f 152/190/172 151/203/184 165/212/191 166/214/192 +f 159/189/171 158/188/170 172/215/193 173/216/194 +f 153/191/173 152/190/172 166/214/192 167/217/195 +f 160/192/174 159/189/171 173/216/194 174/218/196 +f 154/193/175 153/191/173 167/217/195 168/219/197 +f 161/194/176 160/192/174 174/218/196 175/220/198 +f 155/195/177 154/193/175 168/219/197 169/221/199 +f 162/196/178 161/194/176 175/220/198 176/205/185 +f 156/197/179 155/195/177 169/221/199 170/207/187 +f 167/217/195 166/214/192 180/222/200 181/223/201 +f 174/218/196 173/216/194 187/224/202 188/225/203 +f 168/219/197 167/217/195 181/223/201 182/226/204 +f 175/220/198 174/218/196 188/225/203 189/227/205 +f 169/221/199 168/219/197 182/226/204 183/228/206 +f 176/205/185 175/220/198 189/227/205 190/229/207 +f 170/207/187 169/221/199 183/228/206 184/230/208 +f 177/206/186 176/205/185 190/229/207 191/231/209 +f 171/208/188 170/207/187 184/230/208 185/232/210 +f 178/209/189 177/206/186 191/231/209 192/233/211 +f 462/210/190 171/208/188 185/232/210 463/234/212 +f 165/212/191 305/235/22 179/236/213 +f 450/237/24 178/209/189 192/233/211 +f 166/214/192 165/212/191 179/236/213 180/222/200 +f 173/216/194 172/215/193 186/238/214 187/224/202 +f 185/232/210 184/230/208 198/239/215 199/240/216 +f 192/233/211 191/231/209 205/241/217 206/242/218 +f 463/234/212 185/232/210 199/240/216 464/243/219 +f 179/236/213 305/244/22 193/245/220 +f 450/246/24 192/233/211 206/242/218 +f 180/222/200 179/236/213 193/245/220 194/247/221 +f 187/224/202 186/238/214 200/248/222 201/249/223 +f 181/223/201 180/222/200 194/247/221 195/250/224 +f 188/225/203 187/224/202 201/249/223 202/251/225 +f 182/226/204 181/223/201 195/250/224 196/252/226 +f 189/227/205 188/225/203 202/251/225 203/253/227 +f 183/228/206 182/226/204 196/252/226 197/254/228 +f 190/229/207 189/227/205 203/253/227 204/255/229 +f 184/230/208 183/228/206 197/254/228 198/239/215 +f 191/231/209 190/229/207 204/255/229 205/241/217 +f 202/251/225 201/249/223 215/256/230 216/257/231 +f 196/252/226 195/250/224 209/258/232 210/259/233 +f 203/253/227 202/251/225 216/257/231 217/260/234 +f 197/254/228 196/252/226 210/259/233 211/261/235 +f 204/255/229 203/253/227 217/260/234 218/262/236 +f 198/239/215 197/254/228 211/261/235 212/263/237 +f 205/241/217 204/255/229 218/262/236 219/264/238 +f 199/240/216 198/239/215 212/263/237 213/265/239 +f 206/242/218 205/241/217 219/264/238 220/266/240 +f 464/243/219 199/240/216 213/265/239 465/267/241 +f 193/245/220 305/268/22 207/269/242 +f 450/270/24 206/242/218 220/266/240 +f 194/247/221 193/245/220 207/269/242 208/271/243 +f 201/249/223 200/248/222 214/272/244 215/256/230 +f 195/250/224 194/247/221 208/271/243 209/258/232 +f 220/266/240 219/264/238 233/273/245 234/274/246 +f 465/267/241 213/265/239 227/275/247 466/276/248 +f 207/269/242 305/277/22 221/278/249 +f 450/279/24 220/266/240 234/274/246 +f 208/271/243 207/269/242 221/278/249 222/280/250 +f 215/256/230 214/272/244 228/281/251 229/282/252 +f 209/258/232 208/271/243 222/280/250 223/283/253 +f 216/257/231 215/256/230 229/282/252 230/284/254 +f 210/259/233 209/258/232 223/283/253 224/285/255 +f 217/260/234 216/257/231 230/284/254 231/286/256 +f 211/261/235 210/259/233 224/285/255 225/287/257 +f 218/262/236 217/260/234 231/286/256 232/288/258 +f 212/263/237 211/261/235 225/287/257 226/289/259 +f 219/264/238 218/262/236 232/288/258 233/273/245 +f 213/265/239 212/263/237 226/289/259 227/275/247 +f 231/286/256 230/284/254 244/290/260 245/291/261 +f 225/287/257 224/285/255 238/292/262 239/293/263 +f 232/288/258 231/286/256 245/291/261 246/294/264 +f 226/289/259 225/287/257 239/293/263 240/295/265 +f 233/273/245 232/288/258 246/294/264 247/296/266 +f 227/275/247 226/289/259 240/295/265 241/297/267 +f 234/274/246 233/273/245 247/296/266 248/298/268 +f 466/276/248 227/275/247 241/297/267 467/299/269 +f 221/278/249 305/300/22 235/301/270 +f 450/302/24 234/274/246 248/298/268 +f 222/280/250 221/278/249 235/301/270 236/303/271 +f 229/282/252 228/281/251 242/304/272 243/305/273 +f 223/283/253 222/280/250 236/303/271 237/306/274 +f 230/284/254 229/282/252 243/305/273 244/290/260 +f 224/285/255 223/283/253 237/306/274 238/292/262 +f 235/301/270 305/307/22 249/308/275 +f 450/309/24 248/298/268 262/310/276 +f 500/311/277 468/312/278 469/313/279 501/314/280 +f 236/303/271 235/301/270 249/308/275 250/315/281 +f 243/305/273 242/304/272 256/316/282 257/317/283 +f 237/306/274 236/303/271 250/315/281 251/318/284 +f 244/290/260 243/305/273 257/317/283 258/319/285 +f 238/292/262 237/306/274 251/318/284 252/320/286 +f 245/291/261 244/290/260 258/319/285 259/321/287 +f 239/293/263 238/292/262 252/320/286 253/322/288 +f 246/294/264 245/291/261 259/321/287 260/323/289 +f 240/295/265 239/293/263 253/322/288 254/324/290 +f 247/296/266 246/294/264 260/323/289 261/325/291 +f 241/297/267 240/295/265 254/324/290 255/326/292 +f 248/298/268 247/296/266 261/325/291 262/310/276 +f 467/299/269 241/297/267 255/326/292 468/312/278 +f 253/322/288 252/320/286 266/327/293 267/328/294 +f 260/323/289 259/321/287 273/329/295 274/330/296 +f 254/324/290 253/322/288 267/328/294 268/331/297 +f 261/325/291 260/323/289 274/330/296 275/332/298 +f 255/326/292 254/324/290 268/331/297 269/333/299 +f 262/310/276 261/325/291 275/332/298 276/334/300 +f 468/312/278 255/326/292 269/333/299 469/313/279 +f 249/308/275 305/335/22 263/336/301 +f 450/337/24 262/310/276 276/334/300 +f 250/315/281 249/308/275 263/336/301 264/338/302 +f 257/317/283 256/316/282 270/339/303 271/340/304 +f 251/318/284 250/315/281 264/338/302 265/341/305 +f 258/319/285 257/317/283 271/340/304 272/342/306 +f 252/320/286 251/318/284 265/341/305 266/327/293 +f 259/321/287 258/319/285 272/342/306 273/329/295 +f 502/343/307 470/344/308 471/345/309 503/346/310 +f 264/338/302 263/336/301 277/347/311 278/348/312 +f 271/340/304 270/339/303 284/349/313 285/350/314 +f 265/341/305 264/338/302 278/348/312 279/351/315 +f 272/342/306 271/340/304 285/350/314 286/352/316 +f 266/327/293 265/341/305 279/351/315 280/353/317 +f 273/329/295 272/342/306 286/352/316 287/354/318 +f 267/328/294 266/327/293 280/353/317 281/355/319 +f 274/330/296 273/329/295 287/354/318 288/356/320 +f 268/331/297 267/328/294 281/355/319 282/357/321 +f 275/332/298 274/330/296 288/356/320 289/358/322 +f 269/333/299 268/331/297 282/357/321 283/359/323 +f 276/334/300 275/332/298 289/358/322 290/360/324 +f 469/313/279 269/333/299 283/359/323 470/344/308 +f 263/336/301 305/361/22 277/347/311 +f 450/362/24 276/334/300 290/360/324 +f 288/356/320 287/354/318 301/363/325 302/364/326 +f 282/357/321 281/355/319 295/365/327 296/366/328 +f 289/358/322 288/356/320 302/364/326 303/367/329 +f 283/359/323 282/357/321 296/366/328 297/368/330 +f 290/360/324 289/358/322 303/367/329 304/369/331 +f 470/344/308 283/359/323 297/368/330 471/345/309 +f 277/347/311 305/370/22 291/371/332 +f 450/372/24 290/360/324 304/369/331 +f 278/348/312 277/347/311 291/371/332 292/373/333 +f 285/350/314 284/349/313 298/374/334 299/375/335 +f 279/351/315 278/348/312 292/373/333 293/376/336 +f 286/352/316 285/350/314 299/375/335 300/377/337 +f 280/353/317 279/351/315 293/376/336 294/378/338 +f 287/354/318 286/352/316 300/377/337 301/363/325 +f 281/355/319 280/353/317 294/378/338 295/365/327 +f 292/373/333 291/371/332 306/379/339 307/380/340 +f 299/375/335 298/374/334 313/381/341 314/382/342 +f 293/376/336 292/373/333 307/380/340 308/383/343 +f 300/377/337 299/375/335 314/382/342 315/384/344 +f 294/378/338 293/376/336 308/383/343 309/385/345 +f 301/363/325 300/377/337 315/384/344 316/386/346 +f 295/365/327 294/378/338 309/385/345 310/387/347 +f 302/364/326 301/363/325 316/386/346 317/388/348 +f 296/366/328 295/365/327 310/387/347 311/389/349 +f 303/367/329 302/364/326 317/388/348 318/390/350 +f 297/368/330 296/366/328 311/389/349 312/391/351 +f 304/369/331 303/367/329 318/390/350 319/392/352 +f 471/345/309 297/368/330 312/391/351 472/393/353 +f 291/371/332 305/394/22 306/379/339 +f 450/395/24 304/369/331 319/392/352 +f 311/389/349 310/387/347 324/396/354 325/397/355 +f 318/390/350 317/388/348 331/398/356 332/399/357 +f 312/391/351 311/389/349 325/397/355 326/400/358 +f 319/392/352 318/390/350 332/399/357 333/401/359 +f 472/393/353 312/391/351 326/400/358 473/402/360 +f 306/379/339 305/403/22 320/404/361 +f 450/405/24 319/392/352 333/401/359 +f 307/380/340 306/379/339 320/404/361 321/406/362 +f 314/382/342 313/381/341 327/407/363 328/408/364 +f 308/383/343 307/380/340 321/406/362 322/409/365 +f 315/384/344 314/382/342 328/408/364 329/410/366 +f 309/385/345 308/383/343 322/409/365 323/411/367 +f 316/386/346 315/384/344 329/410/366 330/412/368 +f 310/387/347 309/385/345 323/411/367 324/396/354 +f 317/388/348 316/386/346 330/412/368 331/398/356 +f 328/408/364 327/407/363 341/413/369 342/414/370 +f 322/409/365 321/406/362 335/415/371 336/416/372 +f 329/410/366 328/408/364 342/414/370 343/417/373 +f 323/411/367 322/409/365 336/416/372 337/418/374 +f 330/412/368 329/410/366 343/417/373 344/419/375 +f 324/396/354 323/411/367 337/418/374 338/420/376 +f 331/398/356 330/412/368 344/419/375 345/421/377 +f 325/397/355 324/396/354 338/420/376 339/422/378 +f 332/399/357 331/398/356 345/421/377 346/423/379 +f 326/400/358 325/397/355 339/422/378 340/424/380 +f 333/401/359 332/399/357 346/423/379 347/425/381 +f 473/402/360 326/400/358 340/424/380 474/426/382 +f 320/404/361 305/427/22 334/428/383 +f 450/429/24 333/401/359 347/425/381 +f 321/406/362 320/404/361 334/428/383 335/415/371 +f 346/430/379 345/431/377 359/432/384 360/433/385 +f 340/434/380 339/435/378 353/436/386 354/437/387 +f 347/438/381 346/430/379 360/433/385 361/439/388 +f 474/440/382 340/434/380 354/437/387 475/441/389 +f 334/442/383 305/443/22 348/444/390 +f 450/445/24 347/438/381 361/439/388 +f 335/446/371 334/442/383 348/444/390 349/447/391 +f 342/448/370 341/449/369 355/450/392 356/451/393 +f 336/452/372 335/446/371 349/447/391 350/453/394 +f 343/454/373 342/448/370 356/451/393 357/455/395 +f 337/456/374 336/452/372 350/453/394 351/457/396 +f 344/458/375 343/454/373 357/455/395 358/459/397 +f 338/460/376 337/456/374 351/457/396 352/461/398 +f 345/431/377 344/458/375 358/459/397 359/432/384 +f 339/435/378 338/460/376 352/461/398 353/436/386 +f 357/455/395 356/451/393 370/462/399 371/463/400 +f 351/457/396 350/453/394 364/464/401 365/465/402 +f 358/459/397 357/455/395 371/463/400 372/466/403 +f 352/461/398 351/457/396 365/465/402 366/467/404 +f 359/432/384 358/459/397 372/466/403 373/468/405 +f 353/436/386 352/461/398 366/467/404 367/469/406 +f 360/433/385 359/432/384 373/468/405 374/470/407 +f 354/437/387 353/436/386 367/469/406 368/471/408 +f 361/439/388 360/433/385 374/470/407 375/472/409 +f 475/441/389 354/437/387 368/471/408 476/473/410 +f 348/444/390 305/474/22 362/475/411 +f 450/476/24 361/439/388 375/472/409 +f 349/447/391 348/444/390 362/475/411 363/477/412 +f 356/451/393 355/450/392 369/478/413 370/462/399 +f 350/453/394 349/447/391 363/477/412 364/464/401 +f 375/472/409 374/470/407 388/479/414 389/480/415 +f 476/473/410 368/471/408 382/481/416 477/482/417 +f 362/475/411 305/483/22 376/484/418 +f 450/485/24 375/472/409 389/480/415 +f 363/477/412 362/475/411 376/484/418 377/486/419 +f 370/462/399 369/478/413 383/487/420 384/488/421 +f 364/464/401 363/477/412 377/486/419 378/489/422 +f 371/463/400 370/462/399 384/488/421 385/490/423 +f 365/465/402 364/464/401 378/489/422 379/491/424 +f 372/466/403 371/463/400 385/490/423 386/492/425 +f 366/467/404 365/465/402 379/491/424 380/493/426 +f 373/468/405 372/466/403 386/492/425 387/494/427 +f 367/469/406 366/467/404 380/493/426 381/495/428 +f 374/470/407 373/468/405 387/494/427 388/479/414 +f 368/471/408 367/469/406 381/495/428 382/481/416 +f 379/491/424 378/489/422 392/496/429 393/497/430 +f 386/492/425 385/490/423 399/498/431 400/499/432 +f 380/493/426 379/491/424 393/497/430 394/500/433 +f 387/494/427 386/492/425 400/499/432 401/501/434 +f 381/495/428 380/493/426 394/500/433 395/502/435 +f 388/479/414 387/494/427 401/501/434 402/503/436 +f 382/481/416 381/495/428 395/502/435 396/504/437 +f 389/480/415 388/479/414 402/503/436 403/505/438 +f 477/482/417 382/481/416 396/504/437 478/506/439 +f 376/484/418 305/507/22 390/508/440 +f 450/509/24 389/480/415 403/505/438 +f 377/486/419 376/484/418 390/508/440 391/510/441 +f 384/488/421 383/487/420 397/511/442 398/512/443 +f 378/489/422 377/486/419 391/510/441 392/496/429 +f 385/490/423 384/488/421 398/512/443 399/498/431 +f 478/506/439 396/504/437 410/513/444 479/514/445 +f 390/508/440 305/515/22 404/516/446 +f 450/517/24 403/505/438 417/518/447 +f 391/510/441 390/508/440 404/516/446 405/519/448 +f 398/512/443 397/511/442 411/520/449 412/521/450 +f 392/496/429 391/510/441 405/519/448 406/522/451 +f 399/498/431 398/512/443 412/521/450 413/523/452 +f 393/497/430 392/496/429 406/522/451 407/524/453 +f 400/499/432 399/498/431 413/523/452 414/525/454 +f 394/500/433 393/497/430 407/524/453 408/526/455 +f 401/501/434 400/499/432 414/525/454 415/527/456 +f 395/502/435 394/500/433 408/526/455 409/528/457 +f 402/503/436 401/501/434 415/527/456 416/529/458 +f 396/504/437 395/502/435 409/528/457 410/513/444 +f 403/505/438 402/503/436 416/529/458 417/518/447 +f 414/525/454 413/523/452 427/530/459 428/531/460 +f 408/526/455 407/524/453 421/532/461 422/533/462 +f 415/527/456 414/525/454 428/531/460 429/534/463 +f 409/528/457 408/526/455 422/533/462 423/535/464 +f 416/529/458 415/527/456 429/534/463 430/536/465 +f 410/513/444 409/528/457 423/535/464 424/537/466 +f 417/518/447 416/529/458 430/536/465 431/538/467 +f 479/514/445 410/513/444 424/537/466 480/539/468 +f 404/516/446 305/540/22 418/541/469 +f 450/542/24 417/518/447 431/538/467 +f 405/519/448 404/516/446 418/541/469 419/543/470 +f 412/521/450 411/520/449 425/544/471 426/545/472 +f 406/522/451 405/519/448 419/543/470 420/546/473 +f 413/523/452 412/521/450 426/545/472 427/530/459 +f 407/524/453 406/522/451 420/546/473 421/532/461 +f 418/541/469 305/547/22 432/548/474 +f 450/549/24 431/538/467 445/550/475 +f 513/551/476 481/552/477 482/19/19 514/553/478 +f 419/543/470 418/541/469 432/548/474 433/554/479 +f 426/545/472 425/544/471 439/555/480 440/556/481 +f 420/546/473 419/543/470 433/554/479 434/557/482 +f 427/530/459 426/545/472 440/556/481 441/558/483 +f 421/532/461 420/546/473 434/557/482 435/559/484 +f 428/531/460 427/530/459 441/558/483 442/560/485 +f 422/533/462 421/532/461 435/559/484 436/561/486 +f 429/534/463 428/531/460 442/560/485 443/562/487 +f 423/535/464 422/533/462 436/561/486 437/563/488 +f 430/536/465 429/534/463 443/562/487 444/564/489 +f 424/537/466 423/535/464 437/563/488 438/565/490 +f 431/538/467 430/536/465 444/564/489 445/550/475 +f 480/539/468 424/537/466 438/565/490 481/552/477 +f 436/561/486 435/559/484 4/6/6 5/5/5 +f 443/562/487 442/560/485 9/1/1 10/9/9 +f 437/563/488 436/561/486 5/5/5 6/11/11 +f 444/564/489 443/562/487 10/9/9 448/13/13 +f 438/565/490 437/563/488 6/11/11 7/15/15 +f 445/550/475 444/564/489 448/13/13 449/17/17 +f 481/552/477 438/565/490 7/15/15 482/19/19 +f 432/548/474 305/566/22 1/21/21 +f 450/567/24 445/550/475 449/17/17 +f 433/554/479 432/548/474 1/21/21 2/25/25 +f 440/556/481 439/555/480 446/28/28 447/27/27 +f 434/557/482 433/554/479 2/25/25 3/31/31 +f 441/558/483 440/556/481 447/27/27 8/2/2 +f 435/559/484 434/557/482 3/31/31 4/6/6 +f 442/560/485 441/558/483 8/2/2 9/1/1 +f 514/553/478 482/19/19 451/20/20 483/568/491 +f 480/539/468 481/552/477 513/551/476 512/569/492 +f 511/570/493 479/514/445 480/539/468 512/569/492 +f 510/571/494 478/506/439 479/514/445 511/570/493 +f 509/572/495 477/482/417 478/506/439 510/571/494 +f 508/573/496 476/473/410 477/482/417 509/572/495 +f 507/574/497 475/441/389 476/473/410 508/573/496 +f 506/575/498 474/440/382 475/441/389 507/574/497 +f 505/576/499 473/402/360 474/426/382 506/577/498 +f 504/578/500 472/393/353 473/402/360 505/576/499 +f 503/346/310 471/345/309 472/393/353 504/578/500 +f 469/313/279 470/344/308 502/343/307 501/314/280 +f 467/299/269 468/312/278 500/311/277 499/579/501 +f 498/580/502 466/276/248 467/299/269 499/579/501 +f 497/581/503 465/267/241 466/276/248 498/580/502 +f 496/582/504 464/243/219 465/267/241 497/581/503 +f 495/583/505 463/234/212 464/243/219 496/582/504 +f 494/584/506 462/210/190 463/234/212 495/583/505 +f 493/585/507 461/201/183 462/210/190 494/584/506 +f 492/586/508 460/177/161 461/201/183 493/585/507 +f 491/587/509 459/168/154 460/177/161 492/586/508 +f 490/123/112 458/122/111 459/168/154 491/587/509 +f 456/110/102 457/121/110 489/120/109 488/588/510 +f 487/589/511 455/85/79 456/110/102 488/588/510 +f 486/590/512 454/77/73 455/85/79 487/589/511 +f 485/591/513 453/53/51 454/77/73 486/590/512 +f 484/592/514 452/44/44 453/53/51 485/591/513 +f 439/555/480 513/551/476 514/553/478 446/28/28 +f 425/544/471 512/569/492 513/551/476 439/555/480 +f 411/520/449 511/570/493 512/569/492 425/544/471 +f 397/511/442 510/571/494 511/570/493 411/520/449 +f 383/487/420 509/572/495 510/571/494 397/511/442 +f 369/478/413 508/573/496 509/572/495 383/487/420 +f 355/450/392 507/574/497 508/573/496 369/478/413 +f 341/449/369 506/575/498 507/574/497 355/450/392 +f 327/407/363 505/576/499 506/577/498 341/413/369 +f 313/381/341 504/578/500 505/576/499 327/407/363 +f 298/374/334 503/346/310 504/578/500 313/381/341 +f 284/349/313 502/343/307 503/346/310 298/374/334 +f 270/339/303 501/314/280 502/343/307 284/349/313 +f 256/316/282 500/311/277 501/314/280 270/339/303 +f 242/304/272 499/579/501 500/311/277 256/316/282 +f 228/281/251 498/580/502 499/579/501 242/304/272 +f 214/272/244 497/581/503 498/580/502 228/281/251 +f 200/248/222 496/582/504 497/581/503 214/272/244 +f 186/238/214 495/583/505 496/582/504 200/248/222 +f 172/215/193 494/584/506 495/583/505 186/238/214 +f 158/188/170 493/585/507 494/584/506 172/215/193 +f 144/182/164 492/586/508 493/585/507 158/188/170 +f 130/156/142 491/587/509 492/586/508 144/182/164 +f 116/149/135 490/123/112 491/587/509 130/156/142 +f 102/126/115 489/120/109 490/123/112 116/149/135 +f 88/115/105 488/588/510 489/120/109 102/126/115 +f 74/91/83 487/589/511 488/588/510 88/115/105 +f 60/82/76 486/590/512 487/589/511 74/91/83 +f 46/58/54 485/591/513 486/590/512 60/82/76 +f 32/49/47 484/592/514 485/591/513 46/58/54 +f 18/29/29 483/568/491 484/592/514 32/49/47 +f 446/28/28 514/553/478 483/568/491 18/29/29 +f 483/568/491 451/20/20 452/44/44 484/592/514 diff --git a/examples/pill_demo/res/textures/pill_color.png b/examples/pill_demo/res/textures/pill_color.png new file mode 100644 index 0000000000000000000000000000000000000000..8262f8f3c615a5fb3283476665928d6d0a5d237c GIT binary patch literal 99341 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

@FT4{OS1>rW1;lC{ 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�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�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�sDzqsKkgz0uk{>CNw34)sbd@xyD6eR(7_OM9 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 } +// Component for motion blur test pill +define_component!(MovingPillComponent { + target_position: Vector3f, + velocity: f32, + rotation_speed: f32, +}); + // Game pub struct Game {} impl PillGame for Game { fn start(&self, engine: &mut Engine) -> Result<()> { - let _scene_benchmark = create_scene_benchmark(engine, "Benchmark 60k pills")?; - let scene_pbr_grid: SceneHandle = create_scene_pbr_grid(engine, "PBR Grid")?; - engine.set_active_scene(scene_pbr_grid)?; + // let _scene_benchmark = create_scene_benchmark(engine, "Benchmark 60k pills")?; + // let _scene_pills = create_scene_pills(engine, "Pills")?; + // let _scene_dof_proof = create_scene_dof_proof(engine, "DoF Proof")?; + // let _scene_mb_proof = create_scene_mb_proof(engine, "MB Proof")?; + // let _scene_pbr_grid: SceneHandle = create_scene_pbr_grid(engine, "PBR Grid")?; + // let _scene_gltf_glb: SceneHandle = create_scene_gltf_glb(engine, "GLTF glb")?; + // let _scene_gltf_gltf: SceneHandle = create_scene_gltf_gltf(engine, "GLTF gltf")?; + // let _scene_gltf_pbr: SceneHandle = create_scene_gltf_pbr(engine, "GLTF PBR Spheres")?; // TODO: fix + let _scene_gltf_sponza: SceneHandle = create_scene_gltf_sponza(engine, "GLTF Sponza")?; + engine.set_active_scene(_scene_gltf_sponza)?; Ok(()) } } +fn create_scene_dof_proof(engine: &mut Engine, name: &str) -> Result { + let scene = engine.create_scene(name)?; + + // Register components + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + + // Add systems + engine.add_system("PillRotation", pill_rotation_system)?; + + // Mesh + let pill_mesh_handle = get_or_add_mesh(engine, "Pill", "models/pill.obj")?; + + // Textures (same as normal pills scene so PBR bindings are valid) + let pill_color_texture = Texture::new( + "PillColor", + TextureType::Gamma, + ResourceLoadType::Path("textures/pill_color.png".into()), + ); + let pill_color_texture_handle = engine.add_resource::(pill_color_texture)?; + let pill_normal_texture = Texture::new( + "PillNormal", + TextureType::Linear, + ResourceLoadType::Path("textures/pill_normal.png".into()), + ); + let pill_normal_texture_handle = engine.add_resource::(pill_normal_texture)?; + + // Create a few distinct materials so pills are easy to distinguish on-screen. + let tints = [ + Color::new(1.0, 0.2, 0.2), + Color::new(0.2, 1.0, 0.2), + Color::new(0.2, 0.2, 1.0), + Color::new(1.0, 1.0, 0.2), + Color::new(1.0, 0.2, 1.0), + ]; + let mut materials: Vec = Vec::new(); + for (i, tint) in tints.iter().enumerate() { + let mut mat: PBRMaterial = PBRMaterial::new(&format!("DoFProofMat{}", i)); + mat.set_albedo_texture(pill_color_texture_handle.clone()); + mat.set_normal_texture(pill_normal_texture_handle.clone()); + mat.set_base_color_factor(*tint); + mat.set_metallic_factor(0.0); + mat.set_roughness_factor(0.4); + let handle = engine.add_resource::(mat)?; + materials.push(handle); + } + + // Camera: look straight down +Z from z=-16 so depth ordering is obvious. + let camera = engine.create_entity(scene)?; + let cam_xform = TransformComponent::builder() + .position(Vector3f::new(0.0, 0.0, -16.0)) + .rotation(Vector3f::new(0.0, 0.0, 0.0)) + .build(); + engine.add_component_to_entity(scene, camera, cam_xform)?; + let camera_component = CameraComponent::builder().enabled(true).build(); + engine.add_component_to_entity(scene, camera, camera_component)?; + + // Five pills at fixed depths along view direction; sizes/positions make it easy to verify focus changes. + // Distances from camera are approx: 6, 16, 26, 46, 86 units. + let zs = [-10.0, 0.0, 10.0, 30.0, 70.0]; + let xs: [f32; 5] = [-5.0, -3.0, 0.0, 15.0, 50.0]; + for i in 0..zs.len() { + let pill = engine.create_entity(scene)?; + let transform_component = TransformComponent::builder() + .scale(Vector3f::new(1.2, 1.8, 1.2)) + .rotation(Vector3f::new(0.0, 0.0, 0.0)) + .position(Vector3f::new(xs[i], 0.0, zs[i])) + .build(); + engine.add_component_to_entity(scene, pill, transform_component)?; + let mesh_rendering_component: MeshRenderingComponent = MeshRenderingComponent::builder() + .mesh(&pill_mesh_handle) + .material(&materials[i]) + .build(); + engine.add_component_to_entity(scene, pill, mesh_rendering_component)?; + engine.add_component_to_entity(scene, pill, PillComponent {})?; + } + + Ok(scene) +} + +fn create_scene_mb_proof(engine: &mut Engine, name: &str) -> Result { + let scene = engine.create_scene(name)?; + + // Register components + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + + // Add systems + engine.add_system("MotionBlurMovement", motion_blur_movement_system)?; + + // Mesh + let pill_mesh_handle = get_or_add_mesh(engine, "Pill", "models/pill.obj")?; + + // Textures + let pill_color_texture = Texture::new( + "PillColor", + TextureType::Gamma, + ResourceLoadType::Path("textures/pill_color.png".into()), + ); + let pill_color_texture_handle = engine.add_resource::(pill_color_texture)?; + let pill_normal_texture = Texture::new( + "PillNormal", + TextureType::Linear, + ResourceLoadType::Path("textures/pill_normal.png".into()), + ); + let pill_normal_texture_handle = engine.add_resource::(pill_normal_texture)?; + + // Create material for the moving pill (bright color to make motion blur visible) + let mut mat: PBRMaterial = PBRMaterial::new("MotionBlurMat"); + mat.set_albedo_texture(pill_color_texture_handle.clone()); + mat.set_normal_texture(pill_normal_texture_handle.clone()); + mat.set_base_color_factor(Color::new(1.0, 0.8, 0.2)); // Bright yellow-orange + mat.set_metallic_factor(0.1); + mat.set_roughness_factor(0.3); + let material_handle = engine.add_resource::(mat)?; + + // Camera: positioned to see the motion clearly + let camera = engine.create_entity(scene)?; + let cam_xform = TransformComponent::builder() + .position(Vector3f::new(0.0, 0.0, -20.0)) + .rotation(Vector3f::new(0.0, 0.0, 0.0)) + .build(); + engine.add_component_to_entity(scene, camera, cam_xform)?; + let camera_component = CameraComponent::builder().enabled(true).build(); + engine.add_component_to_entity(scene, camera, camera_component)?; + + // Multiple moving pills for motion blur testing + let num_pills = 12; + let positions: Vec = (0..num_pills) + .map(|i| { + let angle = i as f32 * (std::f32::consts::TAU / num_pills as f32); + let radius = 6.0; + Vector3f::new(angle.cos() * radius, 0.0, angle.sin() * radius) + }) + .collect(); + + for (i, pos) in positions.iter().enumerate() { + let angle = i as f32 * (std::f32::consts::TAU / num_pills as f32); + let pill = engine.create_entity(scene)?; + let transform_component = TransformComponent::builder() + .scale(Vector3f::new(1.5, 2.0, 1.5)) + .rotation(Vector3f::new(0.0, 0.0, 0.0)) + .position(*pos) + .build(); + engine.add_component_to_entity(scene, pill, transform_component)?; + + let mesh_rendering_component: MeshRenderingComponent = MeshRenderingComponent::builder() + .mesh(&pill_mesh_handle) + .material(&material_handle) + .build(); + engine.add_component_to_entity(scene, pill, mesh_rendering_component)?; + + // Spread targets around a different circular offset + let angle_offset = angle + std::f32::consts::PI / 2.0; + let target_radius = 5.0; + let target = Vector3f::new( + angle_offset.cos() * target_radius, + 0.0, + angle_offset.sin() * target_radius, + ); + + let moving_pill_component = MovingPillComponent { + target_position: target, + velocity: 6.0 + i as f32 * 0.3, // vary speed a bit for visualization + rotation_speed: 0.0, // Could add spin if you wish + }; + engine.add_component_to_entity(scene, pill, moving_pill_component)?; + } + + Ok(scene) +} + fn create_scene_benchmark(engine: &mut Engine, name: &str) -> Result { let scene = engine.create_scene(name)?; @@ -68,7 +263,7 @@ fn create_scene_benchmark(engine: &mut Engine, name: &str) -> Result Result Result { + let scene = engine.create_scene(name)?; + + // Register components + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + + // Add systems + engine.add_system("PillRotation", pill_rotation_system)?; + + // Add meshes + let pill_mesh_handle = get_or_add_mesh(engine, "Pill", "models/pill.obj")?; + // Add textures + let pill_color_texture = Texture::new( + "PillColor", + TextureType::Gamma, + ResourceLoadType::Path("textures/pill_color.png".into()), + ); + let pill_color_texture_handle = engine.add_resource::(pill_color_texture)?; + let pill_normal_texture = Texture::new( + "PillNormal", + TextureType::Linear, + ResourceLoadType::Path("textures/pill_normal.png".into()), + ); + let pill_normal_texture_handle = engine.add_resource::(pill_normal_texture)?; + + // Material properties showcase: create a small set of materials to reuse + let mut rng = rand::thread_rng(); + let mut materials = Vec::new(); + for j in 0..10 { + let mut mat: PBRMaterial = PBRMaterial::new(&format!("PillMat{}", j)); + mat.set_albedo_texture(pill_color_texture_handle.clone()); + mat.set_normal_texture(pill_normal_texture_handle.clone()); + let tint = Color::new( + rng.gen_range(0.2..=0.8), + rng.gen_range(0.2..=0.8), + rng.gen_range(0.2..=0.8), + ); + let metallic = rng.gen_range(0.0..=1.0); + let roughness = rng.gen_range(0.0..=1.0); + mat.set_base_color_factor(tint); + mat.set_metallic_factor(metallic); + mat.set_roughness_factor(roughness); + let handle = engine.add_resource::(mat)?; + materials.push(handle); + } + + // Create camera entity + let camera = engine.create_entity(scene)?; + let transform_component = TransformComponent::builder() + .position(Vector3f::new(0.0, 0.0, -16.0)) + .rotation(Vector3f::new(0.0, 0.0, -20.0)) + .build(); + engine.add_component_to_entity(scene, camera, transform_component)?; + let camera_component = CameraComponent::builder().enabled(true).build(); + engine.add_component_to_entity(scene, camera, camera_component)?; + + // Create pill entity + for i in 0..3000 { + let pill = engine.create_entity(scene)?; + let posx = rng.gen_range(-100.0..=100.0); + let posy = rng.gen_range(-100.0..=100.0); + let posz = rng.gen_range(-10.0..=100.0); + let rotx = rng.gen_range(-180.0..=180.0); + let roty = rng.gen_range(-180.0..=180.0); + let rotz = rng.gen_range(-180.0..=180.0); + let transform_component = TransformComponent::builder() + .scale(Vector3f::new(0.75, 1.2, 0.75)) + .rotation(Vector3f::new(rotx, roty, rotz)) + .position(Vector3f::new(posx, posy, posz)) + .build(); + engine.add_component_to_entity(scene, pill, transform_component)?; + let mesh_rendering_component = MeshRenderingComponent::builder() + .mesh(&pill_mesh_handle) + .material(&materials[i as usize % materials.len()]) + .build(); + engine.add_component_to_entity(scene, pill, mesh_rendering_component)?; + engine.add_component_to_entity(scene, pill, PillComponent {})?; } Ok(scene) @@ -115,6 +397,8 @@ fn create_scene_pbr_grid(engine: &mut Engine, name: &str) -> Result // Register components engine.register_component::(scene)?; engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; engine.register_component::(scene)?; engine.register_component::(scene)?; @@ -168,6 +452,201 @@ fn create_scene_pbr_grid(engine: &mut Engine, name: &str) -> Result Ok(scene) } +fn create_scene_gltf_glb(engine: &mut Engine, name: &str) -> Result { + let scene = engine.create_scene(name)?; + + // Register components + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + + engine.add_system("PillRotation", pill_rotation_system)?; + + // Camera + let camera = engine.create_entity(scene)?; + let camera_transform = TransformComponent::builder() + .position(Vector3f::new(0.0, 0.0, -2.0)) + .rotation(Vector3f::new(0.0, 0.0, 0.0)) + .build(); + engine.add_component_to_entity(scene, camera, camera_transform)?; + let camera_component = CameraComponent::builder().enabled(true).build(); + engine.add_component_to_entity(scene, camera, camera_component)?; + + // Load Model from GLTF and assemble + // https://github.com/KhronosGroup/glTF-Sample-Models/tree/d7a3cc8e51d7c573771ae77a57f16b0662a905c6/2.0/DamagedHelmet + let model_handle = get_or_add_model(engine, "DamagedHelmet", "models/DamagedHelmet.glb")?; + let entities = assemble_model_into_scene(engine, scene, &model_handle)?; + for entity in entities { + engine.add_component_to_entity(scene, entity, PillComponent {})?; + } + + Ok(scene) +} + +fn create_scene_gltf_pbr(engine: &mut Engine, name: &str) -> Result { + let scene = engine.create_scene(name)?; + + // Register components + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + + // Camera + let camera = engine.create_entity(scene)?; + let camera_transform = TransformComponent::builder() + .position(Vector3f::new(0.0, -30.0, 0.0)) + .rotation(Vector3f::new(-90.0, 0.0, 0.0)) + .build(); + engine.add_component_to_entity(scene, camera, camera_transform)?; + let camera_component = CameraComponent::builder().enabled(true).fov(45.0).build(); + engine.add_component_to_entity(scene, camera, camera_component)?; + + // Load Model from GLTF and assemble + // https://github.com/KhronosGroup/glTF-Sample-Assets/blob/ef86ca2c5996146cf1b14f7478842db4eee920b1/Models/MetalRoughSpheres/glTF-Binary/MetalRoughSpheres.glb + let model_handle = + get_or_add_model(engine, "MetalRoughSpheres", "models/MetalRoughSpheres.glb")?; + let _entities = assemble_model_into_scene(engine, scene, &model_handle)?; + + Ok(scene) +} + +fn create_scene_gltf_gltf(engine: &mut Engine, name: &str) -> Result { + let scene = engine.create_scene(name)?; + + // Register components + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + + engine.add_system("PillRotation", pill_rotation_system)?; + + // Camera + let camera = engine.create_entity(scene)?; + let camera_transform = TransformComponent::builder() + .position(Vector3f::new(0.0, 0.0, -3.0)) + .rotation(Vector3f::new(0.0, 0.0, 0.0)) + .build(); + engine.add_component_to_entity(scene, camera, camera_transform)?; + let camera_component = CameraComponent::builder().enabled(true).build(); + engine.add_component_to_entity(scene, camera, camera_component)?; + + // Load Model from GLTF and assemble + // https://github.com/KhronosGroup/glTF-Sample-Assets/blob/ef86ca2c5996146cf1b14f7478842db4eee920b1/Models/SciFiHelmet/README.md + let model_handle = + get_or_add_model(engine, "SciFiHelmet", "models/SciFiHelmet/SciFiHelmet.gltf")?; + let entities = assemble_model_into_scene(engine, scene, &model_handle)?; + for entity in entities { + engine.add_component_to_entity(scene, entity, PillComponent {})?; + } + + Ok(scene) +} + +fn create_scene_gltf_sponza(engine: &mut Engine, name: &str) -> Result { + let scene = engine.create_scene(name)?; + + // Register components + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + + // Camera + let camera = engine.create_entity(scene)?; + let camera_transform = TransformComponent::builder() + .position(Vector3f::new(5.0, 1.0, 0.0)) + .rotation(Vector3f::new(0.0, -90.0, 0.0)) + .build(); + engine.add_component_to_entity(scene, camera, camera_transform)?; + let camera_component = CameraComponent::builder().enabled(true).build(); + engine.add_component_to_entity(scene, camera, camera_component)?; + + // Load Model from GLTF and assemble + // https://github.com/KhronosGroup/glTF-Sample-Assets/blob/ef86ca2c5996146cf1b14f7478842db4eee920b1/Models/SciFiHelmet/README.md + let model_handle = get_or_add_model(engine, "Sponza", "models/sponza/Sponza.gltf")?; + let entities = assemble_model_into_scene(engine, scene, &model_handle)?; + // for entity in entities { + // engine.add_component_to_entity(scene, entity, PillComponent {})?; + // } + + Ok(scene) +} + +fn get_or_add_model(engine: &mut Engine, name: &str, path: &str) -> Result { + match engine.get_resource_handle::(name) { + Ok(handle) => Ok(handle), + Err(_) => { + let model = Model::new(name, path.into()); + engine.add_resource(model) + } + } +} + +fn assemble_model_into_scene( + engine: &mut Engine, + scene: SceneHandle, + model_handle: &ModelHandle, +) -> Result> { + // Snapshot slots to avoid holding an immutable borrow of engine while mutating it + let (material_handles, slots): ( + Vec, + Vec<(MeshHandle, PBRMaterialHandle, Vector3f, Vector3f, Vector3f)>, + ) = { + let model = engine.get_resource::(model_handle)?; + let material_handles = model.materials.clone(); + let slots = model + .material_slots + .iter() + .map(|s| { + ( + s.mesh.clone(), + s.material.clone(), + s.translation, + s.rotation_euler_deg, + s.scale, + ) + }) + .collect(); + (material_handles, slots) + }; + + // Update emissive for each material handle + for mh in material_handles { + if let Ok(mat) = engine.get_resource_mut::(&mh) { + let cur = mat.emissive; + mat.set_emissive_factor(cur * 100.0); + } + } + let mut entities: Vec = Vec::new(); + for (mesh_h, material_h, translation, rotation_deg, scale) in slots { + let entity = engine.create_entity(scene)?; + let transform = TransformComponent::builder() + .position(translation) + .rotation(rotation_deg) + .scale(scale) + .build(); + engine.add_component_to_entity(scene, entity, transform)?; + let mesh_render = MeshRenderingComponent::builder() + .mesh(&mesh_h) + .material(&material_h) + .build(); + engine.add_component_to_entity(scene, entity, mesh_render)?; + entities.push(entity); + } + Ok(entities) +} + fn get_or_add_mesh(engine: &mut Engine, name: &str, path: &str) -> Result { match engine.get_resource_handle::(name) { Ok(handle) => Ok(handle), @@ -200,3 +679,85 @@ fn pill_rotation_system(engine: &mut Engine) -> Result<()> { Ok(()) } + +fn pick_random_target_opposite_hemisphere( + rng: &mut impl Rng, + cur_pos: Vector3f, + prev_dir: Vector3f, +) -> Vector3f { + let min_distance = 4.0; + let radius = 13.0; + let mut new_target = cur_pos; + + for _ in 0..20 { + let u: f32 = rng.gen(); + let v: f32 = rng.gen(); + let theta = 2.0 * std::f32::consts::PI * u; + let phi = v.acos(); + + let mut rand_dir = + Vector3f::new(phi.sin() * theta.cos(), phi.sin() * theta.sin(), phi.cos()); + if rand_dir.dot(prev_dir) > 0.0 { + rand_dir = -rand_dir; + } + + let new_distance = rng.gen_range(min_distance..=radius); + new_target = cur_pos + rand_dir * new_distance; + + new_target.x = new_target.x.max(-12.0).min(12.0); + new_target.y = new_target.y.max(-8.0).min(8.0); + new_target.z = new_target.z.max(-3.0).min(20.0); + + if (new_target - cur_pos).magnitude() >= min_distance { + break; + } + } + + new_target +} + +fn motion_blur_movement_system(engine: &mut Engine) -> Result<()> { + let delta_time = engine.get_global_component::()?.delta_time; + let mut rng = rand::thread_rng(); + + for (_, transform_component, moving_pill_component) in + engine.iterate_two_components_mut::()? + { + let cur_pos = transform_component.position; + let last_dir_raw = cur_pos - moving_pill_component.target_position; + let last_dir = if last_dir_raw.magnitude2() > 0.0001 { + last_dir_raw.normalize() + } else { + Vector3f::new(0.0, 0.0, 1.0) + }; + + let direction_to_target = moving_pill_component.target_position - cur_pos; + let distance_to_target = direction_to_target.magnitude(); + let step = moving_pill_component.velocity * delta_time; + + if distance_to_target <= step { + transform_component.set_position(moving_pill_component.target_position); + let reached_pos = moving_pill_component.target_position; + + moving_pill_component.target_position = + pick_random_target_opposite_hemisphere(&mut rng, reached_pos, last_dir); + moving_pill_component.velocity = rng.gen_range(8.0..=25.0); + continue; + } + + if distance_to_target > 0.0 { + let normalized_direction = direction_to_target / distance_to_target; + let movement = normalized_direction * step; + let new_position = cur_pos + movement; + transform_component.set_position(new_position); + } + + // Update rotation + // transform_component.rotate_around_axis( + // moving_pill_component.rotation_speed * delta_time, + // Vector3f::new(0.0, 1.0, 0.0), + // ); + } + + Ok(()) +}

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 literal 0 HcmV?d00001 diff --git a/examples/pill_demo/src/game.rs b/examples/pill_demo/src/game.rs new file mode 100644 index 00000000..e3abf4b3 --- /dev/null +++ b/examples/pill_demo/src/game.rs @@ -0,0 +1,121 @@ +use pill_engine::game::*; +use rand::Rng; + +// Define custom component +pub struct PillComponent {} + +impl Component for PillComponent {} + +impl PillTypeMapKey for PillComponent { + type Storage = ComponentStorage; +} + +// Game +pub struct Game {} + +impl PillGame for Game { + fn start(&self, engine: &mut Engine) -> Result<()> { + // Create scene + let active_scene = engine.create_scene("Default")?; + engine.set_active_scene(active_scene)?; + + // Register components + engine.register_component::(active_scene)?; + engine.register_component::(active_scene)?; + engine.register_component::(active_scene)?; + engine.register_component::(active_scene)?; + engine.register_component::(active_scene)?; + engine.register_component::(active_scene)?; + + // Add systems + engine.add_system("PillRotation", pill_rotation_system)?; + + // Add meshes + let pill_mesh = Mesh::new("Pill", "models/pill.obj".into()); + let pill_mesh_handle = engine.add_resource(pill_mesh)?; + // Add textures + let pill_color_texture = Texture::new( + "PillColor", + TextureType::Color, + ResourceLoadType::Path("textures/pill_color.png".into()), + ); + let pill_color_texture_handle = engine.add_resource::(pill_color_texture)?; + let pill_normal_texture = Texture::new( + "PillNormal", + TextureType::Normal, + ResourceLoadType::Path("textures/pill_normal.png".into()), + ); + let pill_normal_texture_handle = engine.add_resource::(pill_normal_texture)?; + // Material properties showcase: create a small set of materials to reuse + let mut rng = rand::thread_rng(); + let mut materials = Vec::new(); + for j in 0..8 { + let mut mat = Material::new(&format!("PillMat{}", j)); + mat.set_texture("color", pill_color_texture_handle.clone())?; + mat.set_texture("normal", pill_normal_texture_handle.clone())?; + let tint = Color::new( + rng.gen_range(0.3..=1.0), + rng.gen_range(0.3..=1.0), + rng.gen_range(0.3..=1.0), + ); + let spec = rng.gen_range(0.0..=1.0); + mat.set_color("tint", tint)?; + mat.set_scalar("specularity", spec)?; + let handle = engine.add_resource::(mat)?; + materials.push(handle); + } + + // Create camera entity + let camera = engine.create_entity(active_scene)?; + let transform_component = TransformComponent::builder() + .position(Vector3f::new(0.0, 0.0, -16.0)) + .rotation(Vector3f::new(0.0, 0.0, -20.0)) + .build(); + engine.add_component_to_entity(active_scene, camera, transform_component)?; + let camera_component = CameraComponent::builder().enabled(true).build(); + engine.add_component_to_entity(active_scene, camera, camera_component)?; + + // Create pill entity + for i in 0..50000 { + let pill = engine.create_entity(active_scene)?; + let posx = rng.gen_range(-10.0..=20.0); + let posy = rng.gen_range(-10.0..=10.0); + let posz = rng.gen_range(-1.0..=1.0); + let rotx = rng.gen_range(-180.0..=180.0); + let transform_component = TransformComponent::builder() + .rotation(Vector3f::new(rotx, 0.0, 0.0)) + .position(Vector3f::new(posx, posy, posz)) + .build(); + engine.add_component_to_entity(active_scene, pill, transform_component)?; + let mesh_rendering_component = MeshRenderingComponent::builder() + .mesh(&pill_mesh_handle) + .material(&materials[i as usize % materials.len()]) + .build(); + engine.add_component_to_entity(active_scene, pill, mesh_rendering_component)?; + engine.add_component_to_entity(active_scene, pill, PillComponent {})?; + } + + Ok(()) + } +} + +fn pill_rotation_system(engine: &mut Engine) -> Result<()> { + let delta_time = engine.get_global_component::()?.delta_time; + let input_component = engine.get_global_component_mut::()?; + + // Exit on Escape + if input_component.get_key_pressed(KeyboardKey::Escape) { + std::process::exit(0); + } + + // Rotate pill if spacebar is not pressed + if !input_component.get_key_pressed(KeyboardKey::Space) { + for (_, transform_component, _) in + engine.iterate_two_components_mut::()? + { + transform_component.rotate_around_axis(90.0 * delta_time, Vector3f::new(0.0, 1.0, 0.0)); + } + } + + Ok(()) +} diff --git a/examples/pill_demo/src/lib.rs b/examples/pill_demo/src/lib.rs new file mode 100644 index 00000000..c0d7b224 --- /dev/null +++ b/examples/pill_demo/src/lib.rs @@ -0,0 +1,4 @@ +mod game; +use pill_engine::game::create_game; + +create_game!(crate::game::Game {}, pill_engine::game::PillGame); \ No newline at end of file From 0252abb56746c8cfc4a56d9047dca58aec16ba7a Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 04:20:56 +0200 Subject: [PATCH 22/69] feat: remove double sorting queue --- engine/pill_engine/src/ecs/systems/rendering_system.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/engine/pill_engine/src/ecs/systems/rendering_system.rs b/engine/pill_engine/src/ecs/systems/rendering_system.rs index 968b232b..30956f0b 100644 --- a/engine/pill_engine/src/ecs/systems/rendering_system.rs +++ b/engine/pill_engine/src/ecs/systems/rendering_system.rs @@ -118,11 +118,6 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { } } - timer.record("Sort render queue"); - - // Sort render queue - engine.render_queue.sort(); - timer.record("Get component storages"); let egui_ui = EguiManagerComponent::get_ui(engine); // egui_manager_component.get_ui(engine); From b40a988ceb4eb7f476c35f53ca71ba5e58b00a73 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 04:21:24 +0200 Subject: [PATCH 23/69] feat: comments vs rendering talk --- .../components/mesh_rendering_component.rs | 144 ++++++++++++------ engine/pill_engine/src/ecs/mod.rs | 98 +++--------- .../src/ecs/systems/rendering_system.rs | 5 + engine/pill_renderer/src/renderer.rs | 6 +- .../src/resources/renderer_pipeline.rs | 4 + 5 files changed, 131 insertions(+), 126 deletions(-) diff --git a/engine/pill_engine/src/ecs/components/mesh_rendering_component.rs b/engine/pill_engine/src/ecs/components/mesh_rendering_component.rs index 8714a3ad..131aa833 100644 --- a/engine/pill_engine/src/ecs/components/mesh_rendering_component.rs +++ b/engine/pill_engine/src/ecs/components/mesh_rendering_component.rs @@ -1,16 +1,18 @@ use crate::{ - engine::Engine, - graphics::{ RenderQueueKey, compose_render_queue_key }, - resources::{ Material, MaterialHandle, Mesh, MeshHandle, ResourceManager }, - ecs::{ EntityHandle, ComponentStorage, Component, SceneHandle, DeferredUpdateComponentRequest, DeferredUpdateManagerPointer, DeferredUpdateComponent }, config::DEFAULT_MATERIAL_HANDLE, + ecs::{ + Component, ComponentStorage, DeferredUpdateComponent, DeferredUpdateComponentRequest, + DeferredUpdateManagerPointer, EntityHandle, SceneHandle, + }, + engine::Engine, + graphics::{compose_render_queue_key, RenderQueueKey}, + resources::{Material, MaterialHandle, Mesh, MeshHandle, ResourceManager}, }; use cgmath::num_traits::Float; -use pill_core::{ PillTypeMap, PillTypeMapKey, PillStyle, get_type_name, PillSlotMapKey }; - -use anyhow::{ Result, Context, Error }; +use pill_core::{get_type_name, PillSlotMapKey, PillStyle, PillTypeMap, PillTypeMapKey}; +use anyhow::{Context, Error, Result}; const DEFERRED_REQUEST_VARIANT_UPDATE_RENDER_QUEUE: usize = 0; const DEFERRED_REQUEST_VARIANT_SET_MATERIAL: usize = 1; @@ -28,7 +30,7 @@ impl MeshRenderingComponentBuilder { component: MeshRenderingComponent::new(), } } - + pub fn mesh(mut self, mesh_handle: &MeshHandle) -> Self { self.component.mesh_handle = Some(mesh_handle.clone()); self @@ -52,7 +54,9 @@ pub struct MeshRenderingComponent { pub mesh_handle: Option, #[readonly] pub material_handle: Option, - pub(crate) render_queue_key: Option, + // [SIMILAR] Precomputes/stores a key for sorting by pipeline/material/mesh outside hot draw loop (per TALK separation of data setup vs draw) + // [API->CLIENT] Key encodes pipeline/material/mesh generational handles used by low-level to minimize state changes + pub(crate) render_queue_key: Option, entity_handle: Option, scene_handle: Option, @@ -65,7 +69,7 @@ impl MeshRenderingComponent { } pub fn new() -> Self { - Self { + Self { mesh_handle: None, material_handle: None, render_queue_key: None, @@ -77,11 +81,13 @@ impl MeshRenderingComponent { pub fn set_material(&mut self, material_handle: &MaterialHandle) { self.material_handle = Some(material_handle.clone()); + // [SIMILAR] Mutations post a deferred update; avoids work in the hot path self.post_deferred_update_request(DEFERRED_REQUEST_VARIANT_SET_MATERIAL); } pub fn set_mesh(&mut self, mesh_handle: &MeshHandle) { self.mesh_handle = Some(mesh_handle.clone()); + // [SIMILAR] Mutations post a deferred update; avoids work in the hot path self.post_deferred_update_request(DEFERRED_REQUEST_VARIANT_SET_MESH); } @@ -103,7 +109,10 @@ impl MeshRenderingComponent { self.mesh_handle = mesh_handle; } - pub(crate) fn update_render_queue_key(&mut self, resource_manager: &ResourceManager) -> Result<()> { + pub(crate) fn update_render_queue_key( + &mut self, + resource_manager: &ResourceManager, + ) -> Result<()> { if self.mesh_handle.is_some() { // Use default material if no material is set let material_handle = match self.material_handle { @@ -112,16 +121,18 @@ impl MeshRenderingComponent { }; // Compose render queue key and set it - if let Ok(render_queue_key) = compose_render_queue_key(resource_manager, &material_handle, &self.mesh_handle.unwrap()) - { + if let Ok(render_queue_key) = compose_render_queue_key( + resource_manager, + &material_handle, + &self.mesh_handle.unwrap(), + ) { + // [SIMILAR] Precompute render binning key when data changes (not per draw) + // [RECOMMENDED] Keep key up-to-date on any mesh/material change self.render_queue_key = Some(render_queue_key); - } - else { + } else { self.render_queue_key = None; } - } - else - { + } else { self.render_queue_key = None; } @@ -130,37 +141,63 @@ impl MeshRenderingComponent { fn post_deferred_update_request(&mut self, request_variant: usize) { if self.deferred_update_manager.is_some() { - let entity_handle = self.entity_handle.expect("Critical: Cannot post deferred update request. No EntityHandle set in Component"); - let scene_handle = self.scene_handle.expect("Critical: Cannot post deferred update request. No SceneHandle set in Component"); - let request = DeferredUpdateComponentRequest::::new(entity_handle, scene_handle, request_variant); - self.deferred_update_manager.as_mut().expect("Critical: No DeferredUpdateManager").post_update_request(request); + let entity_handle = self.entity_handle.expect( + "Critical: Cannot post deferred update request. No EntityHandle set in Component", + ); + let scene_handle = self.scene_handle.expect( + "Critical: Cannot post deferred update request. No SceneHandle set in Component", + ); + let request = DeferredUpdateComponentRequest::::new( + entity_handle, + scene_handle, + request_variant, + ); + self.deferred_update_manager + .as_mut() + .expect("Critical: No DeferredUpdateManager") + .post_update_request(request); } } } impl PillTypeMapKey for MeshRenderingComponent { - type Storage = ComponentStorage; + type Storage = ComponentStorage; } impl Component for MeshRenderingComponent { fn initialize(&mut self, engine: &mut Engine) -> Result<()> { + // [SIMILAR] Uses deferred update system → separates data modification from drawing per TALK // This component is using DeferredUpdateSystem so keep DeferredUpdateManager - let deferred_update_component = engine.get_global_component_mut::().expect("Critical: No DeferredUpdateComponent"); - self.deferred_update_manager = Some(deferred_update_component.borrow_deferred_update_manager()); + let deferred_update_component = engine + .get_global_component_mut::() + .expect("Critical: No DeferredUpdateComponent"); + self.deferred_update_manager = + Some(deferred_update_component.borrow_deferred_update_manager()); // Check if material handle is valid if self.material_handle.is_some() { - engine.get_resource::(&self.material_handle.unwrap()) - .context(format!("Creating {} {} failed", "Component".gobj_style(), get_type_name::().sobj_style()))?; + engine + .get_resource::(&self.material_handle.unwrap()) + .context(format!( + "Creating {} {} failed", + "Component".gobj_style(), + get_type_name::().sobj_style() + ))?; } // Check if mesh handle is valid if self.mesh_handle.is_some() { - engine.get_resource::(&self.mesh_handle.unwrap()) - .context(format!("Creating {} {} failed", "Component".gobj_style(), get_type_name::().sobj_style()))?; + engine + .get_resource::(&self.mesh_handle.unwrap()) + .context(format!( + "Creating {} {} failed", + "Component".gobj_style(), + get_type_name::().sobj_style() + ))?; } // Update mesh rendering queue + // [SIMILAR] Precompute render queue key at init; no per-draw lookups self.update_render_queue_key(&engine.resource_manager)?; Ok(()) @@ -171,35 +208,44 @@ impl Component for MeshRenderingComponent { self.entity_handle = Some(self_entity_handle); } - fn deferred_update(&mut self, engine: &mut Engine, request: usize) -> Result<()> { + fn deferred_update(&mut self, engine: &mut Engine, request: usize) -> Result<()> { match request { - DEFERRED_REQUEST_VARIANT_SET_MATERIAL => - { + DEFERRED_REQUEST_VARIANT_SET_MATERIAL => { // Check if material handle is valid - engine.get_resource::(&self.material_handle.unwrap()) - .context(format!("Setting {} {} failed", "Resource".gobj_style(), "Material".sobj_style()))?; - + engine + .get_resource::(&self.material_handle.unwrap()) + .context(format!( + "Setting {} {} failed", + "Resource".gobj_style(), + "Material".sobj_style() + ))?; + + // [SIMILAR] Update render binning key only when material changes self.update_render_queue_key(&engine.resource_manager)?; - }, - DEFERRED_REQUEST_VARIANT_SET_MESH => - { + } + DEFERRED_REQUEST_VARIANT_SET_MESH => { // Check if mesh handle is valid - engine.get_resource::(&self.mesh_handle.unwrap()) - .context(format!("Setting {} {} failed", "Resource".gobj_style(), "Mesh".sobj_style()))?; - + engine + .get_resource::(&self.mesh_handle.unwrap()) + .context(format!( + "Setting {} {} failed", + "Resource".gobj_style(), + "Mesh".sobj_style() + ))?; + + // [SIMILAR] Update render binning key only when mesh changes self.update_render_queue_key(&engine.resource_manager)?; - }, - DEFERRED_REQUEST_VARIANT_UPDATE_RENDER_QUEUE => - { + } + DEFERRED_REQUEST_VARIANT_UPDATE_RENDER_QUEUE => { // Update mesh rendering queue + // [RECOMMENDED] Keep key consistent when removing mesh/material self.update_render_queue_key(&engine.resource_manager)?; - }, - _ => - { + } + _ => { panic!("Critical: Processing deferred update request with value {} in {} failed. Handling is not implemented", request, get_type_name::().sobj_style()); } } - Ok(()) + Ok(()) } -} \ No newline at end of file +} diff --git a/engine/pill_engine/src/ecs/mod.rs b/engine/pill_engine/src/ecs/mod.rs index d1f6f0d4..b12f2c76 100644 --- a/engine/pill_engine/src/ecs/mod.rs +++ b/engine/pill_engine/src/ecs/mod.rs @@ -1,117 +1,65 @@ #![cfg_attr(debug_assertions, allow(dead_code, unused_variables))] +mod components; mod entity; mod scene; mod scene_manager; -mod components; mod systems; // --- Use --- // - Components -pub use components:: { - Component, - GlobalComponent, - ComponentDestroyer, - ConcreteComponentDestroyer, - ComponentStorage, +pub use components::{ + Component, ComponentDestroyer, ComponentStorage, ConcreteComponentDestroyer, GlobalComponent, GlobalComponentStorage, }; pub use components::camera_component::{ - CameraComponent, - CameraAspectRatio, - get_renderer_resource_handle_from_camera_component, + get_renderer_resource_handle_from_camera_component, CameraAspectRatio, CameraComponent, }; -pub use components::audio_manager_component::{ - AudioManagerComponent, - SoundType, -}; +pub use components::audio_manager_component::{AudioManagerComponent, SoundType}; -pub use components::audio_listener_component::{ - AudioListenerComponent, -}; +pub use components::audio_listener_component::AudioListenerComponent; -pub use components::audio_source_component::{ - AudioSourceComponent -}; +pub use components::audio_source_component::AudioSourceComponent; -pub use components::egui_manager_component::{ - EguiManagerComponent, -}; +pub use components::egui_manager_component::EguiManagerComponent; pub use components::deferred_update_component::{ - DeferredUpdateComponent, - DeferredUpdateManager, - DeferredUpdateManagerPointer, - DeferredUpdateRequest, - DeferredUpdateComponentRequest, - DeferredUpdateResourceRequest + DeferredUpdateComponent, DeferredUpdateComponentRequest, DeferredUpdateManager, + DeferredUpdateManagerPointer, DeferredUpdateRequest, DeferredUpdateResourceRequest, }; -pub use components::input_component::{ - InputComponent, - InputEvent, -}; +pub use components::input_component::{InputComponent, InputEvent}; pub use components::transform_component::{ - TransformComponent, - update_transform_matrices, - get_model_matrix, - get_normal_matrix, + get_model_matrix, get_normal_matrix, update_transform_matrices, TransformComponent, }; -pub use components::mesh_rendering_component::{ - MeshRenderingComponent, -}; +pub use components::mesh_rendering_component::MeshRenderingComponent; -pub use components::time_component::{ - TimeComponent, -}; +pub use components::time_component::TimeComponent; // - Systems -pub use systems::{ - SystemManager, - UpdatePhase, - SystemFunction -}; +pub use systems::{SystemFunction, SystemManager, UpdatePhase}; -pub use systems::rendering_system::{ - rendering_system, -}; +pub use systems::rendering_system::rendering_system; -pub use systems::deferred_update_system::{ - deferred_update_system, -}; +pub use systems::deferred_update_system::deferred_update_system; -pub use systems::input_system::{ - input_system, -}; +pub use systems::input_system::input_system; -pub use systems::time_system::{ - time_system, -}; +pub use systems::time_system::time_system; -pub use systems::audio_system::{ - audio_system, -}; +pub use systems::audio_system::audio_system; // - Other -pub use entity::{ - Entity, - EntityHandle, - EntityBuilder, -}; +pub use entity::{Entity, EntityBuilder, EntityHandle}; -pub use scene::{ - Scene, -}; +pub use scene::Scene; -pub use scene_manager::{ - SceneManager, - SceneHandle, -}; \ No newline at end of file +pub use scene_manager::{SceneHandle, SceneManager}; diff --git a/engine/pill_engine/src/ecs/systems/rendering_system.rs b/engine/pill_engine/src/ecs/systems/rendering_system.rs index 30956f0b..997038f9 100644 --- a/engine/pill_engine/src/ecs/systems/rendering_system.rs +++ b/engine/pill_engine/src/ecs/systems/rendering_system.rs @@ -55,6 +55,7 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { timer.record("Clear render queue"); // Clear the render queue + // [SIMILAR] Build and sort a render queue ahead of draw; separates data prep from draw per TALK engine.render_queue.clear(); engine.render_queue.reserve(200000); // Reserve space for 1000 items @@ -63,6 +64,7 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { let mut dirty_entities: Vec = Vec::new(); // Phase 1: Sweep components; route transforms needing matrix update to a batch, push clean directly + // [SIMILAR] Batch transform updates; avoid per-draw matrix work for (entity_handle, transform_component, mesh_rendering_component) in engine .scene_manager .get_two_component_iterator_mut::( @@ -75,6 +77,7 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { } // Push clean (non-dirty) items directly into the render queue + // [SIMILAR] Use precomputed render_queue_key (pipeline/material/mesh sorting key) if let Some(render_queue_key) = mesh_rendering_component.render_queue_key { let render_queue_item = RenderQueueItem { key: render_queue_key, @@ -85,6 +88,7 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { } // Phase 2: Batch update transforms with matrix_update_required + // [RECOMMENDED] Consolidate transform matrix updates in one batch outside of pass if !dirty_entities.is_empty() { timer.begin_context(&format!("Batch update {} transforms", dirty_entities.len())); for entity_handle in &dirty_entities { @@ -143,6 +147,7 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { timer.begin_context("Render"); // Render + // [API->CLIENT] Low-level renderer expects ordered render_queue and stable storages; scene graph culling/binning lives in client/high-level match engine.renderer.render( active_camera_entity_handle, &engine.render_queue, diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 5e9bdaef..2f97ff83 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -83,7 +83,7 @@ impl PillRenderer for Renderer { _vertex_shader_bytes: &[u8], _fragment_shader_bytes: &[u8], ) -> Result<()> { - // No-op: pipeline created lazily in render + // [DIFFERENT] TALK recommends building PSOs upfront; here pipeline is created in State::new (good), and this setter is a no-op Ok(()) } @@ -336,6 +336,7 @@ pub struct State { per_draw_bind_group_layout: wgpu::BindGroupLayout, per_draw_bind_group: Option, // Prebuilt PSO (static pipeline) + // [SIMILAR] Prebuilt once; no per-draw pipeline churn per TALK default_pipeline: RendererPipeline, // Other config: config::Config, @@ -817,8 +818,8 @@ struct PerDraw { ) }); - // Build grouped command list by (pipeline, material) struct DrawCmd { + // Build grouped instancing batches by (pipeline, material, mesh) mesh_handle: RendererMeshHandle, offset_u32: u32, } @@ -853,6 +854,7 @@ struct PerDraw { } // Milestone 5: Per-frame ring buffer + // [SIMILAR] Batch write dynamic per-draw data once; bind with dynamic offsets timer.begin_context("Per-draw UBO setup"); let needed = visible.len() as u64; if self.per_draw_capacity < needed { diff --git a/engine/pill_renderer/src/resources/renderer_pipeline.rs b/engine/pill_renderer/src/resources/renderer_pipeline.rs index 62176065..7df85cfe 100644 --- a/engine/pill_renderer/src/resources/renderer_pipeline.rs +++ b/engine/pill_renderer/src/resources/renderer_pipeline.rs @@ -20,6 +20,7 @@ impl RendererPipeline { vertex_layouts: &[wgpu::VertexBufferLayout], ) -> Result { // Define material bind group layout (Describes a set of resources and how they can be accessed by a shader) + // [SIMILAR] Predefined bind group layouts matching shader sets; created once, reused per TALK let material_texture_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("material_texture_bind_group_layout"), @@ -78,6 +79,7 @@ impl RendererPipeline { }); // Define camera bind group layout + // [SIMILAR] Camera UBO in a dedicated layout; updated before pass, not per-draw let camera_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("camera_bind_group_layout"), @@ -94,6 +96,7 @@ impl RendererPipeline { }); // Define per-draw bind group layout (dynamic uniform buffer) + // [SIMILAR] Dynamic offset UBO for per-draw data; avoids map/unmap per draw per TALK let per_draw_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("per_draw_bind_group_layout"), @@ -111,6 +114,7 @@ impl RendererPipeline { }); // Create pipeline layout descriptor + // [SIMILAR] Fixed PSO layout: globals(material textures), material params, camera, per-draw let pipeline_layout_descriptor = wgpu::PipelineLayoutDescriptor { label: Some("render_pipeline_layout"), bind_group_layouts: &[ From 813a29ba890eb6d18caf57e28c9a776b1ca93b36 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 04:49:47 +0200 Subject: [PATCH 24/69] feat: instance batching +5-10 fps --- engine/pill_renderer/src/renderer.rs | 96 +++++++++++++++++++--------- 1 file changed, 65 insertions(+), 31 deletions(-) diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 2f97ff83..659991fe 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -701,6 +701,7 @@ struct PerDraw { pipeline_handle: RendererPipelineHandle, material_handle: RendererMaterialHandle, mesh_handle: RendererMeshHandle, + entity_index: u32, mvp: [[f32; 4]; 4], } timer.record("Culling & MVP build"); @@ -803,6 +804,7 @@ struct PerDraw { pipeline_handle, material_handle, mesh_handle, + entity_index: render_queue_item.entity_index, mvp, }); } @@ -818,19 +820,19 @@ struct PerDraw { ) }); - struct DrawCmd { // Build grouped instancing batches by (pipeline, material, mesh) + timer.record("Sort & group draws"); + struct MeshBatch { mesh_handle: RendererMeshHandle, - offset_u32: u32, + instances: Vec<[[f32; 4]; 4]>, + base_offset_u32: u32, // offset into per-draw ring for first instance } - timer.record("Sort & group draws"); struct GroupCmd { pipeline_handle: RendererPipelineHandle, material_handle: RendererMaterialHandle, - draws: Vec, + batches: Vec, } let mut groups: Vec = Vec::new(); - let mut next_offset_u32: u32 = 0; for v in &visible { if groups .last() @@ -842,21 +844,39 @@ struct PerDraw { groups.push(GroupCmd { pipeline_handle: v.pipeline_handle, material_handle: v.material_handle, - draws: Vec::new(), + batches: Vec::new(), }); } let g = groups.last_mut().unwrap(); - g.draws.push(DrawCmd { - mesh_handle: v.mesh_handle, - offset_u32: next_offset_u32, - }); - next_offset_u32 = next_offset_u32.wrapping_add(256); + if let Some(batch) = g + .batches + .iter_mut() + .find(|b| b.mesh_handle == v.mesh_handle) + { + batch.instances.push(v.mvp); + } else { + g.batches.push(MeshBatch { + mesh_handle: v.mesh_handle, + instances: vec![v.mvp], + base_offset_u32: 0, + }); + } } + let _draw_call_count_unused: usize = groups.iter().map(|g| g.batches.len()).sum(); // Milestone 5: Per-frame ring buffer // [SIMILAR] Batch write dynamic per-draw data once; bind with dynamic offsets timer.begin_context("Per-draw UBO setup"); - let needed = visible.len() as u64; + let needed: u64 = groups + .iter() + .map(|g| { + g.batches + .iter() + .map(|b| b.instances.len() as u64) + .sum::() + }) + .sum(); + // total instances; no direct use beyond sanity, avoid unused warning by not binding if self.per_draw_capacity < needed { self.per_draw_capacity = needed.next_power_of_two().max(1); let size = self.per_draw_stride * self.per_draw_capacity; @@ -892,15 +912,23 @@ struct PerDraw { let model: [[f32; 4]; 4] = cgmath::Matrix4::::from_scale(1.0).into(); let tint: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; let mut staging: Vec = Vec::with_capacity((self.per_draw_stride * needed) as usize); - for v in &visible { - let pd = PerDrawStd140 { - mvp: v.mvp, - model, - tint, - }; - staging.extend_from_slice(bytemuck::bytes_of(&pd)); - let pad = (self.per_draw_stride as usize) - std::mem::size_of::(); - staging.extend(std::iter::repeat(0u8).take(pad)); + let mut next_offset_u32: u32 = 0; + for g in groups.iter_mut() { + for b in g.batches.iter_mut() { + b.base_offset_u32 = next_offset_u32; + for mvp in &b.instances { + let pd = PerDrawStd140 { + mvp: *mvp, + model, + tint, + }; + staging.extend_from_slice(bytemuck::bytes_of(&pd)); + let pad = + (self.per_draw_stride as usize) - std::mem::size_of::(); + staging.extend(std::iter::repeat(0u8).take(pad)); + next_offset_u32 = next_offset_u32.wrapping_add(self.per_draw_stride as u32); + } + } } if let Some(buf) = &self.per_draw_buffer { self.queue.write_buffer(buf, 0, &staging); @@ -938,7 +966,7 @@ struct PerDraw { occlusion_query_set: None, }); // [SIMILAR] Bind pipeline/material once per group; change only minimal per-draw state (offsets) - // Iterate groups: bind pipeline/material once per group; mesh + offset per draw + // Instancing: one draw per (pipeline, material, mesh) batch with instance_count for group in &groups { timer.begin_context(&format!( "Pipeline {} / Material {}", @@ -956,22 +984,28 @@ struct PerDraw { rpass.set_bind_group(1, &material.parameter_bind_group, &[]); rpass.set_bind_group(2, &renderer_camera.bind_group, &[]); - for draw in &group.draws { + for batch in &group.batches { let mesh = self .renderer_resource_storage .meshes - .get(draw.mesh_handle) + .get(batch.mesh_handle) .unwrap(); - // [DIFF] VB/IB are rebound each draw; TALK suggests packing meshes in large buffers and using base vertex/index to avoid rebinding // [API->CLIENT] Provide packed mesh atlas + per-draw base offsets so low-level can draw without re-binding buffers + // [DIFF] VB/IB still rebound per batch; packing meshes could avoid rebinding rpass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); rpass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32); - rpass.set_bind_group( - 3, - self.per_draw_bind_group.as_ref().unwrap(), - &[draw.offset_u32], - ); - rpass.draw_indexed(0..mesh.index_count, 0, 0..1); + // Per-instance draws with dynamic uniform offsets (no shader change required) + for i in 0..batch.instances.len() { + let offset = batch + .base_offset_u32 + .wrapping_add((i as u32) * (self.per_draw_stride as u32)); + rpass.set_bind_group( + 3, + self.per_draw_bind_group.as_ref().unwrap(), + &[offset], + ); + rpass.draw_indexed(0..mesh.index_count, 0, 0..1); + } } timer.end_context()?; // End pipeline/material group } From 3cca4259989b2db6b79bc44cf6a193a8cd55fc8e Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 04:58:56 +0200 Subject: [PATCH 25/69] feat: add counters --- engine/pill_core/src/timer.rs | 44 +++++-- .../ecs/components/egui_manager_component.rs | 14 +++ .../src/ecs/systems/system_manager.rs | 110 ++++++++++++++---- engine/pill_renderer/src/renderer.rs | 4 +- 4 files changed, 141 insertions(+), 31 deletions(-) diff --git a/engine/pill_core/src/timer.rs b/engine/pill_core/src/timer.rs index acdd8ca8..5ac84886 100644 --- a/engine/pill_core/src/timer.rs +++ b/engine/pill_core/src/timer.rs @@ -1,10 +1,8 @@ use std::time::Instant; -use anyhow::{Context, Result, Error}; +use anyhow::{Context, Error, Result}; use indexmap::IndexMap; - - #[derive(Debug, Clone)] pub struct TimerRecord { pub label: String, @@ -26,6 +24,8 @@ pub struct Timer { pub records: Vec, current_label: Option, current_label_start: Option, + // Simple per-frame counters keyed by label + counters: IndexMap, } impl Timer { @@ -35,6 +35,7 @@ impl Timer { records: Vec::new(), current_label: None, current_label_start: None, + counters: IndexMap::new(), } } @@ -79,9 +80,11 @@ impl Timer { } fn flush_record(&mut self) { - if let (Some(label), Some(start), Some(current)) = - (&self.current_label, self.current_label_start, self.stack.last_mut()) - { + if let (Some(label), Some(start), Some(current)) = ( + &self.current_label, + self.current_label_start, + self.stack.last_mut(), + ) { let duration = start.elapsed().as_secs_f32() * 1000.0; current.subrecords.push(TimerRecord { label: label.clone(), @@ -98,6 +101,25 @@ impl Timer { self.records.iter().map(|r| r.duration).sum() } + // --- Counters --- + pub fn set_counter(&mut self, label: impl Into, value: u64) { + self.counters.insert(label.into(), value); + } + + // pub fn increment_counter(&mut self, label: impl Into, delta: u64) { + // let key = label.into(); + // let entry = self.counters.entry(key).or_insert(0); + // *entry = entry.saturating_add(delta); + // } + + pub fn get_counter(&self, label: &str) -> Option { + self.counters.get(label).copied() + } + + pub fn counters(&self) -> &IndexMap { + &self.counters + } + pub fn print(&self, indent: usize) { for record in &self.records { Self::print_record(record, indent); @@ -105,9 +127,15 @@ impl Timer { } fn print_record(record: &TimerRecord, indent: usize) { - println!("{:indent$}- {}: {:.3}ms", "", record.label, record.duration, indent = indent); + println!( + "{:indent$}- {}: {:.3}ms", + "", + record.label, + record.duration, + indent = indent + ); for sub in &record.subrecords { Self::print_record(sub, indent + 2); } } -} \ No newline at end of file +} diff --git a/engine/pill_engine/src/ecs/components/egui_manager_component.rs b/engine/pill_engine/src/ecs/components/egui_manager_component.rs index 4410c823..1bb52b66 100644 --- a/engine/pill_engine/src/ecs/components/egui_manager_component.rs +++ b/engine/pill_engine/src/ecs/components/egui_manager_component.rs @@ -70,6 +70,17 @@ impl EguiManagerComponent { let window_w = engine.window_size.width; let window_h = engine.window_size.height; + // Snapshot draw call counter from last frame + let total_draw_calls: Option = engine + .system_manager + .peek_system_timer( + crate::config::RENDERING_SYSTEM.name, + crate::config::RENDERING_SYSTEM.update_phase, + ) + .ok() + .and_then(|t| t) + .and_then(|t| t.get_counter("draw_calls")); + let ui = Box::new(move |ui: &egui::Context| { egui::Window::new("PillEngine") .default_open(true) @@ -91,6 +102,9 @@ impl EguiManagerComponent { frame_delta_time ))); ui.add(egui::Label::new(format!("Entities: {}", entity_count))); + if let Some(dc) = total_draw_calls { + ui.add(egui::Label::new(format!("Draw calls: {}", dc))); + } ui.separator(); ui.add(egui::Label::new(format!( "Systems: {}, Total delta time: {:.3} ms", diff --git a/engine/pill_engine/src/ecs/systems/system_manager.rs b/engine/pill_engine/src/ecs/systems/system_manager.rs index 276b76e2..57014463 100644 --- a/engine/pill_engine/src/ecs/systems/system_manager.rs +++ b/engine/pill_engine/src/ecs/systems/system_manager.rs @@ -2,11 +2,11 @@ use crate::engine::Engine; use pill_core::{EngineError, Timer}; -use core::fmt; -use std::{collections::HashMap, fmt::Display}; -use anyhow::{Result, Context, Error}; +use anyhow::{Context, Error, Result}; use boolinator::Boolinator; +use core::fmt; use indexmap::IndexMap; +use std::{collections::HashMap, fmt::Display}; pub type SystemFunction = fn(engine: &mut Engine) -> Result<()>; @@ -37,39 +37,54 @@ pub struct SystemManager { impl SystemManager { pub fn new() -> Self { - let mut update_phases = IndexMap::>::new(); + let mut update_phases = IndexMap::>::new(); // Register phases update_phases.insert(UpdatePhase::PreGame, IndexMap::::new()); update_phases.insert(UpdatePhase::Game, IndexMap::::new()); update_phases.insert(UpdatePhase::PostGame, IndexMap::::new()); - Self { - update_phases - } + Self { update_phases } } pub fn get_system(&mut self, name: &str, update_phase: UpdatePhase) -> Result<&mut System> { // Find collection of systems for given update phase - let system_collection = self.update_phases.get_mut(&update_phase).ok_or(Error::new(EngineError::SystemUpdatePhaseNotFound(format!("{}", update_phase))))?; + let system_collection = self.update_phases.get_mut(&update_phase).ok_or(Error::new( + EngineError::SystemUpdatePhaseNotFound(format!("{}", update_phase)), + ))?; // Get system by name - system_collection.get_mut(name).ok_or(Error::new(EngineError::SystemNotFound(name.to_string(), format!("{}", update_phase)))) + system_collection + .get_mut(name) + .ok_or(Error::new(EngineError::SystemNotFound( + name.to_string(), + format!("{}", update_phase), + ))) } - pub fn add_system(&mut self, name: &str, system_function: SystemFunction, update_phase: UpdatePhase) -> Result<()> { + pub fn add_system( + &mut self, + name: &str, + system_function: SystemFunction, + update_phase: UpdatePhase, + ) -> Result<()> { // Find collection of systems for given update phase - let system_collection = self.update_phases.get_mut(&update_phase).ok_or(Error::new(EngineError::SystemUpdatePhaseNotFound(format!("{}", update_phase))))?; + let system_collection = self.update_phases.get_mut(&update_phase).ok_or(Error::new( + EngineError::SystemUpdatePhaseNotFound(format!("{}", update_phase)), + ))?; // Check if system with that name already exists if system_collection.contains_key(name) { - return Err(Error::new(EngineError::SystemAlreadyExists(name.to_string(), format!("{}", update_phase)))) + return Err(Error::new(EngineError::SystemAlreadyExists( + name.to_string(), + format!("{}", update_phase), + ))); } // Create system object let system_object = System { name: name.to_string(), - update_phase, + update_phase, system_function, enabled: true, timer: Some(Timer::new()), @@ -81,13 +96,18 @@ impl SystemManager { Ok(()) } - pub fn remove_system(&mut self, name: &str, update_phase: UpdatePhase) -> Result<()> { + pub fn remove_system(&mut self, name: &str, update_phase: UpdatePhase) -> Result<()> { // Find collection of systems for given update phase - let system_collection = self.update_phases.get_mut(&update_phase).ok_or(Error::new(EngineError::SystemUpdatePhaseNotFound(format!("{}", update_phase))))?; + let system_collection = self.update_phases.get_mut(&update_phase).ok_or(Error::new( + EngineError::SystemUpdatePhaseNotFound(format!("{}", update_phase)), + ))?; // Check if system with that name exists if !system_collection.contains_key(name) { - return Err(Error::new(EngineError::SystemNotFound(name.to_string(), format!("{}", update_phase)))) + return Err(Error::new(EngineError::SystemNotFound( + name.to_string(), + format!("{}", update_phase), + ))); } // Remove system @@ -96,12 +116,25 @@ impl SystemManager { Ok(()) } - pub fn toggle_system(&mut self, name: &str, update_phase: UpdatePhase, enabled: bool) -> Result<()> { + pub fn toggle_system( + &mut self, + name: &str, + update_phase: UpdatePhase, + enabled: bool, + ) -> Result<()> { // Find collection of systems for given update phase - let system_collection = self.update_phases.get_mut(&update_phase).ok_or(Error::new(EngineError::SystemUpdatePhaseNotFound(format!("{}", update_phase))))?; + let system_collection = self.update_phases.get_mut(&update_phase).ok_or(Error::new( + EngineError::SystemUpdatePhaseNotFound(format!("{}", update_phase)), + ))?; // Check if system with that name exists - let system = system_collection.get_mut(name).ok_or(Error::new(EngineError::SystemNotFound(name.to_string(), format!("{}", update_phase))))?; + let system = + system_collection + .get_mut(name) + .ok_or(Error::new(EngineError::SystemNotFound( + name.to_string(), + format!("{}", update_phase), + )))?; // Set system state system.enabled = enabled; @@ -113,7 +146,11 @@ impl SystemManager { // It will pass the ownership of the timer to the requsting scope. // This has to be returned back using update_system_timer function, otherwise engine will panic. // NOTE: Before system function is called, engine already starts "System update" context in the timer - pub fn get_system_timer(&mut self, name: &str, update_phase: UpdatePhase) -> Result> { + pub fn get_system_timer( + &mut self, + name: &str, + update_phase: UpdatePhase, + ) -> Result> { // Get system by name let system: &mut System = self.get_system(name, update_phase)?; @@ -121,7 +158,12 @@ impl SystemManager { Ok(system.timer.take()) } - pub fn update_system_timer(&mut self, name: &str, update_phase: UpdatePhase, timer: Timer) -> Result<()> { + pub fn update_system_timer( + &mut self, + name: &str, + update_phase: UpdatePhase, + timer: Timer, + ) -> Result<()> { // Get system by name let system = self.get_system(name, update_phase)?; @@ -130,4 +172,28 @@ impl SystemManager { Ok(()) } -} \ No newline at end of file + + // Peek system timer without taking ownership (cloned snapshot) + pub fn peek_system_timer( + &self, + name: &str, + update_phase: UpdatePhase, + ) -> Result> { + // Find collection of systems for given update phase + let system_collection = self.update_phases.get(&update_phase).ok_or(Error::new( + EngineError::SystemUpdatePhaseNotFound(format!("{}", update_phase)), + ))?; + + // Get system by name + let timer_opt = system_collection + .get(name) + .ok_or(Error::new(EngineError::SystemNotFound( + name.to_string(), + format!("{}", update_phase), + )))? + .timer + .clone(); + + Ok(timer_opt) + } +} diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 659991fe..b911e425 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -862,7 +862,9 @@ struct PerDraw { }); } } - let _draw_call_count_unused: usize = groups.iter().map(|g| g.batches.len()).sum(); + let draw_call_count: usize = groups.iter().map(|g| g.batches.len()).sum(); + // Expose draw call count via per-frame timer counters for UI/metrics + timer.set_counter("draw_calls", draw_call_count as u64); // Milestone 5: Per-frame ring buffer // [SIMILAR] Batch write dynamic per-draw data once; bind with dynamic offsets From 0241ce5cc2c3592a53bbd4ad5f0d5cfb0fe850f8 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 19:22:16 +0200 Subject: [PATCH 26/69] feat: ignore due to file change each build --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index dbe2827b..cf0b3ef6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ build/ crates/pill_standalone/Cargo.toml examples/*/Cargo.toml + +engine/Cargo.toml From 959e969c64b48b710e0ee7af5059b5b1507249ca Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 20:05:42 +0200 Subject: [PATCH 27/69] feat: M6 render to offscreen RT and compose to framebuffer --- engine/pill_renderer/src/renderer.rs | 232 ++++++++++++++++++++++++++- 1 file changed, 225 insertions(+), 7 deletions(-) diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index b911e425..99307aac 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -65,6 +65,48 @@ fn compile_glsl_to_wgsl(source: &str, stage: naga::ShaderStage) -> Result RendererTexture { + let size = wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }; + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some(label), + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Nearest, + lod_min_clamp: 0.0, + lod_max_clamp: 100.0, + ..Default::default() + }); + RendererTexture { + texture, + texture_view, + sampler, + } +} + impl PillRenderer for Renderer { fn new(window: Arc, config: config::Config) -> Self { info!("Initializing {}", "Renderer".mobj_style()); @@ -329,6 +371,7 @@ pub struct State { color_format: wgpu::TextureFormat, depth_format: wgpu::TextureFormat, depth_texture: RendererTexture, + offscreen_color_texture: RendererTexture, // Per-frame dynamic UBO ring (Milestone 5) per_draw_stride: u64, per_draw_capacity: u64, // in elements @@ -338,6 +381,10 @@ pub struct State { // Prebuilt PSO (static pipeline) // [SIMILAR] Prebuilt once; no per-draw pipeline churn per TALK default_pipeline: RendererPipeline, + // Composition (Milestone 6) + composite_bind_group_layout: wgpu::BindGroupLayout, + composite_pipeline: wgpu::RenderPipeline, + composite_bind_group: wgpu::BindGroup, // Other config: config::Config, egui_renderer: crate::egui::EguiRenderer, // TODO: Separate system adding Pass @@ -467,6 +514,15 @@ impl State { let color_format = surface_configuration.format; let depth_format = wgpu::TextureFormat::Depth32Float; + // Milestone 6: Create offscreen color target (RENDER_ATTACHMENT | TEXTURE_BINDING) + let offscreen_color_texture = create_render_target( + &device, + color_format, + surface_configuration.width, + surface_configuration.height, + "offscreen_color", + ); + let egui_renderer = EguiRenderer::new(&device, surface_configuration.format, None, 1, window_ref); @@ -537,6 +593,109 @@ struct PerDraw { ) .unwrap(); + // Milestone 6: Composition pipeline (fullscreen triangle sampling offscreen texture) + let comp_vs_wgsl = r#" +struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; +@vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var pos = array, 3>( + vec2(-1.0, -3.0), + vec2( 3.0, 1.0), + vec2(-1.0, 1.0) + ); + var tuv = array, 3>( + vec2(0.0, 2.0), + vec2(2.0, 0.0), + vec2(0.0, 0.0) + ); + var o: VSOut; + o.pos = vec4(pos[vi], 0.0, 1.0); + o.uv = tuv[vi]; + return o; +} +"#; + let comp_fs_wgsl = r#" +@group(0) @binding(0) var t_src: texture_2d; +@group(0) @binding(1) var s_src: sampler; +@fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + return textureSample(t_src, s_src, uv); +} +"#; + let comp_vs = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("compose_vs"), + source: wgpu::ShaderSource::Wgsl(comp_vs_wgsl.into()), + }); + let comp_fs = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("compose_fs"), + source: wgpu::ShaderSource::Wgsl(comp_fs_wgsl.into()), + }); + let composite_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("compose_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + let comp_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("compose_pl"), + bind_group_layouts: &[&composite_bind_group_layout], + push_constant_ranges: &[], + }); + let composite_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("compose_pipeline"), + layout: Some(&comp_pipeline_layout), + vertex: wgpu::VertexState { + module: &comp_vs, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &comp_fs, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: color_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + let composite_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("compose_bg"), + layout: &composite_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView( + &offscreen_color_texture.texture_view, + ), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&offscreen_color_texture.sampler), + }, + ], + }); + // Create state let per_draw_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { @@ -564,6 +723,7 @@ struct PerDraw { color_format, depth_format, depth_texture, + offscreen_color_texture, // Per-frame UBO ring init per_draw_stride: 256, per_draw_capacity: 0, @@ -573,6 +733,10 @@ struct PerDraw { per_draw_bind_group: None, // Prebuilt PSO default_pipeline, + // Composition + composite_bind_group_layout, + composite_pipeline, + composite_bind_group, // Other config, egui_renderer, @@ -592,6 +756,36 @@ struct PerDraw { "depth_texture", ) .unwrap(); + // Recreate offscreen color target and its bind group + self.offscreen_color_texture = create_render_target( + &self.device, + self.color_format, + self.surface_configuration.width, + self.surface_configuration.height, + "offscreen_color", + ); + self.composite_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("compose_bg"), + layout: &self.composite_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView( + &self.offscreen_color_texture.texture_view, + ), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler( + &self.offscreen_color_texture.sampler, + ), + }, + ], + }); + // Old offscreen texture/view/sampler are dropped when replaced; wgpu defers actual GPU resource + // destruction until safe. See optional early reclamation via device.poll: + // https://docs.rs/wgpu/latest/wgpu/struct.Device.html#method.poll + self.device.poll(wgpu::Maintain::Wait); } } @@ -939,12 +1133,12 @@ struct PerDraw { timer.end_context()?; { - timer.begin_context("Main pass"); - // [DIFF] TALK advocates explicit per-pass transitions; wgpu hides resource states implicitly here + timer.begin_context("Offscreen pass"); + // Render scene into offscreen color target (T0) let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("m2_inline_pass"), + label: Some("m6_offscreen_pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, + view: &self.offscreen_color_texture.texture_view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { @@ -1011,12 +1205,36 @@ struct PerDraw { } timer.end_context()?; // End pipeline/material group } - timer.record("Encode render pass"); + timer.record("Encode offscreen pass"); + } + timer.end_context()?; // End "Offscreen pass" + + // Composition pass: sample T0 and draw to swapchain view + { + timer.begin_context("Fullscreen resolve pass"); + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("m6_compose_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + rpass.set_pipeline(&self.composite_pipeline); + rpass.set_bind_group(0, &self.composite_bind_group, &[]); + rpass.draw(0..3, 0..1); + timer.record("Encode compose pass"); } - timer.end_context()?; // End "Main pass" + timer.end_context()?; // End "Compose pass" self.queue.submit([encoder.finish()]); - timer.record("Submit main pass"); + timer.record("Submit scene+compose passes"); // Egui overlay (load over the rendered frame) timer.begin_context("UI pass"); From eec95f32be31346eb99d9e5969568ec787cd3787 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 20:17:42 +0200 Subject: [PATCH 28/69] feat: M6 HDR, tonnemap --- engine/pill_renderer/src/renderer.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 99307aac..473ca216 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -511,7 +511,12 @@ impl State { RendererTexture::new_depth_texture(&device, &surface_configuration, "depth_texture") .unwrap(); - let color_format = surface_configuration.format; + // Use Rgba16Float for HDR color buffers; it’s the common, well-supported, + // performant choice. Reserve Rgba32Float for niche cases needing extreme + // precision and accept the 2× bandwidth/memory hit. If alpha isn’t needed, + // Rg11b10Float/R11G11B10_FLOAT is a fast alternative. + // Tone-map to the sRGB swapchain in the composite pass. + let color_format = wgpu::TextureFormat::Rgba16Float; let depth_format = wgpu::TextureFormat::Depth32Float; // Milestone 6: Create offscreen color target (RENDER_ATTACHMENT | TEXTURE_BINDING) @@ -617,7 +622,10 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; @group(0) @binding(0) var t_src: texture_2d; @group(0) @binding(1) var s_src: sampler; @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { - return textureSample(t_src, s_src, uv); + let hdr = textureSample(t_src, s_src, uv).rgb; + // Reinhard tone mapping; output remains in linear space. The sRGB swapchain will encode. + let ldr = hdr / (1.0 + hdr); + return vec4(ldr, 1.0); } "#; let comp_vs = device.create_shader_module(wgpu::ShaderModuleDescriptor { @@ -668,7 +676,8 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; module: &comp_fs, entry_point: "main", targets: &[Some(wgpu::ColorTargetState { - format: color_format, + // Composition writes to the swapchain view; target must match surface format + format: surface_configuration.format, blend: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrites::ALL, })], From 01832a7eebd689b1ebdbf56ff62b956f0dbbdc6c Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 21:40:48 +0200 Subject: [PATCH 29/69] feat: M6 overlay quad pass --- engine/pill_renderer/src/renderer.rs | 133 ++++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 1 deletion(-) diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 473ca216..2370e174 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -29,6 +29,7 @@ use anyhow::{Error, Result}; use log::info; use crate::egui::EguiRenderer; +use wgpu::util::DeviceExt; pub const MAX_INSTANCE_BATCH_SIZE: usize = 10000; // Maximum number of instances that can be drawn in a single draw call pub const INITIAL_INSTANCE_VECTOR_CAPACITY: usize = 10000; @@ -385,6 +386,11 @@ pub struct State { composite_bind_group_layout: wgpu::BindGroupLayout, composite_pipeline: wgpu::RenderPipeline, composite_bind_group: wgpu::BindGroup, + // Overlay (UV gradient quad in top-right) + overlay_pipeline: wgpu::RenderPipeline, + overlay_rect_bind_group_layout: wgpu::BindGroupLayout, + overlay_rect_bind_group: wgpu::BindGroup, + overlay_rect_buffer: wgpu::Buffer, // Other config: config::Config, egui_renderer: crate::egui::EguiRenderer, // TODO: Separate system adding Pass @@ -705,6 +711,120 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; ], }); + // Overlay UV gradient pipeline (small quad in top-right corner) + let overlay_vs_wgsl = r#" +@group(0) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] + +struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; +@vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var unit = array, 6>( + vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), + vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) + ); + + // let temp = vec4(0.25, 0.25, 0.75, 0.75); + + let p = unit[vi]; // [-1,1] NDC space + let s = p*0.5+0.5; // [0,1] screen space + let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] + // let r = temp.xy + s * (temp.zw - temp.xy); // move by rect [0,1] + let ndc = r*2.0-1.0; // [-1,1] NDC space + + var out: VSOut; + out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); + // out.pos = vec4(p.x, p.y, 0.0, 1.0); + out.uv = s; + return out; +} +"#; + let overlay_fs_wgsl = r#" +@fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + return vec4(uv, 0.0, 0.5); +} +"#; + let overlay_vs = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_vs"), + source: wgpu::ShaderSource::Wgsl(overlay_vs_wgsl.into()), + }); + let overlay_fs = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_fs"), + source: wgpu::ShaderSource::Wgsl(overlay_fs_wgsl.into()), + }); + let overlay_rect_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_rect_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }], + }); + let overlay_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("overlay_pl"), + bind_group_layouts: &[&overlay_rect_bind_group_layout], + push_constant_ranges: &[], + }); + // No vertex buffer layout needed; vertices are generated procedurally in the vertex shader + let overlay_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("overlay_pipeline"), + layout: Some(&overlay_pipeline_layout), + vertex: wgpu::VertexState { + module: &overlay_vs, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &overlay_fs, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: surface_configuration.format, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + cull_mode: None, // avoid winding flip from Y-flip causing cull + ..wgpu::PrimitiveState::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + // No vertex buffer needed; vertices generated procedurally in VS + let rect: [f32; 4] = [0.75, 0.75, 0.95, 0.95]; // top-right relative to screen + + // Apple Metal buffer alignment rules for constant/uniform buffers (256-byte). + // See Uniform Buffers and Alignment in wgpu’s docs: + // https://docs.rs/wgpu/latest/wgpu/struct.Device.html#method.create_buffer + // and Apple Metal feature set tables (256-byte constant buffer alignment requirement). + let overlay_rect_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_rect_ubo"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + queue.write_buffer(&overlay_rect_buffer, 0, bytemuck::bytes_of(&rect)); + let overlay_rect_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_rect_bg"), + layout: &overlay_rect_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &overlay_rect_buffer, + offset: 0, + size: Some(std::num::NonZeroU64::new(16).unwrap()), + }), + }], + }); + // Create state let per_draw_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { @@ -746,6 +866,11 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; composite_bind_group_layout, composite_pipeline, composite_bind_group, + // Overlay + overlay_pipeline, + overlay_rect_bind_group_layout, + overlay_rect_bind_group, + overlay_rect_buffer, // Other config, egui_renderer, @@ -1218,7 +1343,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; } timer.end_context()?; // End "Offscreen pass" - // Composition pass: sample T0 and draw to swapchain view + // Composition pass: sample T0 and draw to swapchain view, then overlay UV quad { timer.begin_context("Fullscreen resolve pass"); let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { @@ -1238,6 +1363,12 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; rpass.set_pipeline(&self.composite_pipeline); rpass.set_bind_group(0, &self.composite_bind_group, &[]); rpass.draw(0..3, 0..1); + + // Draw small overlay UV quad in normalized rect via UBO + rpass.set_pipeline(&self.overlay_pipeline); + rpass.set_bind_group(0, &self.overlay_rect_bind_group, &[]); + rpass.draw(0..6, 0..1); + timer.record("Encode compose pass"); } timer.end_context()?; // End "Compose pass" From 41f547f889260e40a427daa5f78e013c8775c009 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 21:43:14 +0200 Subject: [PATCH 30/69] feat: M6 overlay bookkeeping --- engine/pill_renderer/src/renderer.rs | 255 ++++++++++++++------------- 1 file changed, 133 insertions(+), 122 deletions(-) diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 2370e174..06f3cd18 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -108,6 +108,134 @@ fn create_render_target( } } +// Overlay (UV gradient quad) resources grouped for clarity +struct OverlayResources { + pipeline: wgpu::RenderPipeline, + rect_bind_group_layout: wgpu::BindGroupLayout, + rect_bind_group: wgpu::BindGroup, + rect_buffer: wgpu::Buffer, +} + +// KISS helper: build overlay pipeline, UBO layout/buffer, and bind group +fn create_overlay_resources( + device: &wgpu::Device, + queue: &wgpu::Queue, + surface_format: wgpu::TextureFormat, +) -> OverlayResources { + let overlay_vs_wgsl = r#" +@group(0) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] + +struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; +@vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var unit = array, 6>( + vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), + vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) + ); + let p = unit[vi]; // [-1,1] NDC space + let s = p*0.5+0.5; // [0,1] screen space + let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] + let ndc = r*2.0-1.0; // [-1,1] NDC space + var out: VSOut; + out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); + out.uv = s; + return out; +} +"#; + let overlay_fs_wgsl = r#" +@fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + return vec4(uv, 0.0, 0.5); +} +"#; + + let overlay_vs = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_vs"), + source: wgpu::ShaderSource::Wgsl(overlay_vs_wgsl.into()), + }); + let overlay_fs = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_fs"), + source: wgpu::ShaderSource::Wgsl(overlay_fs_wgsl.into()), + }); + + let overlay_rect_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_rect_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }], + }); + let overlay_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("overlay_pl"), + bind_group_layouts: &[&overlay_rect_bind_group_layout], + push_constant_ranges: &[], + }); + + // No vertex buffer layout needed; vertices are generated procedurally in the vertex shader + let overlay_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("overlay_pipeline"), + layout: Some(&overlay_pipeline_layout), + vertex: wgpu::VertexState { + module: &overlay_vs, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &overlay_fs, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: surface_format, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + cull_mode: None, + ..wgpu::PrimitiveState::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + // Apple Metal constant buffer alignment (256 bytes) + let overlay_rect_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_rect_ubo"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let rect: [f32; 4] = [0.75, 0.75, 0.95, 0.95]; // top-right relative to screen + queue.write_buffer(&overlay_rect_buffer, 0, bytemuck::bytes_of(&rect)); + + let overlay_rect_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_rect_bg"), + layout: &overlay_rect_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &overlay_rect_buffer, + offset: 0, + size: Some(std::num::NonZeroU64::new(16).unwrap()), + }), + }], + }); + + OverlayResources { + pipeline: overlay_pipeline, + rect_bind_group_layout: overlay_rect_bind_group_layout, + rect_bind_group: overlay_rect_bind_group, + rect_buffer: overlay_rect_buffer, + } +} + impl PillRenderer for Renderer { fn new(window: Arc, config: config::Config) -> Self { info!("Initializing {}", "Renderer".mobj_style()); @@ -387,10 +515,7 @@ pub struct State { composite_pipeline: wgpu::RenderPipeline, composite_bind_group: wgpu::BindGroup, // Overlay (UV gradient quad in top-right) - overlay_pipeline: wgpu::RenderPipeline, - overlay_rect_bind_group_layout: wgpu::BindGroupLayout, - overlay_rect_bind_group: wgpu::BindGroup, - overlay_rect_buffer: wgpu::Buffer, + overlay: OverlayResources, // Other config: config::Config, egui_renderer: crate::egui::EguiRenderer, // TODO: Separate system adding Pass @@ -712,118 +837,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; }); // Overlay UV gradient pipeline (small quad in top-right corner) - let overlay_vs_wgsl = r#" -@group(0) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] - -struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; -@vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { - var unit = array, 6>( - vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), - vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) - ); - - // let temp = vec4(0.25, 0.25, 0.75, 0.75); - - let p = unit[vi]; // [-1,1] NDC space - let s = p*0.5+0.5; // [0,1] screen space - let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] - // let r = temp.xy + s * (temp.zw - temp.xy); // move by rect [0,1] - let ndc = r*2.0-1.0; // [-1,1] NDC space - - var out: VSOut; - out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); - // out.pos = vec4(p.x, p.y, 0.0, 1.0); - out.uv = s; - return out; -} -"#; - let overlay_fs_wgsl = r#" -@fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { - return vec4(uv, 0.0, 0.5); -} -"#; - let overlay_vs = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("overlay_vs"), - source: wgpu::ShaderSource::Wgsl(overlay_vs_wgsl.into()), - }); - let overlay_fs = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("overlay_fs"), - source: wgpu::ShaderSource::Wgsl(overlay_fs_wgsl.into()), - }); - let overlay_rect_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("overlay_rect_bgl"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), - }, - count: None, - }], - }); - let overlay_pipeline_layout = - device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("overlay_pl"), - bind_group_layouts: &[&overlay_rect_bind_group_layout], - push_constant_ranges: &[], - }); - // No vertex buffer layout needed; vertices are generated procedurally in the vertex shader - let overlay_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("overlay_pipeline"), - layout: Some(&overlay_pipeline_layout), - vertex: wgpu::VertexState { - module: &overlay_vs, - entry_point: "main", - buffers: &[], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &overlay_fs, - entry_point: "main", - targets: &[Some(wgpu::ColorTargetState { - format: surface_configuration.format, - blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState { - cull_mode: None, // avoid winding flip from Y-flip causing cull - ..wgpu::PrimitiveState::default() - }, - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview: None, - }); - // No vertex buffer needed; vertices generated procedurally in VS - let rect: [f32; 4] = [0.75, 0.75, 0.95, 0.95]; // top-right relative to screen - - // Apple Metal buffer alignment rules for constant/uniform buffers (256-byte). - // See Uniform Buffers and Alignment in wgpu’s docs: - // https://docs.rs/wgpu/latest/wgpu/struct.Device.html#method.create_buffer - // and Apple Metal feature set tables (256-byte constant buffer alignment requirement). - let overlay_rect_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("overlay_rect_ubo"), - size: 256, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - queue.write_buffer(&overlay_rect_buffer, 0, bytemuck::bytes_of(&rect)); - let overlay_rect_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_rect_bg"), - layout: &overlay_rect_bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &overlay_rect_buffer, - offset: 0, - size: Some(std::num::NonZeroU64::new(16).unwrap()), - }), - }], - }); + let overlay = create_overlay_resources(&device, &queue, surface_configuration.format); // Create state let per_draw_bind_group_layout = @@ -867,10 +881,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; composite_pipeline, composite_bind_group, // Overlay - overlay_pipeline, - overlay_rect_bind_group_layout, - overlay_rect_bind_group, - overlay_rect_buffer, + overlay, // Other config, egui_renderer, @@ -1365,8 +1376,8 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; rpass.draw(0..3, 0..1); // Draw small overlay UV quad in normalized rect via UBO - rpass.set_pipeline(&self.overlay_pipeline); - rpass.set_bind_group(0, &self.overlay_rect_bind_group, &[]); + rpass.set_pipeline(&self.overlay.pipeline); + rpass.set_bind_group(0, &self.overlay.rect_bind_group, &[]); rpass.draw(0..6, 0..1); timer.record("Encode compose pass"); From 1f274af8b37ffecc34eb688cfa9a6ecddb0f2044 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 22:40:36 +0200 Subject: [PATCH 31/69] feat: M6 texture overlay --- .../res/pill_logo_horizontal_white.png | Bin 0 -> 9434 bytes engine/pill_renderer/src/renderer.rs | 297 +++++++++++++++++- 2 files changed, 281 insertions(+), 16 deletions(-) create mode 100644 engine/pill_renderer/res/pill_logo_horizontal_white.png diff --git a/engine/pill_renderer/res/pill_logo_horizontal_white.png b/engine/pill_renderer/res/pill_logo_horizontal_white.png new file mode 100644 index 0000000000000000000000000000000000000000..d4e5bc353398f8fc570b1d93cb403cc45ad579e2 GIT binary patch literal 9434 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 literal 0 HcmV?d00001 diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 06f3cd18..972b121a 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -9,12 +9,15 @@ use crate::{ }, }; -use pill_engine::internal::{ - get_renderer_resource_handle_from_camera_component, CameraComponent, ComponentStorage, - EntityHandle, MaterialParameterMap, MaterialTextureMap, MeshData, PillRenderer, - RenderQueueItem, RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, - RendererPipelineHandle, RendererTextureHandle, TextureType, TransformComponent, - RENDER_QUEUE_KEY_ORDER, +use pill_engine::{ + game::{ResourceLoadType, Texture}, + internal::{ + get_renderer_resource_handle_from_camera_component, CameraComponent, ComponentStorage, + EntityHandle, MaterialParameterMap, MaterialTextureMap, MeshData, PillRenderer, + RenderQueueItem, RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, + RendererPipelineHandle, RendererTextureHandle, TextureType, TransformComponent, + RENDER_QUEUE_KEY_ORDER, + }, }; use pill_core::{PillSlotMapKey, PillSlotMapKeyData, PillStyle, RendererError, Timer}; @@ -29,6 +32,7 @@ use anyhow::{Error, Result}; use log::info; use crate::egui::EguiRenderer; +use image::GenericImageView; use wgpu::util::DeviceExt; pub const MAX_INSTANCE_BATCH_SIZE: usize = 10000; // Maximum number of instances that can be drawn in a single draw call @@ -117,10 +121,11 @@ struct OverlayResources { } // KISS helper: build overlay pipeline, UBO layout/buffer, and bind group -fn create_overlay_resources( +fn create_overlay_uv( device: &wgpu::Device, queue: &wgpu::Queue, surface_format: wgpu::TextureFormat, + rect: [f32; 4], ) -> OverlayResources { let overlay_vs_wgsl = r#" @group(0) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] @@ -212,7 +217,6 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); - let rect: [f32; 4] = [0.75, 0.75, 0.95, 0.95]; // top-right relative to screen queue.write_buffer(&overlay_rect_buffer, 0, bytemuck::bytes_of(&rect)); let overlay_rect_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { @@ -236,6 +240,222 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; } } +// Overlay (UV gradient quad) resources grouped for clarity +struct TextureOverlayResources { + pipeline: wgpu::RenderPipeline, + rect_bind_group_layout: wgpu::BindGroupLayout, + rect_bind_group: wgpu::BindGroup, + rect_buffer: wgpu::Buffer, + // material for textured overlay + material_bind_group_layout: wgpu::BindGroupLayout, + material_bind_group: wgpu::BindGroup, + material_tint_buffer: wgpu::Buffer, +} +// KISS helper: build overlay pipeline, UBO layout/buffer, and bind group +fn create_overlay_logo( + device: &wgpu::Device, + queue: &wgpu::Queue, + surface_format: wgpu::TextureFormat, + texture_handle: RendererTextureHandle, + storage: &RendererResourceStorage, + rect: [f32; 4], +) -> TextureOverlayResources { + let overlay_vs_wgsl = r#" +@group(1) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] + +struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; +@vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var unit = array, 6>( + vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), + vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) + ); + let p = unit[vi]; // [-1,1] NDC space + let s = p*0.5+0.5; // [0,1] screen space + let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] + let ndc = r*2.0-1.0; // [-1,1] NDC space + var out: VSOut; + out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); + out.uv = s; + return out; + } + "#; + + let overlay_fs_wgsl = r#" +@group(0) @binding(0) var tex: texture_2d; +@group(0) @binding(1) var smp: sampler; +@group(0) @binding(2) var UTint: vec4; +@fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let c = textureSample(tex, smp, uv); + return c * UTint; +} +"#; + + let overlay_vs = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_vs"), + source: wgpu::ShaderSource::Wgsl(overlay_vs_wgsl.into()), + }); + let overlay_fs = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_fs"), + source: wgpu::ShaderSource::Wgsl(overlay_fs_wgsl.into()), + }); + + // Group 0: material (texture + sampler + tint) + let overlay_material_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_logo_material_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }, + ], + }); + // Group 1: rect UBO + let overlay_rect_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_rect_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }], + }); + let overlay_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("overlay_pl"), + bind_group_layouts: &[ + &overlay_material_bind_group_layout, + &overlay_rect_bind_group_layout, + ], + push_constant_ranges: &[], + }); + + // No vertex buffer layout needed; vertices are generated procedurally in the vertex shader + let overlay_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("overlay_pipeline"), + layout: Some(&overlay_pipeline_layout), + vertex: wgpu::VertexState { + module: &overlay_vs, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &overlay_fs, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: surface_format, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + cull_mode: None, + ..wgpu::PrimitiveState::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + // Apple Metal constant buffer alignment (256 bytes) + let overlay_rect_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_rect_ubo"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + queue.write_buffer(&overlay_rect_buffer, 0, bytemuck::bytes_of(&rect)); + + let overlay_rect_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_rect_bg"), + layout: &overlay_rect_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &overlay_rect_buffer, + offset: 0, + size: Some(std::num::NonZeroU64::new(16).unwrap()), + }), + }], + }); + + let tex = storage + .textures + .get(texture_handle) + .expect("logo texture handle invalid"); + let logo_texture_view = &tex.texture_view; + let logo_sampler = &tex.sampler; + let tint_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_logo_tint"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let tint: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; + queue.write_buffer(&tint_buffer, 0, bytemuck::bytes_of(&tint)); + + let overlay_material_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_logo_material_bg"), + layout: &overlay_material_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(logo_texture_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(logo_sampler), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &tint_buffer, + offset: 0, + size: Some(std::num::NonZeroU64::new(16).unwrap()), + }), + }, + ], + }); + + TextureOverlayResources { + pipeline: overlay_pipeline, + rect_bind_group_layout: overlay_rect_bind_group_layout, + rect_bind_group: overlay_rect_bind_group, + rect_buffer: overlay_rect_buffer, + material_bind_group_layout: overlay_material_bind_group_layout, + material_bind_group: overlay_material_bind_group, + material_tint_buffer: tint_buffer, + } +} + impl PillRenderer for Renderer { fn new(window: Arc, config: config::Config) -> Self { info!("Initializing {}", "Renderer".mobj_style()); @@ -514,8 +734,10 @@ pub struct State { composite_bind_group_layout: wgpu::BindGroupLayout, composite_pipeline: wgpu::RenderPipeline, composite_bind_group: wgpu::BindGroup, - // Overlay (UV gradient quad in top-right) - overlay: OverlayResources, + + // These look like could be combined into OverlayRenderer, but also good case for user submiter pass API + overlay_uv: OverlayResources, // Overlay (UV gradient quad in top-right) + overlay_logo: TextureOverlayResources, // Overlay (Logo in bot-right) // Other config: config::Config, egui_renderer: crate::egui::EguiRenderer, // TODO: Separate system adding Pass @@ -635,7 +857,7 @@ impl State { surface.configure(&device, &surface_configuration); // Configure collections - let renderer_resource_storage = RendererResourceStorage::new(&config); + let mut renderer_resource_storage = RendererResourceStorage::new(&config); // Create depth and color texture let depth_texture = @@ -837,7 +1059,43 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; }); // Overlay UV gradient pipeline (small quad in top-right corner) - let overlay = create_overlay_resources(&device, &queue, surface_configuration.format); + let overlay_uv = create_overlay_uv( + &device, + &queue, + surface_configuration.format, + [0.75, 0.75, 0.95, 0.95], + ); + + // Ideally Client submitted pass via API + // Create logo texture via renderer API and pass handle into overlay factory + let mut logo_image = image::open( + "/Users/mk/dev/demo/Pill-Engine/engine/pill_renderer/res/pill_logo_horizontal_white.png", + ) + .expect("failed to load overlay logo image"); + logo_image = logo_image.flipv(); + + let logo_handle = { + let tex = RendererTexture::new_texture( + &device, + &queue, + Some("overlay_logo"), + &logo_image, + TextureType::Color, + ) + .expect("failed to create overlay logo texture"); + renderer_resource_storage.textures.insert(tex) + }; + // Overlay logo pipeline (small quad) + // res: 1024 × 320, 1.0 x 0.3 + let h: f32 = 0.04; + let overlay_logo = create_overlay_logo( + &device, + &queue, + surface_configuration.format, + logo_handle, + &renderer_resource_storage, + [0.98 - 3. * h, 0.02, 0.98, 0.02 + h], // bottom right + ); // Create state let per_draw_bind_group_layout = @@ -880,8 +1138,9 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; composite_bind_group_layout, composite_pipeline, composite_bind_group, - // Overlay - overlay, + // Overlays + overlay_uv, + overlay_logo, // Other config, egui_renderer, @@ -1376,8 +1635,14 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; rpass.draw(0..3, 0..1); // Draw small overlay UV quad in normalized rect via UBO - rpass.set_pipeline(&self.overlay.pipeline); - rpass.set_bind_group(0, &self.overlay.rect_bind_group, &[]); + rpass.set_pipeline(&self.overlay_uv.pipeline); + rpass.set_bind_group(0, &self.overlay_uv.rect_bind_group, &[]); + rpass.draw(0..6, 0..1); + + // Draw logo overlay (bind material at group 0 and rect at group 1) + rpass.set_pipeline(&self.overlay_logo.pipeline); + rpass.set_bind_group(0, &self.overlay_logo.material_bind_group, &[]); + rpass.set_bind_group(1, &self.overlay_logo.rect_bind_group, &[]); rpass.draw(0..6, 0..1); timer.record("Encode compose pass"); From f6b9d8cce7756ca0ca6ff8f1d12953a211ce9c6e Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Tue, 14 Oct 2025 23:39:00 +0200 Subject: [PATCH 32/69] feat: M6 depth overlay --- engine/pill_renderer/DESIGN.md | 2 +- engine/pill_renderer/src/renderer.rs | 287 ++++++++++++++++++++++++++- 2 files changed, 280 insertions(+), 9 deletions(-) diff --git a/engine/pill_renderer/DESIGN.md b/engine/pill_renderer/DESIGN.md index c34b7dd5..afcc893f 100644 --- a/engine/pill_renderer/DESIGN.md +++ b/engine/pill_renderer/DESIGN.md @@ -190,7 +190,7 @@ struct VSOut { @builtin(position) pos: vec4; }; - wgpu correctness: - Avoid mapping GPU-only buffers; prefer `write_buffer` for simplicity. -6) Multi-pass + Offscreen Targets + Quad Composition +6) [Done] Multi-pass + Offscreen Targets + Quad Composition - Expected result: Offscreen render (T0) → FSQ/quads composite to swapchain; quads can place T0 anywhere in the framebuffer. - Client API: Add two passes: `Texture` then `Swapchain`; post uses `Inline` to draw a quad (or fullscreen triangle) sampling T0 with UVs that map to desired target rect. diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 972b121a..0b9db308 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -275,7 +275,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; let ndc = r*2.0-1.0; // [-1,1] NDC space var out: VSOut; out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); - out.uv = s; + out.uv = vec2(s.x, 1.0 - s.y); return out; } "#; @@ -708,6 +708,196 @@ impl PillRenderer for Renderer { } } +// KISS helper: build overlay pipeline, UBO layout/buffer, and bind group +fn create_overlay_depth( + device: &wgpu::Device, + queue: &wgpu::Queue, + surface_format: wgpu::TextureFormat, + depth_rt: &RendererTexture, + rect: [f32; 4], +) -> TextureOverlayResources { + let overlay_vs_wgsl = r#" +@group(1) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] + +struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; +@vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var unit = array, 6>( + vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), + vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) + ); + let p = unit[vi]; // [-1,1] NDC space + let s = p*0.5+0.5; // [0,1] screen space + let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] + let ndc = r*2.0-1.0; // [-1,1] NDC space + var out: VSOut; + out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); + out.uv = vec2(s.x, 1.0 - s.y); + return out; + } + "#; + + let overlay_fs_wgsl = r#" +@group(0) @binding(0) var tex: texture_depth_2d; +@group(0) @binding(1) var UTint: vec4; +@fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let dims = textureDimensions(tex, 0u); + let coord = vec2(uv * vec2(dims)); + let d = textureLoad(tex, coord, 0); + let vis = fract(100.0*d); + return vec4(vis, vis, vis, 1.0) * UTint; +} +"#; + + let overlay_vs = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_vs"), + source: wgpu::ShaderSource::Wgsl(overlay_vs_wgsl.into()), + }); + let overlay_fs = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_fs"), + source: wgpu::ShaderSource::Wgsl(overlay_fs_wgsl.into()), + }); + + // Group 0: material (texture + tint) + let overlay_material_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_logo_material_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Depth, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }, + ], + }); + // Group 1: rect UBO + let overlay_rect_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_rect_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }], + }); + let overlay_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("overlay_pl"), + bind_group_layouts: &[ + &overlay_material_bind_group_layout, + &overlay_rect_bind_group_layout, + ], + push_constant_ranges: &[], + }); + + // No vertex buffer layout needed; vertices are generated procedurally in the vertex shader + let overlay_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("overlay_pipeline"), + layout: Some(&overlay_pipeline_layout), + vertex: wgpu::VertexState { + module: &overlay_vs, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &overlay_fs, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: surface_format, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + cull_mode: None, + ..wgpu::PrimitiveState::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + // Apple Metal constant buffer alignment (256 bytes) + let overlay_rect_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_rect_ubo"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + queue.write_buffer(&overlay_rect_buffer, 0, bytemuck::bytes_of(&rect)); + + let overlay_rect_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_rect_bg"), + layout: &overlay_rect_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &overlay_rect_buffer, + offset: 0, + size: Some(std::num::NonZeroU64::new(16).unwrap()), + }), + }], + }); + + let tint_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_tint"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let tint: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; + queue.write_buffer(&tint_buffer, 0, bytemuck::bytes_of(&tint)); + + let overlay_material_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_material_bg"), + layout: &overlay_material_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&depth_rt.texture_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &tint_buffer, + offset: 0, + size: Some(std::num::NonZeroU64::new(16).unwrap()), + }), + }, + ], + }); + + TextureOverlayResources { + pipeline: overlay_pipeline, + rect_bind_group_layout: overlay_rect_bind_group_layout, + rect_bind_group: overlay_rect_bind_group, + rect_buffer: overlay_rect_buffer, + material_bind_group_layout: overlay_material_bind_group_layout, + material_bind_group: overlay_material_bind_group, + material_tint_buffer: tint_buffer, + } +} + pub struct State { // Resources renderer_resource_storage: RendererResourceStorage, @@ -738,6 +928,7 @@ pub struct State { // These look like could be combined into OverlayRenderer, but also good case for user submiter pass API overlay_uv: OverlayResources, // Overlay (UV gradient quad in top-right) overlay_logo: TextureOverlayResources, // Overlay (Logo in bot-right) + overlay_depth: TextureOverlayResources, // Overlay (Depth in top-right) // Other config: config::Config, egui_renderer: crate::egui::EguiRenderer, // TODO: Separate system adding Pass @@ -864,9 +1055,9 @@ impl State { RendererTexture::new_depth_texture(&device, &surface_configuration, "depth_texture") .unwrap(); - // Use Rgba16Float for HDR color buffers; it’s the common, well-supported, + // Use Rgba16Float for HDR color buffers; it's the common, well-supported, // performant choice. Reserve Rgba32Float for niche cases needing extreme - // precision and accept the 2× bandwidth/memory hit. If alpha isn’t needed, + // precision and accept the 2× bandwidth/memory hit. If alpha isn't needed, // Rg11b10Float/R11G11B10_FLOAT is a fast alternative. // Tone-map to the sRGB swapchain in the composite pass. let color_format = wgpu::TextureFormat::Rgba16Float; @@ -1072,7 +1263,6 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; "/Users/mk/dev/demo/Pill-Engine/engine/pill_renderer/res/pill_logo_horizontal_white.png", ) .expect("failed to load overlay logo image"); - logo_image = logo_image.flipv(); let logo_handle = { let tex = RendererTexture::new_texture( @@ -1097,6 +1287,14 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; [0.98 - 3. * h, 0.02, 0.98, 0.02 + h], // bottom right ); + let overlay_depth = create_overlay_depth( + &device, + &queue, + surface_configuration.format, + &depth_texture, + [0.75, 0.55, 0.95, 0.72], // top right, 2 row + ); + // Create state let per_draw_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { @@ -1141,6 +1339,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; // Overlays overlay_uv, overlay_logo, + overlay_depth, // Other config, egui_renderer, @@ -1186,6 +1385,28 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; }, ], }); + // Rebind overlay depth material to the new depth texture view + self.overlay_depth.material_bind_group = + self.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_depth_material_bg"), + layout: &self.overlay_depth.material_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView( + &self.depth_texture.texture_view, + ), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &self.overlay_depth.material_tint_buffer, + offset: 0, + size: Some(std::num::NonZeroU64::new(16).unwrap()), + }), + }, + ], + }); // Old offscreen texture/view/sampler are dropped when replaced; wgpu defers actual GPU resource // destruction until safe. See optional early reclamation via device.poll: // https://docs.rs/wgpu/latest/wgpu/struct.Device.html#method.poll @@ -1229,7 +1450,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; timer.record("Frame: setup view & camera"); // [SIMILAR] Modify resources (camera UBO) before draw; separates data mods from drawing as per TALK - // [DIFF] Bind group slot indices differ from TALK’s convention (0=globals,1=material,2=shader,3=dynamic); here camera is @group(2) + // [DIFF] Bind group slot indices differ from TALK's convention (0=globals,1=material,2=shader,3=dynamic); here camera is @group(2) // Get active camera and update it let camera_storage = camera_component_storage .data @@ -1269,7 +1490,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; label: Some("m2_inline_encoder"), }); - // [SIMILAR] CPU frustum culling and precomputing per-draw transforms before starting the pass matches TALK’s advice + // [SIMILAR] CPU frustum culling and precomputing per-draw transforms before starting the pass matches TALK's advice // [DIFF] Only frustum culling here; TALK discusses 2-pass occlusion and broader CPU-driven pipelines // [API->CLIENT] Culling, model matrix building, and visible set construction should live in client/high-level renderer // Build view before pass: cull and prepare per-draw MVPs @@ -1622,7 +1843,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; view: &view, resolve_target: None, ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store, }, })], @@ -1634,18 +1855,68 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; rpass.set_bind_group(0, &self.composite_bind_group, &[]); rpass.draw(0..3, 0..1); + timer.record("Encode compose pass"); + } + timer.end_context()?; // End "Compose pass" + + // Overlay passes + { + timer.begin_context("Overlay passes"); + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("m6_overlay_passes"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + // Draw small overlay UV quad in normalized rect via UBO rpass.set_pipeline(&self.overlay_uv.pipeline); rpass.set_bind_group(0, &self.overlay_uv.rect_bind_group, &[]); rpass.draw(0..6, 0..1); + // Draw depth overlay (bind material at group 0 and rect at group 1) + // Rebind overlay depth material to the new depth texture view + self.overlay_depth.material_bind_group = + self.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_depth_material_bg"), + layout: &self.overlay_depth.material_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView( + &self.depth_texture.texture_view, + ), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &self.overlay_depth.material_tint_buffer, + offset: 0, + size: Some(std::num::NonZeroU64::new(16).unwrap()), + }), + }, + ], + }); + rpass.set_pipeline(&self.overlay_depth.pipeline); + rpass.set_bind_group(0, &self.overlay_depth.material_bind_group, &[]); + rpass.set_bind_group(1, &self.overlay_depth.rect_bind_group, &[]); + rpass.draw(0..6, 0..1); + // Draw logo overlay (bind material at group 0 and rect at group 1) rpass.set_pipeline(&self.overlay_logo.pipeline); rpass.set_bind_group(0, &self.overlay_logo.material_bind_group, &[]); rpass.set_bind_group(1, &self.overlay_logo.rect_bind_group, &[]); rpass.draw(0..6, 0..1); - timer.record("Encode compose pass"); + timer.record("Encode overlay passes"); } timer.end_context()?; // End "Compose pass" From 4cec7b8a13398a96c0bf30f7a6ba3cdb98d83650 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Wed, 15 Oct 2025 00:59:02 +0200 Subject: [PATCH 33/69] feat: align bind groups with talk --- engine/pill_engine/src/engine.rs | 5 +- engine/pill_engine/src/graphics/renderer.rs | 87 ++++++++++--------- engine/pill_renderer/src/renderer.rs | 47 +++++----- .../src/resources/renderer_pipeline.rs | 4 +- 4 files changed, 71 insertions(+), 72 deletions(-) diff --git a/engine/pill_engine/src/engine.rs b/engine/pill_engine/src/engine.rs index 259aaa98..b5d87c45 100644 --- a/engine/pill_engine/src/engine.rs +++ b/engine/pill_engine/src/engine.rs @@ -116,10 +116,7 @@ impl Engine { self.register_resource_type::(max_sound_count)?; // master shader & defaults ------------------------------------------- - let master_vert = include_bytes!("../res/shaders/master.vert.glsl"); - let master_frag = include_bytes!("../res/shaders/master.frag.glsl"); - self.renderer - .set_master_pipeline(master_vert, master_frag)?; + // Legacy master pipeline removed; PSOs are created upfront by renderer let default_color = Box::new(*include_bytes!("../res/textures/default_color.png")); let default_normal = Box::new(*include_bytes!("../res/textures/default_normal.png")); diff --git a/engine/pill_engine/src/graphics/renderer.rs b/engine/pill_engine/src/graphics/renderer.rs index e5e02054..5340ba92 100644 --- a/engine/pill_engine/src/graphics/renderer.rs +++ b/engine/pill_engine/src/graphics/renderer.rs @@ -1,70 +1,76 @@ -use crate::{ - engine::Engine, - ecs::{ - EntityHandle, - ComponentStorage, - TransformComponent, - CameraComponent, - }, +use crate::{ + ecs::{CameraComponent, ComponentStorage, EntityHandle, TransformComponent}, + engine::Engine, + graphics::RenderQueueItem, resources::{ - MaterialHandle, - MeshData, - MeshHandle, - TextureHandle, - TextureType, - MaterialTextureMap, - MaterialParameterMap - }, - graphics::{ - RenderQueueItem, + MaterialHandle, MaterialParameterMap, MaterialTextureMap, MeshData, MeshHandle, + TextureHandle, TextureType, }, }; -use pill_core::{PillSlotMapKey, Timer}; use pill_core::PillStyle; +use pill_core::{PillSlotMapKey, Timer}; +use anyhow::{Context, Error, Result}; use std::{path::PathBuf, sync::Arc}; use thiserror::Error; -use anyhow::{Result, Context, Error}; - // --- Renderer resource handles --- -pill_core::define_new_pill_slotmap_key! { +pill_core::define_new_pill_slotmap_key! { pub struct RendererMaterialHandle; } -pill_core::define_new_pill_slotmap_key! { +pill_core::define_new_pill_slotmap_key! { pub struct RendererMeshHandle; } -pill_core::define_new_pill_slotmap_key! { +pill_core::define_new_pill_slotmap_key! { pub struct RendererPipelineHandle; } -pill_core::define_new_pill_slotmap_key! { +pill_core::define_new_pill_slotmap_key! { pub struct RendererCameraHandle; } -pill_core::define_new_pill_slotmap_key! { +pill_core::define_new_pill_slotmap_key! { pub struct RendererTextureHandle; } // --- Renderer trait definition --- -pub trait PillRenderer { - fn new(window: Arc, config: config::Config) -> Self where Self: Sized; +pub trait PillRenderer { + fn new(window: Arc, config: config::Config) -> Self + where + Self: Sized; fn resize(&mut self, new_window_size: winit::dpi::PhysicalSize); - fn set_master_pipeline(&mut self, vertex_shader_bytes: &[u8], fragment_shader_bytes: &[u8],) -> Result<()>; - + fn create_mesh(&mut self, name: &str, mesh_data: &MeshData) -> Result; - fn create_texture(&mut self, name: &str, image_data: &image::DynamicImage, texture_type: TextureType) -> Result; - fn create_material(&mut self, name: &str, textures: &MaterialTextureMap, parameters: &MaterialParameterMap) -> Result; + fn create_texture( + &mut self, + name: &str, + image_data: &image::DynamicImage, + texture_type: TextureType, + ) -> Result; + fn create_material( + &mut self, + name: &str, + textures: &MaterialTextureMap, + parameters: &MaterialParameterMap, + ) -> Result; fn create_camera(&mut self) -> Result; - fn update_material_textures(&mut self, renderer_material_handle: RendererMaterialHandle, textures: &MaterialTextureMap) -> Result<()>; - fn update_material_parameters(&mut self, renderer_material_handle: RendererMaterialHandle, parameters: &MaterialParameterMap) -> Result<()>; + fn update_material_textures( + &mut self, + renderer_material_handle: RendererMaterialHandle, + textures: &MaterialTextureMap, + ) -> Result<()>; + fn update_material_parameters( + &mut self, + renderer_material_handle: RendererMaterialHandle, + parameters: &MaterialParameterMap, + ) -> Result<()>; fn destroy_mesh(&mut self, renderer_mesh_handle: RendererMeshHandle) -> Result<()>; fn destroy_texture(&mut self, renderer_texture_handle: RendererTextureHandle) -> Result<()>; @@ -73,20 +79,15 @@ pub trait PillRenderer { fn pass_input_to_egui(&mut self, event: &winit::event::WindowEvent) -> Result<()>; - fn render(&mut self, + fn render( + &mut self, active_camera_entity_handle: EntityHandle, - render_queue: &Vec::, + render_queue: &Vec, camera_component_storage: &ComponentStorage, transform_component_storage: &ComponentStorage, egui_ui: Box, - timer: &mut Timer + timer: &mut Timer, ) -> Result<()>; - } pub type Renderer = Box; - - - - - diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 0b9db308..a7ca998a 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -469,15 +469,6 @@ impl PillRenderer for Renderer { self.state.resize(new_window_size) } - fn set_master_pipeline( - &mut self, - _vertex_shader_bytes: &[u8], - _fragment_shader_bytes: &[u8], - ) -> Result<()> { - // [DIFFERENT] TALK recommends building PSOs upfront; here pipeline is created in State::new (good), and this setter is a no-op - Ok(()) - } - fn create_mesh(&mut self, name: &str, mesh_data: &MeshData) -> Result { let mesh = RendererMesh::new(&self.state.device, name, mesh_data)?; let handle = self.state.renderer_resource_storage.meshes.insert(mesh); @@ -1078,7 +1069,7 @@ impl State { // Build static shader modules and pipeline once ([SIMILAR] prebuilt PSO per TALK) let vertex_wgsl = r#" struct Camera { position: vec4, view_projection_matrix: mat4x4, }; -@group(2) @binding(0) var GCamera: Camera; +@group(0) @binding(0) var GCamera: Camera; struct PerDraw { mvp: mat4x4, @@ -1098,15 +1089,15 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; } "#; let fragment_wgsl = r#" -// Material set(0): -@group(0) @binding(0) var tex_diffuse: texture_2d; -@group(0) @binding(1) var smp_diffuse: sampler; -@group(0) @binding(2) var tex_normal: texture_2d; -@group(0) @binding(3) var smp_normal: sampler; +// Material set(1): +@group(1) @binding(0) var tex_diffuse: texture_2d; +@group(1) @binding(1) var smp_diffuse: sampler; +@group(1) @binding(2) var tex_normal: texture_2d; +@group(1) @binding(3) var smp_normal: sampler; -// Material parameters set(1) - pack tint.rgb + specularity in a single vec4 +// Material parameters set(2) - pack tint.rgb + specularity in a single vec4 struct MaterialParams { tint_spec: vec4, } -@group(1) @binding(0) var UMaterial: MaterialParams; +@group(2) @binding(0) var UMaterial: MaterialParams; // Per-draw (set 3): supports per-entity tint for M4 struct PerDraw { @@ -1259,7 +1250,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; // Ideally Client submitted pass via API // Create logo texture via renderer API and pass handle into overlay factory - let mut logo_image = image::open( + let logo_image = image::open( "/Users/mk/dev/demo/Pill-Engine/engine/pill_renderer/res/pill_logo_horizontal_white.png", ) .expect("failed to load overlay logo image"); @@ -1450,7 +1441,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; timer.record("Frame: setup view & camera"); // [SIMILAR] Modify resources (camera UBO) before draw; separates data mods from drawing as per TALK - // [DIFF] Bind group slot indices differ from TALK's convention (0=globals,1=material,2=shader,3=dynamic); here camera is @group(2) + // [SIMILAR] Bind group slot indices same as in TALK's convention (0=globals,1=material,2=shader,3=dynamic); // Get active camera and update it let camera_storage = camera_component_storage .data @@ -1710,7 +1701,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; self.per_draw_bind_group = Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("per_draw_bind_group"), - layout: &self.per_draw_bind_group_layout, + layout: &self.default_pipeline.per_draw_bind_group_layout, entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { @@ -1796,14 +1787,20 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; )); rpass.set_pipeline(&self.default_pipeline.render_pipeline); + // HOT SoA (per-frame hot path): + // - RendererMaterial: texture_bind_group, parameter_bind_group + // - RendererCamera : bind_group + // - RendererMesh : vertex_buffer, index_buffer, index_count + // COLD metadata (names/descs) lives in *_cold arrays and is not read in the draw loop. + let material = self .renderer_resource_storage .materials .get(group.material_handle) .unwrap(); - rpass.set_bind_group(0, &material.texture_bind_group, &[]); - rpass.set_bind_group(1, &material.parameter_bind_group, &[]); - rpass.set_bind_group(2, &renderer_camera.bind_group, &[]); + rpass.set_bind_group(0, &renderer_camera.bind_group, &[]); + rpass.set_bind_group(1, &material.texture_bind_group, &[]); + rpass.set_bind_group(2, &material.parameter_bind_group, &[]); for batch in &group.batches { let mesh = self @@ -1951,6 +1948,10 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; frame.present(); timer.record("Present"); + // Allow wgpu to process pending work and retire resources referenced by submitted encoders + // without blocking the CPU. See Device::poll docs. + self.device.poll(wgpu::Maintain::Poll); + Ok(()) } diff --git a/engine/pill_renderer/src/resources/renderer_pipeline.rs b/engine/pill_renderer/src/resources/renderer_pipeline.rs index 7df85cfe..ef27e171 100644 --- a/engine/pill_renderer/src/resources/renderer_pipeline.rs +++ b/engine/pill_renderer/src/resources/renderer_pipeline.rs @@ -114,13 +114,13 @@ impl RendererPipeline { }); // Create pipeline layout descriptor - // [SIMILAR] Fixed PSO layout: globals(material textures), material params, camera, per-draw + // [SIMILAR] Fixed PSO layout per TALK: globals(camera), material textures, material params, dynamic per-draw let pipeline_layout_descriptor = wgpu::PipelineLayoutDescriptor { label: Some("render_pipeline_layout"), bind_group_layouts: &[ + &camera_bind_group_layout, &material_texture_bind_group_layout, &material_parameter_bind_group_layout, - &camera_bind_group_layout, &per_draw_bind_group_layout, ], push_constant_ranges: &[], From 9c93c97380a5899a44e8c781f74b054721538be3 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Wed, 15 Oct 2025 01:42:41 +0200 Subject: [PATCH 34/69] docs --- engine/pill_renderer/DESIGN_DEPTH_PREPASS.md | 41 ++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 engine/pill_renderer/DESIGN_DEPTH_PREPASS.md diff --git a/engine/pill_renderer/DESIGN_DEPTH_PREPASS.md b/engine/pill_renderer/DESIGN_DEPTH_PREPASS.md new file mode 100644 index 00000000..35a646b8 --- /dev/null +++ b/engine/pill_renderer/DESIGN_DEPTH_PREPASS.md @@ -0,0 +1,41 @@ +# Align Renderer With TALK: 2-Pass Occlusion + CPU Visibility + +### Scope + +- Add a depth-only prepass and build a HiZ depth pyramid each frame. +- Use last frame’s HiZ to perform conservative CPU occlusion culling after frustum culling. +- Keep overlay/composition unchanged; integrate via feature flag and metrics. + +### Files To Touch + +- `Pill-Engine/engine/pill_renderer/src/resources/renderer_pipeline.rs` (add depth-only pipeline + layout) +- `Pill-Engine/engine/pill_renderer/src/renderer.rs` (insert prepass; build HiZ; visibility stage; drive main pass survivors) +- `Pill-Engine/engine/pill_renderer/src/visibility/` (new: `hiz.rs`, `occlusion.rs` helpers) + +### Design + +- Depth prepass: render opaque geometry with a depth-only pipeline (no color, depth write enabled). +- HiZ pyramid: allocate mip-chain texture for depth; generate with compute (preferred) or render downsampling. Store previous frame’s HiZ for CPU queries. +- CPU occlusion: project entity world AABB to screen-space bounds; query HiZ at appropriate mip level using conservative test; mark occluded entities as culled. +- Frame cadence: use frame N-1 HiZ to cull frame N (avoids GPU→CPU sync); update HiZ after main pass for frame N+1. + +### Integration Points + +- Before main pass in `renderer.rs`: +1) run depth prepass +2) dispatch HiZ generation +3) frustum cull → occlusion cull → build `visible` list +4) draw main pass using `visible` +- Add `Renderer` toggles: `enable_depth_prepass`, `enable_hiz_culling` (config/env). +- Add counters: total, frustum_kept, occluded, drawn; expose in logs. + +### Minimal API/Resource Changes + +- Add depth-only `RenderPipeline` to `RendererPipeline` (e.g., `depth_prepass_pipeline`). +- Add persistent `hiz_texture` with mips; `hiz_bind_group` + compute pipeline for downsampling. +- Store per-mesh AABB (or compute at load); per-entity world AABB is derived via transform. + +### Risks/Notes + +- One-frame-late occlusion may pop on camera cuts; mitigate via camera-move tolerance or disable on large camera deltas. +- Keep culling conservative (AABB expanded slightly) to avoid over-culling. \ No newline at end of file From c5760d0e2dfac80c7b5576ceb6a90424a3c6f396 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Wed, 15 Oct 2025 01:43:21 +0200 Subject: [PATCH 35/69] draft renderer pass API --- examples/pill_demo/src/game.rs | 102 +++++++++++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 5 deletions(-) diff --git a/examples/pill_demo/src/game.rs b/examples/pill_demo/src/game.rs index e3abf4b3..3393b0a8 100644 --- a/examples/pill_demo/src/game.rs +++ b/examples/pill_demo/src/game.rs @@ -1,6 +1,9 @@ use pill_engine::game::*; use rand::Rng; +// Draft import for renderer vNext API (as per DESIGN.md) +// use pill_engine::pill_renderer as pr; + // Define custom component pub struct PillComponent {} @@ -46,17 +49,18 @@ impl PillGame for Game { ResourceLoadType::Path("textures/pill_normal.png".into()), ); let pill_normal_texture_handle = engine.add_resource::(pill_normal_texture)?; + // Material properties showcase: create a small set of materials to reuse let mut rng = rand::thread_rng(); let mut materials = Vec::new(); - for j in 0..8 { + for j in 0..10 { let mut mat = Material::new(&format!("PillMat{}", j)); mat.set_texture("color", pill_color_texture_handle.clone())?; mat.set_texture("normal", pill_normal_texture_handle.clone())?; let tint = Color::new( - rng.gen_range(0.3..=1.0), - rng.gen_range(0.3..=1.0), - rng.gen_range(0.3..=1.0), + rng.gen_range(0.2..=0.8), + rng.gen_range(0.2..=0.8), + rng.gen_range(0.2..=0.8), ); let spec = rng.gen_range(0.0..=1.0); mat.set_color("tint", tint)?; @@ -76,7 +80,7 @@ impl PillGame for Game { engine.add_component_to_entity(active_scene, camera, camera_component)?; // Create pill entity - for i in 0..50000 { + for i in 0..60000 { let pill = engine.create_entity(active_scene)?; let posx = rng.gen_range(-10.0..=20.0); let posy = rng.gen_range(-10.0..=10.0); @@ -95,6 +99,94 @@ impl PillGame for Game { engine.add_component_to_entity(active_scene, pill, PillComponent {})?; } + /* --- Overlay Logo Pass (userland draft using pill_renderer vNext API) --- + { + // Acquire renderer and its resource manager + let renderer = engine.renderer_mut()?; // Draft API accessor + let mut rm = renderer.resources(); + + // Create logo texture + sampler → material bind group (set 1) + let logo_tex = + rm.create_texture(pr::TextureDesc::from_path("textures/pill_logo.png").srgb(true)); + let logo_smp = rm.create_sampler(pr::SamplerDesc::linear_clamp()); + + // Overlay pipeline (fixed-function screen space quad shader) + let overlay_pso = rm.create_pipeline(pr::GraphicsPipelineDesc::overlay_logo( + renderer.surface_format(), + )); + + // Material BG uses @group(1): texture_2d + sampler + let overlay_material_bg = rm.create_bind_group(pr::BindGroupDesc { + debug_name: Some("overlay_logo_material"), + layout: overlay_pso.material_layout(), + textures: vec![logo_tex], + samplers: vec![logo_smp], + ..Default::default() + }); + + // Per-pass globals (@group(0)) with screen-space rect in pixels and tint + #[repr(C)] + #[derive(Copy, Clone)] + struct OverlayGlobals { + rect_px: [f32; 4], + tint: [f32; 4], + } + let globals = OverlayGlobals { + rect_px: [16.0, 16.0, 160.0, 64.0], + tint: [1.0, 1.0, 1.0, 1.0], + }; + let globals_bg = { + // 256B-aligned uniform buffer with initial data + let globals_buf = rm.create_buffer(pr::BufferDesc::uniform_init_aligned( + "overlay_logo_globals", + pr::bytes_of(&globals), + )); + rm.create_bind_group(pr::BindGroupDesc { + debug_name: Some("overlay_logo_globals"), + layout: overlay_pso.globals_layout(), + buffers: vec![pr::BufferBinding { + buffer: globals_buf, + byte_offset: 0, + }], + ..Default::default() + }) + }; + + // Tiny quad mesh (two triangles in NDC via VS or a unit quad mesh) + let quad_mesh = rm.upload_mesh(pr::CpuMesh::unit_quad()); + + // Add pass to master pipeline that draws the logo over the swapchain + let pass = pr::RenderPassDesc { + name: "overlay_logo", + target: pr::TargetDesc::Swapchain, + clear: pr::ClearDesc { + color: None, + depth: None, + stencil: None, + }, + subpipeline: pr::Subpipeline { + pipeline: overlay_pso, + globals: globals_bg, + draws: pr::DrawRecipe::Inline(Box::new( + move |dlb: &mut pr::DrawListBuilder| { + dlb.set_pipeline(overlay_pso); + dlb.set_bind_groups_with_offsets( + globals_bg, + Some(overlay_material_bg), + None, + None, + None, + None, + ); + dlb.set_mesh(quad_mesh, 0, 0); + dlb.draw_indexed(6, 0); + }, + )), + }, + }; + renderer.master().add_pass(pass); + } */ + Ok(()) } } From d9ea8be7939450a44ca8820312e1302bd7262683 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Thu, 16 Oct 2025 00:06:33 +0200 Subject: [PATCH 36/69] feat: add renderer system init --- engine/pill_engine/src/config.rs | 73 ++++++++++----- engine/pill_engine/src/ecs/components/mod.rs | 29 +++--- .../ecs/components/render_state_component.rs | 19 ++++ engine/pill_engine/src/ecs/mod.rs | 2 + .../src/ecs/systems/rendering_system.rs | 72 +++++++++++++++ engine/pill_engine/src/engine.rs | 89 +++++++------------ engine/pill_renderer/src/lib.rs | 16 ++-- 7 files changed, 194 insertions(+), 106 deletions(-) create mode 100644 engine/pill_engine/src/ecs/components/render_state_component.rs diff --git a/engine/pill_engine/src/config.rs b/engine/pill_engine/src/config.rs index c872e460..a4e6625d 100644 --- a/engine/pill_engine/src/config.rs +++ b/engine/pill_engine/src/config.rs @@ -1,13 +1,17 @@ use crate::{ - ecs::{ audio_system, deferred_update_system, input_system, rendering_system, time_system, AudioManagerComponent, DeferredUpdateComponent, EguiManagerComponent, InputComponent, SystemFunction, TimeComponent, UpdatePhase }, - graphics::{ RendererMaterialHandle, RendererTextureHandle }, - resources::{ MaterialHandle, TextureHandle, TextureType }, + ecs::{ + audio_system, deferred_update_system, input_system, rendering_system, time_system, + AudioManagerComponent, DeferredUpdateComponent, EguiManagerComponent, InputComponent, + RenderStateComponent, SystemFunction, TimeComponent, UpdatePhase, + }, + graphics::{RendererMaterialHandle, RendererTextureHandle}, + resources::{MaterialHandle, TextureHandle, TextureType}, }; use pill_core::PillSlotMapKeyData; -use std::{num::NonZeroU32, any::TypeId}; use lazy_static::lazy_static; +use std::{any::TypeId, num::NonZeroU32}; // --- General --- @@ -91,34 +95,60 @@ pub const RENDER_QUEUE_KEY_MESH_INDEX_IDX: u8 = 3; pub const RENDER_QUEUE_KEY_MESH_VERSION_IDX: u8 = 4; // Default resource handle - Color texture -pub const DEFAULT_COLOR_TEXTURE_HANDLE: TextureHandle = TextureHandle { - 0: PillSlotMapKeyData { index: 1, version: unsafe { std::num::NonZeroU32::new_unchecked(1) } } +pub const DEFAULT_COLOR_TEXTURE_HANDLE: TextureHandle = TextureHandle { + 0: PillSlotMapKeyData { + index: 1, + version: unsafe { std::num::NonZeroU32::new_unchecked(1) }, + }, }; -pub const DEFAULT_RENDERER_COLOR_TEXTURE_HANDLE: RendererTextureHandle = RendererTextureHandle { - 0: PillSlotMapKeyData { index: 1, version: unsafe { std::num::NonZeroU32::new_unchecked(1) } } +pub const DEFAULT_RENDERER_COLOR_TEXTURE_HANDLE: RendererTextureHandle = RendererTextureHandle { + 0: PillSlotMapKeyData { + index: 1, + version: unsafe { std::num::NonZeroU32::new_unchecked(1) }, + }, }; // Default resource handle - Normal texture -pub const DEFAULT_NORMAL_TEXTURE_HANDLE: TextureHandle = TextureHandle { - 0: PillSlotMapKeyData { index: 2, version: unsafe { std::num::NonZeroU32::new_unchecked(1) } } +pub const DEFAULT_NORMAL_TEXTURE_HANDLE: TextureHandle = TextureHandle { + 0: PillSlotMapKeyData { + index: 2, + version: unsafe { std::num::NonZeroU32::new_unchecked(1) }, + }, }; -pub const DEFAULT_RENDERER_NORMAL_TEXTURE_HANDLE: RendererTextureHandle = RendererTextureHandle { - 0: PillSlotMapKeyData { index: 2, version: unsafe { std::num::NonZeroU32::new_unchecked(1) } } +pub const DEFAULT_RENDERER_NORMAL_TEXTURE_HANDLE: RendererTextureHandle = RendererTextureHandle { + 0: PillSlotMapKeyData { + index: 2, + version: unsafe { std::num::NonZeroU32::new_unchecked(1) }, + }, }; -pub fn get_default_texture_handles(texture_type: TextureType) -> (TextureHandle, RendererTextureHandle) { +pub fn get_default_texture_handles( + texture_type: TextureType, +) -> (TextureHandle, RendererTextureHandle) { match texture_type { - TextureType::Color => (DEFAULT_COLOR_TEXTURE_HANDLE, DEFAULT_RENDERER_COLOR_TEXTURE_HANDLE), - TextureType::Normal => (DEFAULT_NORMAL_TEXTURE_HANDLE, DEFAULT_RENDERER_NORMAL_TEXTURE_HANDLE), + TextureType::Color => ( + DEFAULT_COLOR_TEXTURE_HANDLE, + DEFAULT_RENDERER_COLOR_TEXTURE_HANDLE, + ), + TextureType::Normal => ( + DEFAULT_NORMAL_TEXTURE_HANDLE, + DEFAULT_RENDERER_NORMAL_TEXTURE_HANDLE, + ), } } // Default resource handle - Material -pub const DEFAULT_MATERIAL_HANDLE: MaterialHandle = MaterialHandle { - 0: PillSlotMapKeyData { index: 1, version: unsafe { std::num::NonZeroU32::new_unchecked(1) } } +pub const DEFAULT_MATERIAL_HANDLE: MaterialHandle = MaterialHandle { + 0: PillSlotMapKeyData { + index: 1, + version: unsafe { std::num::NonZeroU32::new_unchecked(1) }, + }, }; -pub const DEFAULT_RENDERER_MATERIAL_HANDLE: RendererMaterialHandle = RendererMaterialHandle { - 0: PillSlotMapKeyData { index: 1, version: unsafe { std::num::NonZeroU32::new_unchecked(1) } } +pub const DEFAULT_RENDERER_MATERIAL_HANDLE: RendererMaterialHandle = RendererMaterialHandle { + 0: PillSlotMapKeyData { + index: 1, + version: unsafe { std::num::NonZeroU32::new_unchecked(1) }, + }, }; pub fn get_default_material_handles() -> (MaterialHandle, RendererMaterialHandle) { @@ -131,6 +161,7 @@ lazy_static! { TypeId::of::(), TypeId::of::(), TypeId::of::(), - TypeId::of::() + TypeId::of::(), + TypeId::of::() ); -} \ No newline at end of file +} diff --git a/engine/pill_engine/src/ecs/components/mod.rs b/engine/pill_engine/src/ecs/components/mod.rs index 62123804..67703a95 100644 --- a/engine/pill_engine/src/ecs/components/mod.rs +++ b/engine/pill_engine/src/ecs/components/mod.rs @@ -1,28 +1,21 @@ #![cfg_attr(debug_assertions, allow(dead_code, unused_variables))] -mod component_storage; -mod component; -pub(crate) mod mesh_rendering_component; -pub(crate) mod transform_component; +pub(crate) mod audio_listener_component; +pub(crate) mod audio_manager_component; +pub(crate) mod audio_source_component; pub(crate) mod camera_component; +mod component; +mod component_storage; pub(crate) mod deferred_update_component; +pub(crate) mod egui_manager_component; pub(crate) mod input_component; +pub(crate) mod mesh_rendering_component; +pub(crate) mod render_state_component; pub(crate) mod time_component; -pub(crate) mod audio_listener_component; -pub(crate) mod audio_source_component; -pub(crate) mod audio_manager_component; -pub(crate) mod egui_manager_component; +pub(crate) mod transform_component; // --- Use --- -pub use component:: { - Component, - GlobalComponent, - ComponentDestroyer, - ConcreteComponentDestroyer -}; +pub use component::{Component, ComponentDestroyer, ConcreteComponentDestroyer, GlobalComponent}; -pub use component_storage::{ - ComponentStorage, - GlobalComponentStorage, -}; \ No newline at end of file +pub use component_storage::{ComponentStorage, GlobalComponentStorage}; diff --git a/engine/pill_engine/src/ecs/components/render_state_component.rs b/engine/pill_engine/src/ecs/components/render_state_component.rs new file mode 100644 index 00000000..826007c7 --- /dev/null +++ b/engine/pill_engine/src/ecs/components/render_state_component.rs @@ -0,0 +1,19 @@ +use crate::ecs::{GlobalComponent, GlobalComponentStorage}; +use crate::PillTypeMapKey; + +// Keeps renderer/bootstrap state; minimal for now +pub struct RenderStateComponent { + pub boot_done: bool, +} + +impl RenderStateComponent { + pub fn new() -> Self { + Self { boot_done: false } + } +} + +impl PillTypeMapKey for RenderStateComponent { + type Storage = GlobalComponentStorage; +} + +impl GlobalComponent for RenderStateComponent {} diff --git a/engine/pill_engine/src/ecs/mod.rs b/engine/pill_engine/src/ecs/mod.rs index b12f2c76..9fefc130 100644 --- a/engine/pill_engine/src/ecs/mod.rs +++ b/engine/pill_engine/src/ecs/mod.rs @@ -42,6 +42,8 @@ pub use components::mesh_rendering_component::MeshRenderingComponent; pub use components::time_component::TimeComponent; +pub use components::render_state_component::RenderStateComponent; + // - Systems pub use systems::{SystemFunction, SystemManager, UpdatePhase}; diff --git a/engine/pill_engine/src/ecs/systems/rendering_system.rs b/engine/pill_engine/src/ecs/systems/rendering_system.rs index 997038f9..9db7d563 100644 --- a/engine/pill_engine/src/ecs/systems/rendering_system.rs +++ b/engine/pill_engine/src/ecs/systems/rendering_system.rs @@ -17,7 +17,31 @@ use boolinator::Boolinator; use log::debug; use std::{ops::Range, time::Instant}; +use crate::config::{ + DEFAULT_COLOR_TEXTURE_NAME, DEFAULT_MATERIAL_NAME, DEFAULT_NORMAL_TEXTURE_NAME, MAX_MATERIALS, + MAX_MESHES, MAX_SOUNDS, MAX_TEXTURES, +}; +use crate::ecs::components::render_state_component::RenderStateComponent; +use crate::resources::{Resource, ResourceLoadType, Texture, TextureType}; + pub fn rendering_system(engine: &mut Engine) -> Result<()> { + // One-time bootstrap: register resource types and create default resources + // Require RenderStateComponent; if missing, panic via unwrap + let need_bootstrap = !engine + .get_global_component::() + .unwrap() + .boot_done; + if need_bootstrap { + init_default_resources(engine)?; + + if let Ok(rs) = engine.get_global_component_mut::() { + rs.boot_done = true; + } + + // Skip the rest of the frame; camera may not exist yet during bootstrap + return Ok(()); + } + let mut timer = Timer::new(); timer.begin_context("rendering_system update"); timer.record("Get active camera"); @@ -185,3 +209,51 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { } } } + +fn init_default_resources(engine: &mut Engine) -> Result<(), Error> { + let max_texture_count = engine + .config + .get_int("MAX_TEXTURES") + .unwrap_or(MAX_TEXTURES as i64) as usize; + let max_mesh_count = engine + .config + .get_int("MAX_MESHES") + .unwrap_or(MAX_MESHES as i64) as usize; + let max_material_count = engine + .config + .get_int("MAX_MATERIALS") + .unwrap_or(MAX_MATERIALS as i64) as usize; + // TODO: Move to SoundSystem init + let max_sound_count = engine + .config + .get_int("MAX_SOUNDS") + .unwrap_or(MAX_SOUNDS as i64) as usize; + + engine.register_resource_type::(max_texture_count)?; + engine.register_resource_type::(max_mesh_count)?; + engine.register_resource_type::(max_material_count)?; + engine.register_resource_type::(max_sound_count)?; + + let default_color = Box::new(*include_bytes!("../../../res/textures/default_color.png")); + let default_normal = Box::new(*include_bytes!("../../../res/textures/default_normal.png")); + let mut color = Texture::new( + DEFAULT_COLOR_TEXTURE_NAME, + TextureType::Color, + ResourceLoadType::Bytes(default_color), + ); + color.initialize(engine)?; + engine.resource_manager.add_resource(color)?; + let mut normal = Texture::new( + DEFAULT_NORMAL_TEXTURE_NAME, + TextureType::Normal, + ResourceLoadType::Bytes(default_normal), + ); + normal.initialize(engine)?; + engine.resource_manager.add_resource(normal)?; + + let mut mat = Material::new(DEFAULT_MATERIAL_NAME); + mat.initialize(engine)?; + engine.resource_manager.add_resource(mat)?; + + Ok(()) +} diff --git a/engine/pill_engine/src/engine.rs b/engine/pill_engine/src/engine.rs index b5d87c45..2bfbb7c1 100644 --- a/engine/pill_engine/src/engine.rs +++ b/engine/pill_engine/src/engine.rs @@ -90,60 +90,6 @@ impl Engine { } } - fn create_default_resources(&mut self) -> Result<()> { - // limits --------------------------------------------------------------- - let max_texture_count = self - .config - .get_int("MAX_TEXTURES") - .unwrap_or(MAX_TEXTURES as i64) as usize; - let max_mesh_count = self - .config - .get_int("MAX_MESHES") - .unwrap_or(MAX_MESHES as i64) as usize; - let max_material_count = self - .config - .get_int("MAX_MATERIALS") - .unwrap_or(MAX_MATERIALS as i64) as usize; - let max_sound_count = self - .config - .get_int("MAX_SOUNDS") - .unwrap_or(MAX_SOUNDS as i64) as usize; - - // register resource types -------------------------------------------- - self.register_resource_type::(max_texture_count)?; - self.register_resource_type::(max_mesh_count)?; - self.register_resource_type::(max_material_count)?; - self.register_resource_type::(max_sound_count)?; - - // master shader & defaults ------------------------------------------- - // Legacy master pipeline removed; PSOs are created upfront by renderer - - let default_color = Box::new(*include_bytes!("../res/textures/default_color.png")); - let default_normal = Box::new(*include_bytes!("../res/textures/default_normal.png")); - - let mut color = Texture::new( - DEFAULT_COLOR_TEXTURE_NAME, - TextureType::Color, - ResourceLoadType::Bytes(default_color), - ); - color.initialize(self)?; - self.resource_manager.add_resource(color)?; - - let mut normal = Texture::new( - DEFAULT_NORMAL_TEXTURE_NAME, - TextureType::Normal, - ResourceLoadType::Bytes(default_normal), - ); - normal.initialize(self)?; - self.resource_manager.add_resource(normal)?; - - let mut mat = Material::new(DEFAULT_MATERIAL_NAME); - mat.initialize(self)?; - self.resource_manager.add_resource(mat)?; - - Ok(()) - } - fn start_game(&mut self) -> Result<()> { info!("Starting {}", "Game".mobj_style()); @@ -183,6 +129,7 @@ impl Engine { self.add_global_component(TimeComponent::new())?; self.add_global_component(DeferredUpdateComponent::new())?; self.add_global_component(EguiManagerComponent::new())?; + self.add_global_component(RenderStateComponent::new())?; let max_ambient_sink_count = self.config @@ -224,9 +171,7 @@ impl Engine { RENDERING_SYSTEM.update_phase, )?; - // Create default resources - self.create_default_resources() - .context("Failed to create default resources")?; + self.init_renderer(); // Start game self.start_game()?; @@ -234,6 +179,36 @@ impl Engine { Ok(()) } + fn init_renderer(&mut self) { + // KISS: Run only rendering system once for bootstrap (no active scene yet) + // TODO: Loop over PostGame systems to give them a change to init before game starts + { + let update_phase = UpdatePhase::PostGame; + let (system_name, system_function) = { + let sys = self + .system_manager + .get_system(RENDERING_SYSTEM.name, update_phase.clone()) + .unwrap(); + (sys.name.clone(), sys.system_function) + }; + let mut timer = Timer::new(); + timer.begin_context(&format!("{} system bootstrap", system_name)); + self.system_manager + .update_system_timer(system_name.as_str(), update_phase.clone(), timer) + .unwrap(); + (system_function)(self).unwrap(); + let mut timer = self + .system_manager + .get_system_timer(system_name.as_str(), update_phase.clone()) + .unwrap() + .unwrap(); + timer.end_context().unwrap(); + self.system_manager + .update_system_timer(system_name.as_str(), update_phase.clone(), timer) + .unwrap(); + } + } + /// Main engine update function /// /// Runs all systems in order: PreGame -> Game -> PostGame diff --git a/engine/pill_renderer/src/lib.rs b/engine/pill_renderer/src/lib.rs index 071ca3d3..c0af36ec 100644 --- a/engine/pill_renderer/src/lib.rs +++ b/engine/pill_renderer/src/lib.rs @@ -1,20 +1,16 @@ #![cfg_attr(debug_assertions, allow(dead_code, unused_imports))] -pub mod renderer; -pub mod resources; -pub mod renderer_resource_storage; -pub mod instance; pub mod egui; +pub mod instance; pub mod mesh_drawer; +pub mod renderer; +pub mod renderer_resource_storage; +pub mod resources; // --- Use --- pub use renderer::*; -pub use instance::{ - Instance, -}; +pub use instance::Instance; -pub use renderer_resource_storage::{ - RendererResourceStorage, -}; +pub use renderer_resource_storage::RendererResourceStorage; From e20962cde6b26891fe3fc66bfb1c3d5eac1cf822 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Fri, 17 Oct 2025 00:10:37 +0200 Subject: [PATCH 37/69] remove State logic nesting --- engine/pill_renderer/src/renderer.rs | 1511 +++++++++++++------------- 1 file changed, 758 insertions(+), 753 deletions(-) diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index a7ca998a..cfe46182 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -456,249 +456,6 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; } } -impl PillRenderer for Renderer { - fn new(window: Arc, config: config::Config) -> Self { - info!("Initializing {}", "Renderer".mobj_style()); - let state: State = pollster::block_on(State::new(window, config)); - - Self { state } - } - - fn resize(&mut self, new_window_size: winit::dpi::PhysicalSize) { - info!("Resizing {} resources", "Renderer".mobj_style()); - self.state.resize(new_window_size) - } - - fn create_mesh(&mut self, name: &str, mesh_data: &MeshData) -> Result { - let mesh = RendererMesh::new(&self.state.device, name, mesh_data)?; - let handle = self.state.renderer_resource_storage.meshes.insert(mesh); - - Ok(handle) - } - - fn create_texture( - &mut self, - name: &str, - image_data: &image::DynamicImage, - texture_type: TextureType, - ) -> Result { - let texture = RendererTexture::new_texture( - &self.state.device, - &self.state.queue, - Some(name), - image_data, - texture_type, - )?; - let handle = self - .state - .renderer_resource_storage - .textures - .insert(texture); - - Ok(handle) - } - - fn create_material( - &mut self, - name: &str, - textures: &MaterialTextureMap, - parameters: &MaterialParameterMap, - ) -> Result { - // Create bind group layouts inline (avoid pipeline storage dependency) - let material_texture_bind_group_layout = - self.state - .device - .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("material_texture_bind_group_layout"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 2, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 3, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], - }); - let material_parameter_bind_group_layout = - self.state - .device - .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("material_parameter_bind_group_layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }], - }); - - let material = RendererMaterial::new( - &self.state.device, - &self.state.queue, - &self.state.renderer_resource_storage, - name, - MASTER_PIPELINE_HANDLE, - &material_texture_bind_group_layout, - textures, - &material_parameter_bind_group_layout, - parameters, - ) - .unwrap(); - - let handle = self - .state - .renderer_resource_storage - .materials - .insert(material); - - Ok(handle) - } - - fn create_camera(&mut self) -> Result { - let camera_bind_group_layout = - self.state - .device - .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("camera_bind_group_layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }], - }); - let camera = RendererCamera::new(&self.state.device, &camera_bind_group_layout)?; - let handle = self.state.renderer_resource_storage.cameras.insert(camera); - - Ok(handle) - } - - fn update_material_textures( - &mut self, - renderer_material_handle: RendererMaterialHandle, - textures: &MaterialTextureMap, - ) -> Result<()> { - RendererMaterial::update_textures( - &self.state.device, - renderer_material_handle, - &mut self.state.renderer_resource_storage, - textures, - ) - } - - fn update_material_parameters( - &mut self, - renderer_material_handle: RendererMaterialHandle, - parameters: &MaterialParameterMap, - ) -> Result<()> { - RendererMaterial::update_parameters( - &self.state.device, - &self.state.queue, - renderer_material_handle, - &mut self.state.renderer_resource_storage, - parameters, - ) - } - - fn destroy_mesh(&mut self, renderer_mesh_handle: RendererMeshHandle) -> Result<()> { - self.state - .renderer_resource_storage - .meshes - .remove(renderer_mesh_handle) - .unwrap(); - - Ok(()) - } - - fn destroy_texture(&mut self, renderer_texture_handle: RendererTextureHandle) -> Result<()> { - self.state - .renderer_resource_storage - .textures - .remove(renderer_texture_handle) - .unwrap(); - - Ok(()) - } - - fn destroy_material(&mut self, renderer_material_handle: RendererMaterialHandle) -> Result<()> { - self.state - .renderer_resource_storage - .materials - .remove(renderer_material_handle) - .unwrap(); - - Ok(()) - } - - fn destroy_camera(&mut self, renderer_camera_handle: RendererCameraHandle) -> Result<()> { - self.state - .renderer_resource_storage - .cameras - .remove(renderer_camera_handle) - .unwrap(); - - Ok(()) - } - - fn render( - &mut self, - active_camera_entity_handle: EntityHandle, - render_queue: &Vec, - camera_component_storage: &ComponentStorage, - transform_component_storage: &ComponentStorage, - egui_ui: Box, - timer: &mut Timer, - ) -> Result<()> { - self.state.render( - active_camera_entity_handle, - render_queue, - camera_component_storage, - transform_component_storage, - egui_ui, - timer, - ) - } - - fn pass_input_to_egui(&mut self, event: &winit::event::WindowEvent) -> Result<()> { - self.state.egui_renderer.handle_input(event); - Ok(()) - } -} - // KISS helper: build overlay pipeline, UBO layout/buffer, and bind group fn create_overlay_depth( device: &wgpu::Device, @@ -890,9 +647,8 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; } pub struct State { - // Resources + // Resources and GPU objects moved from ctor into here explicitly renderer_resource_storage: RendererResourceStorage, - // Renderer variables surface: wgpu::Surface<'static>, device: wgpu::Device, queue: wgpu::Queue, @@ -925,483 +681,706 @@ pub struct State { egui_renderer: crate::egui::EguiRenderer, // TODO: Separate system adding Pass } -impl State { - // Creating some of the wgpu types requires async code - async fn new(window: Arc, config: config::Config) -> Self { - let window_size = window.inner_size(); +impl PillRenderer for Renderer { + fn new(window: Arc, config: config::Config) -> Self { + info!("Initializing {}", "Renderer".mobj_style()); + let state: State = pollster::block_on(async move { + let window_size = window.inner_size(); - let window_ref = window.clone(); + let window_ref = window.clone(); - let backends = wgpu::util::backend_bits_from_env().unwrap_or_default(); - let dx12_shader_compiler = wgpu::util::dx12_shader_compiler_from_env().unwrap_or_default(); - let gles_minor_version = wgpu::util::gles_minor_version_from_env().unwrap_or_default(); + let backends = wgpu::util::backend_bits_from_env().unwrap_or_default(); + let dx12_shader_compiler = + wgpu::util::dx12_shader_compiler_from_env().unwrap_or_default(); + let gles_minor_version = wgpu::util::gles_minor_version_from_env().unwrap_or_default(); - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends, - flags: wgpu::InstanceFlags::from_build_config().with_env(), - dx12_shader_compiler, - gles_minor_version, - }); - let surface = instance.create_surface(window).unwrap(); + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends, + flags: wgpu::InstanceFlags::from_build_config().with_env(), + dx12_shader_compiler, + gles_minor_version, + }); + let surface = instance.create_surface(window).unwrap(); + + // Specify adapter options (Options passed here are not guaranteed to work for all devices) + let request_adapter_options = wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::default(), + compatible_surface: Some(&surface), + force_fallback_adapter: false, + }; + + // Create adapter + let adapter = instance + .request_adapter(&request_adapter_options) + .await + .unwrap(); + let adapter_info = adapter.get_info(); + info!( + "Using GPU: {} ({:?})", + adapter_info.name, adapter_info.backend + ); - // Specify adapter options (Options passed here are not guaranteed to work for all devices) - let request_adapter_options = wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::default(), - compatible_surface: Some(&surface), - force_fallback_adapter: false, - }; + let features = wgpu::Features::DEPTH_CLIP_CONTROL; + + // Create device descriptor + let device_descriptor = wgpu::DeviceDescriptor { + label: None, + required_features: features, // Allows to specify what extra features of GPU that needs to be included (e.g. depth clamping, push constants, texture compression, etc) + required_limits: wgpu::Limits::default(), // Allows to specify the limit of certain types of resources that will be used (e.g. max samplers, uniform buffers, etc) + //memory_hints: wgpu::MemoryHints::MemoryUsage, + }; + + // Create device and queue + let (device, queue) = adapter + .request_device(&device_descriptor, None) + .await + .unwrap(); + + // Specify surface configuration + let preferred_format = wgpu::TextureFormat::Rgba8UnormSrgb; + + // Get supported present modes and choose the best one + let surface_caps = surface.get_capabilities(&adapter); + let present_mode = if surface_caps + .present_modes + .contains(&wgpu::PresentMode::Mailbox) + { + wgpu::PresentMode::Mailbox + } else if surface_caps + .present_modes + .contains(&wgpu::PresentMode::Immediate) + { + wgpu::PresentMode::Immediate + } else { + wgpu::PresentMode::Fifo + }; + + // Choose the best supported format + let format = if surface_caps.formats.contains(&preferred_format) { + preferred_format + } else if surface_caps + .formats + .contains(&wgpu::TextureFormat::Bgra8UnormSrgb) + { + wgpu::TextureFormat::Bgra8UnormSrgb + } else if surface_caps + .formats + .contains(&wgpu::TextureFormat::Bgra8Unorm) + { + wgpu::TextureFormat::Bgra8Unorm + } else { + surface_caps.formats[0] // Use first available format + }; - // Create adapter - let adapter = instance - .request_adapter(&request_adapter_options) - .await + // macOS (Retina) note: use physical size (inner_size) and prefer premultiplied alpha if supported + let alpha_mode = if surface_caps + .alpha_modes + .contains(&wgpu::CompositeAlphaMode::PreMultiplied) + { + wgpu::CompositeAlphaMode::PreMultiplied + } else { + wgpu::CompositeAlphaMode::Auto + }; + + let surface_configuration = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, // Defines how the swap_chain's underlying textures will be used + format: format, // Defines how the swap_chain's textures will be stored on the gpu + width: window_size.width, + height: window_size.height, + desired_maximum_frame_latency: 2, + present_mode: present_mode, // Defines how to sync the surface with the display + alpha_mode, + view_formats: vec![format], + }; + + // Configure surface + surface.configure(&device, &surface_configuration); + + // Configure collections + let mut renderer_resource_storage = RendererResourceStorage::new(&config); + + // Create depth and color texture + let depth_texture = RendererTexture::new_depth_texture( + &device, + &surface_configuration, + "depth_texture", + ) .unwrap(); - let adapter_info = adapter.get_info(); - info!( - "Using GPU: {} ({:?})", - adapter_info.name, adapter_info.backend - ); - let features = wgpu::Features::DEPTH_CLIP_CONTROL; + // Use Rgba16Float for HDR color buffers; it's the common, well-supported, + // performant choice. Reserve Rgba32Float for niche cases needing extreme + // precision and accept the 2× bandwidth/memory hit. If alpha isn't needed, + // Rg11b10Float/R11G11B10_FLOAT is a fast alternative. + // Tone-map to the sRGB swapchain in the composite pass. + let color_format = wgpu::TextureFormat::Rgba16Float; + let depth_format = wgpu::TextureFormat::Depth32Float; - // Create device descriptor - let device_descriptor = wgpu::DeviceDescriptor { - label: None, - required_features: features, // Allows to specify what extra features of GPU that needs to be included (e.g. depth clamping, push constants, texture compression, etc) - required_limits: wgpu::Limits::default(), // Allows to specify the limit of certain types of resources that will be used (e.g. max samplers, uniform buffers, etc) - //memory_hints: wgpu::MemoryHints::MemoryUsage, - }; + // Milestone 6: Create offscreen color target (RENDER_ATTACHMENT | TEXTURE_BINDING) + let offscreen_color_texture = create_render_target( + &device, + color_format, + surface_configuration.width, + surface_configuration.height, + "offscreen_color", + ); + + let egui_renderer = + EguiRenderer::new(&device, surface_configuration.format, None, 1, window_ref); + + // Build static shader modules and pipeline once ([SIMILAR] prebuilt PSO per TALK) + let vertex_wgsl = r#" +struct Camera { position: vec4, view_projection_matrix: mat4x4, }; +@group(0) @binding(0) var GCamera: Camera; + +struct PerDraw { + mvp: mat4x4, + model: mat4x4, + tint: vec4, +}; +@group(3) @binding(0) var UPerDraw: PerDraw; + +struct VSIn { @location(0) pos: vec3, @location(1) uv: vec2, }; +struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; + +@vertex fn main(input: VSIn) -> VSOut { + var out: VSOut; + out.pos = UPerDraw.mvp * vec4(input.pos, 1.0); + out.uv = input.uv; + return out; +} +"#; + let fragment_wgsl = r#" +// Material set(1): +@group(1) @binding(0) var tex_diffuse: texture_2d; +@group(1) @binding(1) var smp_diffuse: sampler; +@group(1) @binding(2) var tex_normal: texture_2d; +@group(1) @binding(3) var smp_normal: sampler; + +// Material parameters set(2) - pack tint.rgb + specularity in a single vec4 +struct MaterialParams { tint_spec: vec4, } +@group(2) @binding(0) var UMaterial: MaterialParams; + +// Per-draw (set 3): supports per-entity tint for M4 +struct PerDraw { + mvp: mat4x4, + model: mat4x4, + tint: vec4, +}; +@group(3) @binding(0) var UPerDraw: PerDraw; - // Create device and queue - let (device, queue) = adapter - .request_device(&device_descriptor, None) - .await +@fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let albedo = textureSample(tex_diffuse, smp_diffuse, uv); + let tinted = vec4(UMaterial.tint_spec.rgb, 1.0) * UPerDraw.tint; + let spec_boost = 0.5 + 0.5 * UMaterial.tint_spec.a; + let color = albedo * tinted * spec_boost; + return vec4(color.rgb, 1.0); +} +"#; + let vertex_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("m2_vertex_shader"), + source: wgpu::ShaderSource::Wgsl(vertex_wgsl.into()), + }); + let fragment_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("m2_fragment_shader"), + source: wgpu::ShaderSource::Wgsl(fragment_wgsl.into()), + }); + let default_pipeline = RendererPipeline::new( + &device, + vertex_shader, + fragment_shader, + color_format, + Some(depth_format), + &[RendererMesh::data_layout_descriptor()], + ) .unwrap(); - // Specify surface configuration - let preferred_format = wgpu::TextureFormat::Rgba8UnormSrgb; + // Milestone 6: Composition pipeline (fullscreen triangle sampling offscreen texture) + let comp_vs_wgsl = r#" +struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; +@vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var pos = array, 3>( + vec2(-1.0, -3.0), + vec2( 3.0, 1.0), + vec2(-1.0, 1.0) + ); + var tuv = array, 3>( + vec2(0.0, 2.0), + vec2(2.0, 0.0), + vec2(0.0, 0.0) + ); + var o: VSOut; + o.pos = vec4(pos[vi], 0.0, 1.0); + o.uv = tuv[vi]; + return o; +} +"#; + let comp_fs_wgsl = r#" +@group(0) @binding(0) var t_src: texture_2d; +@group(0) @binding(1) var s_src: sampler; +@fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let hdr = textureSample(t_src, s_src, uv).rgb; + // Reinhard tone mapping; output remains in linear space. The sRGB swapchain will encode. + let ldr = hdr / (1.0 + hdr); + return vec4(ldr, 1.0); +} +"#; + let comp_vs = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("compose_vs"), + source: wgpu::ShaderSource::Wgsl(comp_vs_wgsl.into()), + }); + let comp_fs = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("compose_fs"), + source: wgpu::ShaderSource::Wgsl(comp_fs_wgsl.into()), + }); + let composite_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("compose_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + let comp_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("compose_pl"), + bind_group_layouts: &[&composite_bind_group_layout], + push_constant_ranges: &[], + }); + let composite_pipeline = + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("compose_pipeline"), + layout: Some(&comp_pipeline_layout), + vertex: wgpu::VertexState { + module: &comp_vs, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &comp_fs, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + // Composition writes to the swapchain view; target must match surface format + format: surface_configuration.format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + let composite_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("compose_bg"), + layout: &composite_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView( + &offscreen_color_texture.texture_view, + ), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&offscreen_color_texture.sampler), + }, + ], + }); - // Get supported present modes and choose the best one - let surface_caps = surface.get_capabilities(&adapter); - let present_mode = if surface_caps - .present_modes - .contains(&wgpu::PresentMode::Mailbox) - { - wgpu::PresentMode::Mailbox - } else if surface_caps - .present_modes - .contains(&wgpu::PresentMode::Immediate) - { - wgpu::PresentMode::Immediate - } else { - wgpu::PresentMode::Fifo - }; + // Overlay UV gradient pipeline (small quad in top-right corner) + let overlay_uv = create_overlay_uv( + &device, + &queue, + surface_configuration.format, + [0.75, 0.75, 0.95, 0.95], + ); - // Choose the best supported format - let format = if surface_caps.formats.contains(&preferred_format) { - preferred_format - } else if surface_caps - .formats - .contains(&wgpu::TextureFormat::Bgra8UnormSrgb) - { - wgpu::TextureFormat::Bgra8UnormSrgb - } else if surface_caps - .formats - .contains(&wgpu::TextureFormat::Bgra8Unorm) - { - wgpu::TextureFormat::Bgra8Unorm - } else { - surface_caps.formats[0] // Use first available format - }; + // Ideally Client submitted pass via API + // Create logo texture via renderer API and pass handle into overlay factory + let logo_image = image::open( + "/Users/mk/dev/demo/Pill-Engine/engine/pill_renderer/res/pill_logo_horizontal_white.png", + ) + .expect("failed to load overlay logo image"); + + let logo_handle = { + let tex = RendererTexture::new_texture( + &device, + &queue, + Some("overlay_logo"), + &logo_image, + TextureType::Color, + ) + .expect("failed to create overlay logo texture"); + renderer_resource_storage.textures.insert(tex) + }; + // Overlay logo pipeline (small quad) + // res: 1024 × 320, 1.0 x 0.3 + let h: f32 = 0.04; + let overlay_logo = create_overlay_logo( + &device, + &queue, + surface_configuration.format, + logo_handle, + &renderer_resource_storage, + [0.98 - 3. * h, 0.02, 0.98, 0.02 + h], // bottom right + ); - // macOS (Retina) note: use physical size (inner_size) and prefer premultiplied alpha if supported - let alpha_mode = if surface_caps - .alpha_modes - .contains(&wgpu::CompositeAlphaMode::PreMultiplied) - { - wgpu::CompositeAlphaMode::PreMultiplied - } else { - wgpu::CompositeAlphaMode::Auto - }; + let overlay_depth = create_overlay_depth( + &device, + &queue, + surface_configuration.format, + &depth_texture, + [0.75, 0.55, 0.95, 0.72], // top right, 2 row + ); - let surface_configuration = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, // Defines how the swap_chain's underlying textures will be used - format: format, // Defines how the swap_chain's textures will be stored on the gpu - width: window_size.width, - height: window_size.height, - desired_maximum_frame_latency: 2, - present_mode: present_mode, // Defines how to sync the surface with the display - alpha_mode, - view_formats: vec![format], - }; + // Create state + let per_draw_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("per_draw_bind_group_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: Some(std::num::NonZeroU64::new(144).unwrap()), + }, + count: None, + }], + }); + State { + // Resources + renderer_resource_storage, + // Renderer variables + surface, + device, + queue, + surface_configuration, + window_size, + color_format, + depth_format, + depth_texture, + offscreen_color_texture, + // Per-frame UBO ring init + per_draw_stride: 256, + per_draw_capacity: 0, + per_draw_buffer: None, + // [SIMILAR] Per-draw dynamic UBO layout with has_dynamic_offset=true per TALK (Milestone 5) + per_draw_bind_group_layout, + per_draw_bind_group: None, + // Prebuilt PSO + default_pipeline, + // Composition + composite_bind_group_layout, + composite_pipeline, + composite_bind_group, + // Overlays + overlay_uv, + overlay_logo, + overlay_depth, + // Other + config, + egui_renderer, + } + }); - // Configure surface - surface.configure(&device, &surface_configuration); + Self { state } + } - // Configure collections - let mut renderer_resource_storage = RendererResourceStorage::new(&config); + fn create_mesh(&mut self, name: &str, mesh_data: &MeshData) -> Result { + let mesh = RendererMesh::new(&self.state.device, name, mesh_data)?; + let handle = self.state.renderer_resource_storage.meshes.insert(mesh); - // Create depth and color texture - let depth_texture = - RendererTexture::new_depth_texture(&device, &surface_configuration, "depth_texture") - .unwrap(); + Ok(handle) + } - // Use Rgba16Float for HDR color buffers; it's the common, well-supported, - // performant choice. Reserve Rgba32Float for niche cases needing extreme - // precision and accept the 2× bandwidth/memory hit. If alpha isn't needed, - // Rg11b10Float/R11G11B10_FLOAT is a fast alternative. - // Tone-map to the sRGB swapchain in the composite pass. - let color_format = wgpu::TextureFormat::Rgba16Float; - let depth_format = wgpu::TextureFormat::Depth32Float; - - // Milestone 6: Create offscreen color target (RENDER_ATTACHMENT | TEXTURE_BINDING) - let offscreen_color_texture = create_render_target( - &device, - color_format, - surface_configuration.width, - surface_configuration.height, - "offscreen_color", - ); + fn create_texture( + &mut self, + name: &str, + image_data: &image::DynamicImage, + texture_type: TextureType, + ) -> Result { + let texture = RendererTexture::new_texture( + &self.state.device, + &self.state.queue, + Some(name), + image_data, + texture_type, + )?; + let handle = self + .state + .renderer_resource_storage + .textures + .insert(texture); + + Ok(handle) + } + + fn create_material( + &mut self, + name: &str, + textures: &MaterialTextureMap, + parameters: &MaterialParameterMap, + ) -> Result { + // Create bind group layouts inline (avoid pipeline storage dependency) + let material_texture_bind_group_layout = + self.state + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("material_texture_bind_group_layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + let material_parameter_bind_group_layout = + self.state + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("material_parameter_bind_group_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + + let material = RendererMaterial::new( + &self.state.device, + &self.state.queue, + &self.state.renderer_resource_storage, + name, + MASTER_PIPELINE_HANDLE, + &material_texture_bind_group_layout, + textures, + &material_parameter_bind_group_layout, + parameters, + ) + .unwrap(); - let egui_renderer = - EguiRenderer::new(&device, surface_configuration.format, None, 1, window_ref); + let handle = self + .state + .renderer_resource_storage + .materials + .insert(material); - // Build static shader modules and pipeline once ([SIMILAR] prebuilt PSO per TALK) - let vertex_wgsl = r#" -struct Camera { position: vec4, view_projection_matrix: mat4x4, }; -@group(0) @binding(0) var GCamera: Camera; + Ok(handle) + } -struct PerDraw { - mvp: mat4x4, - model: mat4x4, - tint: vec4, -}; -@group(3) @binding(0) var UPerDraw: PerDraw; + fn create_camera(&mut self) -> Result { + let camera_bind_group_layout = + self.state + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("camera_bind_group_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + let camera = RendererCamera::new(&self.state.device, &camera_bind_group_layout)?; + let handle = self.state.renderer_resource_storage.cameras.insert(camera); -struct VSIn { @location(0) pos: vec3, @location(1) uv: vec2, }; -struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; + Ok(handle) + } -@vertex fn main(input: VSIn) -> VSOut { - var out: VSOut; - out.pos = UPerDraw.mvp * vec4(input.pos, 1.0); - out.uv = input.uv; - return out; -} -"#; - let fragment_wgsl = r#" -// Material set(1): -@group(1) @binding(0) var tex_diffuse: texture_2d; -@group(1) @binding(1) var smp_diffuse: sampler; -@group(1) @binding(2) var tex_normal: texture_2d; -@group(1) @binding(3) var smp_normal: sampler; + fn update_material_textures( + &mut self, + renderer_material_handle: RendererMaterialHandle, + textures: &MaterialTextureMap, + ) -> Result<()> { + RendererMaterial::update_textures( + &self.state.device, + renderer_material_handle, + &mut self.state.renderer_resource_storage, + textures, + ) + } -// Material parameters set(2) - pack tint.rgb + specularity in a single vec4 -struct MaterialParams { tint_spec: vec4, } -@group(2) @binding(0) var UMaterial: MaterialParams; + fn update_material_parameters( + &mut self, + renderer_material_handle: RendererMaterialHandle, + parameters: &MaterialParameterMap, + ) -> Result<()> { + RendererMaterial::update_parameters( + &self.state.device, + &self.state.queue, + renderer_material_handle, + &mut self.state.renderer_resource_storage, + parameters, + ) + } -// Per-draw (set 3): supports per-entity tint for M4 -struct PerDraw { - mvp: mat4x4, - model: mat4x4, - tint: vec4, -}; -@group(3) @binding(0) var UPerDraw: PerDraw; + fn destroy_mesh(&mut self, renderer_mesh_handle: RendererMeshHandle) -> Result<()> { + self.state + .renderer_resource_storage + .meshes + .remove(renderer_mesh_handle) + .unwrap(); -@fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { - let albedo = textureSample(tex_diffuse, smp_diffuse, uv); - let tinted = vec4(UMaterial.tint_spec.rgb, 1.0) * UPerDraw.tint; - let spec_boost = 0.5 + 0.5 * UMaterial.tint_spec.a; - let color = albedo * tinted * spec_boost; - return vec4(color.rgb, 1.0); -} -"#; - let vertex_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("m2_vertex_shader"), - source: wgpu::ShaderSource::Wgsl(vertex_wgsl.into()), - }); - let fragment_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("m2_fragment_shader"), - source: wgpu::ShaderSource::Wgsl(fragment_wgsl.into()), - }); - let default_pipeline = RendererPipeline::new( - &device, - vertex_shader, - fragment_shader, - color_format, - Some(depth_format), - &[RendererMesh::data_layout_descriptor()], - ) - .unwrap(); + Ok(()) + } - // Milestone 6: Composition pipeline (fullscreen triangle sampling offscreen texture) - let comp_vs_wgsl = r#" -struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; -@vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { - var pos = array, 3>( - vec2(-1.0, -3.0), - vec2( 3.0, 1.0), - vec2(-1.0, 1.0) - ); - var tuv = array, 3>( - vec2(0.0, 2.0), - vec2(2.0, 0.0), - vec2(0.0, 0.0) - ); - var o: VSOut; - o.pos = vec4(pos[vi], 0.0, 1.0); - o.uv = tuv[vi]; - return o; -} -"#; - let comp_fs_wgsl = r#" -@group(0) @binding(0) var t_src: texture_2d; -@group(0) @binding(1) var s_src: sampler; -@fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { - let hdr = textureSample(t_src, s_src, uv).rgb; - // Reinhard tone mapping; output remains in linear space. The sRGB swapchain will encode. - let ldr = hdr / (1.0 + hdr); - return vec4(ldr, 1.0); -} -"#; - let comp_vs = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("compose_vs"), - source: wgpu::ShaderSource::Wgsl(comp_vs_wgsl.into()), - }); - let comp_fs = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("compose_fs"), - source: wgpu::ShaderSource::Wgsl(comp_fs_wgsl.into()), - }); - let composite_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("compose_bgl"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], - }); - let comp_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("compose_pl"), - bind_group_layouts: &[&composite_bind_group_layout], - push_constant_ranges: &[], - }); - let composite_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("compose_pipeline"), - layout: Some(&comp_pipeline_layout), - vertex: wgpu::VertexState { - module: &comp_vs, - entry_point: "main", - buffers: &[], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &comp_fs, - entry_point: "main", - targets: &[Some(wgpu::ColorTargetState { - // Composition writes to the swapchain view; target must match surface format - format: surface_configuration.format, - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState::default(), - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview: None, - }); - let composite_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("compose_bg"), - layout: &composite_bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView( - &offscreen_color_texture.texture_view, - ), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&offscreen_color_texture.sampler), - }, - ], - }); + fn destroy_texture(&mut self, renderer_texture_handle: RendererTextureHandle) -> Result<()> { + self.state + .renderer_resource_storage + .textures + .remove(renderer_texture_handle) + .unwrap(); - // Overlay UV gradient pipeline (small quad in top-right corner) - let overlay_uv = create_overlay_uv( - &device, - &queue, - surface_configuration.format, - [0.75, 0.75, 0.95, 0.95], - ); + Ok(()) + } - // Ideally Client submitted pass via API - // Create logo texture via renderer API and pass handle into overlay factory - let logo_image = image::open( - "/Users/mk/dev/demo/Pill-Engine/engine/pill_renderer/res/pill_logo_horizontal_white.png", - ) - .expect("failed to load overlay logo image"); + fn destroy_material(&mut self, renderer_material_handle: RendererMaterialHandle) -> Result<()> { + self.state + .renderer_resource_storage + .materials + .remove(renderer_material_handle) + .unwrap(); - let logo_handle = { - let tex = RendererTexture::new_texture( - &device, - &queue, - Some("overlay_logo"), - &logo_image, - TextureType::Color, - ) - .expect("failed to create overlay logo texture"); - renderer_resource_storage.textures.insert(tex) - }; - // Overlay logo pipeline (small quad) - // res: 1024 × 320, 1.0 x 0.3 - let h: f32 = 0.04; - let overlay_logo = create_overlay_logo( - &device, - &queue, - surface_configuration.format, - logo_handle, - &renderer_resource_storage, - [0.98 - 3. * h, 0.02, 0.98, 0.02 + h], // bottom right - ); + Ok(()) + } - let overlay_depth = create_overlay_depth( - &device, - &queue, - surface_configuration.format, - &depth_texture, - [0.75, 0.55, 0.95, 0.72], // top right, 2 row - ); + fn destroy_camera(&mut self, renderer_camera_handle: RendererCameraHandle) -> Result<()> { + self.state + .renderer_resource_storage + .cameras + .remove(renderer_camera_handle) + .unwrap(); - // Create state - let per_draw_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("per_draw_bind_group_layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: true, - min_binding_size: Some(std::num::NonZeroU64::new(144).unwrap()), - }, - count: None, - }], - }); - Self { - // Resources - renderer_resource_storage, - // Renderer variables - surface, - device, - queue, - surface_configuration, - window_size, - color_format, - depth_format, - depth_texture, - offscreen_color_texture, - // Per-frame UBO ring init - per_draw_stride: 256, - per_draw_capacity: 0, - per_draw_buffer: None, - // [SIMILAR] Per-draw dynamic UBO layout with has_dynamic_offset=true per TALK (Milestone 5) - per_draw_bind_group_layout, - per_draw_bind_group: None, - // Prebuilt PSO - default_pipeline, - // Composition - composite_bind_group_layout, - composite_pipeline, - composite_bind_group, - // Overlays - overlay_uv, - overlay_logo, - overlay_depth, - // Other - config, - egui_renderer, - } + Ok(()) } fn resize(&mut self, new_window_size: winit::dpi::PhysicalSize) { + info!("Resizing {} resources", "Renderer".mobj_style()); + // self.state.resize(new_window_size) if new_window_size.width > 0 && new_window_size.height > 0 { - self.window_size = new_window_size; - self.surface_configuration.width = new_window_size.width; - self.surface_configuration.height = new_window_size.height; - self.surface - .configure(&self.device, &self.surface_configuration); - self.depth_texture = RendererTexture::new_depth_texture( - &self.device, - &self.surface_configuration, + self.state.window_size = new_window_size; + self.state.surface_configuration.width = new_window_size.width; + self.state.surface_configuration.height = new_window_size.height; + self.state + .surface + .configure(&self.state.device, &self.state.surface_configuration); + self.state.depth_texture = RendererTexture::new_depth_texture( + &self.state.device, + &self.state.surface_configuration, "depth_texture", ) .unwrap(); // Recreate offscreen color target and its bind group - self.offscreen_color_texture = create_render_target( - &self.device, - self.color_format, - self.surface_configuration.width, - self.surface_configuration.height, + self.state.offscreen_color_texture = create_render_target( + &self.state.device, + self.state.color_format, + self.state.surface_configuration.width, + self.state.surface_configuration.height, "offscreen_color", ); - self.composite_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("compose_bg"), - layout: &self.composite_bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView( - &self.offscreen_color_texture.texture_view, - ), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler( - &self.offscreen_color_texture.sampler, - ), - }, - ], - }); + self.state.composite_bind_group = + self.state + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("compose_bg"), + layout: &self.state.composite_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView( + &self.state.offscreen_color_texture.texture_view, + ), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler( + &self.state.offscreen_color_texture.sampler, + ), + }, + ], + }); // Rebind overlay depth material to the new depth texture view - self.overlay_depth.material_bind_group = - self.device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_depth_material_bg"), - layout: &self.overlay_depth.material_bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView( - &self.depth_texture.texture_view, - ), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &self.overlay_depth.material_tint_buffer, - offset: 0, - size: Some(std::num::NonZeroU64::new(16).unwrap()), - }), - }, - ], - }); + self.state.overlay_depth.material_bind_group = + self.state + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_depth_material_bg"), + layout: &self.state.overlay_depth.material_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView( + &self.state.depth_texture.texture_view, + ), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &self.state.overlay_depth.material_tint_buffer, + offset: 0, + size: Some(std::num::NonZeroU64::new(16).unwrap()), + }), + }, + ], + }); // Old offscreen texture/view/sampler are dropped when replaced; wgpu defers actual GPU resource // destruction until safe. See optional early reclamation via device.poll: // https://docs.rs/wgpu/latest/wgpu/struct.Device.html#method.poll - self.device.poll(wgpu::Maintain::Wait); + self.state.device.poll(wgpu::Maintain::Wait); } } @@ -1414,14 +1393,23 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; egui_ui: Box, timer: &mut Timer, ) -> Result<()> { + // self.state.render( + // active_camera_entity_handle, + // render_queue, + // camera_component_storage, + // transform_component_storage, + // egui_ui, + // timer, + // ) + // [SIMILAR] Prebuilt PSO used; avoid hot-path pipeline creation per TALK // Use prebuilt default_pipeline for bind group layouts and pipeline - let _per_draw_bind_group_layout = &self.default_pipeline.per_draw_bind_group_layout; + let _per_draw_bind_group_layout = &self.state.default_pipeline.per_draw_bind_group_layout; // M3: Hello Mesh + per-draw MVP (dynamic offsets) timer.record("Frame: acquire"); // Get frame or return mapped error if failed - let frame = self.surface.get_current_texture(); + let frame = self.state.surface.get_current_texture(); let frame = match frame { std::result::Result::Ok(frame) => frame, @@ -1449,6 +1437,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; .unwrap(); let active_camera_component = camera_storage.as_ref().unwrap(); let renderer_camera = self + .state .renderer_resource_storage .cameras .get_mut(get_renderer_resource_handle_from_camera_component( @@ -1461,11 +1450,12 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; .unwrap(); let active_camera_transform_component = camera_transform_storage.as_ref().unwrap(); renderer_camera.update( - &self.queue, + &self.state.queue, active_camera_component, active_camera_transform_component, ); let renderer_camera = self + .state .renderer_resource_storage .cameras .get(get_renderer_resource_handle_from_camera_component( @@ -1475,11 +1465,12 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; let clear_color = active_camera_component.clear_color; // Record inline draw pass (M2) - let mut encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("m2_inline_encoder"), - }); + let mut encoder = + self.state + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("m2_inline_encoder"), + }); // [SIMILAR] CPU frustum culling and precomputing per-draw transforms before starting the pass matches TALK's advice // [DIFF] Only frustum culling here; TALK discusses 2-pass occlusion and broader CPU-driven pipelines @@ -1528,6 +1519,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; NonZeroU32::new(key.material_version.into()).unwrap(), ); let material_for_pipeline = self + .state .renderer_resource_storage .materials .get(material_handle) @@ -1547,6 +1539,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; // World AABB from mesh local AABB let mesh = self + .state .renderer_resource_storage .meshes .get(mesh_handle) @@ -1689,28 +1682,30 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; }) .sum(); // total instances; no direct use beyond sanity, avoid unused warning by not binding - if self.per_draw_capacity < needed { - self.per_draw_capacity = needed.next_power_of_two().max(1); - let size = self.per_draw_stride * self.per_draw_capacity; - self.per_draw_buffer = Some(self.device.create_buffer(&wgpu::BufferDescriptor { - label: Some("per_draw_dynamic_ubo_ring"), - size, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - })); - self.per_draw_bind_group = - Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor { + if self.state.per_draw_capacity < needed { + self.state.per_draw_capacity = needed.next_power_of_two().max(1); + let size = self.state.per_draw_stride * self.state.per_draw_capacity; + self.state.per_draw_buffer = + Some(self.state.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("per_draw_dynamic_ubo_ring"), + size, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + })); + self.state.per_draw_bind_group = Some(self.state.device.create_bind_group( + &wgpu::BindGroupDescriptor { label: Some("per_draw_bind_group"), - layout: &self.default_pipeline.per_draw_bind_group_layout, + layout: &self.state.default_pipeline.per_draw_bind_group_layout, entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: self.per_draw_buffer.as_ref().unwrap(), + buffer: self.state.per_draw_buffer.as_ref().unwrap(), offset: 0, size: Some(std::num::NonZeroU64::new(144).unwrap()), }), }], - })); + }, + )); } #[repr(C)] #[derive(Copy, Clone)] @@ -1723,7 +1718,8 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; unsafe impl bytemuck::Pod for PerDrawStd140 {} let model: [[f32; 4]; 4] = cgmath::Matrix4::::from_scale(1.0).into(); let tint: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; - let mut staging: Vec = Vec::with_capacity((self.per_draw_stride * needed) as usize); + let mut staging: Vec = + Vec::with_capacity((self.state.per_draw_stride * needed) as usize); let mut next_offset_u32: u32 = 0; for g in groups.iter_mut() { for b in g.batches.iter_mut() { @@ -1735,15 +1731,16 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; tint, }; staging.extend_from_slice(bytemuck::bytes_of(&pd)); - let pad = - (self.per_draw_stride as usize) - std::mem::size_of::(); + let pad = (self.state.per_draw_stride as usize) + - std::mem::size_of::(); staging.extend(std::iter::repeat(0u8).take(pad)); - next_offset_u32 = next_offset_u32.wrapping_add(self.per_draw_stride as u32); + next_offset_u32 = + next_offset_u32.wrapping_add(self.state.per_draw_stride as u32); } } } - if let Some(buf) = &self.per_draw_buffer { - self.queue.write_buffer(buf, 0, &staging); + if let Some(buf) = &self.state.per_draw_buffer { + self.state.queue.write_buffer(buf, 0, &staging); } timer.record("Per-draw UBO write (ring)"); timer.end_context()?; @@ -1754,7 +1751,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("m6_offscreen_pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &self.offscreen_color_texture.texture_view, + view: &self.state.offscreen_color_texture.texture_view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { @@ -1767,7 +1764,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; }, })], depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &self.depth_texture.texture_view, + view: &self.state.depth_texture.texture_view, depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: wgpu::StoreOp::Store, @@ -1785,7 +1782,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; group.pipeline_handle.data().index, group.material_handle.data().index )); - rpass.set_pipeline(&self.default_pipeline.render_pipeline); + rpass.set_pipeline(&self.state.default_pipeline.render_pipeline); // HOT SoA (per-frame hot path): // - RendererMaterial: texture_bind_group, parameter_bind_group @@ -1794,6 +1791,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; // COLD metadata (names/descs) lives in *_cold arrays and is not read in the draw loop. let material = self + .state .renderer_resource_storage .materials .get(group.material_handle) @@ -1804,6 +1802,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; for batch in &group.batches { let mesh = self + .state .renderer_resource_storage .meshes .get(batch.mesh_handle) @@ -1816,10 +1815,10 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; for i in 0..batch.instances.len() { let offset = batch .base_offset_u32 - .wrapping_add((i as u32) * (self.per_draw_stride as u32)); + .wrapping_add((i as u32) * (self.state.per_draw_stride as u32)); rpass.set_bind_group( 3, - self.per_draw_bind_group.as_ref().unwrap(), + self.state.per_draw_bind_group.as_ref().unwrap(), &[offset], ); rpass.draw_indexed(0..mesh.index_count, 0, 0..1); @@ -1848,8 +1847,8 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; timestamp_writes: None, occlusion_query_set: None, }); - rpass.set_pipeline(&self.composite_pipeline); - rpass.set_bind_group(0, &self.composite_bind_group, &[]); + rpass.set_pipeline(&self.state.composite_pipeline); + rpass.set_bind_group(0, &self.state.composite_bind_group, &[]); rpass.draw(0..3, 0..1); timer.record("Encode compose pass"); @@ -1875,72 +1874,75 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; }); // Draw small overlay UV quad in normalized rect via UBO - rpass.set_pipeline(&self.overlay_uv.pipeline); - rpass.set_bind_group(0, &self.overlay_uv.rect_bind_group, &[]); + rpass.set_pipeline(&self.state.overlay_uv.pipeline); + rpass.set_bind_group(0, &self.state.overlay_uv.rect_bind_group, &[]); rpass.draw(0..6, 0..1); // Draw depth overlay (bind material at group 0 and rect at group 1) // Rebind overlay depth material to the new depth texture view - self.overlay_depth.material_bind_group = - self.device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_depth_material_bg"), - layout: &self.overlay_depth.material_bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView( - &self.depth_texture.texture_view, - ), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &self.overlay_depth.material_tint_buffer, - offset: 0, - size: Some(std::num::NonZeroU64::new(16).unwrap()), - }), - }, - ], - }); - rpass.set_pipeline(&self.overlay_depth.pipeline); - rpass.set_bind_group(0, &self.overlay_depth.material_bind_group, &[]); - rpass.set_bind_group(1, &self.overlay_depth.rect_bind_group, &[]); + self.state.overlay_depth.material_bind_group = + self.state + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_depth_material_bg"), + layout: &self.state.overlay_depth.material_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView( + &self.state.depth_texture.texture_view, + ), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &self.state.overlay_depth.material_tint_buffer, + offset: 0, + size: Some(std::num::NonZeroU64::new(16).unwrap()), + }), + }, + ], + }); + rpass.set_pipeline(&self.state.overlay_depth.pipeline); + rpass.set_bind_group(0, &self.state.overlay_depth.material_bind_group, &[]); + rpass.set_bind_group(1, &self.state.overlay_depth.rect_bind_group, &[]); rpass.draw(0..6, 0..1); // Draw logo overlay (bind material at group 0 and rect at group 1) - rpass.set_pipeline(&self.overlay_logo.pipeline); - rpass.set_bind_group(0, &self.overlay_logo.material_bind_group, &[]); - rpass.set_bind_group(1, &self.overlay_logo.rect_bind_group, &[]); + rpass.set_pipeline(&self.state.overlay_logo.pipeline); + rpass.set_bind_group(0, &self.state.overlay_logo.material_bind_group, &[]); + rpass.set_bind_group(1, &self.state.overlay_logo.rect_bind_group, &[]); rpass.draw(0..6, 0..1); timer.record("Encode overlay passes"); } timer.end_context()?; // End "Compose pass" - self.queue.submit([encoder.finish()]); + self.state.queue.submit([encoder.finish()]); timer.record("Submit scene+compose passes"); // Egui overlay (load over the rendered frame) timer.begin_context("UI pass"); - let mut encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("egui_encoder"), - }); + let mut encoder = + self.state + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("egui_encoder"), + }); let screen_descriptor = egui_wgpu::ScreenDescriptor { - size_in_pixels: [self.window_size.width, self.window_size.height], - pixels_per_point: self.egui_renderer.window_scale_factor, + size_in_pixels: [self.state.window_size.width, self.state.window_size.height], + pixels_per_point: self.state.egui_renderer.window_scale_factor, }; - self.egui_renderer.draw( - &self.device, - &self.queue, + self.state.egui_renderer.draw( + &self.state.device, + &self.state.queue, &mut encoder, &view, screen_descriptor, |ctx| egui_ui(ctx), timer, )?; - self.queue.submit([encoder.finish()]); + self.state.queue.submit([encoder.finish()]); timer.record("UI draw & submit"); timer.end_context()?; // End "UI pass" @@ -1950,10 +1952,13 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; // Allow wgpu to process pending work and retire resources referenced by submitted encoders // without blocking the CPU. See Device::poll docs. - self.device.poll(wgpu::Maintain::Poll); + self.state.device.poll(wgpu::Maintain::Poll); Ok(()) } - // old render path removed (M2 uses inline draws) + fn pass_input_to_egui(&mut self, event: &winit::event::WindowEvent) -> Result<()> { + self.state.egui_renderer.handle_input(event); + Ok(()) + } } From 177b8cb6768f6c94ed293d4b793d8e6e1375cace Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Fri, 17 Oct 2025 21:53:18 +0200 Subject: [PATCH 38/69] feat: renderer factory method, pipeline v2 --- engine/pill_engine/Cargo.toml | 3 + engine/pill_engine/src/engine.rs | 1 + engine/pill_engine/src/graphics/mod.rs | 20 +- engine/pill_engine/src/graphics/renderer.rs | 39 + engine/pill_engine/src/lib.rs | 141 +-- engine/pill_renderer/src/renderer.rs | 995 ++++++++++++------ .../src/renderer_resource_storage.rs | 72 +- engine/pill_renderer/src/resources/mod.rs | 10 +- 8 files changed, 803 insertions(+), 478 deletions(-) diff --git a/engine/pill_engine/Cargo.toml b/engine/pill_engine/Cargo.toml index f2a801f9..ea8ea801 100644 --- a/engine/pill_engine/Cargo.toml +++ b/engine/pill_engine/Cargo.toml @@ -24,6 +24,9 @@ tobj = "3.0" # Audio rodio = { version = "0.20", features = ["wav", "mp3"] } +# Rendering +wgpu = { version = "0.20.1", features = [ "spirv" ] } + # Other readonly = "0.2" cgmath = "0.18" diff --git a/engine/pill_engine/src/engine.rs b/engine/pill_engine/src/engine.rs index 2bfbb7c1..3797649e 100644 --- a/engine/pill_engine/src/engine.rs +++ b/engine/pill_engine/src/engine.rs @@ -171,6 +171,7 @@ impl Engine { RENDERING_SYSTEM.update_phase, )?; + self.renderer.init()?; self.init_renderer(); // Start game diff --git a/engine/pill_engine/src/graphics/mod.rs b/engine/pill_engine/src/graphics/mod.rs index d531bb80..3bb65bfe 100644 --- a/engine/pill_engine/src/graphics/mod.rs +++ b/engine/pill_engine/src/graphics/mod.rs @@ -1,25 +1,17 @@ #![cfg_attr(debug_assertions, allow(dead_code, unused_imports))] -mod renderer; mod render_queue; +mod renderer; // --- Use --- pub use renderer::{ - Renderer, - PillRenderer, - RendererCameraHandle, - RendererMaterialHandle, - RendererMeshHandle, - RendererTextureHandle, - RendererPipelineHandle, + BufferDesc, PillRenderer, PipelineV2Desc, Renderer, RendererBufferHandle, RendererCameraHandle, + RendererMaterialHandle, RendererMeshHandle, RendererPipelineHandle, RendererPipelineV2Handle, + RendererTextureHandle, ShaderDesc, }; pub use render_queue::{ - RenderQueueItem, - RenderQueueKeyFields, - RenderQueueKey, - compose_render_queue_key, - decompose_render_queue_key, - RENDER_QUEUE_KEY_ORDER, + compose_render_queue_key, decompose_render_queue_key, RenderQueueItem, RenderQueueKey, + RenderQueueKeyFields, RENDER_QUEUE_KEY_ORDER, }; diff --git a/engine/pill_engine/src/graphics/renderer.rs b/engine/pill_engine/src/graphics/renderer.rs index 5340ba92..06a491c5 100644 --- a/engine/pill_engine/src/graphics/renderer.rs +++ b/engine/pill_engine/src/graphics/renderer.rs @@ -37,6 +37,41 @@ pill_core::define_new_pill_slotmap_key! { pub struct RendererTextureHandle; } +pill_core::define_new_pill_slotmap_key! { + pub struct RendererBufferHandle; +} + +pill_core::define_new_pill_slotmap_key! { + pub struct RendererPipelineV2Handle; +} + +// --- Descriptors --- + +// Minimal buffer creation helper mirroring the planned ResourceManager API +#[derive(Clone, Copy)] +pub struct BufferDesc<'a> { + pub label: Option<&'a str>, + pub byte_size: u64, + pub usage: wgpu::BufferUsages, +} + +#[derive(Clone, Copy, Debug)] +pub struct ShaderDesc<'a> { + pub source: &'a str, + pub entry_func: &'a str, +} + +#[derive(Clone, Debug)] +pub struct PipelineV2Desc<'a> { + pub label: Option<&'a str>, + pub vs: ShaderDesc<'a>, + pub ps: ShaderDesc<'a>, + pub bindings: Vec, + pub targets: &'a [Option], + pub depth_stencil: Option, + pub multisample: wgpu::MultisampleState, +} + // --- Renderer trait definition --- pub trait PillRenderer { @@ -44,8 +79,12 @@ pub trait PillRenderer { where Self: Sized; + fn init(&mut self) -> Result<()>; fn resize(&mut self, new_window_size: winit::dpi::PhysicalSize); + // Creates a 256B-aligned uniform buffer (COPY_DST) and returns its handle + fn create_buffer(&mut self, desc: BufferDesc) -> Result; + fn create_pipeline_v2(&mut self, desc: PipelineV2Desc) -> Result; fn create_mesh(&mut self, name: &str, mesh_data: &MeshData) -> Result; fn create_texture( &mut self, diff --git a/engine/pill_engine/src/lib.rs b/engine/pill_engine/src/lib.rs index 9e480363..a6b6f587 100644 --- a/engine/pill_engine/src/lib.rs +++ b/engine/pill_engine/src/lib.rs @@ -1,15 +1,15 @@ #![cfg_attr(debug_assertions, allow(dead_code, unused_imports))] +mod config; +mod ecs; mod engine; -mod resources; mod graphics; -mod ecs; -mod config; +mod resources; // --- Macros --- +pub use ecs::{Component, ComponentStorage, GlobalComponent, GlobalComponentStorage}; pub use pill_core::PillTypeMapKey; -pub use ecs::{Component, GlobalComponent, ComponentStorage, GlobalComponentStorage}; #[macro_export] macro_rules! define_component { @@ -54,127 +54,52 @@ macro_rules! define_global_component { #[cfg(feature = "game")] pub mod game { pub use crate::{ - engine::{ - Engine, - PillGame, - KeyboardKey, - MouseButton, - }, ecs::{ - SceneHandle, - MeshRenderingComponent, - TransformComponent, - InputComponent, - CameraComponent, - CameraAspectRatio, - EntityHandle, - AudioSourceComponent, - AudioListenerComponent, - TimeComponent, - UpdatePhase, - AudioManagerComponent, - EguiManagerComponent, - Component, - ComponentStorage, - GlobalComponent, - GlobalComponentStorage, - SoundType, + AudioListenerComponent, AudioManagerComponent, AudioSourceComponent, CameraAspectRatio, + CameraComponent, Component, ComponentStorage, EguiManagerComponent, EntityHandle, + GlobalComponent, GlobalComponentStorage, InputComponent, MeshRenderingComponent, + SceneHandle, SoundType, TimeComponent, TransformComponent, UpdatePhase, }, + engine::{Engine, KeyboardKey, MouseButton, PillGame}, resources::{ - Resource, - ResourceStorage, - Texture, - TextureHandle, - TextureType, - Material, - MaterialHandle, - Mesh, - MeshHandle, - ResourceLoadType, - Sound + Material, MaterialHandle, Mesh, MeshHandle, Resource, ResourceLoadType, + ResourceStorage, Sound, Texture, TextureHandle, TextureType, }, }; - + extern crate pill_core; - pub use pill_core::{ - PillTypeMapKey, - Vector2f, - Vector3f, - Color, - Vector2i, - Vector3i, - Vector3fExt, - create_game, - define_new_pill_slotmap_key, + pub use pill_core::{ + create_game, define_new_pill_slotmap_key, Color, PillTypeMapKey, Vector2f, Vector2i, + Vector3f, Vector3fExt, Vector3i, }; - + extern crate anyhow; - pub use anyhow::{ Context, Result, Error }; + pub use anyhow::{Context, Error, Result}; } #[cfg(feature = "internal")] pub mod internal { pub use crate::{ - engine::{ - Engine, - PillGame, - }, config::*, - graphics::{ - PillRenderer, - RenderQueueKey, - RenderQueueItem, - RenderQueueKeyFields, - decompose_render_queue_key, - - RendererCameraHandle, - RendererMaterialHandle, - RendererMeshHandle, - RendererPipelineHandle, - RendererTextureHandle, - RENDER_QUEUE_KEY_ORDER - }, ecs::{ - Scene, - ComponentStorage, - MeshRenderingComponent, - TransformComponent, - CameraComponent, - EntityHandle, - InputComponent, - TimeComponent, - CameraAspectRatio, - AudioSourceComponent, - AudioListenerComponent, - AudioManagerComponent, - EguiManagerComponent, - get_renderer_resource_handle_from_camera_component, - update_transform_matrices, - get_model_matrix, - get_normal_matrix, + get_model_matrix, get_normal_matrix, + get_renderer_resource_handle_from_camera_component, update_transform_matrices, + AudioListenerComponent, AudioManagerComponent, AudioSourceComponent, CameraAspectRatio, + CameraComponent, ComponentStorage, EguiManagerComponent, EntityHandle, InputComponent, + MeshRenderingComponent, Scene, TimeComponent, TransformComponent, + }, + engine::{Engine, PillGame}, + graphics::{ + decompose_render_queue_key, BufferDesc, PillRenderer, PipelineV2Desc, RenderQueueItem, + RenderQueueKey, RenderQueueKeyFields, RendererBufferHandle, RendererCameraHandle, + RendererMaterialHandle, RendererMeshHandle, RendererPipelineHandle, + RendererPipelineV2Handle, RendererTextureHandle, ShaderDesc, RENDER_QUEUE_KEY_ORDER, }, resources::{ - Texture, - TextureHandle, - TextureType, - - Material, - MaterialHandle, - - Mesh, - MeshHandle, - MeshData, - MeshVertex, - - ResourceLoadType, - ResourceManager, - - MaterialTexture, - MaterialTextureMap, - MaterialParameter, - MaterialParameterMap, - get_renderer_texture_handle_from_material_texture, + get_renderer_texture_handle_from_material_texture, Material, MaterialHandle, + MaterialParameter, MaterialParameterMap, MaterialTexture, MaterialTextureMap, Mesh, + MeshData, MeshHandle, MeshVertex, ResourceLoadType, ResourceManager, Texture, + TextureHandle, TextureType, }, }; } - diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index cfe46182..d4186f2e 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -9,15 +9,12 @@ use crate::{ }, }; -use pill_engine::{ - game::{ResourceLoadType, Texture}, - internal::{ - get_renderer_resource_handle_from_camera_component, CameraComponent, ComponentStorage, - EntityHandle, MaterialParameterMap, MaterialTextureMap, MeshData, PillRenderer, - RenderQueueItem, RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, - RendererPipelineHandle, RendererTextureHandle, TextureType, TransformComponent, - RENDER_QUEUE_KEY_ORDER, - }, +use pill_engine::internal::{ + get_renderer_resource_handle_from_camera_component, BufferDesc, CameraComponent, + ComponentStorage, EntityHandle, MaterialParameterMap, MaterialTextureMap, MeshData, + PillRenderer, PipelineV2Desc, RenderQueueItem, RendererBufferHandle, RendererCameraHandle, + RendererMaterialHandle, RendererMeshHandle, RendererPipelineHandle, RendererPipelineV2Handle, + RendererTextureHandle, ShaderDesc, TextureType, TransformComponent, RENDER_QUEUE_KEY_ORDER, }; use pill_core::{PillSlotMapKey, PillSlotMapKeyData, PillStyle, RendererError, Timer}; @@ -47,10 +44,6 @@ pub const MASTER_PIPELINE_HANDLE: RendererPipelineHandle = RendererPipelineHandl }, }; -pub struct Renderer { - pub state: State, -} - fn compile_glsl_to_wgsl(source: &str, stage: naga::ShaderStage) -> Result { let mut frontend = glsl::Frontend::default(); let options = glsl::Options::from(stage); @@ -111,7 +104,6 @@ fn create_render_target( sampler, } } - // Overlay (UV gradient quad) resources grouped for clarity struct OverlayResources { pipeline: wgpu::RenderPipeline, @@ -122,8 +114,7 @@ struct OverlayResources { // KISS helper: build overlay pipeline, UBO layout/buffer, and bind group fn create_overlay_uv( - device: &wgpu::Device, - queue: &wgpu::Queue, + ctx: &DeviceContext, surface_format: wgpu::TextureFormat, rect: [f32; 4], ) -> OverlayResources { @@ -152,74 +143,84 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; } "#; - let overlay_vs = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("overlay_vs"), - source: wgpu::ShaderSource::Wgsl(overlay_vs_wgsl.into()), - }); - let overlay_fs = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("overlay_fs"), - source: wgpu::ShaderSource::Wgsl(overlay_fs_wgsl.into()), - }); + let overlay_vs = ctx + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_vs"), + source: wgpu::ShaderSource::Wgsl(overlay_vs_wgsl.into()), + }); + let overlay_fs = ctx + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_fs"), + source: wgpu::ShaderSource::Wgsl(overlay_fs_wgsl.into()), + }); let overlay_rect_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("overlay_rect_bgl"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), - }, - count: None, - }], - }); - let overlay_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("overlay_pl"), - bind_group_layouts: &[&overlay_rect_bind_group_layout], - push_constant_ranges: &[], - }); + ctx.device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_rect_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }], + }); + let overlay_pipeline_layout = + ctx.device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("overlay_pl"), + bind_group_layouts: &[&overlay_rect_bind_group_layout], + push_constant_ranges: &[], + }); // No vertex buffer layout needed; vertices are generated procedurally in the vertex shader - let overlay_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("overlay_pipeline"), - layout: Some(&overlay_pipeline_layout), - vertex: wgpu::VertexState { - module: &overlay_vs, - entry_point: "main", - buffers: &[], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &overlay_fs, - entry_point: "main", - targets: &[Some(wgpu::ColorTargetState { - format: surface_format, - blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState { - cull_mode: None, - ..wgpu::PrimitiveState::default() - }, - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview: None, - }); + let overlay_pipeline = ctx + .device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("overlay_pipeline"), + layout: Some(&overlay_pipeline_layout), + vertex: wgpu::VertexState { + module: &overlay_vs, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &overlay_fs, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: surface_format, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + cull_mode: None, + ..wgpu::PrimitiveState::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); // Apple Metal constant buffer alignment (256 bytes) - let overlay_rect_buffer = device.create_buffer(&wgpu::BufferDescriptor { + let overlay_rect_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor { label: Some("overlay_rect_ubo"), size: 256, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); - queue.write_buffer(&overlay_rect_buffer, 0, bytemuck::bytes_of(&rect)); + ctx.queue + .write_buffer(&overlay_rect_buffer, 0, bytemuck::bytes_of(&rect)); - let overlay_rect_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + let overlay_rect_bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("overlay_rect_bg"), layout: &overlay_rect_bind_group_layout, entries: &[wgpu::BindGroupEntry { @@ -646,14 +647,23 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; } } -pub struct State { - // Resources and GPU objects moved from ctor into here explicitly - renderer_resource_storage: RendererResourceStorage, - surface: wgpu::Surface<'static>, +pub struct DeviceContext { + config: config::Config, + // Window + window_ref: Arc, + window_size: winit::dpi::PhysicalSize, + // GPU API device: wgpu::Device, queue: wgpu::Queue, + surface: wgpu::Surface<'static>, + // Framebuffer variables surface_configuration: wgpu::SurfaceConfiguration, - window_size: winit::dpi::PhysicalSize, +} + +pub struct State { + egui_renderer: crate::egui::EguiRenderer, // TODO: Separate system adding Pass + // Resources and GPU objects moved from ctor into here explicitly + renderer_resource_storage: RendererResourceStorage, color_format: wgpu::TextureFormat, depth_format: wgpu::TextureFormat, depth_texture: RendererTexture, @@ -676,17 +686,18 @@ pub struct State { overlay_uv: OverlayResources, // Overlay (UV gradient quad in top-right) overlay_logo: TextureOverlayResources, // Overlay (Logo in bot-right) overlay_depth: TextureOverlayResources, // Overlay (Depth in top-right) - // Other - config: config::Config, - egui_renderer: crate::egui::EguiRenderer, // TODO: Separate system adding Pass +} + +pub struct Renderer { + pub state: State, + pub ctx: DeviceContext, } impl PillRenderer for Renderer { fn new(window: Arc, config: config::Config) -> Self { info!("Initializing {}", "Renderer".mobj_style()); - let state: State = pollster::block_on(async move { + let ctx: DeviceContext = pollster::block_on(async move { let window_size = window.inner_size(); - let window_ref = window.clone(); let backends = wgpu::util::backend_bits_from_env().unwrap_or_default(); @@ -796,13 +807,25 @@ impl PillRenderer for Renderer { // Configure surface surface.configure(&device, &surface_configuration); + DeviceContext { + config, + window_ref, + window_size, + device, + queue, + surface, + surface_configuration, + } + }); + + let state: State = { // Configure collections - let mut renderer_resource_storage = RendererResourceStorage::new(&config); + let mut renderer_resource_storage = RendererResourceStorage::new(&ctx.config); // Create depth and color texture let depth_texture = RendererTexture::new_depth_texture( - &device, - &surface_configuration, + &ctx.device, + &ctx.surface_configuration, "depth_texture", ) .unwrap(); @@ -817,75 +840,84 @@ impl PillRenderer for Renderer { // Milestone 6: Create offscreen color target (RENDER_ATTACHMENT | TEXTURE_BINDING) let offscreen_color_texture = create_render_target( - &device, + &ctx.device, color_format, - surface_configuration.width, - surface_configuration.height, + ctx.surface_configuration.width, + ctx.surface_configuration.height, "offscreen_color", ); - let egui_renderer = - EguiRenderer::new(&device, surface_configuration.format, None, 1, window_ref); + let egui_renderer = EguiRenderer::new( + &ctx.device, + ctx.surface_configuration.format, + None, + 1, + ctx.window_ref.clone(), + ); // Build static shader modules and pipeline once ([SIMILAR] prebuilt PSO per TALK) let vertex_wgsl = r#" -struct Camera { position: vec4, view_projection_matrix: mat4x4, }; -@group(0) @binding(0) var GCamera: Camera; + struct Camera { position: vec4, view_projection_matrix: mat4x4, }; + @group(0) @binding(0) var GCamera: Camera; -struct PerDraw { - mvp: mat4x4, - model: mat4x4, - tint: vec4, -}; -@group(3) @binding(0) var UPerDraw: PerDraw; + struct PerDraw { + mvp: mat4x4, + model: mat4x4, + tint: vec4, + }; + @group(3) @binding(0) var UPerDraw: PerDraw; -struct VSIn { @location(0) pos: vec3, @location(1) uv: vec2, }; -struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; + struct VSIn { @location(0) pos: vec3, @location(1) uv: vec2, }; + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; -@vertex fn main(input: VSIn) -> VSOut { - var out: VSOut; - out.pos = UPerDraw.mvp * vec4(input.pos, 1.0); - out.uv = input.uv; - return out; -} -"#; + @vertex fn main(input: VSIn) -> VSOut { + var out: VSOut; + out.pos = UPerDraw.mvp * vec4(input.pos, 1.0); + out.uv = input.uv; + return out; + } + "#; let fragment_wgsl = r#" -// Material set(1): -@group(1) @binding(0) var tex_diffuse: texture_2d; -@group(1) @binding(1) var smp_diffuse: sampler; -@group(1) @binding(2) var tex_normal: texture_2d; -@group(1) @binding(3) var smp_normal: sampler; - -// Material parameters set(2) - pack tint.rgb + specularity in a single vec4 -struct MaterialParams { tint_spec: vec4, } -@group(2) @binding(0) var UMaterial: MaterialParams; - -// Per-draw (set 3): supports per-entity tint for M4 -struct PerDraw { - mvp: mat4x4, - model: mat4x4, - tint: vec4, -}; -@group(3) @binding(0) var UPerDraw: PerDraw; - -@fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { - let albedo = textureSample(tex_diffuse, smp_diffuse, uv); - let tinted = vec4(UMaterial.tint_spec.rgb, 1.0) * UPerDraw.tint; - let spec_boost = 0.5 + 0.5 * UMaterial.tint_spec.a; - let color = albedo * tinted * spec_boost; - return vec4(color.rgb, 1.0); -} -"#; - let vertex_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("m2_vertex_shader"), - source: wgpu::ShaderSource::Wgsl(vertex_wgsl.into()), - }); - let fragment_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("m2_fragment_shader"), - source: wgpu::ShaderSource::Wgsl(fragment_wgsl.into()), - }); + // Material set(1): + @group(1) @binding(0) var tex_diffuse: texture_2d; + @group(1) @binding(1) var smp_diffuse: sampler; + @group(1) @binding(2) var tex_normal: texture_2d; + @group(1) @binding(3) var smp_normal: sampler; + + // Material parameters set(2) - pack tint.rgb + specularity in a single vec4 + struct MaterialParams { tint_spec: vec4, } + @group(2) @binding(0) var UMaterial: MaterialParams; + + // Per-draw (set 3): supports per-entity tint for M4 + struct PerDraw { + mvp: mat4x4, + model: mat4x4, + tint: vec4, + }; + @group(3) @binding(0) var UPerDraw: PerDraw; + + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let albedo = textureSample(tex_diffuse, smp_diffuse, uv); + let tinted = vec4(UMaterial.tint_spec.rgb, 1.0) * UPerDraw.tint; + let spec_boost = 0.5 + 0.5 * UMaterial.tint_spec.a; + let color = albedo * tinted * spec_boost; + return vec4(color.rgb, 1.0); + } + "#; + let vertex_shader = ctx + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("m2_vertex_shader"), + source: wgpu::ShaderSource::Wgsl(vertex_wgsl.into()), + }); + let fragment_shader = ctx + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("m2_fragment_shader"), + source: wgpu::ShaderSource::Wgsl(fragment_wgsl.into()), + }); let default_pipeline = RendererPipeline::new( - &device, + &ctx.device, vertex_shader, fragment_shader, color_format, @@ -896,97 +928,106 @@ struct PerDraw { // Milestone 6: Composition pipeline (fullscreen triangle sampling offscreen texture) let comp_vs_wgsl = r#" -struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; -@vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { - var pos = array, 3>( - vec2(-1.0, -3.0), - vec2( 3.0, 1.0), - vec2(-1.0, 1.0) - ); - var tuv = array, 3>( - vec2(0.0, 2.0), - vec2(2.0, 0.0), - vec2(0.0, 0.0) - ); - var o: VSOut; - o.pos = vec4(pos[vi], 0.0, 1.0); - o.uv = tuv[vi]; - return o; -} -"#; + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var pos = array, 3>( + vec2(-1.0, -3.0), + vec2( 3.0, 1.0), + vec2(-1.0, 1.0) + ); + var tuv = array, 3>( + vec2(0.0, 2.0), + vec2(2.0, 0.0), + vec2(0.0, 0.0) + ); + var o: VSOut; + o.pos = vec4(pos[vi], 0.0, 1.0); + o.uv = tuv[vi]; + return o; + } + "#; let comp_fs_wgsl = r#" -@group(0) @binding(0) var t_src: texture_2d; -@group(0) @binding(1) var s_src: sampler; -@fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { - let hdr = textureSample(t_src, s_src, uv).rgb; - // Reinhard tone mapping; output remains in linear space. The sRGB swapchain will encode. - let ldr = hdr / (1.0 + hdr); - return vec4(ldr, 1.0); -} -"#; - let comp_vs = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("compose_vs"), - source: wgpu::ShaderSource::Wgsl(comp_vs_wgsl.into()), - }); - let comp_fs = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("compose_fs"), - source: wgpu::ShaderSource::Wgsl(comp_fs_wgsl.into()), - }); + @group(0) @binding(0) var t_src: texture_2d; + @group(0) @binding(1) var s_src: sampler; + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let hdr = textureSample(t_src, s_src, uv).rgb; + // Reinhard tone mapping; output remains in linear space. The sRGB swapchain will encode. + let ldr = hdr / (1.0 + hdr); + return vec4(ldr, 1.0); + } + "#; + let comp_vs = ctx + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("compose_vs"), + source: wgpu::ShaderSource::Wgsl(comp_vs_wgsl.into()), + }); + let comp_fs = ctx + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("compose_fs"), + source: wgpu::ShaderSource::Wgsl(comp_fs_wgsl.into()), + }); let composite_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("compose_bgl"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, + ctx.device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("compose_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { + filterable: true, + }, + }, + count: None, }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], - }); + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); let comp_pipeline_layout = - device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("compose_pl"), - bind_group_layouts: &[&composite_bind_group_layout], - push_constant_ranges: &[], - }); + ctx.device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("compose_pl"), + bind_group_layouts: &[&composite_bind_group_layout], + push_constant_ranges: &[], + }); let composite_pipeline = - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("compose_pipeline"), - layout: Some(&comp_pipeline_layout), - vertex: wgpu::VertexState { - module: &comp_vs, - entry_point: "main", - buffers: &[], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &comp_fs, - entry_point: "main", - targets: &[Some(wgpu::ColorTargetState { - // Composition writes to the swapchain view; target must match surface format - format: surface_configuration.format, - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState::default(), - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview: None, - }); - let composite_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + ctx.device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("compose_pipeline"), + layout: Some(&comp_pipeline_layout), + vertex: wgpu::VertexState { + module: &comp_vs, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &comp_fs, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + // Composition writes to the swapchain view; target must match surface format + format: ctx.surface_configuration.format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + let composite_bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("compose_bg"), layout: &composite_bind_group_layout, entries: &[ @@ -1003,25 +1044,142 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; ], }); - // Overlay UV gradient pipeline (small quad in top-right corner) - let overlay_uv = create_overlay_uv( - &device, - &queue, - surface_configuration.format, - [0.75, 0.75, 0.95, 0.95], - ); + let rect = [0.75, 0.75, 0.95, 0.95]; + let overlay_vs_wgsl = r#" + @group(0) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] + + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var unit = array, 6>( + vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), + vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) + ); + let p = unit[vi]; // [-1,1] NDC space + let s = p*0.5+0.5; // [0,1] screen space + let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] + let ndc = r*2.0-1.0; // [-1,1] NDC space + var out: VSOut; + out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); + out.uv = s; + return out; + } + "#; + let overlay_fs_wgsl = r#" + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + return vec4(uv, 0.0, 0.5); + } + "#; + + let overlay_vs = ctx + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_vs"), + source: wgpu::ShaderSource::Wgsl(overlay_vs_wgsl.into()), + }); + let overlay_fs = ctx + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_fs"), + source: wgpu::ShaderSource::Wgsl(overlay_fs_wgsl.into()), + }); + + let overlay_rect_bind_group_layout = + ctx.device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_rect_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }], + }); + let overlay_pipeline_layout = + ctx.device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("overlay_pl"), + bind_group_layouts: &[&overlay_rect_bind_group_layout], + push_constant_ranges: &[], + }); + + // No vertex buffer layout needed; vertices are generated procedurally in the vertex shader + let overlay_pipeline = + ctx.device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("overlay_pipeline"), + layout: Some(&overlay_pipeline_layout), + vertex: wgpu::VertexState { + module: &overlay_vs, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &overlay_fs, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: ctx.surface_configuration.format, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + cull_mode: None, + ..wgpu::PrimitiveState::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + // Apple Metal constant buffer alignment (256 bytes) + let overlay_rect_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_rect_ubo"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + ctx.queue + .write_buffer(&overlay_rect_buffer, 0, bytemuck::bytes_of(&rect)); + + let overlay_rect_bind_group = + ctx.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_rect_bg"), + layout: &overlay_rect_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &overlay_rect_buffer, + offset: 0, + size: Some(std::num::NonZeroU64::new(16).unwrap()), + }), + }], + }); + + let overlay_uv = OverlayResources { + pipeline: overlay_pipeline, + rect_bind_group_layout: overlay_rect_bind_group_layout, + rect_bind_group: overlay_rect_bind_group, + rect_buffer: overlay_rect_buffer, + }; + // OVERLAY UV END // Ideally Client submitted pass via API // Create logo texture via renderer API and pass handle into overlay factory let logo_image = image::open( - "/Users/mk/dev/demo/Pill-Engine/engine/pill_renderer/res/pill_logo_horizontal_white.png", - ) - .expect("failed to load overlay logo image"); + "/Users/mk/dev/demo/Pill-Engine/engine/pill_renderer/res/pill_logo_horizontal_white.png", + ) + .expect("failed to load overlay logo image"); let logo_handle = { let tex = RendererTexture::new_texture( - &device, - &queue, + &ctx.device, + &ctx.queue, Some("overlay_logo"), &logo_image, TextureType::Color, @@ -1033,46 +1191,45 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; // res: 1024 × 320, 1.0 x 0.3 let h: f32 = 0.04; let overlay_logo = create_overlay_logo( - &device, - &queue, - surface_configuration.format, + &ctx.device, + &ctx.queue, + ctx.surface_configuration.format, logo_handle, &renderer_resource_storage, [0.98 - 3. * h, 0.02, 0.98, 0.02 + h], // bottom right ); let overlay_depth = create_overlay_depth( - &device, - &queue, - surface_configuration.format, + &ctx.device, + &ctx.queue, + ctx.surface_configuration.format, &depth_texture, [0.75, 0.55, 0.95, 0.72], // top right, 2 row ); // Create state let per_draw_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("per_draw_bind_group_layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: true, - min_binding_size: Some(std::num::NonZeroU64::new(144).unwrap()), - }, - count: None, - }], - }); + ctx.device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("per_draw_bind_group_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: Some(std::num::NonZeroU64::new(144).unwrap()), + }, + count: None, + }], + }); + State { + // Other + egui_renderer, // Resources renderer_resource_storage, // Renderer variables - surface, - device, - queue, - surface_configuration, - window_size, color_format, depth_format, depth_texture, @@ -1094,17 +1251,212 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; overlay_uv, overlay_logo, overlay_depth, - // Other - config, - egui_renderer, } + }; + + Self { state, ctx } + } + + fn init(&mut self) -> Result<()> { + // TODO: create whole overlay_uv pass using factory methods from PillRenderer + let rect = [0.75, 0.75, 0.95, 0.95]; + let overlay_rect_buffer = self.create_buffer(BufferDesc { + label: Some("overlay_rect_ubo"), + byte_size: 256, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + let buffer = self + .state + .renderer_resource_storage + .buffers + .get(overlay_rect_buffer.unwrap()) + .unwrap(); + + let vs_wgsl = r#" + @group(0) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] + + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var unit = array, 6>( + vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), + vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) + ); + let p = unit[vi]; // [-1,1] NDC space + let s = p*0.5+0.5; // [0,1] screen space + let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] + let ndc = r*2.0-1.0; // [-1,1] NDC space + var out: VSOut; + out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); + out.uv = s; + return out; + } + "#; + + let fs_wgsl = r#" + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + return vec4(uv, 0.0, 0.5); + } + "#; + + self.ctx + .queue + .write_buffer(&buffer, 0, bytemuck::bytes_of(&rect)); + + let _pipeline_handle = self.create_pipeline_v2(PipelineV2Desc { + label: Some("overlay_uv"), + vs: ShaderDesc { + source: vs_wgsl, + entry_func: "main", + }, + ps: ShaderDesc { + source: fs_wgsl, + entry_func: "main", + }, + bindings: vec![wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }], + targets: &[Some(wgpu::ColorTargetState { + format: self.ctx.surface_configuration.format, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + }); + + Ok(()) + } + + fn create_buffer(&mut self, desc: BufferDesc) -> Result { + let aligned_size = ((desc.byte_size + 255) / 256) * 256; // 256B for Metal UBOs + let buffer = self.ctx.device.create_buffer(&wgpu::BufferDescriptor { + label: desc.label, + size: aligned_size, + usage: desc.usage, + mapped_at_creation: false, }); + let handle = self.state.renderer_resource_storage.buffers.insert(buffer); + Ok(handle) + } + + fn create_pipeline_v2(&mut self, desc: PipelineV2Desc) -> Result { + /* + // Example shader pipeline descriptor for factory method + n_shader = rm->createShader({ + .debugName = "mesh_simple", + .VS (.byteCode = shaderVS, .entryFunc="main"), + .PS (.byteCode = shaderPS, . entryFunc="main"), + .CS (.byteCode = shaderCS, . entryFunc="main"), + .bindGroups = { + { m_globalsBindingsLayout }, // Globals bind group (0) + { materialBindingsLayout }, // Material bind group (1) + }, + .dynamicBuffers = dynamicBindings-getLayout(), + .graphicsState = { + .depthTest = COMPARE::GREATER_OR_EQUAL, // inverse Z + .vertexBufferBindings = { + // Position vertex buffer (8) + .byteStride = 12, .attributes = { + { .byteOffset = 0, .format = FORMAT::RGB32_FLOAT } + } + }, + { + // 2nd vertex buffer: tangent, normal, color, texcoord + .byteStride = 24, .attributes = { + { .byteOffset = 0, .format = FORMAT::RGBA16_FLOAT }, + { .byteOffset = 8, .format = FORMAT::RGBA16_FLOAT }, + { .byteOffset = 16, .format = FORMAT::RGBA8_UNORM }, + { .byteOffset = 20, .format = FORMAT::RG16_FLOAT } + } + } + }, + .renderPassLayout = m_renderPassLayout + }); + */ + + let vs_label_owned = format!("{}{}", desc.label.unwrap_or("program"), "_vs"); + let vs_label = vs_label_owned.as_str(); + let vs = self + .ctx + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some(vs_label), + source: wgpu::ShaderSource::Wgsl(desc.vs.source.into()), + }); + + let ps_label_owned = format!("{}{}", desc.label.unwrap_or("program"), "_ps"); + let ps_label = ps_label_owned.as_str(); + let ps = self + .ctx + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some(ps_label), + source: wgpu::ShaderSource::Wgsl(desc.ps.source.into()), + }); + + let bind_group_layout = + self.ctx + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some(format!("{}{}", desc.label.unwrap_or("program"), "_bgl").as_str()), + entries: &desc.bindings, + }); - Self { state } + let pipeline_layout = + self.ctx + .device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("overlay_pl"), + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[], + }); + + let pl_label_owned = format!("{}{}", desc.label.unwrap_or("program"), "_pipeline"); + let pl_label = pl_label_owned.as_str(); + let pipeline = self + .ctx + .device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some(pl_label), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &vs, + entry_point: desc.vs.entry_func, + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &ps, + entry_point: desc.ps.entry_func, + targets: &desc.targets, + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + cull_mode: None, + ..wgpu::PrimitiveState::default() + }, + depth_stencil: desc.depth_stencil, + multisample: desc.multisample, + multiview: None, + }); + + let handle: RendererPipelineV2Handle = self + .state + .renderer_resource_storage + .pipelines_v2 + .insert(pipeline); + Ok(handle) } fn create_mesh(&mut self, name: &str, mesh_data: &MeshData) -> Result { - let mesh = RendererMesh::new(&self.state.device, name, mesh_data)?; + let mesh = RendererMesh::new(&self.ctx.device, name, mesh_data)?; let handle = self.state.renderer_resource_storage.meshes.insert(mesh); Ok(handle) @@ -1117,8 +1469,8 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; texture_type: TextureType, ) -> Result { let texture = RendererTexture::new_texture( - &self.state.device, - &self.state.queue, + &self.ctx.device, + &self.ctx.queue, Some(name), image_data, texture_type, @@ -1140,7 +1492,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; ) -> Result { // Create bind group layouts inline (avoid pipeline storage dependency) let material_texture_bind_group_layout = - self.state + self.ctx .device .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("material_texture_bind_group_layout"), @@ -1180,7 +1532,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; ], }); let material_parameter_bind_group_layout = - self.state + self.ctx .device .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("material_parameter_bind_group_layout"), @@ -1197,8 +1549,8 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; }); let material = RendererMaterial::new( - &self.state.device, - &self.state.queue, + &self.ctx.device, + &self.ctx.queue, &self.state.renderer_resource_storage, name, MASTER_PIPELINE_HANDLE, @@ -1220,7 +1572,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; fn create_camera(&mut self) -> Result { let camera_bind_group_layout = - self.state + self.ctx .device .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("camera_bind_group_layout"), @@ -1235,7 +1587,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; count: None, }], }); - let camera = RendererCamera::new(&self.state.device, &camera_bind_group_layout)?; + let camera = RendererCamera::new(&self.ctx.device, &camera_bind_group_layout)?; let handle = self.state.renderer_resource_storage.cameras.insert(camera); Ok(handle) @@ -1247,7 +1599,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; textures: &MaterialTextureMap, ) -> Result<()> { RendererMaterial::update_textures( - &self.state.device, + &self.ctx.device, renderer_material_handle, &mut self.state.renderer_resource_storage, textures, @@ -1260,8 +1612,8 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; parameters: &MaterialParameterMap, ) -> Result<()> { RendererMaterial::update_parameters( - &self.state.device, - &self.state.queue, + &self.ctx.device, + &self.ctx.queue, renderer_material_handle, &mut self.state.renderer_resource_storage, parameters, @@ -1312,28 +1664,28 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; info!("Resizing {} resources", "Renderer".mobj_style()); // self.state.resize(new_window_size) if new_window_size.width > 0 && new_window_size.height > 0 { - self.state.window_size = new_window_size; - self.state.surface_configuration.width = new_window_size.width; - self.state.surface_configuration.height = new_window_size.height; - self.state + self.ctx.window_size = new_window_size; + self.ctx.surface_configuration.width = new_window_size.width; + self.ctx.surface_configuration.height = new_window_size.height; + self.ctx .surface - .configure(&self.state.device, &self.state.surface_configuration); + .configure(&self.ctx.device, &self.ctx.surface_configuration); self.state.depth_texture = RendererTexture::new_depth_texture( - &self.state.device, - &self.state.surface_configuration, + &self.ctx.device, + &self.ctx.surface_configuration, "depth_texture", ) .unwrap(); // Recreate offscreen color target and its bind group self.state.offscreen_color_texture = create_render_target( - &self.state.device, + &self.ctx.device, self.state.color_format, - self.state.surface_configuration.width, - self.state.surface_configuration.height, + self.ctx.surface_configuration.width, + self.ctx.surface_configuration.height, "offscreen_color", ); self.state.composite_bind_group = - self.state + self.ctx .device .create_bind_group(&wgpu::BindGroupDescriptor { label: Some("compose_bg"), @@ -1355,7 +1707,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; }); // Rebind overlay depth material to the new depth texture view self.state.overlay_depth.material_bind_group = - self.state + self.ctx .device .create_bind_group(&wgpu::BindGroupDescriptor { label: Some("overlay_depth_material_bg"), @@ -1380,7 +1732,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; // Old offscreen texture/view/sampler are dropped when replaced; wgpu defers actual GPU resource // destruction until safe. See optional early reclamation via device.poll: // https://docs.rs/wgpu/latest/wgpu/struct.Device.html#method.poll - self.state.device.poll(wgpu::Maintain::Wait); + self.ctx.device.poll(wgpu::Maintain::Wait); } } @@ -1393,15 +1745,6 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; egui_ui: Box, timer: &mut Timer, ) -> Result<()> { - // self.state.render( - // active_camera_entity_handle, - // render_queue, - // camera_component_storage, - // transform_component_storage, - // egui_ui, - // timer, - // ) - // [SIMILAR] Prebuilt PSO used; avoid hot-path pipeline creation per TALK // Use prebuilt default_pipeline for bind group layouts and pipeline let _per_draw_bind_group_layout = &self.state.default_pipeline.per_draw_bind_group_layout; @@ -1409,7 +1752,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; timer.record("Frame: acquire"); // Get frame or return mapped error if failed - let frame = self.state.surface.get_current_texture(); + let frame = self.ctx.surface.get_current_texture(); let frame = match frame { std::result::Result::Ok(frame) => frame, @@ -1450,7 +1793,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; .unwrap(); let active_camera_transform_component = camera_transform_storage.as_ref().unwrap(); renderer_camera.update( - &self.state.queue, + &self.ctx.queue, active_camera_component, active_camera_transform_component, ); @@ -1465,12 +1808,12 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; let clear_color = active_camera_component.clear_color; // Record inline draw pass (M2) - let mut encoder = - self.state - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("m2_inline_encoder"), - }); + let mut encoder = self + .ctx + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("m2_inline_encoder"), + }); // [SIMILAR] CPU frustum culling and precomputing per-draw transforms before starting the pass matches TALK's advice // [DIFF] Only frustum culling here; TALK discusses 2-pass occlusion and broader CPU-driven pipelines @@ -1686,13 +2029,13 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; self.state.per_draw_capacity = needed.next_power_of_two().max(1); let size = self.state.per_draw_stride * self.state.per_draw_capacity; self.state.per_draw_buffer = - Some(self.state.device.create_buffer(&wgpu::BufferDescriptor { + Some(self.ctx.device.create_buffer(&wgpu::BufferDescriptor { label: Some("per_draw_dynamic_ubo_ring"), size, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, })); - self.state.per_draw_bind_group = Some(self.state.device.create_bind_group( + self.state.per_draw_bind_group = Some(self.ctx.device.create_bind_group( &wgpu::BindGroupDescriptor { label: Some("per_draw_bind_group"), layout: &self.state.default_pipeline.per_draw_bind_group_layout, @@ -1740,7 +2083,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; } } if let Some(buf) = &self.state.per_draw_buffer { - self.state.queue.write_buffer(buf, 0, &staging); + self.ctx.queue.write_buffer(buf, 0, &staging); } timer.record("Per-draw UBO write (ring)"); timer.end_context()?; @@ -1881,7 +2224,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; // Draw depth overlay (bind material at group 0 and rect at group 1) // Rebind overlay depth material to the new depth texture view self.state.overlay_depth.material_bind_group = - self.state + self.ctx .device .create_bind_group(&wgpu::BindGroupDescriptor { label: Some("overlay_depth_material_bg"), @@ -1918,31 +2261,31 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; } timer.end_context()?; // End "Compose pass" - self.state.queue.submit([encoder.finish()]); + self.ctx.queue.submit([encoder.finish()]); timer.record("Submit scene+compose passes"); // Egui overlay (load over the rendered frame) timer.begin_context("UI pass"); - let mut encoder = - self.state - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("egui_encoder"), - }); + let mut encoder = self + .ctx + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("egui_encoder"), + }); let screen_descriptor = egui_wgpu::ScreenDescriptor { - size_in_pixels: [self.state.window_size.width, self.state.window_size.height], + size_in_pixels: [self.ctx.window_size.width, self.ctx.window_size.height], pixels_per_point: self.state.egui_renderer.window_scale_factor, }; self.state.egui_renderer.draw( - &self.state.device, - &self.state.queue, + &self.ctx.device, + &self.ctx.queue, &mut encoder, &view, screen_descriptor, |ctx| egui_ui(ctx), timer, )?; - self.state.queue.submit([encoder.finish()]); + self.ctx.queue.submit([encoder.finish()]); timer.record("UI draw & submit"); timer.end_context()?; // End "UI pass" @@ -1952,7 +2295,7 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; // Allow wgpu to process pending work and retire resources referenced by submitted encoders // without blocking the CPU. See Device::poll docs. - self.state.device.poll(wgpu::Maintain::Poll); + self.ctx.device.poll(wgpu::Maintain::Poll); Ok(()) } diff --git a/engine/pill_renderer/src/renderer_resource_storage.rs b/engine/pill_renderer/src/renderer_resource_storage.rs index ff7f9bb3..e4e6de1a 100644 --- a/engine/pill_renderer/src/renderer_resource_storage.rs +++ b/engine/pill_renderer/src/renderer_resource_storage.rs @@ -1,49 +1,75 @@ use crate::resources::{ - RendererPipeline, - RendererMaterial, - RendererTexture, - RendererMesh, - RendererCamera + RendererCamera, RendererMaterial, RendererMesh, RendererPipeline, RendererTexture, }; use pill_core::PillSlotMap; use pill_engine::internal::{ - RendererCameraHandle, - RendererMaterialHandle, - RendererMeshHandle, - RendererPipelineHandle, - RendererTextureHandle, + RendererBufferHandle, RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, + RendererPipelineHandle, RendererPipelineV2Handle, RendererTextureHandle, }; pub const MAX_PIPELINES: usize = 10; +pub const MAX_PIPELINES_V2: usize = 10; pub const MAX_TEXTURES: usize = 10; pub const MAX_MATERIALS: usize = 10; pub const MAX_MESHES: usize = 10; pub const MAX_CAMERAS: usize = 10; +pub const MAX_BUFFERS: usize = 64; pub struct RendererResourceStorage { - pub(crate) pipelines: PillSlotMap::, - pub(crate) materials: PillSlotMap::, + pub(crate) pipelines: PillSlotMap, + pub(crate) pipelines_v2: PillSlotMap, + pub(crate) materials: PillSlotMap, pub(crate) textures: PillSlotMap, - pub(crate) meshes: PillSlotMap::, - pub(crate) cameras: PillSlotMap::, + pub(crate) meshes: PillSlotMap, + pub(crate) cameras: PillSlotMap, + pub(crate) buffers: PillSlotMap, } impl RendererResourceStorage { pub fn new(config: &config::Config) -> Self { - let max_pipeline_count = config.get_int("MAX_PIPELINES").unwrap_or(MAX_PIPELINES as i64) as usize; - let max_texture_count = config.get_int("MAX_TEXTURES").unwrap_or(MAX_TEXTURES as i64) as usize; - let max_material_count = config.get_int("MAX_MATERIALS").unwrap_or(MAX_MATERIALS as i64) as usize; + let max_pipeline_count = config + .get_int("MAX_PIPELINES") + .unwrap_or(MAX_PIPELINES as i64) as usize; + let max_pipeline_v2_count = config + .get_int("MAX_PIPELINES_V2") + .unwrap_or(MAX_PIPELINES_V2 as i64) as usize; + let max_texture_count = config + .get_int("MAX_TEXTURES") + .unwrap_or(MAX_TEXTURES as i64) as usize; + let max_material_count = config + .get_int("MAX_MATERIALS") + .unwrap_or(MAX_MATERIALS as i64) as usize; let max_mesh_count = config.get_int("MAX_MESHS").unwrap_or(MAX_MESHES as i64) as usize; let max_camera_count = config.get_int("MAX_CAMERAS").unwrap_or(MAX_CAMERAS as i64) as usize; + let max_buffer_count = config.get_int("MAX_BUFFERS").unwrap_or(MAX_BUFFERS as i64) as usize; RendererResourceStorage { - pipelines: PillSlotMap::::with_capacity_and_key(max_pipeline_count), - textures: PillSlotMap::::with_capacity_and_key(max_texture_count), - materials: PillSlotMap::::with_capacity_and_key(max_material_count), - meshes: PillSlotMap::::with_capacity_and_key(max_mesh_count), - cameras: PillSlotMap::::with_capacity_and_key(max_camera_count), + pipelines: + PillSlotMap::::with_capacity_and_key( + max_pipeline_count, + ), + pipelines_v2: + PillSlotMap::::with_capacity_and_key( + max_pipeline_v2_count, + ), + textures: PillSlotMap::::with_capacity_and_key( + max_texture_count, + ), + materials: + PillSlotMap::::with_capacity_and_key( + max_material_count, + ), + meshes: PillSlotMap::::with_capacity_and_key( + max_mesh_count, + ), + cameras: PillSlotMap::::with_capacity_and_key( + max_camera_count, + ), + buffers: PillSlotMap::::with_capacity_and_key( + max_buffer_count, + ), } } -} \ No newline at end of file +} diff --git a/engine/pill_renderer/src/resources/mod.rs b/engine/pill_renderer/src/resources/mod.rs index 6e136fd0..1fe1eb94 100644 --- a/engine/pill_renderer/src/resources/mod.rs +++ b/engine/pill_renderer/src/resources/mod.rs @@ -1,15 +1,12 @@ -mod renderer_texture; -mod renderer_mesh; mod renderer_camera; mod renderer_material; +mod renderer_mesh; mod renderer_pipeline; +mod renderer_texture; // --- Use --- -pub use renderer_mesh::{ - RendererMesh, - Vertex -}; +pub use renderer_mesh::{RendererMesh, Vertex}; pub use renderer_texture::RendererTexture; @@ -18,4 +15,3 @@ pub use renderer_camera::RendererCamera; pub use renderer_material::RendererMaterial; pub use renderer_pipeline::RendererPipeline; - From 59f54e4d9ed0ced94dd74b9ee3e99defd3c66270 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Sat, 18 Oct 2025 15:54:01 +0200 Subject: [PATCH 39/69] feat: pass interface --- engine/pill_renderer/src/lib.rs | 2 + engine/pill_renderer/src/passes.rs | 96 ++++++++++++++++++++++++++++ engine/pill_renderer/src/renderer.rs | 8 +++ 3 files changed, 106 insertions(+) create mode 100644 engine/pill_renderer/src/passes.rs diff --git a/engine/pill_renderer/src/lib.rs b/engine/pill_renderer/src/lib.rs index c0af36ec..00aa34f2 100644 --- a/engine/pill_renderer/src/lib.rs +++ b/engine/pill_renderer/src/lib.rs @@ -3,6 +3,7 @@ pub mod egui; pub mod instance; pub mod mesh_drawer; +pub mod passes; pub mod renderer; pub mod renderer_resource_storage; pub mod resources; @@ -12,5 +13,6 @@ pub mod resources; pub use renderer::*; pub use instance::Instance; +pub use passes::PassOverlayUV; pub use renderer_resource_storage::RendererResourceStorage; diff --git a/engine/pill_renderer/src/passes.rs b/engine/pill_renderer/src/passes.rs new file mode 100644 index 00000000..320710c6 --- /dev/null +++ b/engine/pill_renderer/src/passes.rs @@ -0,0 +1,96 @@ +use crate::renderer::{Pass, Renderer}; +use pill_engine::internal::{BufferDesc, PillRenderer, PipelineV2Desc, ShaderDesc}; +use wgpu::CommandEncoder; +use wgpu::Queue; + +/// A dummy pass implementation for testing and development +pub struct PassOverlayUV { + label: String, +} + +impl PassOverlayUV { + pub fn new(label: &str) -> Self { + Self { + label: label.to_string(), + } + } +} + +impl Pass for PassOverlayUV { + fn get_label(&self) -> &str { + &self.label + } + + fn init(&mut self, _queue: &wgpu::Queue, pr: &mut Renderer) { + println!("Building pass: {}", self.label); + + let _rect = [0.75, 0.75, 0.95, 0.95]; + let _h_buffer = pr.create_buffer(BufferDesc { + label: Some("overlay_rect_ubo"), + byte_size: 256, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + // TODO: Get actual buffer from handle + // queue.write_buffer(&h_buffer.unwrap(), 0, bytemuck::bytes_of(&rect)); + + let vs_wgsl = r#" + @group(0) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] + + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var unit = array, 6>( + vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), + vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) + ); + let p = unit[vi]; // [-1,1] NDC space + let s = p*0.5+0.5; // [0,1] screen space + let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] + let ndc = r*2.0-1.0; // [-1,1] NDC space + var out: VSOut; + out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); + out.uv = s; + return out; + } + "#; + + let fs_wgsl = r#" + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + return vec4(uv, 0.0, 0.5); + } + "#; + + let _pipeline_handle = pr.create_pipeline_v2(PipelineV2Desc { + label: Some("overlay_uv"), + vs: ShaderDesc { + source: vs_wgsl, + entry_func: "main", + }, + ps: ShaderDesc { + source: fs_wgsl, + entry_func: "main", + }, + bindings: vec![wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }], + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Bgra8UnormSrgb, // TODO: Get actual surface format + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + }); + } + + fn render(&self, _encoder: &mut CommandEncoder) -> Result<(), Box> { + println!("Rendering dummy pass: {}", self.label); + Ok(()) + } +} diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index d4186f2e..6a9946c6 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -647,6 +647,12 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; } } +pub trait Pass { + fn get_label(&self) -> &str; + fn init(&mut self, queue: &wgpu::Queue, pill_renderer: &mut Renderer); + fn render(&self, encoder: &mut wgpu::CommandEncoder) -> Result<(), Box>; +} + pub struct DeviceContext { config: config::Config, // Window @@ -661,6 +667,7 @@ pub struct DeviceContext { } pub struct State { + passes: Vec>, egui_renderer: crate::egui::EguiRenderer, // TODO: Separate system adding Pass // Resources and GPU objects moved from ctor into here explicitly renderer_resource_storage: RendererResourceStorage, @@ -1225,6 +1232,7 @@ impl PillRenderer for Renderer { }); State { + passes: vec![], // Other egui_renderer, // Resources From c0b99556260da5a77326c6be2bacebc3f5a96cb6 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Sat, 18 Oct 2025 16:52:11 +0200 Subject: [PATCH 40/69] fixme: mutability workadround until happy with passes --- engine/pill_engine/src/graphics/renderer.rs | 10 +- engine/pill_renderer/src/passes.rs | 77 ++--- engine/pill_renderer/src/renderer.rs | 313 +++++++++----------- 3 files changed, 194 insertions(+), 206 deletions(-) diff --git a/engine/pill_engine/src/graphics/renderer.rs b/engine/pill_engine/src/graphics/renderer.rs index 06a491c5..669a657f 100644 --- a/engine/pill_engine/src/graphics/renderer.rs +++ b/engine/pill_engine/src/graphics/renderer.rs @@ -83,17 +83,17 @@ pub trait PillRenderer { fn resize(&mut self, new_window_size: winit::dpi::PhysicalSize); // Creates a 256B-aligned uniform buffer (COPY_DST) and returns its handle - fn create_buffer(&mut self, desc: BufferDesc) -> Result; - fn create_pipeline_v2(&mut self, desc: PipelineV2Desc) -> Result; - fn create_mesh(&mut self, name: &str, mesh_data: &MeshData) -> Result; + fn create_buffer(&self, desc: BufferDesc) -> Result; + fn create_pipeline_v2(&self, desc: PipelineV2Desc) -> Result; + fn create_mesh(&self, name: &str, mesh_data: &MeshData) -> Result; fn create_texture( - &mut self, + &self, name: &str, image_data: &image::DynamicImage, texture_type: TextureType, ) -> Result; fn create_material( - &mut self, + &self, name: &str, textures: &MaterialTextureMap, parameters: &MaterialParameterMap, diff --git a/engine/pill_renderer/src/passes.rs b/engine/pill_renderer/src/passes.rs index 320710c6..a12b1f6f 100644 --- a/engine/pill_renderer/src/passes.rs +++ b/engine/pill_renderer/src/passes.rs @@ -1,4 +1,5 @@ use crate::renderer::{Pass, Renderer}; +use anyhow::Result; use pill_engine::internal::{BufferDesc, PillRenderer, PipelineV2Desc, ShaderDesc}; use wgpu::CommandEncoder; use wgpu::Queue; @@ -21,45 +22,50 @@ impl Pass for PassOverlayUV { &self.label } - fn init(&mut self, _queue: &wgpu::Queue, pr: &mut Renderer) { - println!("Building pass: {}", self.label); + fn init(&mut self, queue: &wgpu::Queue, renderer: &Renderer) -> Result<()> { + println!("Initializing pass: {}", self.label); - let _rect = [0.75, 0.75, 0.95, 0.95]; - let _h_buffer = pr.create_buffer(BufferDesc { + // TODO: create whole overlay_uv pass using factory methods from PillRenderer + let rect = [0.75, 0.75, 0.95, 0.95]; + let buffer_handle = renderer.create_buffer(BufferDesc { label: Some("overlay_rect_ubo"), byte_size: 256, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - }); - // TODO: Get actual buffer from handle - // queue.write_buffer(&h_buffer.unwrap(), 0, bytemuck::bytes_of(&rect)); + })?; + + // Get buffer from RefCell storage + let storage = renderer.get_resource_storage().borrow(); + let buffer = storage.buffers.get(buffer_handle).unwrap(); let vs_wgsl = r#" - @group(0) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] - - struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; - @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { - var unit = array, 6>( - vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), - vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) - ); - let p = unit[vi]; // [-1,1] NDC space - let s = p*0.5+0.5; // [0,1] screen space - let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] - let ndc = r*2.0-1.0; // [-1,1] NDC space - var out: VSOut; - out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); - out.uv = s; - return out; - } - "#; + @group(0) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] + + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var unit = array, 6>( + vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), + vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) + ); + let p = unit[vi]; // [-1,1] NDC space + let s = p*0.5+0.5; // [0,1] screen space + let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] + let ndc = r*2.0-1.0; // [-1,1] NDC space + var out: VSOut; + out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); + out.uv = s; + return out; + } + "#; let fs_wgsl = r#" - @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { - return vec4(uv, 0.0, 0.5); - } - "#; + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + return vec4(uv, 0.0, 0.5); + } + "#; + + queue.write_buffer(&buffer, 0, bytemuck::bytes_of(&rect)); - let _pipeline_handle = pr.create_pipeline_v2(PipelineV2Desc { + let _pipeline_handle = renderer.create_pipeline_v2(PipelineV2Desc { label: Some("overlay_uv"), vs: ShaderDesc { source: vs_wgsl, @@ -80,17 +86,20 @@ impl Pass for PassOverlayUV { count: None, }], targets: &[Some(wgpu::ColorTargetState { - format: wgpu::TextureFormat::Bgra8UnormSrgb, // TODO: Get actual surface format + format: renderer.get_surface_format(), blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), write_mask: wgpu::ColorWrites::ALL, })], depth_stencil: None, multisample: wgpu::MultisampleState::default(), - }); + })?; + + Ok(()) } - fn render(&self, _encoder: &mut CommandEncoder) -> Result<(), Box> { - println!("Rendering dummy pass: {}", self.label); + fn draw(&self, encoder: &mut CommandEncoder, renderer: &Renderer) -> Result<()> { + // println!("Rendering dummy pass: {}", self.label); + Ok(()) } } diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 6a9946c6..d5603ab5 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -3,12 +3,16 @@ // https://github.com/emilk/egui/discussions/3067 use crate::{ + passes::PassOverlayUV, renderer_resource_storage::RendererResourceStorage, resources::{ RendererCamera, RendererMaterial, RendererMesh, RendererPipeline, RendererTexture, Vertex, }, }; +use std::cell::RefCell; +use std::rc::Rc; + use pill_engine::internal::{ get_renderer_resource_handle_from_camera_component, BufferDesc, CameraComponent, ComponentStorage, EntityHandle, MaterialParameterMap, MaterialTextureMap, MeshData, @@ -649,8 +653,8 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; pub trait Pass { fn get_label(&self) -> &str; - fn init(&mut self, queue: &wgpu::Queue, pill_renderer: &mut Renderer); - fn render(&self, encoder: &mut wgpu::CommandEncoder) -> Result<(), Box>; + fn init(&mut self, queue: &wgpu::Queue, renderer: &Renderer) -> Result<()>; + fn draw(&self, encoder: &mut wgpu::CommandEncoder, renderer: &Renderer) -> Result<()>; } pub struct DeviceContext { @@ -670,7 +674,7 @@ pub struct State { passes: Vec>, egui_renderer: crate::egui::EguiRenderer, // TODO: Separate system adding Pass // Resources and GPU objects moved from ctor into here explicitly - renderer_resource_storage: RendererResourceStorage, + renderer_resource_storage: Rc>, color_format: wgpu::TextureFormat, depth_format: wgpu::TextureFormat, depth_texture: RendererTexture, @@ -1236,7 +1240,7 @@ impl PillRenderer for Renderer { // Other egui_renderer, // Resources - renderer_resource_storage, + renderer_resource_storage: Rc::new(RefCell::new(renderer_resource_storage)), // Renderer variables color_format, depth_format, @@ -1266,83 +1270,23 @@ impl PillRenderer for Renderer { } fn init(&mut self) -> Result<()> { - // TODO: create whole overlay_uv pass using factory methods from PillRenderer - let rect = [0.75, 0.75, 0.95, 0.95]; - let overlay_rect_buffer = self.create_buffer(BufferDesc { - label: Some("overlay_rect_ubo"), - byte_size: 256, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - }); - let buffer = self - .state - .renderer_resource_storage - .buffers - .get(overlay_rect_buffer.unwrap()) - .unwrap(); - - let vs_wgsl = r#" - @group(0) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] - - struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; - @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { - var unit = array, 6>( - vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), - vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) - ); - let p = unit[vi]; // [-1,1] NDC space - let s = p*0.5+0.5; // [0,1] screen space - let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] - let ndc = r*2.0-1.0; // [-1,1] NDC space - var out: VSOut; - out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); - out.uv = s; - return out; - } - "#; - - let fs_wgsl = r#" - @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { - return vec4(uv, 0.0, 0.5); + self.state + .passes + .push(Box::new(PassOverlayUV::new("overlay_uv"))); + + // Initialize passes - call init on each pass + let queue = &self.ctx.queue; + // Temporarily move passes out to avoid borrowing conflicts + let mut passes = std::mem::take(&mut self.state.passes); + for pass in passes.iter_mut() { + let _ = pass.init(queue, self); } - "#; - - self.ctx - .queue - .write_buffer(&buffer, 0, bytemuck::bytes_of(&rect)); - - let _pipeline_handle = self.create_pipeline_v2(PipelineV2Desc { - label: Some("overlay_uv"), - vs: ShaderDesc { - source: vs_wgsl, - entry_func: "main", - }, - ps: ShaderDesc { - source: fs_wgsl, - entry_func: "main", - }, - bindings: vec![wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), - }, - count: None, - }], - targets: &[Some(wgpu::ColorTargetState { - format: self.ctx.surface_configuration.format, - blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - }); + self.state.passes = passes; Ok(()) } - fn create_buffer(&mut self, desc: BufferDesc) -> Result { + fn create_buffer(&self, desc: BufferDesc) -> Result { let aligned_size = ((desc.byte_size + 255) / 256) * 256; // 256B for Metal UBOs let buffer = self.ctx.device.create_buffer(&wgpu::BufferDescriptor { label: desc.label, @@ -1350,11 +1294,14 @@ impl PillRenderer for Renderer { usage: desc.usage, mapped_at_creation: false, }); - let handle = self.state.renderer_resource_storage.buffers.insert(buffer); + let handle = unsafe { + let storage = &mut *self.state.renderer_resource_storage.as_ptr(); + storage.buffers.insert(buffer) + }; Ok(handle) } - fn create_pipeline_v2(&mut self, desc: PipelineV2Desc) -> Result { + fn create_pipeline_v2(&self, desc: PipelineV2Desc) -> Result { /* // Example shader pipeline descriptor for factory method n_shader = rm->createShader({ @@ -1455,23 +1402,25 @@ impl PillRenderer for Renderer { multiview: None, }); - let handle: RendererPipelineV2Handle = self - .state - .renderer_resource_storage - .pipelines_v2 - .insert(pipeline); + let handle: RendererPipelineV2Handle = unsafe { + let storage = &mut *self.state.renderer_resource_storage.as_ptr(); + storage.pipelines_v2.insert(pipeline) + }; Ok(handle) } - fn create_mesh(&mut self, name: &str, mesh_data: &MeshData) -> Result { + fn create_mesh(&self, name: &str, mesh_data: &MeshData) -> Result { let mesh = RendererMesh::new(&self.ctx.device, name, mesh_data)?; - let handle = self.state.renderer_resource_storage.meshes.insert(mesh); + let handle = unsafe { + let storage = &mut *self.state.renderer_resource_storage.as_ptr(); + storage.meshes.insert(mesh) + }; Ok(handle) } fn create_texture( - &mut self, + &self, name: &str, image_data: &image::DynamicImage, texture_type: TextureType, @@ -1483,17 +1432,16 @@ impl PillRenderer for Renderer { image_data, texture_type, )?; - let handle = self - .state - .renderer_resource_storage - .textures - .insert(texture); + let handle = unsafe { + let storage = &mut *self.state.renderer_resource_storage.as_ptr(); + storage.textures.insert(texture) + }; Ok(handle) } fn create_material( - &mut self, + &self, name: &str, textures: &MaterialTextureMap, parameters: &MaterialParameterMap, @@ -1556,10 +1504,11 @@ impl PillRenderer for Renderer { }], }); + let storage = self.state.renderer_resource_storage.borrow(); let material = RendererMaterial::new( &self.ctx.device, &self.ctx.queue, - &self.state.renderer_resource_storage, + &storage, name, MASTER_PIPELINE_HANDLE, &material_texture_bind_group_layout, @@ -1569,11 +1518,10 @@ impl PillRenderer for Renderer { ) .unwrap(); - let handle = self - .state - .renderer_resource_storage - .materials - .insert(material); + let handle = unsafe { + let storage = &mut *self.state.renderer_resource_storage.as_ptr(); + storage.materials.insert(material) + }; Ok(handle) } @@ -1596,7 +1544,10 @@ impl PillRenderer for Renderer { }], }); let camera = RendererCamera::new(&self.ctx.device, &camera_bind_group_layout)?; - let handle = self.state.renderer_resource_storage.cameras.insert(camera); + let handle = unsafe { + let storage = &mut *self.state.renderer_resource_storage.as_ptr(); + storage.cameras.insert(camera) + }; Ok(handle) } @@ -1606,12 +1557,15 @@ impl PillRenderer for Renderer { renderer_material_handle: RendererMaterialHandle, textures: &MaterialTextureMap, ) -> Result<()> { - RendererMaterial::update_textures( - &self.ctx.device, - renderer_material_handle, - &mut self.state.renderer_resource_storage, - textures, - ) + unsafe { + let storage = &mut *self.state.renderer_resource_storage.as_ptr(); + RendererMaterial::update_textures( + &self.ctx.device, + renderer_material_handle, + storage, + textures, + ) + } } fn update_material_parameters( @@ -1619,51 +1573,50 @@ impl PillRenderer for Renderer { renderer_material_handle: RendererMaterialHandle, parameters: &MaterialParameterMap, ) -> Result<()> { - RendererMaterial::update_parameters( - &self.ctx.device, - &self.ctx.queue, - renderer_material_handle, - &mut self.state.renderer_resource_storage, - parameters, - ) + unsafe { + let storage = &mut *self.state.renderer_resource_storage.as_ptr(); + RendererMaterial::update_parameters( + &self.ctx.device, + &self.ctx.queue, + renderer_material_handle, + storage, + parameters, + ) + } } fn destroy_mesh(&mut self, renderer_mesh_handle: RendererMeshHandle) -> Result<()> { - self.state - .renderer_resource_storage - .meshes - .remove(renderer_mesh_handle) - .unwrap(); + unsafe { + let storage = &mut *self.state.renderer_resource_storage.as_ptr(); + storage.meshes.remove(renderer_mesh_handle).unwrap(); + } Ok(()) } fn destroy_texture(&mut self, renderer_texture_handle: RendererTextureHandle) -> Result<()> { - self.state - .renderer_resource_storage - .textures - .remove(renderer_texture_handle) - .unwrap(); + unsafe { + let storage = &mut *self.state.renderer_resource_storage.as_ptr(); + storage.textures.remove(renderer_texture_handle).unwrap(); + } Ok(()) } fn destroy_material(&mut self, renderer_material_handle: RendererMaterialHandle) -> Result<()> { - self.state - .renderer_resource_storage - .materials - .remove(renderer_material_handle) - .unwrap(); + unsafe { + let storage = &mut *self.state.renderer_resource_storage.as_ptr(); + storage.materials.remove(renderer_material_handle).unwrap(); + } Ok(()) } fn destroy_camera(&mut self, renderer_camera_handle: RendererCameraHandle) -> Result<()> { - self.state - .renderer_resource_storage - .cameras - .remove(renderer_camera_handle) - .unwrap(); + unsafe { + let storage = &mut *self.state.renderer_resource_storage.as_ptr(); + storage.cameras.remove(renderer_camera_handle).unwrap(); + } Ok(()) } @@ -1787,14 +1740,15 @@ impl PillRenderer for Renderer { .get(active_camera_entity_handle.data().index as usize) .unwrap(); let active_camera_component = camera_storage.as_ref().unwrap(); - let renderer_camera = self - .state - .renderer_resource_storage - .cameras - .get_mut(get_renderer_resource_handle_from_camera_component( - active_camera_component, - )) - .ok_or(Error::new(RendererError::RendererResourceNotFound))?; + let renderer_camera = unsafe { + let storage = &mut *self.state.renderer_resource_storage.as_ptr(); + storage + .cameras + .get_mut(get_renderer_resource_handle_from_camera_component( + active_camera_component, + )) + .ok_or(Error::new(RendererError::RendererResourceNotFound))? + }; let camera_transform_storage = transform_component_storage .data .get(active_camera_entity_handle.data().index as usize) @@ -1805,9 +1759,8 @@ impl PillRenderer for Renderer { active_camera_component, active_camera_transform_component, ); - let renderer_camera = self - .state - .renderer_resource_storage + let storage = self.state.renderer_resource_storage.borrow(); + let renderer_camera = storage .cameras .get(get_renderer_resource_handle_from_camera_component( active_camera_component, @@ -1869,12 +1822,8 @@ impl PillRenderer for Renderer { key.material_index.into(), NonZeroU32::new(key.material_version.into()).unwrap(), ); - let material_for_pipeline = self - .state - .renderer_resource_storage - .materials - .get(material_handle) - .unwrap(); + let storage = self.state.renderer_resource_storage.borrow(); + let material_for_pipeline = storage.materials.get(material_handle).unwrap(); let pipeline_handle = material_for_pipeline.pipeline_handle; // Transform and model @@ -1889,12 +1838,8 @@ impl PillRenderer for Renderer { let model: cgmath::Matrix4 = model_arr.into(); // World AABB from mesh local AABB - let mesh = self - .state - .renderer_resource_storage - .meshes - .get(mesh_handle) - .unwrap(); + let storage = self.state.renderer_resource_storage.borrow(); + let mesh = storage.meshes.get(mesh_handle).unwrap(); let local_min = cgmath::Vector3::new(mesh.aabb_min[0], mesh.aabb_min[1], mesh.aabb_min[2]); let local_max = @@ -2141,23 +2086,21 @@ impl PillRenderer for Renderer { // - RendererMesh : vertex_buffer, index_buffer, index_count // COLD metadata (names/descs) lives in *_cold arrays and is not read in the draw loop. - let material = self - .state - .renderer_resource_storage - .materials - .get(group.material_handle) - .unwrap(); + // Get material and set bind groups + let material = unsafe { + let storage = &*self.state.renderer_resource_storage.as_ptr(); + storage.materials.get(group.material_handle).unwrap() + }; rpass.set_bind_group(0, &renderer_camera.bind_group, &[]); rpass.set_bind_group(1, &material.texture_bind_group, &[]); rpass.set_bind_group(2, &material.parameter_bind_group, &[]); + // Process batches with separate storage borrows to avoid lifetime issues for batch in &group.batches { - let mesh = self - .state - .renderer_resource_storage - .meshes - .get(batch.mesh_handle) - .unwrap(); + let mesh = unsafe { + let storage = &*self.state.renderer_resource_storage.as_ptr(); + storage.meshes.get(batch.mesh_handle).unwrap() + }; // [API->CLIENT] Provide packed mesh atlas + per-draw base offsets so low-level can draw without re-binding buffers // [DIFF] VB/IB still rebound per batch; packing meshes could avoid rebinding rpass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); @@ -2272,6 +2215,26 @@ impl PillRenderer for Renderer { self.ctx.queue.submit([encoder.finish()]); timer.record("Submit scene+compose passes"); + { + // Egui overlay (load over the rendered frame) + timer.begin_context("User passes"); + let encoder = self + .ctx + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("user_passes"), + }); + + // Loop over state.passes, for each + // get label + // call render with encoder + // call timer.record with label + + self.ctx.queue.submit([encoder.finish()]); + timer.record("User passes submit"); + timer.end_context()?; // End "User passes" + } + // Egui overlay (load over the rendered frame) timer.begin_context("UI pass"); let mut encoder = self @@ -2313,3 +2276,19 @@ impl PillRenderer for Renderer { Ok(()) } } + +impl Renderer { + pub fn get_buffer(&self, _handle: RendererBufferHandle) -> Option<&wgpu::Buffer> { + // This is a temporary workaround - we'll need to restructure this + // For now, we'll return None to avoid the borrowing issue + None + } + + pub fn get_surface_format(&self) -> wgpu::TextureFormat { + self.ctx.surface_configuration.format + } + + pub fn get_resource_storage(&self) -> &RefCell { + &self.state.renderer_resource_storage + } +} From e4ab0608ba60e6b74d4ca8b3b1a502f5eed298e8 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Sat, 18 Oct 2025 18:37:53 +0200 Subject: [PATCH 41/69] feat: pass example --- engine/pill_engine/src/graphics/renderer.rs | 6 +- engine/pill_renderer/src/passes.rs | 100 +++++++- engine/pill_renderer/src/renderer.rs | 266 +++++++++++--------- 3 files changed, 244 insertions(+), 128 deletions(-) diff --git a/engine/pill_engine/src/graphics/renderer.rs b/engine/pill_engine/src/graphics/renderer.rs index 669a657f..2723b6d3 100644 --- a/engine/pill_engine/src/graphics/renderer.rs +++ b/engine/pill_engine/src/graphics/renderer.rs @@ -84,7 +84,11 @@ pub trait PillRenderer { // Creates a 256B-aligned uniform buffer (COPY_DST) and returns its handle fn create_buffer(&self, desc: BufferDesc) -> Result; - fn create_pipeline_v2(&self, desc: PipelineV2Desc) -> Result; + fn create_pipeline_v2( + &self, + desc: PipelineV2Desc, + ) -> Result<(RendererPipelineV2Handle, &wgpu::RenderPipeline)>; + fn get_pipeline_v2(&self, handle: RendererPipelineV2Handle) -> &wgpu::RenderPipeline; fn create_mesh(&self, name: &str, mesh_data: &MeshData) -> Result; fn create_texture( &self, diff --git a/engine/pill_renderer/src/passes.rs b/engine/pill_renderer/src/passes.rs index a12b1f6f..c09d5ce6 100644 --- a/engine/pill_renderer/src/passes.rs +++ b/engine/pill_renderer/src/passes.rs @@ -4,15 +4,24 @@ use pill_engine::internal::{BufferDesc, PillRenderer, PipelineV2Desc, ShaderDesc use wgpu::CommandEncoder; use wgpu::Queue; -/// A dummy pass implementation for testing and development pub struct PassOverlayUV { label: String, + buffer_handle: Option, + pipeline_handle: Option, + bind_group_layout: Option, + bind_group: Option, + rect: [f32; 4], } impl PassOverlayUV { pub fn new(label: &str) -> Self { Self { label: label.to_string(), + buffer_handle: None, + pipeline_handle: None, + bind_group_layout: None, + bind_group: None, + rect: [0.75, 0.75, 0.95, 0.95], } } } @@ -25,17 +34,18 @@ impl Pass for PassOverlayUV { fn init(&mut self, queue: &wgpu::Queue, renderer: &Renderer) -> Result<()> { println!("Initializing pass: {}", self.label); - // TODO: create whole overlay_uv pass using factory methods from PillRenderer - let rect = [0.75, 0.75, 0.95, 0.95]; - let buffer_handle = renderer.create_buffer(BufferDesc { + // Create buffer for overlay rect UBO + self.buffer_handle = Some(renderer.create_buffer(BufferDesc { label: Some("overlay_rect_ubo"), byte_size: 256, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - })?; + })?); - // Get buffer from RefCell storage - let storage = renderer.get_resource_storage().borrow(); - let buffer = storage.buffers.get(buffer_handle).unwrap(); + // Get buffer from RefCell storage and write data + let buffer = unsafe { + let storage = &*renderer.get_resource_storage().as_ptr(); + storage.buffers.get(self.buffer_handle.unwrap()).unwrap() + }; let vs_wgsl = r#" @group(0) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] @@ -63,9 +73,38 @@ impl Pass for PassOverlayUV { } "#; - queue.write_buffer(&buffer, 0, bytemuck::bytes_of(&rect)); + queue.write_buffer(buffer, 0, bytemuck::bytes_of(&self.rect)); + + // Create bind group layout and bind group + let bind_group_layout = + renderer + .get_device() + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_rect_bind_group_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }], + }); + + let bind_group = renderer + .get_device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_rect_bind_group"), + layout: &bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + }); - let _pipeline_handle = renderer.create_pipeline_v2(PipelineV2Desc { + let (pipeline_handle, _pipeline_ref) = renderer.create_pipeline_v2(PipelineV2Desc { label: Some("overlay_uv"), vs: ShaderDesc { source: vs_wgsl, @@ -94,11 +133,48 @@ impl Pass for PassOverlayUV { multisample: wgpu::MultisampleState::default(), })?; + // Store pipeline handle + self.pipeline_handle = Some(pipeline_handle); + + // Store the bind group layout and bind group + self.bind_group_layout = Some(bind_group_layout); + self.bind_group = Some(bind_group); + Ok(()) } - fn draw(&self, encoder: &mut CommandEncoder, renderer: &Renderer) -> Result<()> { - // println!("Rendering dummy pass: {}", self.label); + fn draw( + &self, + encoder: &mut CommandEncoder, + renderer: &Renderer, + frame: &wgpu::SurfaceTexture, + ) -> Result<()> { + // Create render pass for this pass using the provided frame + let view = frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.label), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + // Draw small overlay UV quad in normalized rect via UBO + let pipeline_handle = self.pipeline_handle.as_ref().unwrap(); + let pipeline = renderer.get_pipeline_v2(*pipeline_handle); + pass.set_pipeline(pipeline); + pass.set_bind_group(0, self.bind_group.as_ref().unwrap(), &[]); + pass.draw(0..6, 0..1); Ok(()) } diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index d5603ab5..210b8339 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -654,7 +654,12 @@ struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; pub trait Pass { fn get_label(&self) -> &str; fn init(&mut self, queue: &wgpu::Queue, renderer: &Renderer) -> Result<()>; - fn draw(&self, encoder: &mut wgpu::CommandEncoder, renderer: &Renderer) -> Result<()>; + fn draw( + &self, + encoder: &mut wgpu::CommandEncoder, + renderer: &Renderer, + frame: &wgpu::SurfaceTexture, + ) -> Result<()>; } pub struct DeviceContext { @@ -1055,98 +1060,8 @@ impl PillRenderer for Renderer { ], }); - let rect = [0.75, 0.75, 0.95, 0.95]; - let overlay_vs_wgsl = r#" - @group(0) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] - - struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; - @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { - var unit = array, 6>( - vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), - vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) - ); - let p = unit[vi]; // [-1,1] NDC space - let s = p*0.5+0.5; // [0,1] screen space - let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] - let ndc = r*2.0-1.0; // [-1,1] NDC space - var out: VSOut; - out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); - out.uv = s; - return out; - } - "#; - let overlay_fs_wgsl = r#" - @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { - return vec4(uv, 0.0, 0.5); - } - "#; - - let overlay_vs = ctx - .device - .create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("overlay_vs"), - source: wgpu::ShaderSource::Wgsl(overlay_vs_wgsl.into()), - }); - let overlay_fs = ctx - .device - .create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("overlay_fs"), - source: wgpu::ShaderSource::Wgsl(overlay_fs_wgsl.into()), - }); - - let overlay_rect_bind_group_layout = - ctx.device - .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("overlay_rect_bgl"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), - }, - count: None, - }], - }); - let overlay_pipeline_layout = - ctx.device - .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("overlay_pl"), - bind_group_layouts: &[&overlay_rect_bind_group_layout], - push_constant_ranges: &[], - }); - - // No vertex buffer layout needed; vertices are generated procedurally in the vertex shader - let overlay_pipeline = - ctx.device - .create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("overlay_pipeline"), - layout: Some(&overlay_pipeline_layout), - vertex: wgpu::VertexState { - module: &overlay_vs, - entry_point: "main", - buffers: &[], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &overlay_fs, - entry_point: "main", - targets: &[Some(wgpu::ColorTargetState { - format: ctx.surface_configuration.format, - blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState { - cull_mode: None, - ..wgpu::PrimitiveState::default() - }, - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview: None, - }); + let (rect, overlay_rect_bind_group_layout, overlay_pipeline) = + create_overlay_uv_pass_inline(&ctx); // Apple Metal constant buffer alignment (256 bytes) let overlay_rect_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor { @@ -1301,7 +1216,10 @@ impl PillRenderer for Renderer { Ok(handle) } - fn create_pipeline_v2(&self, desc: PipelineV2Desc) -> Result { + fn create_pipeline_v2( + &self, + desc: PipelineV2Desc, + ) -> Result<(RendererPipelineV2Handle, &wgpu::RenderPipeline)> { /* // Example shader pipeline descriptor for factory method n_shader = rm->createShader({ @@ -1406,7 +1324,21 @@ impl PillRenderer for Renderer { let storage = &mut *self.state.renderer_resource_storage.as_ptr(); storage.pipelines_v2.insert(pipeline) }; - Ok(handle) + + // Get a reference to the pipeline from storage + let pipeline_ref = unsafe { + let storage = &*self.state.renderer_resource_storage.as_ptr(); + storage.pipelines_v2.get(handle).unwrap() + }; + + Ok((handle, pipeline_ref)) + } + + fn get_pipeline_v2(&self, handle: RendererPipelineV2Handle) -> &wgpu::RenderPipeline { + unsafe { + let storage = &*self.state.renderer_resource_storage.as_ptr(); + storage.pipelines_v2.get(handle).unwrap() + } } fn create_mesh(&self, name: &str, mesh_data: &MeshData) -> Result { @@ -2168,9 +2100,9 @@ impl PillRenderer for Renderer { }); // Draw small overlay UV quad in normalized rect via UBO - rpass.set_pipeline(&self.state.overlay_uv.pipeline); - rpass.set_bind_group(0, &self.state.overlay_uv.rect_bind_group, &[]); - rpass.draw(0..6, 0..1); + // rpass.set_pipeline(&self.state.overlay_uv.pipeline); + // rpass.set_bind_group(0, &self.state.overlay_uv.rect_bind_group, &[]); + // rpass.draw(0..6, 0..1); // Draw depth overlay (bind material at group 0 and rect at group 1) // Rebind overlay depth material to the new depth texture view @@ -2216,19 +2148,23 @@ impl PillRenderer for Renderer { timer.record("Submit scene+compose passes"); { - // Egui overlay (load over the rendered frame) + // User passes with separate encoder timer.begin_context("User passes"); - let encoder = self - .ctx - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("user_passes"), - }); + let mut encoder = + self.ctx + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("user_passes"), + }); // Loop over state.passes, for each - // get label - // call render with encoder - // call timer.record with label + for pass in &self.state.passes { + let label = pass.get_label(); + timer.begin_context(label); + let _ = pass.draw(&mut encoder, self, &frame); + timer.record(label); + timer.end_context()?; + } self.ctx.queue.submit([encoder.finish()]); timer.record("User passes submit"); @@ -2278,17 +2214,117 @@ impl PillRenderer for Renderer { } impl Renderer { - pub fn get_buffer(&self, _handle: RendererBufferHandle) -> Option<&wgpu::Buffer> { - // This is a temporary workaround - we'll need to restructure this - // For now, we'll return None to avoid the borrowing issue - None - } - pub fn get_surface_format(&self) -> wgpu::TextureFormat { self.ctx.surface_configuration.format } - pub fn get_resource_storage(&self) -> &RefCell { + pub fn get_device(&self) -> &wgpu::Device { + &self.ctx.device + } + + pub fn get_surface(&self) -> &wgpu::Surface<'_> { + &self.ctx.surface + } + + pub fn get_resource_storage(&self) -> &Rc> { &self.state.renderer_resource_storage } } + +fn create_overlay_uv_pass_inline( + ctx: &DeviceContext, +) -> ([f64; 4], wgpu::BindGroupLayout, wgpu::RenderPipeline) { + let rect = [0.75, 0.75, 0.95, 0.95]; + let overlay_vs_wgsl = r#" + @group(0) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] + + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var unit = array, 6>( + vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), + vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) + ); + let p = unit[vi]; // [-1,1] NDC space + let s = p*0.5+0.5; // [0,1] screen space + let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] + let ndc = r*2.0-1.0; // [-1,1] NDC space + var out: VSOut; + out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); + out.uv = s; + return out; + } + "#; + let overlay_fs_wgsl = r#" + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + return vec4(uv, 0.0, 0.5); + } + "#; + + let overlay_vs = ctx + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_vs"), + source: wgpu::ShaderSource::Wgsl(overlay_vs_wgsl.into()), + }); + let overlay_fs = ctx + .device + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_fs"), + source: wgpu::ShaderSource::Wgsl(overlay_fs_wgsl.into()), + }); + + let overlay_rect_bind_group_layout = + ctx.device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_rect_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }], + }); + let overlay_pipeline_layout = + ctx.device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("overlay_pl"), + bind_group_layouts: &[&overlay_rect_bind_group_layout], + push_constant_ranges: &[], + }); + + // No vertex buffer layout needed; vertices are generated procedurally in the vertex shader + let overlay_pipeline = ctx + .device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("overlay_pipeline"), + layout: Some(&overlay_pipeline_layout), + vertex: wgpu::VertexState { + module: &overlay_vs, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &overlay_fs, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: ctx.surface_configuration.format, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + cull_mode: None, + ..wgpu::PrimitiveState::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + (rect, overlay_rect_bind_group_layout, overlay_pipeline) +} From d12dfc7ef8a40c5c8f92cf88282934494043534f Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Sat, 18 Oct 2025 19:43:12 +0200 Subject: [PATCH 42/69] feat: simplify pass with no resource storage --- engine/pill_engine/src/graphics/mod.rs | 6 +- engine/pill_engine/src/graphics/renderer.rs | 13 +-- engine/pill_engine/src/lib.rs | 9 +- engine/pill_renderer/src/passes.rs | 92 +++++++-------------- engine/pill_renderer/src/renderer.rs | 56 +++++-------- 5 files changed, 66 insertions(+), 110 deletions(-) diff --git a/engine/pill_engine/src/graphics/mod.rs b/engine/pill_engine/src/graphics/mod.rs index 3bb65bfe..857aa975 100644 --- a/engine/pill_engine/src/graphics/mod.rs +++ b/engine/pill_engine/src/graphics/mod.rs @@ -6,9 +6,9 @@ mod renderer; // --- Use --- pub use renderer::{ - BufferDesc, PillRenderer, PipelineV2Desc, Renderer, RendererBufferHandle, RendererCameraHandle, - RendererMaterialHandle, RendererMeshHandle, RendererPipelineHandle, RendererPipelineV2Handle, - RendererTextureHandle, ShaderDesc, + BufferDesc, PillRenderer, PipelineV2, PipelineV2Desc, Renderer, RendererBufferHandle, + RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, RendererPipelineHandle, + RendererPipelineV2Handle, RendererTextureHandle, ShaderDesc, }; pub use render_queue::{ diff --git a/engine/pill_engine/src/graphics/renderer.rs b/engine/pill_engine/src/graphics/renderer.rs index 2723b6d3..167165aa 100644 --- a/engine/pill_engine/src/graphics/renderer.rs +++ b/engine/pill_engine/src/graphics/renderer.rs @@ -74,6 +74,11 @@ pub struct PipelineV2Desc<'a> { // --- Renderer trait definition --- +pub struct PipelineV2 { + pub pipeline: wgpu::RenderPipeline, + pub bind_group_layout: wgpu::BindGroupLayout, +} + pub trait PillRenderer { fn new(window: Arc, config: config::Config) -> Self where @@ -83,12 +88,8 @@ pub trait PillRenderer { fn resize(&mut self, new_window_size: winit::dpi::PhysicalSize); // Creates a 256B-aligned uniform buffer (COPY_DST) and returns its handle - fn create_buffer(&self, desc: BufferDesc) -> Result; - fn create_pipeline_v2( - &self, - desc: PipelineV2Desc, - ) -> Result<(RendererPipelineV2Handle, &wgpu::RenderPipeline)>; - fn get_pipeline_v2(&self, handle: RendererPipelineV2Handle) -> &wgpu::RenderPipeline; + fn create_buffer(&self, desc: BufferDesc) -> Result; + fn create_pipeline_v2(&self, desc: PipelineV2Desc) -> Result; fn create_mesh(&self, name: &str, mesh_data: &MeshData) -> Result; fn create_texture( &self, diff --git a/engine/pill_engine/src/lib.rs b/engine/pill_engine/src/lib.rs index a6b6f587..d65a8ba6 100644 --- a/engine/pill_engine/src/lib.rs +++ b/engine/pill_engine/src/lib.rs @@ -90,10 +90,11 @@ pub mod internal { }, engine::{Engine, PillGame}, graphics::{ - decompose_render_queue_key, BufferDesc, PillRenderer, PipelineV2Desc, RenderQueueItem, - RenderQueueKey, RenderQueueKeyFields, RendererBufferHandle, RendererCameraHandle, - RendererMaterialHandle, RendererMeshHandle, RendererPipelineHandle, - RendererPipelineV2Handle, RendererTextureHandle, ShaderDesc, RENDER_QUEUE_KEY_ORDER, + decompose_render_queue_key, BufferDesc, PillRenderer, PipelineV2, PipelineV2Desc, + RenderQueueItem, RenderQueueKey, RenderQueueKeyFields, RendererBufferHandle, + RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, + RendererPipelineHandle, RendererPipelineV2Handle, RendererTextureHandle, ShaderDesc, + RENDER_QUEUE_KEY_ORDER, }, resources::{ get_renderer_texture_handle_from_material_texture, Material, MaterialHandle, diff --git a/engine/pill_renderer/src/passes.rs b/engine/pill_renderer/src/passes.rs index c09d5ce6..ac1dd078 100644 --- a/engine/pill_renderer/src/passes.rs +++ b/engine/pill_renderer/src/passes.rs @@ -1,14 +1,13 @@ use crate::renderer::{Pass, Renderer}; use anyhow::Result; -use pill_engine::internal::{BufferDesc, PillRenderer, PipelineV2Desc, ShaderDesc}; +use pill_engine::internal::{BufferDesc, PillRenderer, PipelineV2, PipelineV2Desc, ShaderDesc}; use wgpu::CommandEncoder; use wgpu::Queue; pub struct PassOverlayUV { label: String, - buffer_handle: Option, - pipeline_handle: Option, - bind_group_layout: Option, + buffer: Option, + pipeline: Option, bind_group: Option, rect: [f32; 4], } @@ -17,9 +16,8 @@ impl PassOverlayUV { pub fn new(label: &str) -> Self { Self { label: label.to_string(), - buffer_handle: None, - pipeline_handle: None, - bind_group_layout: None, + buffer: None, + pipeline: None, bind_group: None, rect: [0.75, 0.75, 0.95, 0.95], } @@ -35,19 +33,13 @@ impl Pass for PassOverlayUV { println!("Initializing pass: {}", self.label); // Create buffer for overlay rect UBO - self.buffer_handle = Some(renderer.create_buffer(BufferDesc { + let buffer = renderer.create_buffer(BufferDesc { label: Some("overlay_rect_ubo"), - byte_size: 256, + byte_size: 4 * 32, // 4 floats * 32 bytes per float = 128 bytes, will be aligned to 256 bytes for Metal UBOs usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - })?); - - // Get buffer from RefCell storage and write data - let buffer = unsafe { - let storage = &*renderer.get_resource_storage().as_ptr(); - storage.buffers.get(self.buffer_handle.unwrap()).unwrap() - }; + })?; - let vs_wgsl = r#" + let vs = r#" @group(0) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; @@ -67,51 +59,22 @@ impl Pass for PassOverlayUV { } "#; - let fs_wgsl = r#" + let fs = r#" @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { return vec4(uv, 0.0, 0.5); } "#; - queue.write_buffer(buffer, 0, bytemuck::bytes_of(&self.rect)); - - // Create bind group layout and bind group - let bind_group_layout = - renderer - .get_device() - .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("overlay_rect_bind_group_layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), - }, - count: None, - }], - }); - - let bind_group = renderer - .get_device() - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_rect_bind_group"), - layout: &bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: buffer.as_entire_binding(), - }], - }); + queue.write_buffer(&buffer, 0, bytemuck::bytes_of(&self.rect)); - let (pipeline_handle, _pipeline_ref) = renderer.create_pipeline_v2(PipelineV2Desc { + let pipeline = renderer.create_pipeline_v2(PipelineV2Desc { label: Some("overlay_uv"), vs: ShaderDesc { - source: vs_wgsl, + source: vs, entry_func: "main", }, ps: ShaderDesc { - source: fs_wgsl, + source: fs, entry_func: "main", }, bindings: vec![wgpu::BindGroupLayoutEntry { @@ -133,11 +96,19 @@ impl Pass for PassOverlayUV { multisample: wgpu::MultisampleState::default(), })?; - // Store pipeline handle - self.pipeline_handle = Some(pipeline_handle); + let bind_group = renderer + .get_device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_rect_bind_group"), + layout: &pipeline.bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + }); - // Store the bind group layout and bind group - self.bind_group_layout = Some(bind_group_layout); + // Store pipeline handle + self.pipeline = Some(pipeline); self.bind_group = Some(bind_group); Ok(()) @@ -146,14 +117,11 @@ impl Pass for PassOverlayUV { fn draw( &self, encoder: &mut CommandEncoder, - renderer: &Renderer, + _renderer: &Renderer, frame: &wgpu::SurfaceTexture, + view: &wgpu::TextureView, ) -> Result<()> { // Create render pass for this pass using the provided frame - let view = frame - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some(&self.label), color_attachments: &[Some(wgpu::RenderPassColorAttachment { @@ -170,9 +138,7 @@ impl Pass for PassOverlayUV { }); // Draw small overlay UV quad in normalized rect via UBO - let pipeline_handle = self.pipeline_handle.as_ref().unwrap(); - let pipeline = renderer.get_pipeline_v2(*pipeline_handle); - pass.set_pipeline(pipeline); + pass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); pass.set_bind_group(0, self.bind_group.as_ref().unwrap(), &[]); pass.draw(0..6, 0..1); diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 210b8339..1a5e8f42 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -16,9 +16,10 @@ use std::rc::Rc; use pill_engine::internal::{ get_renderer_resource_handle_from_camera_component, BufferDesc, CameraComponent, ComponentStorage, EntityHandle, MaterialParameterMap, MaterialTextureMap, MeshData, - PillRenderer, PipelineV2Desc, RenderQueueItem, RendererBufferHandle, RendererCameraHandle, - RendererMaterialHandle, RendererMeshHandle, RendererPipelineHandle, RendererPipelineV2Handle, - RendererTextureHandle, ShaderDesc, TextureType, TransformComponent, RENDER_QUEUE_KEY_ORDER, + PillRenderer, PipelineV2, PipelineV2Desc, RenderQueueItem, RendererBufferHandle, + RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, RendererPipelineHandle, + RendererPipelineV2Handle, RendererTextureHandle, ShaderDesc, TextureType, TransformComponent, + RENDER_QUEUE_KEY_ORDER, }; use pill_core::{PillSlotMapKey, PillSlotMapKeyData, PillStyle, RendererError, Timer}; @@ -659,6 +660,7 @@ pub trait Pass { encoder: &mut wgpu::CommandEncoder, renderer: &Renderer, frame: &wgpu::SurfaceTexture, + view: &wgpu::TextureView, ) -> Result<()>; } @@ -1201,7 +1203,7 @@ impl PillRenderer for Renderer { Ok(()) } - fn create_buffer(&self, desc: BufferDesc) -> Result { + fn create_buffer(&self, desc: BufferDesc) -> Result { let aligned_size = ((desc.byte_size + 255) / 256) * 256; // 256B for Metal UBOs let buffer = self.ctx.device.create_buffer(&wgpu::BufferDescriptor { label: desc.label, @@ -1209,17 +1211,14 @@ impl PillRenderer for Renderer { usage: desc.usage, mapped_at_creation: false, }); - let handle = unsafe { - let storage = &mut *self.state.renderer_resource_storage.as_ptr(); - storage.buffers.insert(buffer) - }; - Ok(handle) + // let handle = unsafe { + // let storage = &mut *self.state.renderer_resource_storage.as_ptr(); + // storage.buffers.insert(buffer) + // }; + Ok(buffer) } - fn create_pipeline_v2( - &self, - desc: PipelineV2Desc, - ) -> Result<(RendererPipelineV2Handle, &wgpu::RenderPipeline)> { + fn create_pipeline_v2(&self, desc: PipelineV2Desc) -> Result { /* // Example shader pipeline descriptor for factory method n_shader = rm->createShader({ @@ -1320,25 +1319,10 @@ impl PillRenderer for Renderer { multiview: None, }); - let handle: RendererPipelineV2Handle = unsafe { - let storage = &mut *self.state.renderer_resource_storage.as_ptr(); - storage.pipelines_v2.insert(pipeline) - }; - - // Get a reference to the pipeline from storage - let pipeline_ref = unsafe { - let storage = &*self.state.renderer_resource_storage.as_ptr(); - storage.pipelines_v2.get(handle).unwrap() - }; - - Ok((handle, pipeline_ref)) - } - - fn get_pipeline_v2(&self, handle: RendererPipelineV2Handle) -> &wgpu::RenderPipeline { - unsafe { - let storage = &*self.state.renderer_resource_storage.as_ptr(); - storage.pipelines_v2.get(handle).unwrap() - } + Ok(PipelineV2 { + pipeline, + bind_group_layout, + }) } fn create_mesh(&self, name: &str, mesh_data: &MeshData) -> Result { @@ -1658,7 +1642,7 @@ impl PillRenderer for Renderer { }, }; - let view = frame + let view: wgpu::TextureView = frame .texture .create_view(&wgpu::TextureViewDescriptor::default()); @@ -2161,7 +2145,7 @@ impl PillRenderer for Renderer { for pass in &self.state.passes { let label = pass.get_label(); timer.begin_context(label); - let _ = pass.draw(&mut encoder, self, &frame); + let _ = pass.draw(&mut encoder, self, &frame, &view); timer.record(label); timer.end_context()?; } @@ -2226,6 +2210,10 @@ impl Renderer { &self.ctx.surface } + pub fn get_surface_view(&self) -> &wgpu::TextureView { + &self.state.offscreen_color_texture.texture_view + } + pub fn get_resource_storage(&self) -> &Rc> { &self.state.renderer_resource_storage } From 557dcad42b736ff1efcdd455f9e229f35f25aeed Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Sat, 18 Oct 2025 21:09:03 +0200 Subject: [PATCH 43/69] feat: pass overlay logo --- engine/pill_engine/src/graphics/renderer.rs | 4 +- engine/pill_renderer/src/lib.rs | 5 +- engine/pill_renderer/src/pass_overlay_logo.rs | 237 ++++++++++ .../src/{passes.rs => pass_overlay_uv.rs} | 11 +- engine/pill_renderer/src/renderer.rs | 446 ++---------------- 5 files changed, 294 insertions(+), 409 deletions(-) create mode 100644 engine/pill_renderer/src/pass_overlay_logo.rs rename engine/pill_renderer/src/{passes.rs => pass_overlay_uv.rs} (96%) diff --git a/engine/pill_engine/src/graphics/renderer.rs b/engine/pill_engine/src/graphics/renderer.rs index 167165aa..f0d418f5 100644 --- a/engine/pill_engine/src/graphics/renderer.rs +++ b/engine/pill_engine/src/graphics/renderer.rs @@ -66,7 +66,7 @@ pub struct PipelineV2Desc<'a> { pub label: Option<&'a str>, pub vs: ShaderDesc<'a>, pub ps: ShaderDesc<'a>, - pub bindings: Vec, + pub bind_groups: Vec>, pub targets: &'a [Option], pub depth_stencil: Option, pub multisample: wgpu::MultisampleState, @@ -76,7 +76,7 @@ pub struct PipelineV2Desc<'a> { pub struct PipelineV2 { pub pipeline: wgpu::RenderPipeline, - pub bind_group_layout: wgpu::BindGroupLayout, + pub bind_group_layouts: Vec, } pub trait PillRenderer { diff --git a/engine/pill_renderer/src/lib.rs b/engine/pill_renderer/src/lib.rs index 00aa34f2..39f272a4 100644 --- a/engine/pill_renderer/src/lib.rs +++ b/engine/pill_renderer/src/lib.rs @@ -3,7 +3,8 @@ pub mod egui; pub mod instance; pub mod mesh_drawer; -pub mod passes; +pub mod pass_overlay_logo; +pub mod pass_overlay_uv; pub mod renderer; pub mod renderer_resource_storage; pub mod resources; @@ -13,6 +14,6 @@ pub mod resources; pub use renderer::*; pub use instance::Instance; -pub use passes::PassOverlayUV; +pub use pass_overlay_uv::PassOverlayUV; pub use renderer_resource_storage::RendererResourceStorage; diff --git a/engine/pill_renderer/src/pass_overlay_logo.rs b/engine/pill_renderer/src/pass_overlay_logo.rs new file mode 100644 index 00000000..4612223b --- /dev/null +++ b/engine/pill_renderer/src/pass_overlay_logo.rs @@ -0,0 +1,237 @@ +use crate::renderer::{Pass, Renderer}; +use anyhow::Result; +use pill_engine::internal::{ + BufferDesc, PillRenderer, PipelineV2, PipelineV2Desc, RendererTextureHandle, ShaderDesc, +}; +use wgpu::CommandEncoder; +use wgpu::Queue; + +pub struct PassOverlayLogo { + label: String, + tex_logo: RendererTextureHandle, + buffer: Option, + pipeline: Option, + bind_group_rect: Option, + bind_group_material: Option, + rect: [f32; 4], + tint: [f32; 4], +} + +impl PassOverlayLogo { + pub fn new(label: &str, tex_logo: RendererTextureHandle) -> Self { + Self { + label: label.to_string(), + tex_logo, + buffer: None, + pipeline: None, + bind_group_rect: None, + bind_group_material: None, + rect: [0.25, 0.25, 0.75, 0.75], + tint: [1.0, 1.0, 1.0, 1.0], + } + } +} + +impl Pass for PassOverlayLogo { + fn get_label(&self) -> &str { + &self.label + } + + fn init(&mut self, queue: &wgpu::Queue, renderer: &Renderer) -> Result<()> { + println!("Initializing pass: {}", self.label); + + let h: f32 = 0.04; + self.rect = [0.98 - 3. * h, 0.02, 0.98, 0.02 + h]; // bottom right + + // Create buffer for overlay rect UBO + let buffer = renderer.create_buffer(BufferDesc { + label: Some("overlay_logo_ubo"), + byte_size: 4 * 32, // 4 floats * 32 bytes per float = 128 bytes, will be aligned to 256 bytes for Metal UBOs + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + })?; + queue.write_buffer(&buffer, 0, bytemuck::bytes_of(&self.rect)); + + let vs = r#" + @group(1) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] + + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var unit = array, 6>( + vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), + vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) + ); + let p = unit[vi]; // [-1,1] NDC space + let s = p*0.5+0.5; // [0,1] screen space + let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] + let ndc = r*2.0-1.0; // [-1,1] NDC space + var out: VSOut; + out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); + out.uv = vec2(s.x, 1.0 - s.y); + return out; + } + "#; + + let fs = r#" + @group(0) @binding(0) var tex: texture_2d; + @group(0) @binding(1) var smp: sampler; + @group(0) @binding(2) var UTint: vec4; + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let c = textureSample(tex, smp, uv); + return c * UTint; + } + "#; + + let pipeline = renderer.create_pipeline_v2(PipelineV2Desc { + label: Some("overlay_logo"), + vs: ShaderDesc { + source: vs, + entry_func: "main", + }, + ps: ShaderDesc { + source: fs, + entry_func: "main", + }, + bind_groups: vec![ + // Group 0: Material bindings (texture, sampler, tint buffer) + vec![ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }, + ], + // Group 1: Rect UBO + vec![wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }], + ], + targets: &[Some(wgpu::ColorTargetState { + format: renderer.get_surface_format(), + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + })?; + + let bind_group_rect = renderer + .get_device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_rect_bind_group"), + layout: &pipeline.bind_group_layouts[1], // Group 1: Rect UBO + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + }); + + let storage = renderer.get_resource_storage().borrow(); + let tex = storage + .textures + .get(self.tex_logo) + .expect("logo texture handle invalid"); + let logo_texture_view = &tex.texture_view; + let logo_sampler = &tex.sampler; + let tint_buffer = renderer + .get_device() + .create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_logo_tint"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + queue.write_buffer(&tint_buffer, 0, bytemuck::bytes_of(&self.tint)); + + let bind_group_material = + renderer + .get_device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_logo_material_bg"), + layout: &pipeline.bind_group_layouts[0], // Group 0: Material bindings + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(logo_texture_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(logo_sampler), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &tint_buffer, + offset: 0, + size: Some(std::num::NonZeroU64::new(16).unwrap()), + }), + }, + ], + }); + + // Store pipeline handle + self.pipeline = Some(pipeline); + self.bind_group_rect = Some(bind_group_rect); + self.bind_group_material = Some(bind_group_material); + + Ok(()) + } + + fn draw( + &self, + encoder: &mut CommandEncoder, + _renderer: &Renderer, + _frame: &wgpu::SurfaceTexture, + view: &wgpu::TextureView, + ) -> Result<()> { + // Create render pass for this pass using the provided frame + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.label), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + pass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); + pass.set_bind_group(0, &self.bind_group_material.as_ref().unwrap(), &[]); + pass.set_bind_group(1, &self.bind_group_rect.as_ref().unwrap(), &[]); + pass.draw(0..6, 0..1); + + Ok(()) + } +} diff --git a/engine/pill_renderer/src/passes.rs b/engine/pill_renderer/src/pass_overlay_uv.rs similarity index 96% rename from engine/pill_renderer/src/passes.rs rename to engine/pill_renderer/src/pass_overlay_uv.rs index ac1dd078..0b96968c 100644 --- a/engine/pill_renderer/src/passes.rs +++ b/engine/pill_renderer/src/pass_overlay_uv.rs @@ -38,6 +38,7 @@ impl Pass for PassOverlayUV { byte_size: 4 * 32, // 4 floats * 32 bytes per float = 128 bytes, will be aligned to 256 bytes for Metal UBOs usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, })?; + queue.write_buffer(&buffer, 0, bytemuck::bytes_of(&self.rect)); let vs = r#" @group(0) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] @@ -65,8 +66,6 @@ impl Pass for PassOverlayUV { } "#; - queue.write_buffer(&buffer, 0, bytemuck::bytes_of(&self.rect)); - let pipeline = renderer.create_pipeline_v2(PipelineV2Desc { label: Some("overlay_uv"), vs: ShaderDesc { @@ -77,7 +76,7 @@ impl Pass for PassOverlayUV { source: fs, entry_func: "main", }, - bindings: vec![wgpu::BindGroupLayoutEntry { + bind_groups: vec![vec![wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Buffer { @@ -86,7 +85,7 @@ impl Pass for PassOverlayUV { min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), }, count: None, - }], + }]], targets: &[Some(wgpu::ColorTargetState { format: renderer.get_surface_format(), blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), @@ -100,7 +99,7 @@ impl Pass for PassOverlayUV { .get_device() .create_bind_group(&wgpu::BindGroupDescriptor { label: Some("overlay_rect_bind_group"), - layout: &pipeline.bind_group_layout, + layout: &pipeline.bind_group_layouts[0], entries: &[wgpu::BindGroupEntry { binding: 0, resource: buffer.as_entire_binding(), @@ -118,7 +117,7 @@ impl Pass for PassOverlayUV { &self, encoder: &mut CommandEncoder, _renderer: &Renderer, - frame: &wgpu::SurfaceTexture, + _frame: &wgpu::SurfaceTexture, view: &wgpu::TextureView, ) -> Result<()> { // Create render pass for this pass using the provided frame diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 1a5e8f42..c16c6603 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -3,7 +3,8 @@ // https://github.com/emilk/egui/discussions/3067 use crate::{ - passes::PassOverlayUV, + pass_overlay_logo::PassOverlayLogo, + pass_overlay_uv::PassOverlayUV, renderer_resource_storage::RendererResourceStorage, resources::{ RendererCamera, RendererMaterial, RendererMesh, RendererPipeline, RendererTexture, Vertex, @@ -109,144 +110,14 @@ fn create_render_target( sampler, } } -// Overlay (UV gradient quad) resources grouped for clarity +// Overlay quad resources grouped for clarity struct OverlayResources { pipeline: wgpu::RenderPipeline, rect_bind_group_layout: wgpu::BindGroupLayout, rect_bind_group: wgpu::BindGroup, rect_buffer: wgpu::Buffer, } - -// KISS helper: build overlay pipeline, UBO layout/buffer, and bind group -fn create_overlay_uv( - ctx: &DeviceContext, - surface_format: wgpu::TextureFormat, - rect: [f32; 4], -) -> OverlayResources { - let overlay_vs_wgsl = r#" -@group(0) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] - -struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; -@vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { - var unit = array, 6>( - vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), - vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) - ); - let p = unit[vi]; // [-1,1] NDC space - let s = p*0.5+0.5; // [0,1] screen space - let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] - let ndc = r*2.0-1.0; // [-1,1] NDC space - var out: VSOut; - out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); - out.uv = s; - return out; -} -"#; - let overlay_fs_wgsl = r#" -@fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { - return vec4(uv, 0.0, 0.5); -} -"#; - - let overlay_vs = ctx - .device - .create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("overlay_vs"), - source: wgpu::ShaderSource::Wgsl(overlay_vs_wgsl.into()), - }); - let overlay_fs = ctx - .device - .create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("overlay_fs"), - source: wgpu::ShaderSource::Wgsl(overlay_fs_wgsl.into()), - }); - - let overlay_rect_bind_group_layout = - ctx.device - .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("overlay_rect_bgl"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), - }, - count: None, - }], - }); - let overlay_pipeline_layout = - ctx.device - .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("overlay_pl"), - bind_group_layouts: &[&overlay_rect_bind_group_layout], - push_constant_ranges: &[], - }); - - // No vertex buffer layout needed; vertices are generated procedurally in the vertex shader - let overlay_pipeline = ctx - .device - .create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("overlay_pipeline"), - layout: Some(&overlay_pipeline_layout), - vertex: wgpu::VertexState { - module: &overlay_vs, - entry_point: "main", - buffers: &[], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &overlay_fs, - entry_point: "main", - targets: &[Some(wgpu::ColorTargetState { - format: surface_format, - blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState { - cull_mode: None, - ..wgpu::PrimitiveState::default() - }, - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview: None, - }); - - // Apple Metal constant buffer alignment (256 bytes) - let overlay_rect_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor { - label: Some("overlay_rect_ubo"), - size: 256, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - ctx.queue - .write_buffer(&overlay_rect_buffer, 0, bytemuck::bytes_of(&rect)); - - let overlay_rect_bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_rect_bg"), - layout: &overlay_rect_bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &overlay_rect_buffer, - offset: 0, - size: Some(std::num::NonZeroU64::new(16).unwrap()), - }), - }], - }); - - OverlayResources { - pipeline: overlay_pipeline, - rect_bind_group_layout: overlay_rect_bind_group_layout, - rect_bind_group: overlay_rect_bind_group, - rect_buffer: overlay_rect_buffer, - } -} - -// Overlay (UV gradient quad) resources grouped for clarity +// Overlay quad resources grouped for clarity struct TextureOverlayResources { pipeline: wgpu::RenderPipeline, rect_bind_group_layout: wgpu::BindGroupLayout, @@ -257,210 +128,6 @@ struct TextureOverlayResources { material_bind_group: wgpu::BindGroup, material_tint_buffer: wgpu::Buffer, } -// KISS helper: build overlay pipeline, UBO layout/buffer, and bind group -fn create_overlay_logo( - device: &wgpu::Device, - queue: &wgpu::Queue, - surface_format: wgpu::TextureFormat, - texture_handle: RendererTextureHandle, - storage: &RendererResourceStorage, - rect: [f32; 4], -) -> TextureOverlayResources { - let overlay_vs_wgsl = r#" -@group(1) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] - -struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; -@vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { - var unit = array, 6>( - vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), - vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) - ); - let p = unit[vi]; // [-1,1] NDC space - let s = p*0.5+0.5; // [0,1] screen space - let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] - let ndc = r*2.0-1.0; // [-1,1] NDC space - var out: VSOut; - out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); - out.uv = vec2(s.x, 1.0 - s.y); - return out; - } - "#; - - let overlay_fs_wgsl = r#" -@group(0) @binding(0) var tex: texture_2d; -@group(0) @binding(1) var smp: sampler; -@group(0) @binding(2) var UTint: vec4; -@fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { - let c = textureSample(tex, smp, uv); - return c * UTint; -} -"#; - - let overlay_vs = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("overlay_vs"), - source: wgpu::ShaderSource::Wgsl(overlay_vs_wgsl.into()), - }); - let overlay_fs = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("overlay_fs"), - source: wgpu::ShaderSource::Wgsl(overlay_fs_wgsl.into()), - }); - - // Group 0: material (texture + sampler + tint) - let overlay_material_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("overlay_logo_material_bgl"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 2, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), - }, - count: None, - }, - ], - }); - // Group 1: rect UBO - let overlay_rect_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("overlay_rect_bgl"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), - }, - count: None, - }], - }); - let overlay_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("overlay_pl"), - bind_group_layouts: &[ - &overlay_material_bind_group_layout, - &overlay_rect_bind_group_layout, - ], - push_constant_ranges: &[], - }); - - // No vertex buffer layout needed; vertices are generated procedurally in the vertex shader - let overlay_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("overlay_pipeline"), - layout: Some(&overlay_pipeline_layout), - vertex: wgpu::VertexState { - module: &overlay_vs, - entry_point: "main", - buffers: &[], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &overlay_fs, - entry_point: "main", - targets: &[Some(wgpu::ColorTargetState { - format: surface_format, - blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState { - cull_mode: None, - ..wgpu::PrimitiveState::default() - }, - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview: None, - }); - - // Apple Metal constant buffer alignment (256 bytes) - let overlay_rect_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("overlay_rect_ubo"), - size: 256, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - queue.write_buffer(&overlay_rect_buffer, 0, bytemuck::bytes_of(&rect)); - - let overlay_rect_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_rect_bg"), - layout: &overlay_rect_bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &overlay_rect_buffer, - offset: 0, - size: Some(std::num::NonZeroU64::new(16).unwrap()), - }), - }], - }); - - let tex = storage - .textures - .get(texture_handle) - .expect("logo texture handle invalid"); - let logo_texture_view = &tex.texture_view; - let logo_sampler = &tex.sampler; - let tint_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("overlay_logo_tint"), - size: 256, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - let tint: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; - queue.write_buffer(&tint_buffer, 0, bytemuck::bytes_of(&tint)); - - let overlay_material_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_logo_material_bg"), - layout: &overlay_material_bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(logo_texture_view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(logo_sampler), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &tint_buffer, - offset: 0, - size: Some(std::num::NonZeroU64::new(16).unwrap()), - }), - }, - ], - }); - - TextureOverlayResources { - pipeline: overlay_pipeline, - rect_bind_group_layout: overlay_rect_bind_group_layout, - rect_bind_group: overlay_rect_bind_group, - rect_buffer: overlay_rect_buffer, - material_bind_group_layout: overlay_material_bind_group_layout, - material_bind_group: overlay_material_bind_group, - material_tint_buffer: tint_buffer, - } -} // KISS helper: build overlay pipeline, UBO layout/buffer, and bind group fn create_overlay_depth( @@ -701,9 +368,10 @@ pub struct State { composite_bind_group: wgpu::BindGroup, // These look like could be combined into OverlayRenderer, but also good case for user submiter pass API - overlay_uv: OverlayResources, // Overlay (UV gradient quad in top-right) - overlay_logo: TextureOverlayResources, // Overlay (Logo in bot-right) + // overlay_uv: OverlayResources, // Overlay (UV gradient quad in top-right) + // overlay_logo: OverlayLogoResources, // Overlay (Logo in bot-right) overlay_depth: TextureOverlayResources, // Overlay (Depth in top-right) + tex_logo: RendererTextureHandle, } pub struct Renderer { @@ -1062,41 +730,6 @@ impl PillRenderer for Renderer { ], }); - let (rect, overlay_rect_bind_group_layout, overlay_pipeline) = - create_overlay_uv_pass_inline(&ctx); - - // Apple Metal constant buffer alignment (256 bytes) - let overlay_rect_buffer = ctx.device.create_buffer(&wgpu::BufferDescriptor { - label: Some("overlay_rect_ubo"), - size: 256, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - ctx.queue - .write_buffer(&overlay_rect_buffer, 0, bytemuck::bytes_of(&rect)); - - let overlay_rect_bind_group = - ctx.device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_rect_bg"), - layout: &overlay_rect_bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &overlay_rect_buffer, - offset: 0, - size: Some(std::num::NonZeroU64::new(16).unwrap()), - }), - }], - }); - - let overlay_uv = OverlayResources { - pipeline: overlay_pipeline, - rect_bind_group_layout: overlay_rect_bind_group_layout, - rect_bind_group: overlay_rect_bind_group, - rect_buffer: overlay_rect_buffer, - }; - // OVERLAY UV END - // Ideally Client submitted pass via API // Create logo texture via renderer API and pass handle into overlay factory let logo_image = image::open( @@ -1104,7 +737,7 @@ impl PillRenderer for Renderer { ) .expect("failed to load overlay logo image"); - let logo_handle = { + let tex_logo = { let tex = RendererTexture::new_texture( &ctx.device, &ctx.queue, @@ -1117,15 +750,15 @@ impl PillRenderer for Renderer { }; // Overlay logo pipeline (small quad) // res: 1024 × 320, 1.0 x 0.3 - let h: f32 = 0.04; - let overlay_logo = create_overlay_logo( - &ctx.device, - &ctx.queue, - ctx.surface_configuration.format, - logo_handle, - &renderer_resource_storage, - [0.98 - 3. * h, 0.02, 0.98, 0.02 + h], // bottom right - ); + // let h: f32 = 0.04; + // let overlay_logo = create_overlay_logo( + // &ctx.device, + // &ctx.queue, + // ctx.surface_configuration.format, + // tex_logo, + // &renderer_resource_storage, + // [0.98 - 3. * h, 0.02, 0.98, 0.02 + h], // bottom right + // ); let overlay_depth = create_overlay_depth( &ctx.device, @@ -1177,9 +810,10 @@ impl PillRenderer for Renderer { composite_pipeline, composite_bind_group, // Overlays - overlay_uv, - overlay_logo, + // overlay_uv, + // overlay_logo, overlay_depth, + tex_logo, } }; @@ -1191,6 +825,11 @@ impl PillRenderer for Renderer { .passes .push(Box::new(PassOverlayUV::new("overlay_uv"))); + self.state.passes.push(Box::new(PassOverlayLogo::new( + "overlay_logo", + self.state.tex_logo, + ))); + // Initialize passes - call init on each pass let queue = &self.ctx.queue; // Temporarily move passes out to avoid borrowing conflicts @@ -1273,20 +912,29 @@ impl PillRenderer for Renderer { source: wgpu::ShaderSource::Wgsl(desc.ps.source.into()), }); - let bind_group_layout = - self.ctx - .device - .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some(format!("{}{}", desc.label.unwrap_or("program"), "_bgl").as_str()), - entries: &desc.bindings, - }); + // Create multiple bind group layouts + let mut bind_group_layouts = Vec::new(); + for (i, bindings) in desc.bind_groups.iter().enumerate() { + let layout = + self.ctx + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some( + format!("{}{}_bgl_{}", desc.label.unwrap_or("program"), "_bgl", i) + .as_str(), + ), + entries: bindings, + }); + bind_group_layouts.push(layout); + } + // Create pipeline layout with all bind groups let pipeline_layout = self.ctx .device .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("overlay_pl"), - bind_group_layouts: &[&bind_group_layout], + bind_group_layouts: &bind_group_layouts.iter().collect::>(), push_constant_ranges: &[], }); @@ -1321,7 +969,7 @@ impl PillRenderer for Renderer { Ok(PipelineV2 { pipeline, - bind_group_layout, + bind_group_layouts, }) } @@ -2119,10 +1767,10 @@ impl PillRenderer for Renderer { rpass.draw(0..6, 0..1); // Draw logo overlay (bind material at group 0 and rect at group 1) - rpass.set_pipeline(&self.state.overlay_logo.pipeline); - rpass.set_bind_group(0, &self.state.overlay_logo.material_bind_group, &[]); - rpass.set_bind_group(1, &self.state.overlay_logo.rect_bind_group, &[]); - rpass.draw(0..6, 0..1); + // rpass.set_pipeline(&self.state.overlay_logo.pipeline); + // rpass.set_bind_group(0, &self.state.overlay_logo.material_bind_group, &[]); + // rpass.set_bind_group(1, &self.state.overlay_logo.rect_bind_group, &[]); + // rpass.draw(0..6, 0..1); timer.record("Encode overlay passes"); } From ef4998c21223b3d02f0f215dae94b89b71025ed3 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Sat, 18 Oct 2025 22:28:15 +0200 Subject: [PATCH 44/69] feat: pass overlay depth --- engine/pill_renderer/src/lib.rs | 1 + .../pill_renderer/src/pass_overlay_depth.rs | 422 +++++++++++++++ engine/pill_renderer/src/pass_overlay_logo.rs | 14 +- engine/pill_renderer/src/pass_overlay_uv.rs | 4 +- engine/pill_renderer/src/renderer.rs | 502 ++---------------- .../src/resources/renderer_texture.rs | 119 +++-- 6 files changed, 550 insertions(+), 512 deletions(-) create mode 100644 engine/pill_renderer/src/pass_overlay_depth.rs diff --git a/engine/pill_renderer/src/lib.rs b/engine/pill_renderer/src/lib.rs index 39f272a4..fb99ea1b 100644 --- a/engine/pill_renderer/src/lib.rs +++ b/engine/pill_renderer/src/lib.rs @@ -3,6 +3,7 @@ pub mod egui; pub mod instance; pub mod mesh_drawer; +pub mod pass_overlay_depth; pub mod pass_overlay_logo; pub mod pass_overlay_uv; pub mod renderer; diff --git a/engine/pill_renderer/src/pass_overlay_depth.rs b/engine/pill_renderer/src/pass_overlay_depth.rs new file mode 100644 index 00000000..232e2e2c --- /dev/null +++ b/engine/pill_renderer/src/pass_overlay_depth.rs @@ -0,0 +1,422 @@ +use crate::renderer::{Pass, Renderer}; +use crate::resources::RendererTexture; +use anyhow::Result; +use pill_engine::internal::{BufferDesc, PillRenderer, PipelineV2, PipelineV2Desc, ShaderDesc}; +use std::sync::Arc; +use wgpu::CommandEncoder; +use wgpu::Queue; + +pub struct PassOverlayDepth { + label: String, + depth_texture: Arc, + pipeline: Option, + bind_group_rect: Option, + bind_group_material: Option, + tint_buffer: Option, + rect: [f32; 4], + tint: [f32; 4], +} + +impl PassOverlayDepth { + pub fn new( + label: &str, + rect: [f32; 4], + tint: [f32; 4], + depth_texture: Arc, + ) -> Self { + Self { + label: label.to_string(), + depth_texture, + pipeline: None, + bind_group_rect: None, + bind_group_material: None, + tint_buffer: None, + rect: rect, + tint: tint, + } + } +} + +impl Pass for PassOverlayDepth { + fn get_label(&self) -> &str { + &self.label + } + + fn init(&mut self, queue: &wgpu::Queue, renderer: &Renderer) -> Result<()> { + println!("Initializing pass: {}", self.label); + + // Create buffer for overlay rect UBO + let buffer = renderer.create_buffer(BufferDesc { + label: Some("overlay_rect_ubo"), + byte_size: 4 * 32, // 4 floats * 32 bytes per float = 128 bytes, will be aligned to 256 bytes for Metal UBOs + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + })?; + queue.write_buffer(&buffer, 0, bytemuck::bytes_of(&self.rect)); + + let vs = r#" + @group(1) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] + + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var unit = array, 6>( + vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), + vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) + ); + let p = unit[vi]; // [-1,1] NDC space + let s = p*0.5+0.5; // [0,1] screen space + let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] + let ndc = r*2.0-1.0; // [-1,1] NDC space + var out: VSOut; + out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); + out.uv = vec2(s.x, 1.0 - s.y); + return out; + } + "#; + + let fs = r#" + @group(0) @binding(0) var tex: texture_depth_2d; + @group(0) @binding(1) var UTint: vec4; + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let dims = textureDimensions(tex, 0u); + let coord = vec2(uv * vec2(dims)); + let d = textureLoad(tex, coord, 0); + let vis = fract(1000.0*d); + return vec4(vis, vis, vis, 1.0) * UTint; + } + "#; + + let pipeline = renderer.create_pipeline_v2(PipelineV2Desc { + label: Some("overlay_depth"), + vs: ShaderDesc { + source: vs, + entry_func: "main", + }, + ps: ShaderDesc { + source: fs, + entry_func: "main", + }, + bind_groups: vec![ + // Group 0: Material bindings (texture, tint buffer) + vec![ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Depth, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }, + ], + // Group 1: Rect UBO + vec![wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }], + ], + targets: &[Some(wgpu::ColorTargetState { + format: renderer.get_surface_format(), + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + })?; + + let bind_group_rect = renderer + .get_device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_rect_bind_group"), + layout: &pipeline.bind_group_layouts[1], // Group 1: Rect UBO + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + }); + + // Create tint buffer + let tint_buffer = renderer + .get_device() + .create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_depth_tint"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + queue.write_buffer(&tint_buffer, 0, bytemuck::bytes_of(&self.tint)); + + // Create material bind group + let bind_group_material = + renderer + .get_device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_depth_material_bg"), + layout: &pipeline.bind_group_layouts[0], // Group 0: Material bindings + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView( + &self.depth_texture.texture_view, + ), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &tint_buffer, + offset: 0, + size: Some(std::num::NonZeroU64::new(16).unwrap()), + }), + }, + ], + }); + + // Store pipeline handle + self.pipeline = Some(pipeline); + self.bind_group_rect = Some(bind_group_rect); + self.bind_group_material = Some(bind_group_material); + self.tint_buffer = Some(tint_buffer); + + Ok(()) + } + + fn draw( + &self, + encoder: &mut CommandEncoder, + _renderer: &Renderer, + _frame: &wgpu::SurfaceTexture, + view: &wgpu::TextureView, + ) -> Result<()> { + // Create render pass for this pass using the provided frame + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.label), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + // Draw depth overlay (bind material at group 0 and rect at group 1) + pass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); + pass.set_bind_group(0, &self.bind_group_material.as_ref().unwrap(), &[]); + pass.set_bind_group(1, &self.bind_group_rect.as_ref().unwrap(), &[]); + pass.draw(0..6, 0..1); + + Ok(()) + } +} + +/* +fn create_overlay_depth( + device: &wgpu::Device, + queue: &wgpu::Queue, + surface_format: wgpu::TextureFormat, + depth_rt: &RendererTexture, + rect: [f32; 4], +) -> TextureOverlayResources { + let overlay_vs_wgsl = r#" +@group(1) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] + +struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; +@vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var unit = array, 6>( + vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), + vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) + ); + let p = unit[vi]; // [-1,1] NDC space + let s = p*0.5+0.5; // [0,1] screen space + let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] + let ndc = r*2.0-1.0; // [-1,1] NDC space + var out: VSOut; + out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); + out.uv = vec2(s.x, 1.0 - s.y); + return out; + } + "#; + + let overlay_fs_wgsl = r#" +@group(0) @binding(0) var tex: texture_depth_2d; +@group(0) @binding(1) var UTint: vec4; +@fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let dims = textureDimensions(tex, 0u); + let coord = vec2(uv * vec2(dims)); + let d = textureLoad(tex, coord, 0); + let vis = fract(100.0*d); + return vec4(vis, vis, vis, 1.0) * UTint; +} +"#; + + let overlay_vs = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_vs"), + source: wgpu::ShaderSource::Wgsl(overlay_vs_wgsl.into()), + }); + let overlay_fs = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_fs"), + source: wgpu::ShaderSource::Wgsl(overlay_fs_wgsl.into()), + }); + + // Group 0: material (texture + tint) + let overlay_material_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_logo_material_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Depth, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }, + ], + }); + // Group 1: rect UBO + let overlay_rect_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_rect_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }], + }); + let overlay_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("overlay_pl"), + bind_group_layouts: &[ + &overlay_material_bind_group_layout, + &overlay_rect_bind_group_layout, + ], + push_constant_ranges: &[], + }); + + // No vertex buffer layout needed; vertices are generated procedurally in the vertex shader + let overlay_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("overlay_pipeline"), + layout: Some(&overlay_pipeline_layout), + vertex: wgpu::VertexState { + module: &overlay_vs, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &overlay_fs, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: surface_format, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + cull_mode: None, + ..wgpu::PrimitiveState::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + // Apple Metal constant buffer alignment (256 bytes) + let overlay_rect_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_rect_ubo"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + queue.write_buffer(&overlay_rect_buffer, 0, bytemuck::bytes_of(&rect)); + + let overlay_rect_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_rect_bg"), + layout: &overlay_rect_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &overlay_rect_buffer, + offset: 0, + size: Some(std::num::NonZeroU64::new(16).unwrap()), + }), + }], + }); + + let tint_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_tint"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let tint: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; + queue.write_buffer(&tint_buffer, 0, bytemuck::bytes_of(&tint)); + + let overlay_material_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_material_bg"), + layout: &overlay_material_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&depth_rt.texture_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &tint_buffer, + offset: 0, + size: Some(std::num::NonZeroU64::new(16).unwrap()), + }), + }, + ], + }); + + TextureOverlayResources { + pipeline: overlay_pipeline, + rect_bind_group_layout: overlay_rect_bind_group_layout, + rect_bind_group: overlay_rect_bind_group, + rect_buffer: overlay_rect_buffer, + material_bind_group_layout: overlay_material_bind_group_layout, + material_bind_group: overlay_material_bind_group, + material_tint_buffer: tint_buffer, + } +} + +*/ diff --git a/engine/pill_renderer/src/pass_overlay_logo.rs b/engine/pill_renderer/src/pass_overlay_logo.rs index 4612223b..71083bd0 100644 --- a/engine/pill_renderer/src/pass_overlay_logo.rs +++ b/engine/pill_renderer/src/pass_overlay_logo.rs @@ -18,7 +18,12 @@ pub struct PassOverlayLogo { } impl PassOverlayLogo { - pub fn new(label: &str, tex_logo: RendererTextureHandle) -> Self { + pub fn new( + label: &str, + rect: [f32; 4], + tint: [f32; 4], + tex_logo: RendererTextureHandle, + ) -> Self { Self { label: label.to_string(), tex_logo, @@ -26,8 +31,8 @@ impl PassOverlayLogo { pipeline: None, bind_group_rect: None, bind_group_material: None, - rect: [0.25, 0.25, 0.75, 0.75], - tint: [1.0, 1.0, 1.0, 1.0], + rect: rect, + tint: tint, } } } @@ -40,9 +45,6 @@ impl Pass for PassOverlayLogo { fn init(&mut self, queue: &wgpu::Queue, renderer: &Renderer) -> Result<()> { println!("Initializing pass: {}", self.label); - let h: f32 = 0.04; - self.rect = [0.98 - 3. * h, 0.02, 0.98, 0.02 + h]; // bottom right - // Create buffer for overlay rect UBO let buffer = renderer.create_buffer(BufferDesc { label: Some("overlay_logo_ubo"), diff --git a/engine/pill_renderer/src/pass_overlay_uv.rs b/engine/pill_renderer/src/pass_overlay_uv.rs index 0b96968c..39d2d89d 100644 --- a/engine/pill_renderer/src/pass_overlay_uv.rs +++ b/engine/pill_renderer/src/pass_overlay_uv.rs @@ -13,13 +13,13 @@ pub struct PassOverlayUV { } impl PassOverlayUV { - pub fn new(label: &str) -> Self { + pub fn new(label: &str, rect: [f32; 4]) -> Self { Self { label: label.to_string(), buffer: None, pipeline: None, bind_group: None, - rect: [0.75, 0.75, 0.95, 0.95], + rect: rect, } } } diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index c16c6603..7325acf3 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -3,6 +3,7 @@ // https://github.com/emilk/egui/discussions/3067 use crate::{ + pass_overlay_depth::PassOverlayDepth, pass_overlay_logo::PassOverlayLogo, pass_overlay_uv::PassOverlayUV, renderer_resource_storage::RendererResourceStorage, @@ -13,6 +14,7 @@ use crate::{ use std::cell::RefCell; use std::rc::Rc; +use std::sync::Arc; use pill_engine::internal::{ get_renderer_resource_handle_from_camera_component, BufferDesc, CameraComponent, @@ -25,7 +27,7 @@ use pill_engine::internal::{ use pill_core::{PillSlotMapKey, PillSlotMapKeyData, PillStyle, RendererError, Timer}; -use std::{num::NonZeroU32, sync::Arc}; +use std::num::NonZeroU32; use cgmath::{Deg, InnerSpace}; use naga::back::wgsl; @@ -110,214 +112,6 @@ fn create_render_target( sampler, } } -// Overlay quad resources grouped for clarity -struct OverlayResources { - pipeline: wgpu::RenderPipeline, - rect_bind_group_layout: wgpu::BindGroupLayout, - rect_bind_group: wgpu::BindGroup, - rect_buffer: wgpu::Buffer, -} -// Overlay quad resources grouped for clarity -struct TextureOverlayResources { - pipeline: wgpu::RenderPipeline, - rect_bind_group_layout: wgpu::BindGroupLayout, - rect_bind_group: wgpu::BindGroup, - rect_buffer: wgpu::Buffer, - // material for textured overlay - material_bind_group_layout: wgpu::BindGroupLayout, - material_bind_group: wgpu::BindGroup, - material_tint_buffer: wgpu::Buffer, -} - -// KISS helper: build overlay pipeline, UBO layout/buffer, and bind group -fn create_overlay_depth( - device: &wgpu::Device, - queue: &wgpu::Queue, - surface_format: wgpu::TextureFormat, - depth_rt: &RendererTexture, - rect: [f32; 4], -) -> TextureOverlayResources { - let overlay_vs_wgsl = r#" -@group(1) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] - -struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; -@vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { - var unit = array, 6>( - vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), - vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) - ); - let p = unit[vi]; // [-1,1] NDC space - let s = p*0.5+0.5; // [0,1] screen space - let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] - let ndc = r*2.0-1.0; // [-1,1] NDC space - var out: VSOut; - out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); - out.uv = vec2(s.x, 1.0 - s.y); - return out; - } - "#; - - let overlay_fs_wgsl = r#" -@group(0) @binding(0) var tex: texture_depth_2d; -@group(0) @binding(1) var UTint: vec4; -@fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { - let dims = textureDimensions(tex, 0u); - let coord = vec2(uv * vec2(dims)); - let d = textureLoad(tex, coord, 0); - let vis = fract(100.0*d); - return vec4(vis, vis, vis, 1.0) * UTint; -} -"#; - - let overlay_vs = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("overlay_vs"), - source: wgpu::ShaderSource::Wgsl(overlay_vs_wgsl.into()), - }); - let overlay_fs = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("overlay_fs"), - source: wgpu::ShaderSource::Wgsl(overlay_fs_wgsl.into()), - }); - - // Group 0: material (texture + tint) - let overlay_material_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("overlay_logo_material_bgl"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Depth, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), - }, - count: None, - }, - ], - }); - // Group 1: rect UBO - let overlay_rect_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("overlay_rect_bgl"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), - }, - count: None, - }], - }); - let overlay_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("overlay_pl"), - bind_group_layouts: &[ - &overlay_material_bind_group_layout, - &overlay_rect_bind_group_layout, - ], - push_constant_ranges: &[], - }); - - // No vertex buffer layout needed; vertices are generated procedurally in the vertex shader - let overlay_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("overlay_pipeline"), - layout: Some(&overlay_pipeline_layout), - vertex: wgpu::VertexState { - module: &overlay_vs, - entry_point: "main", - buffers: &[], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &overlay_fs, - entry_point: "main", - targets: &[Some(wgpu::ColorTargetState { - format: surface_format, - blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState { - cull_mode: None, - ..wgpu::PrimitiveState::default() - }, - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview: None, - }); - - // Apple Metal constant buffer alignment (256 bytes) - let overlay_rect_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("overlay_rect_ubo"), - size: 256, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - queue.write_buffer(&overlay_rect_buffer, 0, bytemuck::bytes_of(&rect)); - - let overlay_rect_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_rect_bg"), - layout: &overlay_rect_bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &overlay_rect_buffer, - offset: 0, - size: Some(std::num::NonZeroU64::new(16).unwrap()), - }), - }], - }); - - let tint_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("overlay_tint"), - size: 256, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - let tint: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; - queue.write_buffer(&tint_buffer, 0, bytemuck::bytes_of(&tint)); - - let overlay_material_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_material_bg"), - layout: &overlay_material_bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&depth_rt.texture_view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &tint_buffer, - offset: 0, - size: Some(std::num::NonZeroU64::new(16).unwrap()), - }), - }, - ], - }); - - TextureOverlayResources { - pipeline: overlay_pipeline, - rect_bind_group_layout: overlay_rect_bind_group_layout, - rect_bind_group: overlay_rect_bind_group, - rect_buffer: overlay_rect_buffer, - material_bind_group_layout: overlay_material_bind_group_layout, - material_bind_group: overlay_material_bind_group, - material_tint_buffer: tint_buffer, - } -} pub trait Pass { fn get_label(&self) -> &str; @@ -351,7 +145,7 @@ pub struct State { renderer_resource_storage: Rc>, color_format: wgpu::TextureFormat, depth_format: wgpu::TextureFormat, - depth_texture: RendererTexture, + depth_texture: Arc, offscreen_color_texture: RendererTexture, // Per-frame dynamic UBO ring (Milestone 5) per_draw_stride: u64, @@ -366,11 +160,7 @@ pub struct State { composite_bind_group_layout: wgpu::BindGroupLayout, composite_pipeline: wgpu::RenderPipeline, composite_bind_group: wgpu::BindGroup, - - // These look like could be combined into OverlayRenderer, but also good case for user submiter pass API - // overlay_uv: OverlayResources, // Overlay (UV gradient quad in top-right) - // overlay_logo: OverlayLogoResources, // Overlay (Logo in bot-right) - overlay_depth: TextureOverlayResources, // Overlay (Depth in top-right) + // resources tex_logo: RendererTextureHandle, } @@ -612,6 +402,23 @@ impl PillRenderer for Renderer { ) .unwrap(); + // Create state + let per_draw_bind_group_layout = + ctx.device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("per_draw_bind_group_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: Some(std::num::NonZeroU64::new(144).unwrap()), + }, + count: None, + }], + }); + // Milestone 6: Composition pipeline (fullscreen triangle sampling offscreen texture) let comp_vs_wgsl = r#" struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; @@ -748,42 +555,6 @@ impl PillRenderer for Renderer { .expect("failed to create overlay logo texture"); renderer_resource_storage.textures.insert(tex) }; - // Overlay logo pipeline (small quad) - // res: 1024 × 320, 1.0 x 0.3 - // let h: f32 = 0.04; - // let overlay_logo = create_overlay_logo( - // &ctx.device, - // &ctx.queue, - // ctx.surface_configuration.format, - // tex_logo, - // &renderer_resource_storage, - // [0.98 - 3. * h, 0.02, 0.98, 0.02 + h], // bottom right - // ); - - let overlay_depth = create_overlay_depth( - &ctx.device, - &ctx.queue, - ctx.surface_configuration.format, - &depth_texture, - [0.75, 0.55, 0.95, 0.72], // top right, 2 row - ); - - // Create state - let per_draw_bind_group_layout = - ctx.device - .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("per_draw_bind_group_layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: true, - min_binding_size: Some(std::num::NonZeroU64::new(144).unwrap()), - }, - count: None, - }], - }); State { passes: vec![], @@ -794,7 +565,7 @@ impl PillRenderer for Renderer { // Renderer variables color_format, depth_format, - depth_texture, + depth_texture: Arc::new(depth_texture), offscreen_color_texture, // Per-frame UBO ring init per_draw_stride: 256, @@ -810,9 +581,6 @@ impl PillRenderer for Renderer { composite_pipeline, composite_bind_group, // Overlays - // overlay_uv, - // overlay_logo, - overlay_depth, tex_logo, } }; @@ -821,15 +589,28 @@ impl PillRenderer for Renderer { } fn init(&mut self) -> Result<()> { - self.state - .passes - .push(Box::new(PassOverlayUV::new("overlay_uv"))); + self.state.passes.clear(); + + self.state.passes.push(Box::new(PassOverlayUV::new( + "overlay_uv", + [0.75, 0.75, 0.95, 0.95], + ))); + let h: f32 = 0.04; // Logo height self.state.passes.push(Box::new(PassOverlayLogo::new( "overlay_logo", + [0.98 - 3. * h, 0.02, 0.98, 0.02 + h], // bottom right + [1.0, 1.0, 1.0, 1.0], self.state.tex_logo, ))); + self.state.passes.push(Box::new(PassOverlayDepth::new( + "overlay_depth", + [0.75, 0.50, 0.95, 0.70], + [1.0, 1.0, 1.0, 1.0], + self.state.depth_texture.clone(), + ))); + // Initialize passes - call init on each pass let queue = &self.ctx.queue; // Temporarily move passes out to avoid borrowing conflicts @@ -1195,12 +976,20 @@ impl PillRenderer for Renderer { self.ctx .surface .configure(&self.ctx.device, &self.ctx.surface_configuration); - self.state.depth_texture = RendererTexture::new_depth_texture( - &self.ctx.device, - &self.ctx.surface_configuration, - "depth_texture", - ) - .unwrap(); + self.state.depth_texture = Arc::new( + RendererTexture::new_depth_texture( + &self.ctx.device, + &self.ctx.surface_configuration, + "depth_texture", + ) + .unwrap(), + ); + + // ================================ + // Clear and re-initialize resizable passes + let _ = self.init(); + + // ================================ // Recreate offscreen color target and its bind group self.state.offscreen_color_texture = create_render_target( &self.ctx.device, @@ -1230,30 +1019,6 @@ impl PillRenderer for Renderer { }, ], }); - // Rebind overlay depth material to the new depth texture view - self.state.overlay_depth.material_bind_group = - self.ctx - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_depth_material_bg"), - layout: &self.state.overlay_depth.material_bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView( - &self.state.depth_texture.texture_view, - ), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &self.state.overlay_depth.material_tint_buffer, - offset: 0, - size: Some(std::num::NonZeroU64::new(16).unwrap()), - }), - }, - ], - }); // Old offscreen texture/view/sampler are dropped when replaced; wgpu defers actual GPU resource // destruction until safe. See optional early reclamation via device.poll: // https://docs.rs/wgpu/latest/wgpu/struct.Device.html#method.poll @@ -1713,69 +1478,6 @@ impl PillRenderer for Renderer { } timer.end_context()?; // End "Compose pass" - // Overlay passes - { - timer.begin_context("Overlay passes"); - let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("m6_overlay_passes"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - // Draw small overlay UV quad in normalized rect via UBO - // rpass.set_pipeline(&self.state.overlay_uv.pipeline); - // rpass.set_bind_group(0, &self.state.overlay_uv.rect_bind_group, &[]); - // rpass.draw(0..6, 0..1); - - // Draw depth overlay (bind material at group 0 and rect at group 1) - // Rebind overlay depth material to the new depth texture view - self.state.overlay_depth.material_bind_group = - self.ctx - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_depth_material_bg"), - layout: &self.state.overlay_depth.material_bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView( - &self.state.depth_texture.texture_view, - ), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &self.state.overlay_depth.material_tint_buffer, - offset: 0, - size: Some(std::num::NonZeroU64::new(16).unwrap()), - }), - }, - ], - }); - rpass.set_pipeline(&self.state.overlay_depth.pipeline); - rpass.set_bind_group(0, &self.state.overlay_depth.material_bind_group, &[]); - rpass.set_bind_group(1, &self.state.overlay_depth.rect_bind_group, &[]); - rpass.draw(0..6, 0..1); - - // Draw logo overlay (bind material at group 0 and rect at group 1) - // rpass.set_pipeline(&self.state.overlay_logo.pipeline); - // rpass.set_bind_group(0, &self.state.overlay_logo.material_bind_group, &[]); - // rpass.set_bind_group(1, &self.state.overlay_logo.rect_bind_group, &[]); - // rpass.draw(0..6, 0..1); - - timer.record("Encode overlay passes"); - } - timer.end_context()?; // End "Compose pass" - self.ctx.queue.submit([encoder.finish()]); timer.record("Submit scene+compose passes"); @@ -1866,101 +1568,3 @@ impl Renderer { &self.state.renderer_resource_storage } } - -fn create_overlay_uv_pass_inline( - ctx: &DeviceContext, -) -> ([f64; 4], wgpu::BindGroupLayout, wgpu::RenderPipeline) { - let rect = [0.75, 0.75, 0.95, 0.95]; - let overlay_vs_wgsl = r#" - @group(0) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] - - struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; - @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { - var unit = array, 6>( - vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), - vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) - ); - let p = unit[vi]; // [-1,1] NDC space - let s = p*0.5+0.5; // [0,1] screen space - let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] - let ndc = r*2.0-1.0; // [-1,1] NDC space - var out: VSOut; - out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); - out.uv = s; - return out; - } - "#; - let overlay_fs_wgsl = r#" - @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { - return vec4(uv, 0.0, 0.5); - } - "#; - - let overlay_vs = ctx - .device - .create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("overlay_vs"), - source: wgpu::ShaderSource::Wgsl(overlay_vs_wgsl.into()), - }); - let overlay_fs = ctx - .device - .create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("overlay_fs"), - source: wgpu::ShaderSource::Wgsl(overlay_fs_wgsl.into()), - }); - - let overlay_rect_bind_group_layout = - ctx.device - .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("overlay_rect_bgl"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), - }, - count: None, - }], - }); - let overlay_pipeline_layout = - ctx.device - .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("overlay_pl"), - bind_group_layouts: &[&overlay_rect_bind_group_layout], - push_constant_ranges: &[], - }); - - // No vertex buffer layout needed; vertices are generated procedurally in the vertex shader - let overlay_pipeline = ctx - .device - .create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("overlay_pipeline"), - layout: Some(&overlay_pipeline_layout), - vertex: wgpu::VertexState { - module: &overlay_vs, - entry_point: "main", - buffers: &[], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &overlay_fs, - entry_point: "main", - targets: &[Some(wgpu::ColorTargetState { - format: ctx.surface_configuration.format, - blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState { - cull_mode: None, - ..wgpu::PrimitiveState::default() - }, - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview: None, - }); - (rect, overlay_rect_bind_group_layout, overlay_pipeline) -} diff --git a/engine/pill_renderer/src/resources/renderer_texture.rs b/engine/pill_renderer/src/resources/renderer_texture.rs index 6a731fd5..6341f095 100644 --- a/engine/pill_renderer/src/resources/renderer_texture.rs +++ b/engine/pill_renderer/src/resources/renderer_texture.rs @@ -1,12 +1,9 @@ +use anyhow::Result; +use image::{DynamicImage, GenericImageView}; use pill_engine::internal::TextureType; +use wgpu::{Device, Queue, SurfaceConfiguration, TextureFormat}; -use anyhow::*; -use image::GenericImageView; -use std::{ num::NonZeroU32 }; - -// --- Texture --- - -pill_core::define_new_pill_slotmap_key! { +pill_core::define_new_pill_slotmap_key! { pub struct RendererTextureHandle; } @@ -24,43 +21,33 @@ impl RendererTexture { queue: &wgpu::Queue, name: Option<&str>, image_data: &image::DynamicImage, - texture_type: TextureType, + _texture_type: TextureType, ) -> Result { let dimensions = image_data.dimensions(); let rgba = image_data.to_rgba8(); - - // Get size let size = wgpu::Extent3d { width: dimensions.0, height: dimensions.1, depth_or_array_layers: 1, }; - // Specify texture format - let format = match texture_type { - TextureType::Color => wgpu::TextureFormat::Rgba8UnormSrgb, - TextureType::Normal => wgpu::TextureFormat::Rgba8Unorm, - }; - - // Create texture let texture = device.create_texture(&wgpu::TextureDescriptor { label: name, size, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format, + format: wgpu::TextureFormat::Rgba8UnormSrgb, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, view_formats: &[], }); - // Write data to texture queue.write_texture( wgpu::ImageCopyTexture { - aspect: wgpu::TextureAspect::All, texture: &texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, }, &rgba, wgpu::ImageDataLayout { @@ -71,81 +58,103 @@ impl RendererTexture { size, ); - // Create texture view let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - - // Create sampler let sampler = device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::Repeat, - address_mode_v: wgpu::AddressMode::Repeat, - address_mode_w: wgpu::AddressMode::Repeat, + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Nearest, mipmap_filter: wgpu::FilterMode::Nearest, - lod_min_clamp: 0.0, // Fix: Must be 0.0 or greater - lod_max_clamp: 100.0, // You can set this based on your texture's mipmap levels ..Default::default() }); - // Create final texture - let texture = Self { + Ok(Self { texture, texture_view, sampler, - }; - - Ok(texture) + }) } pub fn new_depth_texture( device: &wgpu::Device, - surface_configuration: &wgpu::SurfaceConfiguration, - label: &str, + surface_config: &SurfaceConfiguration, + name: &str, ) -> Result { - - // Get size - let size = wgpu::Extent3d { // Depth texture needs to be the same size as window - width: surface_configuration.width, - height: surface_configuration.height, + let size = wgpu::Extent3d { + width: surface_config.width, + height: surface_config.height, depth_or_array_layers: 1, }; - // Create texture let texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some(label), + label: Some(name), size, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: Self::DEPTH_FORMAT, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, // Rendering to this texture so RENDER_ATTACHMENT flag is needed + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, view_formats: &[], }); - // Create texture view let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - - // Create sampler let sampler = device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::Repeat, - address_mode_v: wgpu::AddressMode::Repeat, - address_mode_w: wgpu::AddressMode::Repeat, + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Linear, mipmap_filter: wgpu::FilterMode::Nearest, compare: Some(wgpu::CompareFunction::LessEqual), - lod_min_clamp: 0.0, - lod_max_clamp: 100.0, ..Default::default() }); - // Create final texture - let texture = Self { + Ok(Self { texture, texture_view, sampler, + }) + } + + pub fn new_render_target( + device: &wgpu::Device, + surface_config: &SurfaceConfiguration, + format: TextureFormat, + name: &str, + ) -> Result { + let size = wgpu::Extent3d { + width: surface_config.width, + height: surface_config.height, + depth_or_array_layers: 1, }; - Ok(texture) + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some(name), + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + + let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + + Ok(Self { + texture, + texture_view, + sampler, + }) } -} \ No newline at end of file +} From 676cd3d38cf788507be118fad36373220d99381a Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Mon, 20 Oct 2025 00:48:37 +0200 Subject: [PATCH 45/69] feat: move compose to a pass --- engine/pill_renderer/src/lib.rs | 2 + engine/pill_renderer/src/pass_compose.rs | 162 +++++++++++++++++++ engine/pill_renderer/src/renderer.rs | 198 ++--------------------- 3 files changed, 179 insertions(+), 183 deletions(-) create mode 100644 engine/pill_renderer/src/pass_compose.rs diff --git a/engine/pill_renderer/src/lib.rs b/engine/pill_renderer/src/lib.rs index fb99ea1b..1abdbfb2 100644 --- a/engine/pill_renderer/src/lib.rs +++ b/engine/pill_renderer/src/lib.rs @@ -3,6 +3,7 @@ pub mod egui; pub mod instance; pub mod mesh_drawer; +pub mod pass_compose; pub mod pass_overlay_depth; pub mod pass_overlay_logo; pub mod pass_overlay_uv; @@ -15,6 +16,7 @@ pub mod resources; pub use renderer::*; pub use instance::Instance; +pub use pass_compose::PassCompose; pub use pass_overlay_uv::PassOverlayUV; pub use renderer_resource_storage::RendererResourceStorage; diff --git a/engine/pill_renderer/src/pass_compose.rs b/engine/pill_renderer/src/pass_compose.rs new file mode 100644 index 00000000..36b483b7 --- /dev/null +++ b/engine/pill_renderer/src/pass_compose.rs @@ -0,0 +1,162 @@ +use crate::renderer::{Pass, Renderer}; +use crate::resources::RendererTexture; +use anyhow::Result; +use pill_engine::internal::{PillRenderer, PipelineV2, PipelineV2Desc, ShaderDesc}; +use std::sync::Arc; +use wgpu::CommandEncoder; +use wgpu::Queue; + +pub struct PassCompose { + label: String, + offscreen_texture: Arc, + pipeline: Option, + bind_group: Option, +} + +impl PassCompose { + pub fn new(label: &str, offscreen_texture: Arc) -> Self { + Self { + label: label.to_string(), + offscreen_texture, + pipeline: None, + bind_group: None, + } + } +} + +impl Pass for PassCompose { + fn get_label(&self) -> &str { + &self.label + } + + fn init(&mut self, _queue: &wgpu::Queue, renderer: &Renderer) -> Result<()> { + println!("Initializing pass: {}", self.label); + + let vs = r#" + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var pos = array, 3>( + vec2(-1.0, -3.0), + vec2( 3.0, 1.0), + vec2(-1.0, 1.0) + ); + var tuv = array, 3>( + vec2(0.0, 2.0), + vec2(2.0, 0.0), + vec2(0.0, 0.0) + ); + var o: VSOut; + o.pos = vec4(pos[vi], 0.0, 1.0); + o.uv = tuv[vi]; + return o; + } + "#; + + let fs = r#" + @group(0) @binding(0) var t_src: texture_2d; + @group(0) @binding(1) var s_src: sampler; + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let hdr = textureSample(t_src, s_src, uv).rgb; + // Reinhard tone mapping; output remains in linear space. The sRGB swapchain will encode. + let ldr = hdr / (1.0 + hdr); + return vec4(ldr, 1.0); + } + "#; + + let pipeline = renderer.create_pipeline_v2(PipelineV2Desc { + label: Some("compose"), + vs: ShaderDesc { + source: vs, + entry_func: "main", + }, + ps: ShaderDesc { + source: fs, + entry_func: "main", + }, + bind_groups: vec![ + // Group 0: Texture and sampler bindings + vec![ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + ], + targets: &[Some(wgpu::ColorTargetState { + format: renderer.get_surface_format(), + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + })?; + + let bind_group = renderer + .get_device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("compose_bind_group"), + layout: &pipeline.bind_group_layouts[0], // Group 0: Texture and sampler bindings + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView( + &self.offscreen_texture.texture_view, + ), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&self.offscreen_texture.sampler), + }, + ], + }); + + // Store pipeline handle + self.pipeline = Some(pipeline); + self.bind_group = Some(bind_group); + + Ok(()) + } + + fn draw( + &self, + encoder: &mut CommandEncoder, + _renderer: &Renderer, + _frame: &wgpu::SurfaceTexture, + view: &wgpu::TextureView, + ) -> Result<()> { + // Create render pass for this pass using the provided frame + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.label), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + // Draw fullscreen triangle for composition + pass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); + pass.set_bind_group(0, &self.bind_group.as_ref().unwrap(), &[]); + pass.draw(0..3, 0..1); + + Ok(()) + } +} diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 7325acf3..80e6c6ff 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -3,6 +3,7 @@ // https://github.com/emilk/egui/discussions/3067 use crate::{ + pass_compose::PassCompose, pass_overlay_depth::PassOverlayDepth, pass_overlay_logo::PassOverlayLogo, pass_overlay_uv::PassOverlayUV, @@ -34,7 +35,7 @@ use naga::back::wgsl; use naga::front::glsl; use anyhow::{Error, Result}; -use log::info; +use log::{error, info}; use crate::egui::EguiRenderer; use image::GenericImageView; @@ -146,7 +147,7 @@ pub struct State { color_format: wgpu::TextureFormat, depth_format: wgpu::TextureFormat, depth_texture: Arc, - offscreen_color_texture: RendererTexture, + offscreen_color_texture: Arc, // Per-frame dynamic UBO ring (Milestone 5) per_draw_stride: u64, per_draw_capacity: u64, // in elements @@ -156,10 +157,6 @@ pub struct State { // Prebuilt PSO (static pipeline) // [SIMILAR] Prebuilt once; no per-draw pipeline churn per TALK default_pipeline: RendererPipeline, - // Composition (Milestone 6) - composite_bind_group_layout: wgpu::BindGroupLayout, - composite_pipeline: wgpu::RenderPipeline, - composite_bind_group: wgpu::BindGroup, // resources tex_logo: RendererTextureHandle, } @@ -315,13 +312,13 @@ impl PillRenderer for Renderer { let depth_format = wgpu::TextureFormat::Depth32Float; // Milestone 6: Create offscreen color target (RENDER_ATTACHMENT | TEXTURE_BINDING) - let offscreen_color_texture = create_render_target( + let offscreen_color_texture = Arc::new(create_render_target( &ctx.device, color_format, ctx.surface_configuration.width, ctx.surface_configuration.height, "offscreen_color", - ); + )); let egui_renderer = EguiRenderer::new( &ctx.device, @@ -419,124 +416,6 @@ impl PillRenderer for Renderer { }], }); - // Milestone 6: Composition pipeline (fullscreen triangle sampling offscreen texture) - let comp_vs_wgsl = r#" - struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; - @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { - var pos = array, 3>( - vec2(-1.0, -3.0), - vec2( 3.0, 1.0), - vec2(-1.0, 1.0) - ); - var tuv = array, 3>( - vec2(0.0, 2.0), - vec2(2.0, 0.0), - vec2(0.0, 0.0) - ); - var o: VSOut; - o.pos = vec4(pos[vi], 0.0, 1.0); - o.uv = tuv[vi]; - return o; - } - "#; - let comp_fs_wgsl = r#" - @group(0) @binding(0) var t_src: texture_2d; - @group(0) @binding(1) var s_src: sampler; - @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { - let hdr = textureSample(t_src, s_src, uv).rgb; - // Reinhard tone mapping; output remains in linear space. The sRGB swapchain will encode. - let ldr = hdr / (1.0 + hdr); - return vec4(ldr, 1.0); - } - "#; - let comp_vs = ctx - .device - .create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("compose_vs"), - source: wgpu::ShaderSource::Wgsl(comp_vs_wgsl.into()), - }); - let comp_fs = ctx - .device - .create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("compose_fs"), - source: wgpu::ShaderSource::Wgsl(comp_fs_wgsl.into()), - }); - let composite_bind_group_layout = - ctx.device - .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("compose_bgl"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { - filterable: true, - }, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], - }); - let comp_pipeline_layout = - ctx.device - .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("compose_pl"), - bind_group_layouts: &[&composite_bind_group_layout], - push_constant_ranges: &[], - }); - let composite_pipeline = - ctx.device - .create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("compose_pipeline"), - layout: Some(&comp_pipeline_layout), - vertex: wgpu::VertexState { - module: &comp_vs, - entry_point: "main", - buffers: &[], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &comp_fs, - entry_point: "main", - targets: &[Some(wgpu::ColorTargetState { - // Composition writes to the swapchain view; target must match surface format - format: ctx.surface_configuration.format, - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState::default(), - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview: None, - }); - let composite_bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("compose_bg"), - layout: &composite_bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView( - &offscreen_color_texture.texture_view, - ), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&offscreen_color_texture.sampler), - }, - ], - }); - // Ideally Client submitted pass via API // Create logo texture via renderer API and pass handle into overlay factory let logo_image = image::open( @@ -576,10 +455,6 @@ impl PillRenderer for Renderer { per_draw_bind_group: None, // Prebuilt PSO default_pipeline, - // Composition - composite_bind_group_layout, - composite_pipeline, - composite_bind_group, // Overlays tex_logo, } @@ -591,6 +466,11 @@ impl PillRenderer for Renderer { fn init(&mut self) -> Result<()> { self.state.passes.clear(); + self.state.passes.push(Box::new(PassCompose::new( + "compose", + self.state.offscreen_color_texture.clone(), + ))); + self.state.passes.push(Box::new(PassOverlayUV::new( "overlay_uv", [0.75, 0.75, 0.95, 0.95], @@ -986,39 +866,16 @@ impl PillRenderer for Renderer { ); // ================================ - // Clear and re-initialize resizable passes - let _ = self.init(); - - // ================================ - // Recreate offscreen color target and its bind group - self.state.offscreen_color_texture = create_render_target( + // Recreate offscreen color target and reinitialize passes + self.state.offscreen_color_texture = Arc::new(create_render_target( &self.ctx.device, self.state.color_format, self.ctx.surface_configuration.width, self.ctx.surface_configuration.height, "offscreen_color", - ); - self.state.composite_bind_group = - self.ctx - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("compose_bg"), - layout: &self.state.composite_bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView( - &self.state.offscreen_color_texture.texture_view, - ), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler( - &self.state.offscreen_color_texture.sampler, - ), - }, - ], - }); + )); + // Reinitialize all passes (including compose pass) + let _ = self.init(); // Old offscreen texture/view/sampler are dropped when replaced; wgpu defers actual GPU resource // destruction until safe. See optional early reclamation via device.poll: // https://docs.rs/wgpu/latest/wgpu/struct.Device.html#method.poll @@ -1453,31 +1310,6 @@ impl PillRenderer for Renderer { } timer.end_context()?; // End "Offscreen pass" - // Composition pass: sample T0 and draw to swapchain view, then overlay UV quad - { - timer.begin_context("Fullscreen resolve pass"); - let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("m6_compose_pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - rpass.set_pipeline(&self.state.composite_pipeline); - rpass.set_bind_group(0, &self.state.composite_bind_group, &[]); - rpass.draw(0..3, 0..1); - - timer.record("Encode compose pass"); - } - timer.end_context()?; // End "Compose pass" - self.ctx.queue.submit([encoder.finish()]); timer.record("Submit scene+compose passes"); From 24a8cd51bc77dfd0dbd4c48a13cb1de5a5ff86da Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Mon, 20 Oct 2025 01:26:12 +0200 Subject: [PATCH 46/69] fix: remove model matrix, per draw 144 -> 80 bytes --- engine/pill_renderer/src/renderer.rs | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 80e6c6ff..dc6d04dd 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -335,7 +335,6 @@ impl PillRenderer for Renderer { struct PerDraw { mvp: mat4x4, - model: mat4x4, tint: vec4, }; @group(3) @binding(0) var UPerDraw: PerDraw; @@ -364,7 +363,6 @@ impl PillRenderer for Renderer { // Per-draw (set 3): supports per-entity tint for M4 struct PerDraw { mvp: mat4x4, - model: mat4x4, tint: vec4, }; @group(3) @binding(0) var UPerDraw: PerDraw; @@ -410,7 +408,7 @@ impl PillRenderer for Renderer { ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: true, - min_binding_size: Some(std::num::NonZeroU64::new(144).unwrap()), + min_binding_size: Some(std::num::NonZeroU64::new(80).unwrap()), }, count: None, }], @@ -447,7 +445,7 @@ impl PillRenderer for Renderer { depth_texture: Arc::new(depth_texture), offscreen_color_texture, // Per-frame UBO ring init - per_draw_stride: 256, + per_draw_stride: 256, // 80 bytes + 256 byte alignment requirement per_draw_capacity: 0, per_draw_buffer: None, // [SIMILAR] Per-draw dynamic UBO layout with has_dynamic_offset=true per TALK (Milestone 5) @@ -1192,13 +1190,11 @@ impl PillRenderer for Renderer { #[repr(C)] #[derive(Copy, Clone)] struct PerDrawStd140 { - mvp: [[f32; 4]; 4], - model: [[f32; 4]; 4], - tint: [f32; 4], + mvp: [[f32; 4]; 4], // 4x4 MVP matrix (16 floats) + tint: [f32; 4], // RGBA color (4 floats) } unsafe impl bytemuck::Zeroable for PerDrawStd140 {} unsafe impl bytemuck::Pod for PerDrawStd140 {} - let model: [[f32; 4]; 4] = cgmath::Matrix4::::from_scale(1.0).into(); let tint: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; let mut staging: Vec = Vec::with_capacity((self.state.per_draw_stride * needed) as usize); @@ -1207,11 +1203,7 @@ impl PillRenderer for Renderer { for b in g.batches.iter_mut() { b.base_offset_u32 = next_offset_u32; for mvp in &b.instances { - let pd = PerDrawStd140 { - mvp: *mvp, - model, - tint, - }; + let pd = PerDrawStd140 { mvp: *mvp, tint }; staging.extend_from_slice(bytemuck::bytes_of(&pd)); let pad = (self.state.per_draw_stride as usize) - std::mem::size_of::(); From 325566f7c3c9cd5f7fb849ff15df2ff3b337ebe5 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Mon, 20 Oct 2025 04:20:40 +0200 Subject: [PATCH 47/69] feat: preallocate render() containers --- engine/pill_renderer/src/renderer.rs | 130 +++++++++++++++++---------- 1 file changed, 84 insertions(+), 46 deletions(-) diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index dc6d04dd..57d51903 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -139,6 +139,27 @@ pub struct DeviceContext { surface_configuration: wgpu::SurfaceConfiguration, } +// Preallocated buffer structs for hot path optimization +struct VisiblePreDraw { + pipeline_handle: RendererPipelineHandle, + material_handle: RendererMaterialHandle, + mesh_handle: RendererMeshHandle, + entity_index: u32, + mvp: [[f32; 4]; 4], +} + +struct MeshBatch { + mesh_handle: RendererMeshHandle, + instances: Vec<[[f32; 4]; 4]>, + base_offset_u32: u32, // offset into per-draw ring for first instance +} + +struct GroupCmd { + pipeline_handle: RendererPipelineHandle, + material_handle: RendererMaterialHandle, + batches: Vec, +} + pub struct State { passes: Vec>, egui_renderer: crate::egui::EguiRenderer, // TODO: Separate system adding Pass @@ -159,6 +180,10 @@ pub struct State { default_pipeline: RendererPipeline, // resources tex_logo: RendererTextureHandle, + // Preallocated buffers for hot path optimization + visible_pre_draw_buffer: Vec, + groups_buffer: Vec, + staging_buffer: Vec, } pub struct Renderer { @@ -455,6 +480,10 @@ impl PillRenderer for Renderer { default_pipeline, // Overlays tex_logo, + // Preallocated buffers for hot path optimization (60k instances capacity) + visible_pre_draw_buffer: Vec::with_capacity(100_000), + groups_buffer: Vec::with_capacity(2000), + staging_buffer: Vec::with_capacity(100_000 * 144), // 60k instances * 144 bytes per instance } }; @@ -897,19 +926,21 @@ impl PillRenderer for Renderer { timer.record("Frame: acquire"); // Get frame or return mapped error if failed - let frame = self.ctx.surface.get_current_texture(); - - let frame = match frame { - std::result::Result::Ok(frame) => frame, - std::result::Result::Err(error) => match error { - wgpu::SurfaceError::Lost => return Err(RendererError::SurfaceLost.into()), - wgpu::SurfaceError::OutOfMemory => { - return Err(RendererError::SurfaceOutOfMemory.into()) - } - _ => return Err(RendererError::SurfaceOther.into()), - }, - }; + // ALLOCATION: Surface texture allocation (GPU memory) - ~4MB for 1920x1080 RGBA8 + let frame = self.ctx.surface.get_current_texture().unwrap(); + + // let frame = match frame { + // std::result::Result::Ok(frame) => frame, + // std::result::Result::Err(error) => match error { + // wgpu::SurfaceError::Lost => return Err(RendererError::SurfaceLost.into()), + // wgpu::SurfaceError::OutOfMemory => { + // return Err(RendererError::SurfaceOutOfMemory.into()) + // } + // _ => return Err(RendererError::SurfaceOther.into()), + // }, + // }; + // ALLOCATION: TextureView creation (GPU memory) - ~64 bytes metadata let view: wgpu::TextureView = frame .texture .create_view(&wgpu::TextureViewDescriptor::default()); @@ -953,6 +984,7 @@ impl PillRenderer for Renderer { let clear_color = active_camera_component.clear_color; // Record inline draw pass (M2) + // ALLOCATION: CommandEncoder creation (CPU memory) - ~1KB command buffer let mut encoder = self .ctx .device @@ -986,15 +1018,9 @@ impl PillRenderer for Renderer { make_plane(row(3) - row(2)), // far ]; - struct VisiblePreDraw { - pipeline_handle: RendererPipelineHandle, - material_handle: RendererMaterialHandle, - mesh_handle: RendererMeshHandle, - entity_index: u32, - mvp: [[f32; 4]; 4], - } timer.record("Culling & MVP build"); - let mut visible: Vec = Vec::with_capacity(render_queue.len()); + // REUSE PREALLOCATED BUFFER: Clear and reuse preallocated buffer (no allocation in hot path) + self.state.visible_pre_draw_buffer.clear(); for render_queue_item in render_queue.iter() { let key = pill_engine::internal::decompose_render_queue_key(render_queue_item.key); @@ -1083,7 +1109,8 @@ impl PillRenderer for Renderer { let view_proj: cgmath::Matrix4 = renderer_camera.uniform.view_projection_matrix.into(); let mvp: [[f32; 4]; 4] = (view_proj * model).into(); - visible.push(VisiblePreDraw { + // REUSE PREALLOCATED BUFFER: Push to preallocated buffer (no allocation in hot path) + self.state.visible_pre_draw_buffer.push(VisiblePreDraw { pipeline_handle, material_handle, mesh_handle, @@ -1095,7 +1122,7 @@ impl PillRenderer for Renderer { // [SIMILAR] Sort by pipeline/material/mesh to minimize state changes as in TALK // [API->CLIENT] Sorting/grouping is client responsibility; low-level should accept an already ordered draw stream // Sort by pipeline -> material -> mesh to minimize state changes - visible.sort_by_key(|v| { + self.state.visible_pre_draw_buffer.sort_by_key(|v| { ( v.pipeline_handle.data().index, v.material_handle.data().index, @@ -1105,32 +1132,25 @@ impl PillRenderer for Renderer { // Build grouped instancing batches by (pipeline, material, mesh) timer.record("Sort & group draws"); - struct MeshBatch { - mesh_handle: RendererMeshHandle, - instances: Vec<[[f32; 4]; 4]>, - base_offset_u32: u32, // offset into per-draw ring for first instance - } - struct GroupCmd { - pipeline_handle: RendererPipelineHandle, - material_handle: RendererMaterialHandle, - batches: Vec, - } - let mut groups: Vec = Vec::new(); - for v in &visible { - if groups + // REUSE PREALLOCATED BUFFER: Clear and reuse preallocated groups buffer (no allocation in hot path) + self.state.groups_buffer.clear(); + for v in &self.state.visible_pre_draw_buffer { + if self + .state + .groups_buffer .last() .map(|g| { g.pipeline_handle != v.pipeline_handle || g.material_handle != v.material_handle }) .unwrap_or(true) { - groups.push(GroupCmd { + self.state.groups_buffer.push(GroupCmd { pipeline_handle: v.pipeline_handle, material_handle: v.material_handle, batches: Vec::new(), }); } - let g = groups.last_mut().unwrap(); + let g = self.state.groups_buffer.last_mut().unwrap(); if let Some(batch) = g .batches .iter_mut() @@ -1138,6 +1158,7 @@ impl PillRenderer for Renderer { { batch.instances.push(v.mvp); } else { + // ALLOCATION: Vec<[[f32; 4]; 4]> for instances (CPU memory) - optimistic: 1 instance * 64 bytes, pessimistic: 60k instances * 64 bytes = ~3.8MB per batch g.batches.push(MeshBatch { mesh_handle: v.mesh_handle, instances: vec![v.mvp], @@ -1145,14 +1166,21 @@ impl PillRenderer for Renderer { }); } } - let draw_call_count: usize = groups.iter().map(|g| g.batches.len()).sum(); + let draw_call_count: usize = self + .state + .groups_buffer + .iter() + .map(|g| g.batches.len()) + .sum(); // Expose draw call count via per-frame timer counters for UI/metrics timer.set_counter("draw_calls", draw_call_count as u64); // Milestone 5: Per-frame ring buffer // [SIMILAR] Batch write dynamic per-draw data once; bind with dynamic offsets timer.begin_context("Per-draw UBO setup"); - let needed: u64 = groups + let needed: u64 = self + .state + .groups_buffer .iter() .map(|g| { g.batches @@ -1165,6 +1193,7 @@ impl PillRenderer for Renderer { if self.state.per_draw_capacity < needed { self.state.per_draw_capacity = needed.next_power_of_two().max(1); let size = self.state.per_draw_stride * self.state.per_draw_capacity; + // ALLOCATION: GPU buffer creation (GPU memory) - optimistic: 1KB, pessimistic: 60k instances * 144 bytes = ~8.6MB self.state.per_draw_buffer = Some(self.ctx.device.create_buffer(&wgpu::BufferDescriptor { label: Some("per_draw_dynamic_ubo_ring"), @@ -1172,6 +1201,7 @@ impl PillRenderer for Renderer { usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, })); + // ALLOCATION: BindGroup creation (GPU memory) - ~128 bytes metadata self.state.per_draw_bind_group = Some(self.ctx.device.create_bind_group( &wgpu::BindGroupDescriptor { label: Some("per_draw_bind_group"), @@ -1196,25 +1226,31 @@ impl PillRenderer for Renderer { unsafe impl bytemuck::Zeroable for PerDrawStd140 {} unsafe impl bytemuck::Pod for PerDrawStd140 {} let tint: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; - let mut staging: Vec = - Vec::with_capacity((self.state.per_draw_stride * needed) as usize); + // REUSE PREALLOCATED BUFFER: Clear and reuse preallocated staging buffer (no allocation in hot path) + self.state.staging_buffer.clear(); let mut next_offset_u32: u32 = 0; - for g in groups.iter_mut() { + for g in self.state.groups_buffer.iter_mut() { for b in g.batches.iter_mut() { b.base_offset_u32 = next_offset_u32; for mvp in &b.instances { let pd = PerDrawStd140 { mvp: *mvp, tint }; - staging.extend_from_slice(bytemuck::bytes_of(&pd)); + self.state + .staging_buffer + .extend_from_slice(bytemuck::bytes_of(&pd)); let pad = (self.state.per_draw_stride as usize) - std::mem::size_of::(); - staging.extend(std::iter::repeat(0u8).take(pad)); + self.state + .staging_buffer + .extend(std::iter::repeat(0u8).take(pad)); next_offset_u32 = next_offset_u32.wrapping_add(self.state.per_draw_stride as u32); } } } if let Some(buf) = &self.state.per_draw_buffer { - self.ctx.queue.write_buffer(buf, 0, &staging); + self.ctx + .queue + .write_buffer(buf, 0, &self.state.staging_buffer); } timer.record("Per-draw UBO write (ring)"); timer.end_context()?; @@ -1250,7 +1286,7 @@ impl PillRenderer for Renderer { }); // [SIMILAR] Bind pipeline/material once per group; change only minimal per-draw state (offsets) // Instancing: one draw per (pipeline, material, mesh) batch with instance_count - for group in &groups { + for group in &self.state.groups_buffer { timer.begin_context(&format!( "Pipeline {} / Material {}", group.pipeline_handle.data().index, @@ -1308,6 +1344,7 @@ impl PillRenderer for Renderer { { // User passes with separate encoder timer.begin_context("User passes"); + // ALLOCATION: CommandEncoder creation (CPU memory) - ~1KB command buffer let mut encoder = self.ctx .device @@ -1331,6 +1368,7 @@ impl PillRenderer for Renderer { // Egui overlay (load over the rendered frame) timer.begin_context("UI pass"); + // ALLOCATION: CommandEncoder creation (CPU memory) - ~1KB command buffer let mut encoder = self .ctx .device From 4a9568d8e7b1bcfd59da153e1eeffdcc9749e7b2 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Mon, 20 Oct 2025 04:32:57 +0200 Subject: [PATCH 48/69] feat: use glam with SIMD for culling --- engine/.cargo/config.toml | 16 ++++ engine/pill_renderer/Cargo.toml | 1 + engine/pill_renderer/src/renderer.rs | 118 ++++++++++++++------------- 3 files changed, 77 insertions(+), 58 deletions(-) create mode 100644 engine/.cargo/config.toml diff --git a/engine/.cargo/config.toml b/engine/.cargo/config.toml new file mode 100644 index 00000000..1938115a --- /dev/null +++ b/engine/.cargo/config.toml @@ -0,0 +1,16 @@ +[build] +# Enable SIMD optimizations for math operations +# This will auto-vectorize cgmath operations when possible +rustflags = ["-C", "target-cpu=native"] + +# Alternative: More conservative SIMD flags +# rustflags = ["-C", "target-feature=+avx2,+fma"] + +[target.x86_64-unknown-linux-gnu] +rustflags = ["-C", "target-cpu=native"] + +[target.x86_64-apple-darwin] +rustflags = ["-C", "target-cpu=native"] + +[target.x86_64-pc-windows-msvc] +rustflags = ["-C", "target-cpu=native"] diff --git a/engine/pill_renderer/Cargo.toml b/engine/pill_renderer/Cargo.toml index 4898e93b..fb1f7a70 100644 --- a/engine/pill_renderer/Cargo.toml +++ b/engine/pill_renderer/Cargo.toml @@ -25,6 +25,7 @@ log = "0.4" # Other winit = "0.29" cgmath = "0.18" +glam = "0.27" wgpu = { version = "0.20.1", features = [ "spirv" ] } futures = "0.3" pollster = "0.2" diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 57d51903..990d3dd0 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -31,6 +31,7 @@ use pill_core::{PillSlotMapKey, PillSlotMapKeyData, PillStyle, RendererError, Ti use std::num::NonZeroU32; use cgmath::{Deg, InnerSpace}; +use glam::{Mat4, Vec3, Vec4}; use naga::back::wgsl; use naga::front::glsl; @@ -929,17 +930,6 @@ impl PillRenderer for Renderer { // ALLOCATION: Surface texture allocation (GPU memory) - ~4MB for 1920x1080 RGBA8 let frame = self.ctx.surface.get_current_texture().unwrap(); - // let frame = match frame { - // std::result::Result::Ok(frame) => frame, - // std::result::Result::Err(error) => match error { - // wgpu::SurfaceError::Lost => return Err(RendererError::SurfaceLost.into()), - // wgpu::SurfaceError::OutOfMemory => { - // return Err(RendererError::SurfaceOutOfMemory.into()) - // } - // _ => return Err(RendererError::SurfaceOther.into()), - // }, - // }; - // ALLOCATION: TextureView creation (GPU memory) - ~64 bytes metadata let view: wgpu::TextureView = frame .texture @@ -996,26 +986,36 @@ impl PillRenderer for Renderer { // [DIFF] Only frustum culling here; TALK discusses 2-pass occlusion and broader CPU-driven pipelines // [API->CLIENT] Culling, model matrix building, and visible set construction should live in client/high-level renderer // Build view before pass: cull and prepare per-draw MVPs - let vp_mat: cgmath::Matrix4 = renderer_camera.uniform.view_projection_matrix.into(); - let m = vp_mat; - let row = |i: usize| -> cgmath::Vector4 { - // cgmath is column-major; construct row i - cgmath::Vector4::new(m.x[i], m.y[i], m.z[i], m.w[i]) - }; - let make_plane = |v: cgmath::Vector4| -> (cgmath::Vector3, f32) { - let n = cgmath::Vector3::new(v.x, v.y, v.z); - let len = n.magnitude(); - let normal = if len > 0.0 { n / len } else { n }; - let d = v.w / if len > 0.0 { len } else { 1.0 }; - (normal, d) + // SIMD OPTIMIZATION: Use glam for SIMD-accelerated frustum culling + let vp_mat: Mat4 = + Mat4::from_cols_array_2d(&renderer_camera.uniform.view_projection_matrix); + + // Extract frustum planes using SIMD operations + let row3 = vp_mat.row(3); // w row + let row0 = vp_mat.row(0); // x row + let row1 = vp_mat.row(1); // y row + let row2 = vp_mat.row(2); // z row + + // SIMD plane construction: normalize plane equations + let make_plane = |plane_vec: Vec4| -> (Vec3, f32) { + let normal = Vec3::new(plane_vec.x, plane_vec.y, plane_vec.z); + let len = normal.length(); + if len > 0.0 { + let normalized = normal / len; + (normalized, plane_vec.w / len) + } else { + (normal, plane_vec.w) + } }; + + // Frustum planes using SIMD operations let planes = [ - make_plane(row(3) + row(0)), // left - make_plane(row(3) - row(0)), // right - make_plane(row(3) + row(1)), // bottom - make_plane(row(3) - row(1)), // top - make_plane(row(3) + row(2)), // near - make_plane(row(3) - row(2)), // far + make_plane(row3 + row0), // left + make_plane(row3 - row0), // right + make_plane(row3 + row1), // bottom + make_plane(row3 - row1), // top + make_plane(row3 + row2), // near + make_plane(row3 - row2), // far ]; timer.record("Culling & MVP build"); @@ -1045,41 +1045,42 @@ impl PillRenderer for Renderer { .as_ref() .unwrap(); let model_arr = pill_engine::internal::get_model_matrix(transform); - let model: cgmath::Matrix4 = model_arr.into(); + let model: Mat4 = Mat4::from_cols_array_2d(&model_arr); - // World AABB from mesh local AABB + // World AABB from mesh local AABB using SIMD operations let storage = self.state.renderer_resource_storage.borrow(); let mesh = storage.meshes.get(mesh_handle).unwrap(); - let local_min = - cgmath::Vector3::new(mesh.aabb_min[0], mesh.aabb_min[1], mesh.aabb_min[2]); - let local_max = - cgmath::Vector3::new(mesh.aabb_max[0], mesh.aabb_max[1], mesh.aabb_max[2]); + let local_min = Vec3::new(mesh.aabb_min[0], mesh.aabb_min[1], mesh.aabb_min[2]); + let local_max = Vec3::new(mesh.aabb_max[0], mesh.aabb_max[1], mesh.aabb_max[2]); + + // SIMD AABB corner transformation let corners = [ - cgmath::Vector3::new(local_min.x, local_min.y, local_min.z), - cgmath::Vector3::new(local_max.x, local_min.y, local_min.z), - cgmath::Vector3::new(local_min.x, local_max.y, local_min.z), - cgmath::Vector3::new(local_max.x, local_max.y, local_min.z), - cgmath::Vector3::new(local_min.x, local_min.y, local_max.z), - cgmath::Vector3::new(local_max.x, local_min.y, local_max.z), - cgmath::Vector3::new(local_min.x, local_max.y, local_max.z), - cgmath::Vector3::new(local_max.x, local_max.y, local_max.z), + Vec3::new(local_min.x, local_min.y, local_min.z), + Vec3::new(local_max.x, local_min.y, local_min.z), + Vec3::new(local_min.x, local_max.y, local_min.z), + Vec3::new(local_max.x, local_max.y, local_min.z), + Vec3::new(local_min.x, local_min.y, local_max.z), + Vec3::new(local_max.x, local_min.y, local_max.z), + Vec3::new(local_min.x, local_max.y, local_max.z), + Vec3::new(local_max.x, local_max.y, local_max.z), ]; - let mut world_min = cgmath::Vector3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY); - let mut world_max = - cgmath::Vector3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY); + + let mut world_min = Vec3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY); + let mut world_max = Vec3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY); + + // SIMD matrix-vector multiplication for corner transformation for c in &corners { - let p4 = model * cgmath::Vector4::new(c.x, c.y, c.z, 1.0); - let p = cgmath::Vector3::new(p4.x, p4.y, p4.z); - world_min.x = world_min.x.min(p.x); - world_min.y = world_min.y.min(p.y); - world_min.z = world_min.z.min(p.z); - world_max.x = world_max.x.max(p.x); - world_max.y = world_max.y.max(p.y); - world_max.z = world_max.z.max(p.z); + let p4 = model * Vec4::new(c.x, c.y, c.z, 1.0); + let p = Vec3::new(p4.x, p4.y, p4.z); + world_min = world_min.min(p); + world_max = world_max.max(p); } + + // SIMD frustum culling test let mut outside = false; for (normal, d) in &planes { - let p = cgmath::Vector3::new( + // SIMD select operation for AABB corner selection + let p = Vec3::new( if normal.x >= 0.0 { world_max.x } else { @@ -1096,6 +1097,7 @@ impl PillRenderer for Renderer { world_min.z }, ); + // SIMD dot product let dist = normal.dot(p) + *d; if dist < 0.0 { outside = true; @@ -1106,9 +1108,9 @@ impl PillRenderer for Renderer { continue; } - let view_proj: cgmath::Matrix4 = - renderer_camera.uniform.view_projection_matrix.into(); - let mvp: [[f32; 4]; 4] = (view_proj * model).into(); + let view_proj: Mat4 = + Mat4::from_cols_array_2d(&renderer_camera.uniform.view_projection_matrix); + let mvp: [[f32; 4]; 4] = (view_proj * model).to_cols_array_2d(); // REUSE PREALLOCATED BUFFER: Push to preallocated buffer (no allocation in hot path) self.state.visible_pre_draw_buffer.push(VisiblePreDraw { pipeline_handle, From e4076b1219887a794a67e452236f2be874d8f63c Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Mon, 20 Oct 2025 05:17:06 +0200 Subject: [PATCH 49/69] feat: preallocate system dirty entities --- .../src/ecs/systems/rendering_system.rs | 115 ++++++++++-------- engine/pill_engine/src/engine.rs | 2 +- 2 files changed, 68 insertions(+), 49 deletions(-) diff --git a/engine/pill_engine/src/ecs/systems/rendering_system.rs b/engine/pill_engine/src/ecs/systems/rendering_system.rs index 9db7d563..d13249d2 100644 --- a/engine/pill_engine/src/ecs/systems/rendering_system.rs +++ b/engine/pill_engine/src/ecs/systems/rendering_system.rs @@ -24,6 +24,13 @@ use crate::config::{ use crate::ecs::components::render_state_component::RenderStateComponent; use crate::resources::{Resource, ResourceLoadType, Texture, TextureType}; +// Constants for hot path optimization +const MAX_RENDERABLES_CAPACITY: usize = 100000; // Maximum expected renderables per frame + +// Static preallocated Vec for dirty entities to avoid per-frame allocation +// SAFETY: Single-threaded rendering system - no concurrency issues +static mut DIRTY_ENTITIES: Vec = Vec::new(); + pub fn rendering_system(engine: &mut Engine) -> Result<()> { // One-time bootstrap: register resource types and create default resources // Require RenderStateComponent; if missing, panic via unwrap @@ -34,6 +41,14 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { if need_bootstrap { init_default_resources(engine)?; + // Preallocate render queue capacity for hot path optimization + engine.render_queue.reserve(MAX_RENDERABLES_CAPACITY); // Reserve space for up to 100k renderables + + // Preallocate static dirty_entities Vec capacity + unsafe { + DIRTY_ENTITIES.reserve(MAX_RENDERABLES_CAPACITY); // Reserve space for up to 100k entities + } + if let Ok(rs) = engine.get_global_component_mut::() { rs.boot_done = true; } @@ -81,61 +96,30 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { // Clear the render queue // [SIMILAR] Build and sort a render queue ahead of draw; separates data prep from draw per TALK engine.render_queue.clear(); - engine.render_queue.reserve(200000); // Reserve space for 1000 items timer.record("Prepare render queue"); - let mut dirty_entities: Vec = Vec::new(); - - // Phase 1: Sweep components; route transforms needing matrix update to a batch, push clean directly - // [SIMILAR] Batch transform updates; avoid per-draw matrix work - for (entity_handle, transform_component, mesh_rendering_component) in engine - .scene_manager - .get_two_component_iterator_mut::( - active_scene_handle, - )? { - if transform_component.matrix_update_required { - // defer update to batch - dirty_entities.push(entity_handle); - continue; - } + // Use static preallocated dirty_entities Vec + unsafe { + DIRTY_ENTITIES.clear(); // Clear and reuse preallocated Vec - // Push clean (non-dirty) items directly into the render queue - // [SIMILAR] Use precomputed render_queue_key (pipeline/material/mesh sorting key) - if let Some(render_queue_key) = mesh_rendering_component.render_queue_key { - let render_queue_item = RenderQueueItem { - key: render_queue_key, - entity_index: entity_handle.data().index as u32, - }; - engine.render_queue.push(render_queue_item); - } - } - - // Phase 2: Batch update transforms with matrix_update_required - // [RECOMMENDED] Consolidate transform matrix updates in one batch outside of pass - if !dirty_entities.is_empty() { - timer.begin_context(&format!("Batch update {} transforms", dirty_entities.len())); - for entity_handle in &dirty_entities { - // Pull the transform component mutably and update matrices - if let Ok(transform_component) = engine + // Phase 1: Sweep components; route transforms needing matrix update to a batch, push clean directly + // [SIMILAR] Batch transform updates; avoid per-draw matrix work + for (entity_handle, transform_component, mesh_rendering_component) in + engine .scene_manager - .get_entity_component::(*entity_handle, active_scene_handle) - { - if transform_component.matrix_update_required { - update_transform_matrices(transform_component); - transform_component.matrix_update_required = false; - } + .get_two_component_iterator_mut::( + active_scene_handle, + )? + { + if transform_component.matrix_update_required { + // defer update to batch + DIRTY_ENTITIES.push(entity_handle); + continue; } - } - timer.end_context()?; - } - // Phase 3: Add updated (previously dirty) items to the render queue - for entity_handle in dirty_entities { - if let Ok(mesh_rendering_component) = engine - .scene_manager - .get_entity_component::(entity_handle, active_scene_handle) - { + // Push clean (non-dirty) items directly into the render queue + // [SIMILAR] Use precomputed render_queue_key (pipeline/material/mesh sorting key) if let Some(render_queue_key) = mesh_rendering_component.render_queue_key { let render_queue_item = RenderQueueItem { key: render_queue_key, @@ -144,6 +128,41 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { engine.render_queue.push(render_queue_item); } } + + // Phase 2: Batch update transforms with matrix_update_required + // [RECOMMENDED] Consolidate transform matrix updates in one batch outside of pass + if !DIRTY_ENTITIES.is_empty() { + timer.begin_context(&format!("Batch update {} transforms", DIRTY_ENTITIES.len())); + for entity_handle in DIRTY_ENTITIES.iter() { + // Pull the transform component mutably and update matrices + if let Ok(transform_component) = engine + .scene_manager + .get_entity_component::(*entity_handle, active_scene_handle) + { + if transform_component.matrix_update_required { + update_transform_matrices(transform_component); + transform_component.matrix_update_required = false; + } + } + } + timer.end_context()?; + } + + // Phase 3: Add updated (previously dirty) items to the render queue + for entity_handle in DIRTY_ENTITIES.iter() { + if let Ok(mesh_rendering_component) = engine + .scene_manager + .get_entity_component::(*entity_handle, active_scene_handle) + { + if let Some(render_queue_key) = mesh_rendering_component.render_queue_key { + let render_queue_item = RenderQueueItem { + key: render_queue_key, + entity_index: entity_handle.data().index as u32, + }; + engine.render_queue.push(render_queue_item); + } + } + } } timer.record("Get component storages"); diff --git a/engine/pill_engine/src/engine.rs b/engine/pill_engine/src/engine.rs index 3797649e..3e9dc5e4 100644 --- a/engine/pill_engine/src/engine.rs +++ b/engine/pill_engine/src/engine.rs @@ -171,7 +171,7 @@ impl Engine { RENDERING_SYSTEM.update_phase, )?; - self.renderer.init()?; + self.renderer.init(); self.init_renderer(); // Start game From 12d3c782cdbd6494341347171749496fd46b2828 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Sun, 2 Nov 2025 02:40:23 +0100 Subject: [PATCH 50/69] feat: resource pools with minimal handles and vector lookup --- engine/Cargo.toml | 2 +- engine/pill_core/src/handle.rs | 116 +++++++ engine/pill_core/src/lib.rs | 17 ++ engine/pill_core/src/tags.rs | 7 + engine/pill_engine/src/config.rs | 23 +- engine/pill_engine/src/graphics/mod.rs | 4 + .../pill_engine/src/graphics/render_queue.rs | 16 +- engine/pill_engine/src/graphics/renderer.rs | 46 ++- engine/pill_engine/src/lib.rs | 7 +- engine/pill_renderer/src/lib.rs | 6 +- engine/pill_renderer/src/mesh_drawer.rs | 246 --------------- engine/pill_renderer/src/pass_compose.rs | 121 +++++--- .../pill_renderer/src/pass_overlay_depth.rs | 217 +++++++------ engine/pill_renderer/src/pass_overlay_logo.rs | 242 ++++++++------- engine/pill_renderer/src/pass_overlay_uv.rs | 91 ++++-- engine/pill_renderer/src/renderer.rs | 284 +++++++++--------- engine/pill_renderer/src/resource_manager.rs | 119 ++++++++ engine/pill_renderer/src/resource_snapshot.rs | 28 ++ .../src/resources/renderer_material.rs | 173 +++++++---- 19 files changed, 980 insertions(+), 785 deletions(-) create mode 100644 engine/pill_core/src/handle.rs create mode 100644 engine/pill_core/src/tags.rs delete mode 100644 engine/pill_renderer/src/mesh_drawer.rs create mode 100644 engine/pill_renderer/src/resource_manager.rs create mode 100644 engine/pill_renderer/src/resource_snapshot.rs diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 106db231..0332a48f 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -5,7 +5,7 @@ members = [ "pill_engine", "pill_renderer", "pill_standalone", - "NO_PATH", ### Game project crate (This will be changed by Pill Launcher on build to allow proper compilation of game project) + "/Users/mk/dev/demo/Pill-Demo", ### Game project crate (This will be changed by Pill Launcher on build to allow proper compilation of game project) ] exclude = [ diff --git a/engine/pill_core/src/handle.rs b/engine/pill_core/src/handle.rs new file mode 100644 index 00000000..8598dde2 --- /dev/null +++ b/engine/pill_core/src/handle.rs @@ -0,0 +1,116 @@ +use core::fmt; +use core::hash::{Hash, Hasher}; +use core::marker::PhantomData; + +#[repr(transparent)] +pub struct Handle { + raw: u64, + _marker: PhantomData, +} + +impl Handle { + #[inline] + pub const fn from_parts(index: u32, generation: u32) -> Self { + Self { + raw: ((generation as u64) << 32) | (index as u64), + _marker: PhantomData, + } + } + + #[inline] + pub const fn index(&self) -> u32 { self.raw as u32 } + + #[inline] + pub const fn generation(&self) -> u32 { (self.raw >> 32) as u32 } + + pub const INVALID: Self = Self { raw: u64::MAX, _marker: PhantomData }; + + #[inline] + pub const fn is_valid(&self) -> bool { self.raw != u64::MAX } +} + +impl Copy for Handle {} +impl Clone for Handle { fn clone(&self) -> Self { *self } } +impl PartialEq for Handle { fn eq(&self, other: &Self) -> bool { self.raw == other.raw } } +impl Eq for Handle {} +impl Hash for Handle { fn hash(&self, state: &mut H) { self.raw.hash(state) } } +impl fmt::Debug for Handle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Handle({:#x})", self.raw) } } + +struct Entry { + generation: u32, + resource: Option, +} + +pub struct ResourcePool { + entries: Vec>, + free_list: Vec, + _marker: PhantomData, +} + +impl ResourcePool { + pub fn new() -> Self { + Self { + entries: Vec::new(), + free_list: Vec::new(), + _marker: PhantomData, + } + } + + #[inline] + pub fn insert(&mut self, resource: V) -> Handle { + if let Some(idx) = self.free_list.pop() { + let e = &mut self.entries[idx as usize]; + e.generation = e.generation.wrapping_add(1); + e.resource = Some(resource); + Handle::from_parts(idx, e.generation) + } else { + let idx = self.entries.len() as u32; + self.entries.push(Entry { generation: 0, resource: Some(resource) }); + Handle::from_parts(idx, 0) + } + } + + #[inline] + pub fn remove(&mut self, h: Handle) -> Option { + let idx = h.index() as usize; + if idx >= self.entries.len() { + return None; + } + let e = &mut self.entries[idx]; + if e.generation != h.generation() { + return None; + } + let res = e.resource.take(); + e.generation = e.generation.wrapping_add(1); + self.free_list.push(h.index()); + res + } + + #[inline] + pub fn get(&self, h: Handle) -> Option<&V> { + let idx = h.index() as usize; + if idx >= self.entries.len() { + return None; + } + let e = &self.entries[idx]; + if e.generation != h.generation() { + return None; + } + e.resource.as_ref() + } + + #[inline] + pub fn get_mut(&mut self, h: Handle) -> Option<&mut V> { + let idx = h.index() as usize; + if idx >= self.entries.len() { + return None; + } + let e = &mut self.entries[idx]; + if e.generation != h.generation() { + return None; + } + e.resource.as_mut() + } +} + + diff --git a/engine/pill_core/src/lib.rs b/engine/pill_core/src/lib.rs index 8a06fac8..357470e5 100644 --- a/engine/pill_core/src/lib.rs +++ b/engine/pill_core/src/lib.rs @@ -9,6 +9,8 @@ mod pill_twinmap; mod pill_typemap; mod bitmask_utils; mod timer; +mod handle; +mod tags; // --- Use --- @@ -62,4 +64,19 @@ pub use utils::{ pub use timer::{ Timer, TimerRecord +}; + +pub use handle::{ + Handle, + ResourcePool, +}; + +pub use tags::{ + RendererBufferTag, + RendererCameraTag, + RendererMaterialTag, + RendererMeshTag, + RendererPipelineTag, + RendererPipelineV2Tag, + RendererTextureTag, }; \ No newline at end of file diff --git a/engine/pill_core/src/tags.rs b/engine/pill_core/src/tags.rs new file mode 100644 index 00000000..cfd219c7 --- /dev/null +++ b/engine/pill_core/src/tags.rs @@ -0,0 +1,7 @@ +pub struct RendererBufferTag; +pub struct RendererCameraTag; +pub struct RendererMaterialTag; +pub struct RendererMeshTag; +pub struct RendererPipelineTag; +pub struct RendererPipelineV2Tag; +pub struct RendererTextureTag; diff --git a/engine/pill_engine/src/config.rs b/engine/pill_engine/src/config.rs index a4e6625d..4750962f 100644 --- a/engine/pill_engine/src/config.rs +++ b/engine/pill_engine/src/config.rs @@ -8,7 +8,7 @@ use crate::{ resources::{MaterialHandle, TextureHandle, TextureType}, }; -use pill_core::PillSlotMapKeyData; +use pill_core::{Handle, PillSlotMapKeyData}; use lazy_static::lazy_static; use std::{any::TypeId, num::NonZeroU32}; @@ -101,12 +101,7 @@ pub const DEFAULT_COLOR_TEXTURE_HANDLE: TextureHandle = TextureHandle { version: unsafe { std::num::NonZeroU32::new_unchecked(1) }, }, }; -pub const DEFAULT_RENDERER_COLOR_TEXTURE_HANDLE: RendererTextureHandle = RendererTextureHandle { - 0: PillSlotMapKeyData { - index: 1, - version: unsafe { std::num::NonZeroU32::new_unchecked(1) }, - }, -}; +pub const DEFAULT_RENDERER_COLOR_TEXTURE_HANDLE: RendererTextureHandle = Handle::from_parts(1, 1); // Default resource handle - Normal texture pub const DEFAULT_NORMAL_TEXTURE_HANDLE: TextureHandle = TextureHandle { @@ -115,12 +110,7 @@ pub const DEFAULT_NORMAL_TEXTURE_HANDLE: TextureHandle = TextureHandle { version: unsafe { std::num::NonZeroU32::new_unchecked(1) }, }, }; -pub const DEFAULT_RENDERER_NORMAL_TEXTURE_HANDLE: RendererTextureHandle = RendererTextureHandle { - 0: PillSlotMapKeyData { - index: 2, - version: unsafe { std::num::NonZeroU32::new_unchecked(1) }, - }, -}; +pub const DEFAULT_RENDERER_NORMAL_TEXTURE_HANDLE: RendererTextureHandle = Handle::from_parts(2, 1); pub fn get_default_texture_handles( texture_type: TextureType, @@ -144,12 +134,7 @@ pub const DEFAULT_MATERIAL_HANDLE: MaterialHandle = MaterialHandle { version: unsafe { std::num::NonZeroU32::new_unchecked(1) }, }, }; -pub const DEFAULT_RENDERER_MATERIAL_HANDLE: RendererMaterialHandle = RendererMaterialHandle { - 0: PillSlotMapKeyData { - index: 1, - version: unsafe { std::num::NonZeroU32::new_unchecked(1) }, - }, -}; +pub const DEFAULT_RENDERER_MATERIAL_HANDLE: RendererMaterialHandle = Handle::from_parts(1, 1); pub fn get_default_material_handles() -> (MaterialHandle, RendererMaterialHandle) { (DEFAULT_MATERIAL_HANDLE, DEFAULT_RENDERER_MATERIAL_HANDLE) diff --git a/engine/pill_engine/src/graphics/mod.rs b/engine/pill_engine/src/graphics/mod.rs index 857aa975..e24f82df 100644 --- a/engine/pill_engine/src/graphics/mod.rs +++ b/engine/pill_engine/src/graphics/mod.rs @@ -5,6 +5,10 @@ mod renderer; // --- Use --- +pub use pill_core::{ + RendererBufferTag, RendererCameraTag, RendererMaterialTag, RendererMeshTag, + RendererPipelineTag, RendererPipelineV2Tag, RendererTextureTag, +}; pub use renderer::{ BufferDesc, PillRenderer, PipelineV2, PipelineV2Desc, Renderer, RendererBufferHandle, RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, RendererPipelineHandle, diff --git a/engine/pill_engine/src/graphics/render_queue.rs b/engine/pill_engine/src/graphics/render_queue.rs index 95247a13..a5fffb4a 100644 --- a/engine/pill_engine/src/graphics/render_queue.rs +++ b/engine/pill_engine/src/graphics/render_queue.rs @@ -10,7 +10,7 @@ use crate::{ config::*, }; -use pill_core::PillSlotMapKey; +use pill_core::Handle; use std::{ cmp::Ordering, @@ -108,12 +108,14 @@ pub fn compose_render_queue_key(resource_manager: &ResourceManager, material_han let material = resource_manager.get_resource::(material_handle)?; let mesh = resource_manager.get_resource::(mesh_handle)?; - let render_queue_key: RenderQueueKey = - ((RENDER_QUEUE_KEY_ORDER.max - material.rendering_order as RenderQueueKey) << RENDER_QUEUE_KEY_ORDER.mask_shift) | // Order has to be inverted for proper sorting - ((material.renderer_resource_handle.unwrap().data().index as RenderQueueKey) << RENDER_QUEUE_KEY_MATERIAL_INDEX.mask_shift) | - ((material.renderer_resource_handle.unwrap().data().version.get() as RenderQueueKey) << RENDER_QUEUE_KEY_MATERIAL_VERSION.mask_shift) | - ((mesh.renderer_resource_handle.unwrap().data().index as RenderQueueKey) << RENDER_QUEUE_KEY_MESH_INDEX.mask_shift ) | - ((mesh.renderer_resource_handle.unwrap().data().version.get() as RenderQueueKey) << RENDER_QUEUE_KEY_MESH_VERSION.mask_shift); + let h_mat = material.renderer_resource_handle.unwrap(); + let h_mesh = mesh.renderer_resource_handle.unwrap(); + let render_queue_key: RenderQueueKey = + ((RENDER_QUEUE_KEY_ORDER.max - material.rendering_order as RenderQueueKey) << RENDER_QUEUE_KEY_ORDER.mask_shift) + | ((h_mat.index() as RenderQueueKey) << RENDER_QUEUE_KEY_MATERIAL_INDEX.mask_shift) + | ((h_mat.generation() as RenderQueueKey) << RENDER_QUEUE_KEY_MATERIAL_VERSION.mask_shift) + | ((h_mesh.index() as RenderQueueKey) << RENDER_QUEUE_KEY_MESH_INDEX.mask_shift) + | ((h_mesh.generation() as RenderQueueKey) << RENDER_QUEUE_KEY_MESH_VERSION.mask_shift); Ok(render_queue_key) } diff --git a/engine/pill_engine/src/graphics/renderer.rs b/engine/pill_engine/src/graphics/renderer.rs index f0d418f5..6706cfb3 100644 --- a/engine/pill_engine/src/graphics/renderer.rs +++ b/engine/pill_engine/src/graphics/renderer.rs @@ -9,41 +9,29 @@ use crate::{ }; use pill_core::PillStyle; -use pill_core::{PillSlotMapKey, Timer}; +use pill_core::{ + Handle, RendererBufferTag, RendererCameraTag, RendererMaterialTag, RendererMeshTag, + RendererPipelineTag, RendererPipelineV2Tag, RendererTextureTag, Timer, +}; use anyhow::{Context, Error, Result}; use std::{path::PathBuf, sync::Arc}; use thiserror::Error; -// --- Renderer resource handles --- +// --- Renderer resource handles (typed generational handles) --- +pub type RendererMaterialHandle = Handle; -pill_core::define_new_pill_slotmap_key! { - pub struct RendererMaterialHandle; -} +pub type RendererMeshHandle = Handle; -pill_core::define_new_pill_slotmap_key! { - pub struct RendererMeshHandle; -} +pub type RendererPipelineHandle = Handle; -pill_core::define_new_pill_slotmap_key! { - pub struct RendererPipelineHandle; -} +pub type RendererCameraHandle = Handle; -pill_core::define_new_pill_slotmap_key! { - pub struct RendererCameraHandle; -} +pub type RendererTextureHandle = Handle; -pill_core::define_new_pill_slotmap_key! { - pub struct RendererTextureHandle; -} +pub type RendererBufferHandle = Handle; -pill_core::define_new_pill_slotmap_key! { - pub struct RendererBufferHandle; -} - -pill_core::define_new_pill_slotmap_key! { - pub struct RendererPipelineV2Handle; -} +pub type RendererPipelineV2Handle = Handle; // --- Descriptors --- @@ -88,17 +76,17 @@ pub trait PillRenderer { fn resize(&mut self, new_window_size: winit::dpi::PhysicalSize); // Creates a 256B-aligned uniform buffer (COPY_DST) and returns its handle - fn create_buffer(&self, desc: BufferDesc) -> Result; - fn create_pipeline_v2(&self, desc: PipelineV2Desc) -> Result; - fn create_mesh(&self, name: &str, mesh_data: &MeshData) -> Result; + fn create_buffer(&mut self, desc: BufferDesc) -> Result; + fn create_pipeline_v2(&mut self, desc: PipelineV2Desc) -> Result; + fn create_mesh(&mut self, name: &str, mesh_data: &MeshData) -> Result; fn create_texture( - &self, + &mut self, name: &str, image_data: &image::DynamicImage, texture_type: TextureType, ) -> Result; fn create_material( - &self, + &mut self, name: &str, textures: &MaterialTextureMap, parameters: &MaterialParameterMap, diff --git a/engine/pill_engine/src/lib.rs b/engine/pill_engine/src/lib.rs index d65a8ba6..0ee6df15 100644 --- a/engine/pill_engine/src/lib.rs +++ b/engine/pill_engine/src/lib.rs @@ -92,9 +92,10 @@ pub mod internal { graphics::{ decompose_render_queue_key, BufferDesc, PillRenderer, PipelineV2, PipelineV2Desc, RenderQueueItem, RenderQueueKey, RenderQueueKeyFields, RendererBufferHandle, - RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, - RendererPipelineHandle, RendererPipelineV2Handle, RendererTextureHandle, ShaderDesc, - RENDER_QUEUE_KEY_ORDER, + RendererBufferTag, RendererCameraHandle, RendererCameraTag, RendererMaterialHandle, + RendererMaterialTag, RendererMeshHandle, RendererMeshTag, RendererPipelineHandle, + RendererPipelineTag, RendererPipelineV2Handle, RendererPipelineV2Tag, + RendererTextureHandle, RendererTextureTag, ShaderDesc, RENDER_QUEUE_KEY_ORDER, }, resources::{ get_renderer_texture_handle_from_material_texture, Material, MaterialHandle, diff --git a/engine/pill_renderer/src/lib.rs b/engine/pill_renderer/src/lib.rs index 1abdbfb2..7d45bb09 100644 --- a/engine/pill_renderer/src/lib.rs +++ b/engine/pill_renderer/src/lib.rs @@ -2,13 +2,13 @@ pub mod egui; pub mod instance; -pub mod mesh_drawer; pub mod pass_compose; pub mod pass_overlay_depth; pub mod pass_overlay_logo; pub mod pass_overlay_uv; pub mod renderer; -pub mod renderer_resource_storage; +pub mod resource_manager; +pub mod resource_snapshot; pub mod resources; // --- Use --- @@ -18,5 +18,3 @@ pub use renderer::*; pub use instance::Instance; pub use pass_compose::PassCompose; pub use pass_overlay_uv::PassOverlayUV; - -pub use renderer_resource_storage::RendererResourceStorage; diff --git a/engine/pill_renderer/src/mesh_drawer.rs b/engine/pill_renderer/src/mesh_drawer.rs deleted file mode 100644 index 9d6c351f..00000000 --- a/engine/pill_renderer/src/mesh_drawer.rs +++ /dev/null @@ -1,246 +0,0 @@ -use std::{num::NonZeroU32, ops::Range}; - -use crate::{ - resources::RendererCamera, Instance, RendererResourceStorage, INITIAL_INSTANCE_VECTOR_CAPACITY, -}; -use pill_core::{RendererError, Timer}; -use pill_engine::{ - internal::{ - RenderQueueItem, RendererMaterialHandle, RendererMeshHandle, RendererPipelineHandle, - TransformComponent, RENDER_QUEUE_KEY_ORDER, - }, - ComponentStorage, -}; - -use anyhow::{Error, Result}; - -pub struct MeshDrawer { - max_instance_batch_size: u32, - instances: Vec, - instance_buffer: wgpu::Buffer, -} - -impl MeshDrawer { - pub fn new(device: &wgpu::Device, max_instance_batch_size: u32) -> Self { - // Create instance buffer - let buffer_size = (size_of::() * max_instance_batch_size as usize) as u64; - let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("instance_buffer"), - size: buffer_size, - usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - MeshDrawer { - max_instance_batch_size, - instances: Vec::::with_capacity(INITIAL_INSTANCE_VECTOR_CAPACITY), - instance_buffer, - } - } - - pub fn record_draw_commands( - &mut self, - // Resources - queue: &wgpu::Queue, - device: &wgpu::Device, - renderer_resource_storage: &RendererResourceStorage, - color_attachment: wgpu::RenderPassColorAttachment, - depth_stencil_attachment: wgpu::RenderPassDepthStencilAttachment, - // Rendring data - camera: &RendererCamera, - render_queue: &Vec, - transform_component_storage: &ComponentStorage, - timer: &mut Timer, - ) -> Result<()> { - timer.record("Prepare render pass"); - - let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("mesh_drawer_encoder"), - }); - - let mut _instance_batch_number = 0; - let mut _rendering_context_change_number = 0; - - for (i, instance_batch) in render_queue - .chunks(self.max_instance_batch_size as usize) - .enumerate() - { - let batch_size = instance_batch.len(); - _instance_batch_number += 1; - - timer.begin_context(&format!("Prepare draw instance batch {}", i)); - - timer.record(&format!("Write instance buffer")); - - // Prepare instance data and load it to buffer - self.instances.clear(); - self.instances.reserve(instance_batch.len()); // Pre-allocate exact capacity - - for render_queue_item in instance_batch { - let transform_slot = transform_component_storage - .data - .get(render_queue_item.entity_index as usize) - .ok_or_else(|| Error::new(RendererError::Other))?; - let transform_component = transform_slot - .as_ref() - .ok_or_else(|| Error::new(RendererError::Other))?; - //println!("Creating new instance with transform component: {:?} {:?}", i, transform_component.position); - self.instances.push(Instance::new(transform_component)); - } - - queue.write_buffer( - &self.instance_buffer, - 0, - bytemuck::cast_slice(&self.instances), - ); // Update instance buffer - - let mut current_rendering_order: u8 = 0; - let mut current_pipeline_handle: Option = None; - let mut current_material_handle: Option = None; - let mut current_mesh_handle: Option = None; - let mut current_mesh_index_count: u32 = 0; // Number of indices in the current mesh - - // Start encoding render pass - let load_op = if i == 0 { - wgpu::LoadOp::Clear(wgpu::Color::BLACK) - } else { - wgpu::LoadOp::Load - }; - - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("render_pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - ops: wgpu::Operations { - load: load_op, - store: wgpu::StoreOp::Store, - }, - ..color_attachment.clone() - })], - depth_stencil_attachment: Some(depth_stencil_attachment.clone()), - timestamp_writes: None, - occlusion_query_set: None, - }); - - render_pass.set_vertex_buffer(1, self.instance_buffer.slice(..)); // Set instance buffer - - timer.record("Record draw instances commands"); - - // Reset instance range for each batch - // Start inclusive, end exclusive (e.g. 0..3 means indices 0, 1, 2. e.g. 5..7 means indices 5, 6) - let mut accumulated_instance_range: Range = 0..0; - let mut accumulated_instance_count: u32 = 0; - for (j, render_queue_item) in instance_batch.iter().enumerate() { - let render_queue_key_fields = - pill_engine::internal::decompose_render_queue_key(render_queue_item.key); - - // Recreate resource handles - let renderer_material_handle = RendererMaterialHandle::new( - render_queue_key_fields.material_index.into(), - NonZeroU32::new(render_queue_key_fields.material_version.into()).unwrap(), - ); - let renderer_mesh_handle = RendererMeshHandle::new( - render_queue_key_fields.mesh_index.into(), - NonZeroU32::new(render_queue_key_fields.mesh_version.into()).unwrap(), - ); - - // Check rendering order - if current_rendering_order > render_queue_key_fields.order { - if accumulated_instance_count > 0 { - render_pass.draw_indexed( - 0..current_mesh_index_count, - 0, - accumulated_instance_range.clone(), - ); - accumulated_instance_range = - accumulated_instance_range.end..accumulated_instance_range.end; - } - - // Set new order - current_rendering_order = render_queue_key_fields.order; - _rendering_context_change_number += 1; - } - - // Check material - if current_material_handle != Some(renderer_material_handle) { - // Render accumulated instances - if accumulated_instance_count > 0 { - render_pass.draw_indexed( - 0..current_mesh_index_count, - 0, - accumulated_instance_range.clone(), - ); - accumulated_instance_range = - accumulated_instance_range.end..accumulated_instance_range.end; - } - // Set new material - current_material_handle = Some(renderer_material_handle); - let material = renderer_resource_storage - .materials - .get(current_material_handle.unwrap()) - .unwrap(); - - // Set pipeline if new material is using different one - if current_pipeline_handle != Some(material.pipeline_handle) { - current_pipeline_handle = Some(material.pipeline_handle); - let pipeline = renderer_resource_storage - .pipelines - .get(current_pipeline_handle.unwrap()) - .unwrap(); - render_pass.set_pipeline(&pipeline.render_pipeline); - } - - render_pass.set_bind_group(0, &material.texture_bind_group, &[]); - render_pass.set_bind_group(1, &material.parameter_bind_group, &[]); - render_pass.set_bind_group(2, &camera.bind_group, &[]); - - _rendering_context_change_number += 1; - } - - // Check mesh - if current_mesh_handle != Some(renderer_mesh_handle) { - // Render accumulated instances - if accumulated_instance_count > 0 { - render_pass.draw_indexed( - 0..current_mesh_index_count, - 0, - accumulated_instance_range.clone(), - ); - accumulated_instance_range = - accumulated_instance_range.end..accumulated_instance_range.end; - } - // Set new mesh - current_mesh_handle = Some(renderer_mesh_handle); - let mesh = renderer_resource_storage - .meshes - .get(current_mesh_handle.unwrap()) - .unwrap(); - current_mesh_index_count = mesh.index_count; - render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); - render_pass - .set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32); - - _rendering_context_change_number += 1; - } - - // Add new instance - accumulated_instance_range = - accumulated_instance_range.start..accumulated_instance_range.end + 1; - accumulated_instance_count = - accumulated_instance_range.end - accumulated_instance_range.start; - - // If last in batch, render accumulated instances - if j == batch_size - 1 && accumulated_instance_count > 0 { - render_pass.draw_indexed( - 0..current_mesh_index_count, - 0, - accumulated_instance_range.clone(), - ); - } - } - - timer.end_context()?; // End "Draw instance batch" context - } - queue.submit(std::iter::once(encoder.finish())); - Ok(()) - } -} diff --git a/engine/pill_renderer/src/pass_compose.rs b/engine/pill_renderer/src/pass_compose.rs index 36b483b7..d27d6185 100644 --- a/engine/pill_renderer/src/pass_compose.rs +++ b/engine/pill_renderer/src/pass_compose.rs @@ -1,4 +1,5 @@ use crate::renderer::{Pass, Renderer}; +use crate::resource_manager::ResourceManager; use crate::resources::RendererTexture; use anyhow::Result; use pill_engine::internal::{PillRenderer, PipelineV2, PipelineV2Desc, ShaderDesc}; @@ -9,15 +10,21 @@ use wgpu::Queue; pub struct PassCompose { label: String, offscreen_texture: Arc, + target_format: wgpu::TextureFormat, pipeline: Option, bind_group: Option, } impl PassCompose { - pub fn new(label: &str, offscreen_texture: Arc) -> Self { + pub fn new( + label: &str, + offscreen_texture: Arc, + target_format: wgpu::TextureFormat, + ) -> Self { Self { label: label.to_string(), offscreen_texture, + target_format, pipeline: None, bind_group: None, } @@ -29,7 +36,7 @@ impl Pass for PassCompose { &self.label } - fn init(&mut self, _queue: &wgpu::Queue, renderer: &Renderer) -> Result<()> { + fn init(&mut self, device: &wgpu::Device, _res: &mut ResourceManager) -> Result<()> { println!("Initializing pass: {}", self.label); let vs = r#" @@ -63,19 +70,11 @@ impl Pass for PassCompose { } "#; - let pipeline = renderer.create_pipeline_v2(PipelineV2Desc { - label: Some("compose"), - vs: ShaderDesc { - source: vs, - entry_func: "main", - }, - ps: ShaderDesc { - source: fs, - entry_func: "main", - }, - bind_groups: vec![ - // Group 0: Texture and sampler bindings - vec![ + let pipeline = { + // Build bind group layout + let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("compose_bgl"), + entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::FRAGMENT, @@ -93,37 +92,73 @@ impl Pass for PassCompose { count: None, }, ], - ], - targets: &[Some(wgpu::ColorTargetState { - format: renderer.get_surface_format(), - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - })?; - - let bind_group = renderer - .get_device() - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("compose_bind_group"), - layout: &pipeline.bind_group_layouts[0], // Group 0: Texture and sampler bindings - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView( - &self.offscreen_texture.texture_view, - ), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&self.offscreen_texture.sampler), - }, - ], }); + let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("compose_pl"), + bind_group_layouts: &[&bgl], + push_constant_ranges: &[], + }); + + let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("compose_vs"), + source: wgpu::ShaderSource::Wgsl(vs.into()), + }); + let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("compose_fs"), + source: wgpu::ShaderSource::Wgsl(fs.into()), + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("compose_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fs_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: self.target_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + (pipeline, bgl) + }; + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("compose_bind_group"), + layout: &pipeline.1, // Group 0: Texture and sampler bindings + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView( + &self.offscreen_texture.texture_view, + ), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&self.offscreen_texture.sampler), + }, + ], + }); + // Store pipeline handle - self.pipeline = Some(pipeline); + self.pipeline = Some(PipelineV2 { + pipeline: pipeline.0, + bind_group_layouts: vec![pipeline.1], + }); self.bind_group = Some(bind_group); Ok(()) diff --git a/engine/pill_renderer/src/pass_overlay_depth.rs b/engine/pill_renderer/src/pass_overlay_depth.rs index 232e2e2c..8a50af13 100644 --- a/engine/pill_renderer/src/pass_overlay_depth.rs +++ b/engine/pill_renderer/src/pass_overlay_depth.rs @@ -1,4 +1,5 @@ use crate::renderer::{Pass, Renderer}; +use crate::resource_manager::ResourceManager; use crate::resources::RendererTexture; use anyhow::Result; use pill_engine::internal::{BufferDesc, PillRenderer, PipelineV2, PipelineV2Desc, ShaderDesc}; @@ -15,6 +16,7 @@ pub struct PassOverlayDepth { tint_buffer: Option, rect: [f32; 4], tint: [f32; 4], + target_format: wgpu::TextureFormat, } impl PassOverlayDepth { @@ -23,6 +25,7 @@ impl PassOverlayDepth { rect: [f32; 4], tint: [f32; 4], depth_texture: Arc, + target_format: wgpu::TextureFormat, ) -> Self { Self { label: label.to_string(), @@ -33,6 +36,7 @@ impl PassOverlayDepth { tint_buffer: None, rect: rect, tint: tint, + target_format, } } } @@ -42,16 +46,22 @@ impl Pass for PassOverlayDepth { &self.label } - fn init(&mut self, queue: &wgpu::Queue, renderer: &Renderer) -> Result<()> { + fn init(&mut self, device: &wgpu::Device, _res: &mut ResourceManager) -> Result<()> { println!("Initializing pass: {}", self.label); // Create buffer for overlay rect UBO - let buffer = renderer.create_buffer(BufferDesc { + let buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("overlay_rect_ubo"), - byte_size: 4 * 32, // 4 floats * 32 bytes per float = 128 bytes, will be aligned to 256 bytes for Metal UBOs - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - })?; - queue.write_buffer(&buffer, 0, bytemuck::bytes_of(&self.rect)); + size: 256, + usage: wgpu::BufferUsages::UNIFORM, + mapped_at_creation: true, + }); + { + let mut view = buffer.slice(..).get_mapped_range_mut(); + let src = bytemuck::bytes_of(&self.rect); + view[..src.len()].copy_from_slice(src); + } + buffer.unmap(); let vs = r#" @group(1) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] @@ -85,110 +95,131 @@ impl Pass for PassOverlayDepth { } "#; - let pipeline = renderer.create_pipeline_v2(PipelineV2Desc { - label: Some("overlay_depth"), - vs: ShaderDesc { - source: vs, - entry_func: "main", - }, - ps: ShaderDesc { - source: fs, - entry_func: "main", - }, - bind_groups: vec![ - // Group 0: Material bindings (texture, tint buffer) - vec![ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Depth, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), - }, - count: None, - }, - ], - // Group 1: Rect UBO - vec![wgpu::BindGroupLayoutEntry { + // Build bind group layouts + let bgl_material = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_depth_material_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { binding: 0, - visibility: wgpu::ShaderStages::VERTEX, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Depth, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), }, count: None, - }], + }, ], - targets: &[Some(wgpu::ColorTargetState { - format: renderer.get_surface_format(), - blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], + }); + let bgl_rect = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_depth_rect_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }], + }); + let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("overlay_depth_pl"), + bind_group_layouts: &[&bgl_material, &bgl_rect], + push_constant_ranges: &[], + }); + let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_depth_vs"), + source: wgpu::ShaderSource::Wgsl(vs.into()), + }); + let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_depth_fs"), + source: wgpu::ShaderSource::Wgsl(fs.into()), + }); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("overlay_depth_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fs_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: self.target_format, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), depth_stencil: None, multisample: wgpu::MultisampleState::default(), - })?; + multiview: None, + }); - let bind_group_rect = renderer - .get_device() - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_rect_bind_group"), - layout: &pipeline.bind_group_layouts[1], // Group 1: Rect UBO - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: buffer.as_entire_binding(), - }], - }); + let bind_group_rect = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_rect_bind_group"), + layout: &bgl_rect, // Group 1: Rect UBO + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + }); // Create tint buffer - let tint_buffer = renderer - .get_device() - .create_buffer(&wgpu::BufferDescriptor { - label: Some("overlay_depth_tint"), - size: 256, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - queue.write_buffer(&tint_buffer, 0, bytemuck::bytes_of(&self.tint)); + let tint_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_depth_tint"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM, + mapped_at_creation: true, + }); + { + let mut view = tint_buffer.slice(..).get_mapped_range_mut(); + let src = bytemuck::bytes_of(&self.tint); + view[..src.len()].copy_from_slice(src); + } + tint_buffer.unmap(); // Create material bind group - let bind_group_material = - renderer - .get_device() - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_depth_material_bg"), - layout: &pipeline.bind_group_layouts[0], // Group 0: Material bindings - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView( - &self.depth_texture.texture_view, - ), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &tint_buffer, - offset: 0, - size: Some(std::num::NonZeroU64::new(16).unwrap()), - }), - }, - ], - }); + let bind_group_material = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_depth_material_bg"), + layout: &bgl_material, // Group 0: Material bindings + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&self.depth_texture.texture_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &tint_buffer, + offset: 0, + size: Some(std::num::NonZeroU64::new(16).unwrap()), + }), + }, + ], + }); // Store pipeline handle - self.pipeline = Some(pipeline); + self.pipeline = Some(PipelineV2 { + pipeline, + bind_group_layouts: vec![bgl_material, bgl_rect], + }); self.bind_group_rect = Some(bind_group_rect); self.bind_group_material = Some(bind_group_material); self.tint_buffer = Some(tint_buffer); diff --git a/engine/pill_renderer/src/pass_overlay_logo.rs b/engine/pill_renderer/src/pass_overlay_logo.rs index 71083bd0..72fcfe01 100644 --- a/engine/pill_renderer/src/pass_overlay_logo.rs +++ b/engine/pill_renderer/src/pass_overlay_logo.rs @@ -1,4 +1,5 @@ use crate::renderer::{Pass, Renderer}; +use crate::resource_manager::ResourceManager; use anyhow::Result; use pill_engine::internal::{ BufferDesc, PillRenderer, PipelineV2, PipelineV2Desc, RendererTextureHandle, ShaderDesc, @@ -15,6 +16,7 @@ pub struct PassOverlayLogo { bind_group_material: Option, rect: [f32; 4], tint: [f32; 4], + target_format: wgpu::TextureFormat, } impl PassOverlayLogo { @@ -23,6 +25,7 @@ impl PassOverlayLogo { rect: [f32; 4], tint: [f32; 4], tex_logo: RendererTextureHandle, + target_format: wgpu::TextureFormat, ) -> Self { Self { label: label.to_string(), @@ -33,6 +36,7 @@ impl PassOverlayLogo { bind_group_material: None, rect: rect, tint: tint, + target_format, } } } @@ -42,16 +46,22 @@ impl Pass for PassOverlayLogo { &self.label } - fn init(&mut self, queue: &wgpu::Queue, renderer: &Renderer) -> Result<()> { + fn init(&mut self, device: &wgpu::Device, _res: &mut ResourceManager) -> Result<()> { println!("Initializing pass: {}", self.label); // Create buffer for overlay rect UBO - let buffer = renderer.create_buffer(BufferDesc { + let buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("overlay_logo_ubo"), - byte_size: 4 * 32, // 4 floats * 32 bytes per float = 128 bytes, will be aligned to 256 bytes for Metal UBOs - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - })?; - queue.write_buffer(&buffer, 0, bytemuck::bytes_of(&self.rect)); + size: 256, + usage: wgpu::BufferUsages::UNIFORM, + mapped_at_creation: true, + }); + { + let mut view = buffer.slice(..).get_mapped_range_mut(); + let src = bytemuck::bytes_of(&self.rect); + view[..src.len()].copy_from_slice(src); + } + buffer.unmap(); let vs = r#" @group(1) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] @@ -83,123 +93,145 @@ impl Pass for PassOverlayLogo { } "#; - let pipeline = renderer.create_pipeline_v2(PipelineV2Desc { - label: Some("overlay_logo"), - vs: ShaderDesc { - source: vs, - entry_func: "main", - }, - ps: ShaderDesc { - source: fs, - entry_func: "main", - }, - bind_groups: vec![ - // Group 0: Material bindings (texture, sampler, tint buffer) - vec![ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 2, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), - }, - count: None, - }, - ], - // Group 1: Rect UBO - vec![wgpu::BindGroupLayoutEntry { + // Build bind group layouts + let bgl_material = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_logo_material_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { binding: 0, - visibility: wgpu::ShaderStages::VERTEX, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), }, count: None, - }], + }, ], - targets: &[Some(wgpu::ColorTargetState { - format: renderer.get_surface_format(), - blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], + }); + let bgl_rect = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_logo_rect_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }], + }); + let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("overlay_logo_pl"), + bind_group_layouts: &[&bgl_material, &bgl_rect], + push_constant_ranges: &[], + }); + let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_logo_vs"), + source: wgpu::ShaderSource::Wgsl(vs.into()), + }); + let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_logo_fs"), + source: wgpu::ShaderSource::Wgsl(fs.into()), + }); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("overlay_logo_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fs_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: self.target_format, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), depth_stencil: None, multisample: wgpu::MultisampleState::default(), - })?; - - let bind_group_rect = renderer - .get_device() - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_rect_bind_group"), - layout: &pipeline.bind_group_layouts[1], // Group 1: Rect UBO - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: buffer.as_entire_binding(), - }], - }); + multiview: None, + }); - let storage = renderer.get_resource_storage().borrow(); - let tex = storage + let bind_group_rect = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_rect_bind_group"), + layout: &bgl_rect, // Group 1: Rect UBO + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + }); + + let tex = _res .textures .get(self.tex_logo) .expect("logo texture handle invalid"); let logo_texture_view = &tex.texture_view; let logo_sampler = &tex.sampler; - let tint_buffer = renderer - .get_device() - .create_buffer(&wgpu::BufferDescriptor { - label: Some("overlay_logo_tint"), - size: 256, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - queue.write_buffer(&tint_buffer, 0, bytemuck::bytes_of(&self.tint)); - - let bind_group_material = - renderer - .get_device() - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_logo_material_bg"), - layout: &pipeline.bind_group_layouts[0], // Group 0: Material bindings - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(logo_texture_view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(logo_sampler), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &tint_buffer, - offset: 0, - size: Some(std::num::NonZeroU64::new(16).unwrap()), - }), - }, - ], - }); + let tint_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_logo_tint"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM, + mapped_at_creation: true, + }); + { + let mut view = tint_buffer.slice(..).get_mapped_range_mut(); + let src = bytemuck::bytes_of(&self.tint); + view[..src.len()].copy_from_slice(src); + } + tint_buffer.unmap(); + + let bind_group_material = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_logo_material_bg"), + layout: &bgl_material, // Group 0: Material bindings + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(logo_texture_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(logo_sampler), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &tint_buffer, + offset: 0, + size: Some(std::num::NonZeroU64::new(16).unwrap()), + }), + }, + ], + }); // Store pipeline handle - self.pipeline = Some(pipeline); + self.pipeline = Some(PipelineV2 { + pipeline, + bind_group_layouts: vec![bgl_material, bgl_rect], + }); self.bind_group_rect = Some(bind_group_rect); self.bind_group_material = Some(bind_group_material); diff --git a/engine/pill_renderer/src/pass_overlay_uv.rs b/engine/pill_renderer/src/pass_overlay_uv.rs index 39d2d89d..8ebbfb82 100644 --- a/engine/pill_renderer/src/pass_overlay_uv.rs +++ b/engine/pill_renderer/src/pass_overlay_uv.rs @@ -1,4 +1,5 @@ use crate::renderer::{Pass, Renderer}; +use crate::resource_manager::ResourceManager; use anyhow::Result; use pill_engine::internal::{BufferDesc, PillRenderer, PipelineV2, PipelineV2Desc, ShaderDesc}; use wgpu::CommandEncoder; @@ -10,16 +11,18 @@ pub struct PassOverlayUV { pipeline: Option, bind_group: Option, rect: [f32; 4], + target_format: wgpu::TextureFormat, } impl PassOverlayUV { - pub fn new(label: &str, rect: [f32; 4]) -> Self { + pub fn new(label: &str, rect: [f32; 4], target_format: wgpu::TextureFormat) -> Self { Self { label: label.to_string(), buffer: None, pipeline: None, bind_group: None, rect: rect, + target_format, } } } @@ -29,16 +32,23 @@ impl Pass for PassOverlayUV { &self.label } - fn init(&mut self, queue: &wgpu::Queue, renderer: &Renderer) -> Result<()> { + fn init(&mut self, device: &wgpu::Device, _res: &mut ResourceManager) -> Result<()> { println!("Initializing pass: {}", self.label); // Create buffer for overlay rect UBO - let buffer = renderer.create_buffer(BufferDesc { + let aligned_size = 256u64; + let buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("overlay_rect_ubo"), - byte_size: 4 * 32, // 4 floats * 32 bytes per float = 128 bytes, will be aligned to 256 bytes for Metal UBOs - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - })?; - queue.write_buffer(&buffer, 0, bytemuck::bytes_of(&self.rect)); + size: aligned_size, + usage: wgpu::BufferUsages::UNIFORM, + mapped_at_creation: true, + }); + { + let mut view = buffer.slice(..).get_mapped_range_mut(); + let src = bytemuck::bytes_of(&self.rect); + view[..src.len()].copy_from_slice(src); + } + buffer.unmap(); let vs = r#" @group(0) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] @@ -66,17 +76,10 @@ impl Pass for PassOverlayUV { } "#; - let pipeline = renderer.create_pipeline_v2(PipelineV2Desc { - label: Some("overlay_uv"), - vs: ShaderDesc { - source: vs, - entry_func: "main", - }, - ps: ShaderDesc { - source: fs, - entry_func: "main", - }, - bind_groups: vec![vec![wgpu::BindGroupLayoutEntry { + // Build bind group layout and pipeline + let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_uv_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::VERTEX, ty: wgpu::BindingType::Buffer { @@ -85,21 +88,49 @@ impl Pass for PassOverlayUV { min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), }, count: None, - }]], - targets: &[Some(wgpu::ColorTargetState { - format: renderer.get_surface_format(), - blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], + }], + }); + let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("overlay_uv_pl"), + bind_group_layouts: &[&bgl], + push_constant_ranges: &[], + }); + let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_uv_vs"), + source: wgpu::ShaderSource::Wgsl(vs.into()), + }); + let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_uv_fs"), + source: wgpu::ShaderSource::Wgsl(fs.into()), + }); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("overlay_uv_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fs_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: self.target_format, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), depth_stencil: None, multisample: wgpu::MultisampleState::default(), - })?; + multiview: None, + }); - let bind_group = renderer - .get_device() - .create_bind_group(&wgpu::BindGroupDescriptor { + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("overlay_rect_bind_group"), - layout: &pipeline.bind_group_layouts[0], + layout: &bgl, entries: &[wgpu::BindGroupEntry { binding: 0, resource: buffer.as_entire_binding(), @@ -107,7 +138,7 @@ impl Pass for PassOverlayUV { }); // Store pipeline handle - self.pipeline = Some(pipeline); + self.pipeline = Some(PipelineV2 { pipeline, bind_group_layouts: vec![bgl] }); self.bind_group = Some(bind_group); Ok(()) diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 990d3dd0..8b864c36 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -7,14 +7,12 @@ use crate::{ pass_overlay_depth::PassOverlayDepth, pass_overlay_logo::PassOverlayLogo, pass_overlay_uv::PassOverlayUV, - renderer_resource_storage::RendererResourceStorage, + resource_manager::ResourceManager, resources::{ RendererCamera, RendererMaterial, RendererMesh, RendererPipeline, RendererTexture, Vertex, }, }; -use std::cell::RefCell; -use std::rc::Rc; use std::sync::Arc; use pill_engine::internal::{ @@ -26,7 +24,7 @@ use pill_engine::internal::{ RENDER_QUEUE_KEY_ORDER, }; -use pill_core::{PillSlotMapKey, PillSlotMapKeyData, PillStyle, RendererError, Timer}; +use pill_core::{Handle, PillSlotMapKey, PillStyle, RendererError, Timer}; use std::num::NonZeroU32; @@ -47,12 +45,7 @@ pub const INITIAL_INSTANCE_VECTOR_CAPACITY: usize = 10000; // M2 inline draw: no MeshDrawer/instance batching // Default resource handle - Master pipeline -pub const MASTER_PIPELINE_HANDLE: RendererPipelineHandle = RendererPipelineHandle { - 0: PillSlotMapKeyData { - index: 1, - version: unsafe { std::num::NonZeroU32::new_unchecked(1) }, - }, -}; +pub const MASTER_PIPELINE_HANDLE: RendererPipelineHandle = Handle::INVALID; fn compile_glsl_to_wgsl(source: &str, stage: naga::ShaderStage) -> Result { let mut frontend = glsl::Frontend::default(); @@ -117,7 +110,11 @@ fn create_render_target( pub trait Pass { fn get_label(&self) -> &str; - fn init(&mut self, queue: &wgpu::Queue, renderer: &Renderer) -> Result<()>; + fn init( + &mut self, + device: &wgpu::Device, + res: &mut crate::resource_manager::ResourceManager, + ) -> Result<()>; fn draw( &self, encoder: &mut wgpu::CommandEncoder, @@ -165,7 +162,7 @@ pub struct State { passes: Vec>, egui_renderer: crate::egui::EguiRenderer, // TODO: Separate system adding Pass // Resources and GPU objects moved from ctor into here explicitly - renderer_resource_storage: Rc>, + resource_manager: ResourceManager, color_format: wgpu::TextureFormat, depth_format: wgpu::TextureFormat, depth_texture: Arc, @@ -176,9 +173,9 @@ pub struct State { per_draw_buffer: Option, per_draw_bind_group_layout: wgpu::BindGroupLayout, per_draw_bind_group: Option, - // Prebuilt PSO (static pipeline) + // Prebuilt PSO handle // [SIMILAR] Prebuilt once; no per-draw pipeline churn per TALK - default_pipeline: RendererPipeline, + master_pipeline_handle: RendererPipelineHandle, // resources tex_logo: RendererTextureHandle, // Preallocated buffers for hot path optimization @@ -319,7 +316,7 @@ impl PillRenderer for Renderer { let state: State = { // Configure collections - let mut renderer_resource_storage = RendererResourceStorage::new(&ctx.config); + let mut resource_manager = ResourceManager::new(); // Create depth and color texture let depth_texture = RendererTexture::new_depth_texture( @@ -422,6 +419,7 @@ impl PillRenderer for Renderer { &[RendererMesh::data_layout_descriptor()], ) .unwrap(); + let master_handle = resource_manager.pipelines.insert(default_pipeline); // Create state let per_draw_bind_group_layout = @@ -456,7 +454,7 @@ impl PillRenderer for Renderer { TextureType::Color, ) .expect("failed to create overlay logo texture"); - renderer_resource_storage.textures.insert(tex) + resource_manager.textures.insert(tex) }; State { @@ -464,7 +462,7 @@ impl PillRenderer for Renderer { // Other egui_renderer, // Resources - renderer_resource_storage: Rc::new(RefCell::new(renderer_resource_storage)), + resource_manager, // Renderer variables color_format, depth_format, @@ -477,8 +475,8 @@ impl PillRenderer for Renderer { // [SIMILAR] Per-draw dynamic UBO layout with has_dynamic_offset=true per TALK (Milestone 5) per_draw_bind_group_layout, per_draw_bind_group: None, - // Prebuilt PSO - default_pipeline, + // Prebuilt PSO handle + master_pipeline_handle: master_handle, // Overlays tex_logo, // Preallocated buffers for hot path optimization (60k instances capacity) @@ -487,7 +485,6 @@ impl PillRenderer for Renderer { staging_buffer: Vec::with_capacity(100_000 * 144), // 60k instances * 144 bytes per instance } }; - Self { state, ctx } } @@ -497,11 +494,13 @@ impl PillRenderer for Renderer { self.state.passes.push(Box::new(PassCompose::new( "compose", self.state.offscreen_color_texture.clone(), + self.ctx.surface_configuration.format, ))); self.state.passes.push(Box::new(PassOverlayUV::new( "overlay_uv", [0.75, 0.75, 0.95, 0.95], + self.ctx.surface_configuration.format, ))); let h: f32 = 0.04; // Logo height @@ -510,6 +509,7 @@ impl PillRenderer for Renderer { [0.98 - 3. * h, 0.02, 0.98, 0.02 + h], // bottom right [1.0, 1.0, 1.0, 1.0], self.state.tex_logo, + self.ctx.surface_configuration.format, ))); self.state.passes.push(Box::new(PassOverlayDepth::new( @@ -517,21 +517,21 @@ impl PillRenderer for Renderer { [0.75, 0.50, 0.95, 0.70], [1.0, 1.0, 1.0, 1.0], self.state.depth_texture.clone(), + self.ctx.surface_configuration.format, ))); // Initialize passes - call init on each pass - let queue = &self.ctx.queue; // Temporarily move passes out to avoid borrowing conflicts let mut passes = std::mem::take(&mut self.state.passes); for pass in passes.iter_mut() { - let _ = pass.init(queue, self); + let _ = pass.init(&self.ctx.device, &mut self.state.resource_manager); } self.state.passes = passes; Ok(()) } - fn create_buffer(&self, desc: BufferDesc) -> Result { + fn create_buffer(&mut self, desc: BufferDesc) -> Result { let aligned_size = ((desc.byte_size + 255) / 256) * 256; // 256B for Metal UBOs let buffer = self.ctx.device.create_buffer(&wgpu::BufferDescriptor { label: desc.label, @@ -546,7 +546,7 @@ impl PillRenderer for Renderer { Ok(buffer) } - fn create_pipeline_v2(&self, desc: PipelineV2Desc) -> Result { + fn create_pipeline_v2(&mut self, desc: PipelineV2Desc) -> Result { /* // Example shader pipeline descriptor for factory method n_shader = rm->createShader({ @@ -609,8 +609,7 @@ impl PillRenderer for Renderer { .device .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some( - format!("{}{}_bgl_{}", desc.label.unwrap_or("program"), "_bgl", i) - .as_str(), + format!("{}{}{}", desc.label.unwrap_or("program"), "_bgl_", i).as_str(), ), entries: bindings, }); @@ -662,18 +661,15 @@ impl PillRenderer for Renderer { }) } - fn create_mesh(&self, name: &str, mesh_data: &MeshData) -> Result { + fn create_mesh(&mut self, name: &str, mesh_data: &MeshData) -> Result { let mesh = RendererMesh::new(&self.ctx.device, name, mesh_data)?; - let handle = unsafe { - let storage = &mut *self.state.renderer_resource_storage.as_ptr(); - storage.meshes.insert(mesh) - }; + let handle = self.state.resource_manager.meshes.insert(mesh); Ok(handle) } fn create_texture( - &self, + &mut self, name: &str, image_data: &image::DynamicImage, texture_type: TextureType, @@ -685,16 +681,13 @@ impl PillRenderer for Renderer { image_data, texture_type, )?; - let handle = unsafe { - let storage = &mut *self.state.renderer_resource_storage.as_ptr(); - storage.textures.insert(texture) - }; + let handle = self.state.resource_manager.textures.insert(texture); Ok(handle) } fn create_material( - &self, + &mut self, name: &str, textures: &MaterialTextureMap, parameters: &MaterialParameterMap, @@ -757,13 +750,17 @@ impl PillRenderer for Renderer { }], }); - let storage = self.state.renderer_resource_storage.borrow(); + // ensure default textures exist for material fallback + self.state + .resource_manager + .ensure_default_textures(&self.ctx.device, &self.ctx.queue); + let material = RendererMaterial::new( &self.ctx.device, &self.ctx.queue, - &storage, + &self.state.resource_manager, name, - MASTER_PIPELINE_HANDLE, + self.state.master_pipeline_handle, &material_texture_bind_group_layout, textures, &material_parameter_bind_group_layout, @@ -771,10 +768,7 @@ impl PillRenderer for Renderer { ) .unwrap(); - let handle = unsafe { - let storage = &mut *self.state.renderer_resource_storage.as_ptr(); - storage.materials.insert(material) - }; + let handle = self.state.resource_manager.materials.insert(material); Ok(handle) } @@ -797,10 +791,7 @@ impl PillRenderer for Renderer { }], }); let camera = RendererCamera::new(&self.ctx.device, &camera_bind_group_layout)?; - let handle = unsafe { - let storage = &mut *self.state.renderer_resource_storage.as_ptr(); - storage.cameras.insert(camera) - }; + let handle = self.state.resource_manager.cameras.insert(camera); Ok(handle) } @@ -810,15 +801,13 @@ impl PillRenderer for Renderer { renderer_material_handle: RendererMaterialHandle, textures: &MaterialTextureMap, ) -> Result<()> { - unsafe { - let storage = &mut *self.state.renderer_resource_storage.as_ptr(); - RendererMaterial::update_textures( - &self.ctx.device, - renderer_material_handle, - storage, - textures, - ) - } + RendererMaterial::update_textures( + &self.ctx.device, + &self.ctx.queue, + renderer_material_handle, + &mut self.state.resource_manager, + textures, + ) } fn update_material_parameters( @@ -826,50 +815,51 @@ impl PillRenderer for Renderer { renderer_material_handle: RendererMaterialHandle, parameters: &MaterialParameterMap, ) -> Result<()> { - unsafe { - let storage = &mut *self.state.renderer_resource_storage.as_ptr(); - RendererMaterial::update_parameters( - &self.ctx.device, - &self.ctx.queue, - renderer_material_handle, - storage, - parameters, - ) - } + RendererMaterial::update_parameters( + &self.ctx.device, + &self.ctx.queue, + renderer_material_handle, + &mut self.state.resource_manager, + parameters, + ) } fn destroy_mesh(&mut self, renderer_mesh_handle: RendererMeshHandle) -> Result<()> { - unsafe { - let storage = &mut *self.state.renderer_resource_storage.as_ptr(); - storage.meshes.remove(renderer_mesh_handle).unwrap(); - } + self.state + .resource_manager + .meshes + .remove(renderer_mesh_handle) + .unwrap(); Ok(()) } fn destroy_texture(&mut self, renderer_texture_handle: RendererTextureHandle) -> Result<()> { - unsafe { - let storage = &mut *self.state.renderer_resource_storage.as_ptr(); - storage.textures.remove(renderer_texture_handle).unwrap(); - } + self.state + .resource_manager + .textures + .remove(renderer_texture_handle) + .unwrap(); Ok(()) } fn destroy_material(&mut self, renderer_material_handle: RendererMaterialHandle) -> Result<()> { - unsafe { - let storage = &mut *self.state.renderer_resource_storage.as_ptr(); - storage.materials.remove(renderer_material_handle).unwrap(); - } + self.state + .resource_manager + .materials + .remove(renderer_material_handle) + .unwrap(); Ok(()) } fn destroy_camera(&mut self, renderer_camera_handle: RendererCameraHandle) -> Result<()> { - unsafe { - let storage = &mut *self.state.renderer_resource_storage.as_ptr(); - storage.cameras.remove(renderer_camera_handle).unwrap(); - } + self.state + .resource_manager + .cameras + .remove(renderer_camera_handle) + .unwrap(); Ok(()) } @@ -921,8 +911,11 @@ impl PillRenderer for Renderer { timer: &mut Timer, ) -> Result<()> { // [SIMILAR] Prebuilt PSO used; avoid hot-path pipeline creation per TALK - // Use prebuilt default_pipeline for bind group layouts and pipeline - let _per_draw_bind_group_layout = &self.state.default_pipeline.per_draw_bind_group_layout; + let _per_draw_bind_group_layout = &self + .state + .resource_manager + .pipeline(self.state.master_pipeline_handle) + .per_draw_bind_group_layout; // M3: Hello Mesh + per-draw MVP (dynamic offsets) timer.record("Frame: acquire"); @@ -945,15 +938,14 @@ impl PillRenderer for Renderer { .get(active_camera_entity_handle.data().index as usize) .unwrap(); let active_camera_component = camera_storage.as_ref().unwrap(); - let renderer_camera = unsafe { - let storage = &mut *self.state.renderer_resource_storage.as_ptr(); - storage - .cameras - .get_mut(get_renderer_resource_handle_from_camera_component( - active_camera_component, - )) - .ok_or(Error::new(RendererError::RendererResourceNotFound))? - }; + let renderer_camera = self + .state + .resource_manager + .cameras + .get_mut(get_renderer_resource_handle_from_camera_component( + active_camera_component, + )) + .ok_or(Error::new(RendererError::RendererResourceNotFound))?; let camera_transform_storage = transform_component_storage .data .get(active_camera_entity_handle.data().index as usize) @@ -964,8 +956,9 @@ impl PillRenderer for Renderer { active_camera_component, active_camera_transform_component, ); - let storage = self.state.renderer_resource_storage.borrow(); - let renderer_camera = storage + let renderer_camera = self + .state + .resource_manager .cameras .get(get_renderer_resource_handle_from_camera_component( active_camera_component, @@ -1024,16 +1017,18 @@ impl PillRenderer for Renderer { for render_queue_item in render_queue.iter() { let key = pill_engine::internal::decompose_render_queue_key(render_queue_item.key); - let mesh_handle = RendererMeshHandle::new( - key.mesh_index.into(), - NonZeroU32::new(key.mesh_version.into()).unwrap(), - ); - let material_handle = RendererMaterialHandle::new( - key.material_index.into(), - NonZeroU32::new(key.material_version.into()).unwrap(), + let mesh_handle = + RendererMeshHandle::from_parts(key.mesh_index as u32, key.mesh_version as u32); + let material_handle = RendererMaterialHandle::from_parts( + key.material_index as u32, + key.material_version as u32, ); - let storage = self.state.renderer_resource_storage.borrow(); - let material_for_pipeline = storage.materials.get(material_handle).unwrap(); + let material_for_pipeline = self + .state + .resource_manager + .materials + .get(material_handle) + .unwrap(); let pipeline_handle = material_for_pipeline.pipeline_handle; // Transform and model @@ -1048,8 +1043,7 @@ impl PillRenderer for Renderer { let model: Mat4 = Mat4::from_cols_array_2d(&model_arr); // World AABB from mesh local AABB using SIMD operations - let storage = self.state.renderer_resource_storage.borrow(); - let mesh = storage.meshes.get(mesh_handle).unwrap(); + let mesh = self.state.resource_manager.meshes.get(mesh_handle).unwrap(); let local_min = Vec3::new(mesh.aabb_min[0], mesh.aabb_min[1], mesh.aabb_min[2]); let local_max = Vec3::new(mesh.aabb_max[0], mesh.aabb_max[1], mesh.aabb_max[2]); @@ -1126,9 +1120,9 @@ impl PillRenderer for Renderer { // Sort by pipeline -> material -> mesh to minimize state changes self.state.visible_pre_draw_buffer.sort_by_key(|v| { ( - v.pipeline_handle.data().index, - v.material_handle.data().index, - v.mesh_handle.data().index, + v.pipeline_handle.index(), + v.material_handle.index(), + v.mesh_handle.index(), ) }); @@ -1204,20 +1198,26 @@ impl PillRenderer for Renderer { mapped_at_creation: false, })); // ALLOCATION: BindGroup creation (GPU memory) - ~128 bytes metadata - self.state.per_draw_bind_group = Some(self.ctx.device.create_bind_group( - &wgpu::BindGroupDescriptor { - label: Some("per_draw_bind_group"), - layout: &self.state.default_pipeline.per_draw_bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: self.state.per_draw_buffer.as_ref().unwrap(), - offset: 0, - size: Some(std::num::NonZeroU64::new(144).unwrap()), - }), - }], - }, - )); + self.state.per_draw_bind_group = Some( + self.ctx + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("per_draw_bind_group"), + layout: &self + .state + .resource_manager + .pipeline(self.state.master_pipeline_handle) + .per_draw_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: self.state.per_draw_buffer.as_ref().unwrap(), + offset: 0, + size: Some(std::num::NonZeroU64::new(144).unwrap()), + }), + }], + }), + ); } #[repr(C)] #[derive(Copy, Clone)] @@ -1291,10 +1291,16 @@ impl PillRenderer for Renderer { for group in &self.state.groups_buffer { timer.begin_context(&format!( "Pipeline {} / Material {}", - group.pipeline_handle.data().index, - group.material_handle.data().index + group.pipeline_handle.index(), + group.material_handle.index() )); - rpass.set_pipeline(&self.state.default_pipeline.render_pipeline); + rpass.set_pipeline( + &self + .state + .resource_manager + .pipeline(self.state.master_pipeline_handle) + .render_pipeline, + ); // HOT SoA (per-frame hot path): // - RendererMaterial: texture_bind_group, parameter_bind_group @@ -1303,20 +1309,24 @@ impl PillRenderer for Renderer { // COLD metadata (names/descs) lives in *_cold arrays and is not read in the draw loop. // Get material and set bind groups - let material = unsafe { - let storage = &*self.state.renderer_resource_storage.as_ptr(); - storage.materials.get(group.material_handle).unwrap() - }; + let material = self + .state + .resource_manager + .materials + .get(group.material_handle) + .unwrap(); rpass.set_bind_group(0, &renderer_camera.bind_group, &[]); rpass.set_bind_group(1, &material.texture_bind_group, &[]); rpass.set_bind_group(2, &material.parameter_bind_group, &[]); // Process batches with separate storage borrows to avoid lifetime issues for batch in &group.batches { - let mesh = unsafe { - let storage = &*self.state.renderer_resource_storage.as_ptr(); - storage.meshes.get(batch.mesh_handle).unwrap() - }; + let mesh = self + .state + .resource_manager + .meshes + .get(batch.mesh_handle) + .unwrap(); // [API->CLIENT] Provide packed mesh atlas + per-draw base offsets so low-level can draw without re-binding buffers // [DIFF] VB/IB still rebound per batch; packing meshes could avoid rebinding rpass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); @@ -1427,8 +1437,4 @@ impl Renderer { pub fn get_surface_view(&self) -> &wgpu::TextureView { &self.state.offscreen_color_texture.texture_view } - - pub fn get_resource_storage(&self) -> &Rc> { - &self.state.renderer_resource_storage - } } diff --git a/engine/pill_renderer/src/resource_manager.rs b/engine/pill_renderer/src/resource_manager.rs new file mode 100644 index 00000000..512b42ed --- /dev/null +++ b/engine/pill_renderer/src/resource_manager.rs @@ -0,0 +1,119 @@ +use std::collections::HashMap; + +use pill_core::{ + Handle, RendererBufferTag, RendererCameraTag, RendererMaterialTag, RendererMeshTag, + RendererPipelineTag, RendererTextureTag, ResourcePool, +}; +use pill_engine::internal::{ + RendererBufferHandle, RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, + RendererPipelineHandle, RendererTextureHandle, +}; + +use crate::resources::{ + RendererCamera, RendererMaterial, RendererMesh, RendererPipeline, RendererTexture, +}; +use image::{DynamicImage, Rgba, RgbaImage}; + +#[derive(Hash, PartialEq, Eq, Clone, Copy)] +pub struct PipelineKey(pub u64); + +pub struct PipelineCache { + key_to_handle: HashMap, +} + +impl PipelineCache { + pub fn new() -> Self { + Self { + key_to_handle: HashMap::new(), + } + } + + pub fn lookup(&self, key: &PipelineKey) -> Option { + self.key_to_handle.get(key).copied() + } + + pub fn insert(&mut self, key: PipelineKey, h: RendererPipelineHandle) { + self.key_to_handle.insert(key, h); + } +} + +pub struct ResourceManager { + pub buffers: ResourcePool, + pub pipelines: ResourcePool, + pub textures: ResourcePool, + pub materials: ResourcePool, + pub meshes: ResourcePool, + pub cameras: ResourcePool, + pub pipeline_cache: PipelineCache, + // cached defaults + pub default_color_tex: Option, + pub default_normal_tex: Option, +} + +impl ResourceManager { + pub fn new() -> Self { + Self { + buffers: ResourcePool::new(), + pipelines: ResourcePool::new(), + textures: ResourcePool::new(), + materials: ResourcePool::new(), + meshes: ResourcePool::new(), + cameras: ResourcePool::new(), + pipeline_cache: PipelineCache::new(), + default_color_tex: None, + default_normal_tex: None, + } + } + + #[inline] + pub fn buffer(&self, h: RendererBufferHandle) -> &wgpu::Buffer { + self.buffers.get(h).expect("Invalid buffer handle") + } + + #[inline] + pub fn pipeline(&self, h: RendererPipelineHandle) -> &RendererPipeline { + self.pipelines.get(h).expect("Invalid pipeline handle") + } + + pub fn ensure_default_textures( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + ) -> (RendererTextureHandle, RendererTextureHandle) { + if self.default_color_tex.is_none() { + let mut img = RgbaImage::new(1, 1); + img.put_pixel(0, 0, Rgba([255, 255, 255, 255])); + let dyn_img = DynamicImage::ImageRgba8(img); + let tex = RendererTexture::new_texture( + device, + queue, + Some("default_color"), + &dyn_img, + pill_engine::internal::TextureType::Color, + ) + .expect("create default color tex"); + let h = self.textures.insert(tex); + self.default_color_tex = Some(h); + } + if self.default_normal_tex.is_none() { + let mut img = RgbaImage::new(1, 1); + // normal map flat (128,128,255) + img.put_pixel(0, 0, Rgba([128, 128, 255, 255])); + let dyn_img = DynamicImage::ImageRgba8(img); + let tex = RendererTexture::new_texture( + device, + queue, + Some("default_normal"), + &dyn_img, + pill_engine::internal::TextureType::Normal, + ) + .expect("create default normal tex"); + let h = self.textures.insert(tex); + self.default_normal_tex = Some(h); + } + ( + self.default_color_tex.unwrap(), + self.default_normal_tex.unwrap(), + ) + } +} diff --git a/engine/pill_renderer/src/resource_snapshot.rs b/engine/pill_renderer/src/resource_snapshot.rs new file mode 100644 index 00000000..175352e9 --- /dev/null +++ b/engine/pill_renderer/src/resource_snapshot.rs @@ -0,0 +1,28 @@ +use pill_core::{ResourcePool, RendererBufferTag, RendererCameraTag, RendererMaterialTag, RendererMeshTag, RendererPipelineTag, RendererTextureTag}; + +use crate::resources::{RendererCamera, RendererMaterial, RendererMesh, RendererTexture, RendererPipeline}; +use crate::resource_manager::ResourceManager; + +pub struct ResourceSnapshot { + pub buffers: ResourcePool, + pub pipelines: ResourcePool, + pub textures: ResourcePool, + pub materials: ResourcePool, + pub meshes: ResourcePool, + pub cameras: ResourcePool, +} + +impl From for ResourceSnapshot { + fn from(m: ResourceManager) -> Self { + Self { + buffers: m.buffers, + pipelines: m.pipelines, + textures: m.textures, + materials: m.materials, + meshes: m.meshes, + cameras: m.cameras, + } + } +} + + diff --git a/engine/pill_renderer/src/resources/renderer_material.rs b/engine/pill_renderer/src/resources/renderer_material.rs index 51dd627e..3f2d1c9d 100644 --- a/engine/pill_renderer/src/resources/renderer_material.rs +++ b/engine/pill_renderer/src/resources/renderer_material.rs @@ -1,18 +1,15 @@ -use crate::RendererResourceStorage; +use crate::resource_manager::ResourceManager; use pill_core::RendererError; use pill_engine::internal::{ - MaterialParameterMap, - MaterialTextureMap, - RendererMaterialHandle, - RendererPipelineHandle, - MASTER_SHADER_COLOR_TEXTURE_SLOT, - MASTER_SHADER_NORMAL_TEXTURE_SLOT, - MASTER_SHADER_TINT_PARAMETER_SLOT, get_default_texture_handles, get_renderer_texture_handle_from_material_texture, MASTER_SHADER_SPECULARITY_PARAMETER_SLOT, + get_default_texture_handles, get_renderer_texture_handle_from_material_texture, + MaterialParameterMap, MaterialTextureMap, RendererMaterialHandle, RendererPipelineHandle, + MASTER_SHADER_COLOR_TEXTURE_SLOT, MASTER_SHADER_NORMAL_TEXTURE_SLOT, + MASTER_SHADER_SPECULARITY_PARAMETER_SLOT, MASTER_SHADER_TINT_PARAMETER_SLOT, }; +use anyhow::{Error, Result}; use wgpu::util::DeviceExt; -use anyhow::{ Result, Error}; // --- Material Uniform --- @@ -26,7 +23,7 @@ pub(crate) struct MaterialUniform { impl MaterialUniform { pub fn new() -> Self { Self { - tint: cgmath::Vector3::::new(0.0,0.0,0.0).into(), + tint: cgmath::Vector3::::new(0.0, 0.0, 0.0).into(), specularity: 0.0, } } @@ -46,8 +43,8 @@ pub struct RendererMaterial { impl RendererMaterial { pub fn new( device: &wgpu::Device, - queue: &wgpu::Queue, - rendering_resource_storage: &RendererResourceStorage, + queue: &wgpu::Queue, + resources: &ResourceManager, name: &str, pipeline_handle: RendererPipelineHandle, texture_bind_group_layout: &wgpu::BindGroupLayout, @@ -55,7 +52,6 @@ impl RendererMaterial { parameter_bind_group_layout: &wgpu::BindGroupLayout, parameters: &MaterialParameterMap, ) -> Result { - // Create parameter buffer and write data to it let mut uniform = MaterialUniform::new(); let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { @@ -63,14 +59,20 @@ impl RendererMaterial { contents: bytemuck::cast_slice(&[uniform]), usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, }); - uniform.tint = parameters.get_color(MASTER_SHADER_TINT_PARAMETER_SLOT).unwrap().into(); - uniform.specularity = parameters.get_scalar(MASTER_SHADER_SPECULARITY_PARAMETER_SLOT).unwrap().into(); + uniform.tint = parameters + .get_color(MASTER_SHADER_TINT_PARAMETER_SLOT) + .unwrap() + .into(); + uniform.specularity = parameters + .get_scalar(MASTER_SHADER_SPECULARITY_PARAMETER_SLOT) + .unwrap() + .into(); queue.write_buffer(&buffer, 0, bytemuck::cast_slice(&[uniform])); // Create texture binding group let texture_bind_group = Self::create_texture_bind_group( device, - rendering_resource_storage, + resources, &texture_bind_group_layout, &(name.to_owned() + "_textures"), textures, @@ -97,49 +99,74 @@ impl RendererMaterial { } pub fn update_textures( - device: &wgpu::Device, + device: &wgpu::Device, + queue: &wgpu::Queue, material_renderer_handle: RendererMaterialHandle, - rendering_resource_storage: &mut RendererResourceStorage, - textures: &MaterialTextureMap + resources: &mut ResourceManager, + textures: &MaterialTextureMap, ) -> Result<()> { - let material = rendering_resource_storage.materials.get(material_renderer_handle).ok_or(Error::new(RendererError::RendererResourceNotFound))?; + // Ensure defaults are available for fallback + let _ = resources.ensure_default_textures(device, queue); + let material = resources + .materials + .get(material_renderer_handle) + .ok_or(Error::new(RendererError::RendererResourceNotFound))?; let material_name = material.name.clone(); - let pipeline = rendering_resource_storage.pipelines.get(material.pipeline_handle).ok_or(Error::new(RendererError::RendererResourceNotFound))?; - - let texture_bind_group = RendererMaterial::create_texture_bind_group( - &device, - &rendering_resource_storage, - &pipeline.material_texture_bind_group_layout, - &(material_name.to_owned() + "_textures"), - textures - )?; + let texture_bind_group = { + let pipeline = resources + .pipelines + .get(material.pipeline_handle) + .ok_or(Error::new(RendererError::RendererResourceNotFound))?; + RendererMaterial::create_texture_bind_group( + &device, + resources, + &pipeline.material_texture_bind_group_layout, + &(material_name.to_owned() + "_textures"), + textures, + )? + }; - let material = rendering_resource_storage.materials.get_mut(material_renderer_handle).ok_or(Error::new(RendererError::RendererResourceNotFound))?; + let material = resources + .materials + .get_mut(material_renderer_handle) + .ok_or(Error::new(RendererError::RendererResourceNotFound))?; material.texture_bind_group = texture_bind_group; Ok(()) } pub fn create_texture_bind_group( - device: &wgpu::Device, - rendering_resource_storage: &RendererResourceStorage, + device: &wgpu::Device, + resources: &ResourceManager, texture_bind_group_layout: &wgpu::BindGroupLayout, name: &str, - textures: &MaterialTextureMap + textures: &MaterialTextureMap, ) -> Result { - // Get texture renderer handles, if is it None use default texture for this type of slot let color_texture = textures.data.get(MASTER_SHADER_COLOR_TEXTURE_SLOT).unwrap(); - let color_renderer_texture_handle = - get_renderer_texture_handle_from_material_texture(color_texture) - .unwrap_or_else(|| get_default_texture_handles(color_texture.texture_type).1); - - let normal_texture = textures.data.get(MASTER_SHADER_NORMAL_TEXTURE_SLOT).unwrap(); - let normal_renderer_textur_handle = - get_renderer_texture_handle_from_material_texture(normal_texture) - .unwrap_or_else(|| get_default_texture_handles(normal_texture.texture_type).1); - - let color_texture = rendering_resource_storage.textures.get(color_renderer_texture_handle).unwrap(); - let normal_texture = rendering_resource_storage.textures.get(normal_renderer_textur_handle).unwrap(); + let def_color = resources + .default_color_tex + .expect("default color tex must exist"); + let def_normal = resources + .default_normal_tex + .expect("default normal tex must exist"); + let color_renderer_texture_handle = + get_renderer_texture_handle_from_material_texture(color_texture).unwrap_or(def_color); + + let normal_texture = textures + .data + .get(MASTER_SHADER_NORMAL_TEXTURE_SLOT) + .unwrap(); + let normal_renderer_textur_handle = + get_renderer_texture_handle_from_material_texture(normal_texture).unwrap_or(def_normal); + + let color_texture = resources + .textures + .get(color_renderer_texture_handle) + .ok_or(Error::new(RendererError::RendererResourceNotFound))?; + let normal_texture = resources + .textures + .get(normal_renderer_textur_handle) + .ok_or(Error::new(RendererError::RendererResourceNotFound))?; // Set texture resources to the bind group let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { @@ -169,43 +196,57 @@ impl RendererMaterial { } pub fn update_parameters( - device: &wgpu::Device, - queue: &wgpu::Queue, + device: &wgpu::Device, + queue: &wgpu::Queue, material_renderer_handle: RendererMaterialHandle, - rendering_resource_storage: &mut RendererResourceStorage, - parameters: &MaterialParameterMap + resources: &mut ResourceManager, + parameters: &MaterialParameterMap, ) -> Result<()> { - let material = rendering_resource_storage.materials.get_mut(material_renderer_handle).ok_or(Error::new(RendererError::RendererResourceNotFound))?; - let pipeline = rendering_resource_storage.pipelines.get(material.pipeline_handle).ok_or(Error::new(RendererError::RendererResourceNotFound))?; - - material.uniform.tint = parameters.get_color(MASTER_SHADER_TINT_PARAMETER_SLOT).unwrap().into(); - material.uniform.specularity = parameters.get_scalar(MASTER_SHADER_SPECULARITY_PARAMETER_SLOT).unwrap().into(); - queue.write_buffer(&material.buffer, 0, bytemuck::cast_slice(&[material.uniform])); + let material = resources + .materials + .get_mut(material_renderer_handle) + .ok_or(Error::new(RendererError::RendererResourceNotFound))?; + let pipeline = resources + .pipelines + .get(material.pipeline_handle) + .ok_or(Error::new(RendererError::RendererResourceNotFound))?; + + material.uniform.tint = parameters + .get_color(MASTER_SHADER_TINT_PARAMETER_SLOT) + .unwrap() + .into(); + material.uniform.specularity = parameters + .get_scalar(MASTER_SHADER_SPECULARITY_PARAMETER_SLOT) + .unwrap() + .into(); + queue.write_buffer( + &material.buffer, + 0, + bytemuck::cast_slice(&[material.uniform]), + ); material.parameter_bind_group = RendererMaterial::create_parameter_bind_group( - device, - &pipeline.material_parameter_bind_group_layout, - &(material.name.to_owned() + "_parameter"), - &material.buffer + device, + &pipeline.material_parameter_bind_group_layout, + &(material.name.to_owned() + "_parameter"), + &material.buffer, )?; Ok(()) } fn create_parameter_bind_group( - device: &wgpu::Device, + device: &wgpu::Device, parameter_bind_group_layout: &wgpu::BindGroupLayout, name: &str, buffer: &wgpu::Buffer, ) -> Result { let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { layout: ¶meter_bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: buffer.as_entire_binding(), - }, - ], + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], label: Some(name), }); From 4a619f536bd913996cbfc83ac36c07a606ec5084 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Sun, 2 Nov 2025 05:14:49 +0100 Subject: [PATCH 51/69] wip: pass scene --- .../src/ecs/systems/rendering_system.rs | 48 +- engine/pill_engine/src/graphics/mod.rs | 2 + .../pill_engine/src/graphics/render_queue.rs | 21 + engine/pill_engine/src/graphics/renderer.rs | 53 +- engine/pill_renderer/src/lib.rs | 1 + engine/pill_renderer/src/pass_compose.rs | 7 +- .../pill_renderer/src/pass_overlay_depth.rs | 7 +- engine/pill_renderer/src/pass_overlay_logo.rs | 7 +- engine/pill_renderer/src/pass_overlay_uv.rs | 24 +- engine/pill_renderer/src/pass_scene.rs | 405 +++++++++++++ engine/pill_renderer/src/renderer.rs | 543 ++---------------- 11 files changed, 592 insertions(+), 526 deletions(-) create mode 100644 engine/pill_renderer/src/pass_scene.rs diff --git a/engine/pill_engine/src/ecs/systems/rendering_system.rs b/engine/pill_engine/src/ecs/systems/rendering_system.rs index d13249d2..9a3bb4fe 100644 --- a/engine/pill_engine/src/ecs/systems/rendering_system.rs +++ b/engine/pill_engine/src/ecs/systems/rendering_system.rs @@ -6,7 +6,9 @@ use crate::{ TransformComponent, UpdatePhase, }, engine::Engine, - graphics::{compose_render_queue_key, RenderQueueItem, RenderQueueKey}, + graphics::{ + compose_render_queue_key, RenderQuery, RenderQueueFactory, RenderQueueItem, RenderQueueKey, + }, resources::{Material, MaterialHandle, Mesh, MeshHandle, ResourceManager}, }; @@ -169,33 +171,29 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { let egui_ui = EguiManagerComponent::get_ui(engine); // egui_manager_component.get_ui(engine); - let active_scene = engine.scene_manager.get_active_scene_mut()?; - // Get storages - let camera_component_storage = active_scene - .get_component_storage::() - .context(format!( - "{}: Cannot get active {}", - "rendering_system".sobj_style(), - "Camera".gobj_style() - ))?; - let transform_component_storage = active_scene - .get_component_storage::() - .context(format!( - "{}: Cannot get {}", - "rendering_system".sobj_style(), - "TransformComponents".sobj_style() - )) - .unwrap(); + // Storages fetched inside renderer via immutable Engine timer.begin_context("Render"); - // Render - // [API->CLIENT] Low-level renderer expects ordered render_queue and stable storages; scene graph culling/binning lives in client/high-level - match engine.renderer.render( - active_camera_entity_handle, - &engine.render_queue, - camera_component_storage, - transform_component_storage, + // Build WorldView with raw pointers to avoid borrow conflicts + let world_view = { + let active_scene = engine.scene_manager.get_active_scene()?; + let camera_components = + active_scene.get_component_storage::()? as *const _; + let transform_components = + active_scene.get_component_storage::()? as *const _; + let render_queue_ptr = &engine.render_queue as *const _; + crate::graphics::WorldView { + active_camera: active_camera_entity_handle, + render_queue_ptr, + camera_components_ptr: camera_components, + transform_components_ptr: transform_components, + } + }; + let factory = crate::graphics::WorldViewFactory { world: world_view }; + match crate::graphics::render_with_factory( + engine.renderer.as_mut(), + &factory, egui_ui, &mut timer, ) { diff --git a/engine/pill_engine/src/graphics/mod.rs b/engine/pill_engine/src/graphics/mod.rs index e24f82df..6dee7492 100644 --- a/engine/pill_engine/src/graphics/mod.rs +++ b/engine/pill_engine/src/graphics/mod.rs @@ -9,6 +9,7 @@ pub use pill_core::{ RendererBufferTag, RendererCameraTag, RendererMaterialTag, RendererMeshTag, RendererPipelineTag, RendererPipelineV2Tag, RendererTextureTag, }; +pub use renderer::{render_with_factory, WorldView, WorldViewFactory}; pub use renderer::{ BufferDesc, PillRenderer, PipelineV2, PipelineV2Desc, Renderer, RendererBufferHandle, RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, RendererPipelineHandle, @@ -19,3 +20,4 @@ pub use render_queue::{ compose_render_queue_key, decompose_render_queue_key, RenderQueueItem, RenderQueueKey, RenderQueueKeyFields, RENDER_QUEUE_KEY_ORDER, }; +pub use render_queue::{RenderQuery, RenderQueueFactory}; diff --git a/engine/pill_engine/src/graphics/render_queue.rs b/engine/pill_engine/src/graphics/render_queue.rs index a5fffb4a..2c2b3df0 100644 --- a/engine/pill_engine/src/graphics/render_queue.rs +++ b/engine/pill_engine/src/graphics/render_queue.rs @@ -174,4 +174,25 @@ lazy_static! { // This will be initialized in runtime instead of compile-time (t pub static ref RENDER_QUEUE_KEY_MATERIAL_VERSION: RenderQueueField = RenderQueueField::::new(get_render_queue_key_item_range(RENDER_QUEUE_KEY_MATERIAL_VERSION_IDX)); pub static ref RENDER_QUEUE_KEY_MESH_INDEX: RenderQueueField = RenderQueueField::::new(get_render_queue_key_item_range(RENDER_QUEUE_KEY_MESH_INDEX_IDX)); pub static ref RENDER_QUEUE_KEY_MESH_VERSION: RenderQueueField = RenderQueueField::::new(get_render_queue_key_item_range(RENDER_QUEUE_KEY_MESH_VERSION_IDX)); +} + +// --- Borrow-only render query bundle and alias --- + +use crate::ecs::{CameraComponent, ComponentStorage, EntityHandle, TransformComponent}; + +/// Borrow-only bundle of references used by the renderer hot path. +/// Zero-cost: constructed by the caller and passed by value; no allocations/copies. +pub struct RenderQueueRefs<'a> { + pub active_camera: EntityHandle, + pub render_queue: &'a Vec, + pub camera_components: &'a ComponentStorage, + pub transform_components: &'a ComponentStorage, +} + +/// Back-compat alias to match existing plan/task wording. +pub type RenderQuery<'a> = RenderQueueRefs<'a>; + +/// Zero-cost factory trait producing borrowed render query refs. +pub trait RenderQueueFactory { + fn get<'a>(&'a self) -> RenderQuery<'a>; } \ No newline at end of file diff --git a/engine/pill_engine/src/graphics/renderer.rs b/engine/pill_engine/src/graphics/renderer.rs index 6706cfb3..67c9304a 100644 --- a/engine/pill_engine/src/graphics/renderer.rs +++ b/engine/pill_engine/src/graphics/renderer.rs @@ -1,7 +1,7 @@ use crate::{ ecs::{CameraComponent, ComponentStorage, EntityHandle, TransformComponent}, engine::Engine, - graphics::RenderQueueItem, + graphics::{RenderQuery, RenderQueueFactory, RenderQueueItem}, resources::{ MaterialHandle, MaterialParameterMap, MaterialTextureMap, MeshData, MeshHandle, TextureHandle, TextureType, @@ -123,3 +123,54 @@ pub trait PillRenderer { } pub type Renderer = Box; + +// --- Zero-cost factory helper -------------------------------------------------- + +/// Renders using a zero-cost factory that provides borrowed references. +/// Keeps the PillRenderer trait object-safe while allowing call sites to be generic and inlined. +#[inline(always)] +pub fn render_with_factory( + renderer: &mut R, + factory: &F, + egui_ui: Box, + timer: &mut Timer, +) -> Result<()> +where + R: PillRenderer + ?Sized, + F: RenderQueueFactory, +{ + let q = factory.get(); + renderer.render( + q.active_camera, + q.render_queue, + q.camera_components, + q.transform_components, + egui_ui, + timer, + ) +} + +// WorldView raw-pointer based view to avoid borrow conflicts at call site +pub struct WorldView { + pub active_camera: crate::ecs::EntityHandle, + pub render_queue_ptr: *const Vec, + pub camera_components_ptr: *const ComponentStorage, + pub transform_components_ptr: *const ComponentStorage, +} + +pub struct WorldViewFactory { + pub world: WorldView, +} + +impl RenderQueueFactory for WorldViewFactory { + fn get<'b>(&'b self) -> RenderQuery<'b> { + unsafe { + RenderQuery { + active_camera: self.world.active_camera, + render_queue: &*self.world.render_queue_ptr, + camera_components: &*self.world.camera_components_ptr, + transform_components: &*self.world.transform_components_ptr, + } + } + } +} diff --git a/engine/pill_renderer/src/lib.rs b/engine/pill_renderer/src/lib.rs index 7d45bb09..5b37f506 100644 --- a/engine/pill_renderer/src/lib.rs +++ b/engine/pill_renderer/src/lib.rs @@ -6,6 +6,7 @@ pub mod pass_compose; pub mod pass_overlay_depth; pub mod pass_overlay_logo; pub mod pass_overlay_uv; +pub mod pass_scene; pub mod renderer; pub mod resource_manager; pub mod resource_snapshot; diff --git a/engine/pill_renderer/src/pass_compose.rs b/engine/pill_renderer/src/pass_compose.rs index d27d6185..f125cbaf 100644 --- a/engine/pill_renderer/src/pass_compose.rs +++ b/engine/pill_renderer/src/pass_compose.rs @@ -1,4 +1,4 @@ -use crate::renderer::{Pass, Renderer}; +use crate::renderer::{Pass, Renderer, WorldQuery}; use crate::resource_manager::ResourceManager; use crate::resources::RendererTexture; use anyhow::Result; @@ -165,11 +165,12 @@ impl Pass for PassCompose { } fn draw( - &self, + &mut self, encoder: &mut CommandEncoder, - _renderer: &Renderer, + _renderer: &mut Renderer, _frame: &wgpu::SurfaceTexture, view: &wgpu::TextureView, + _world: &WorldQuery, ) -> Result<()> { // Create render pass for this pass using the provided frame let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { diff --git a/engine/pill_renderer/src/pass_overlay_depth.rs b/engine/pill_renderer/src/pass_overlay_depth.rs index 8a50af13..cff073b1 100644 --- a/engine/pill_renderer/src/pass_overlay_depth.rs +++ b/engine/pill_renderer/src/pass_overlay_depth.rs @@ -1,4 +1,4 @@ -use crate::renderer::{Pass, Renderer}; +use crate::renderer::{Pass, Renderer, WorldQuery}; use crate::resource_manager::ResourceManager; use crate::resources::RendererTexture; use anyhow::Result; @@ -228,11 +228,12 @@ impl Pass for PassOverlayDepth { } fn draw( - &self, + &mut self, encoder: &mut CommandEncoder, - _renderer: &Renderer, + _renderer: &mut Renderer, _frame: &wgpu::SurfaceTexture, view: &wgpu::TextureView, + _world: &WorldQuery, ) -> Result<()> { // Create render pass for this pass using the provided frame let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { diff --git a/engine/pill_renderer/src/pass_overlay_logo.rs b/engine/pill_renderer/src/pass_overlay_logo.rs index 72fcfe01..2f3a7621 100644 --- a/engine/pill_renderer/src/pass_overlay_logo.rs +++ b/engine/pill_renderer/src/pass_overlay_logo.rs @@ -1,4 +1,4 @@ -use crate::renderer::{Pass, Renderer}; +use crate::renderer::{Pass, Renderer, WorldQuery}; use crate::resource_manager::ResourceManager; use anyhow::Result; use pill_engine::internal::{ @@ -239,11 +239,12 @@ impl Pass for PassOverlayLogo { } fn draw( - &self, + &mut self, encoder: &mut CommandEncoder, - _renderer: &Renderer, + _renderer: &mut Renderer, _frame: &wgpu::SurfaceTexture, view: &wgpu::TextureView, + _world: &WorldQuery, ) -> Result<()> { // Create render pass for this pass using the provided frame let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { diff --git a/engine/pill_renderer/src/pass_overlay_uv.rs b/engine/pill_renderer/src/pass_overlay_uv.rs index 8ebbfb82..32159a40 100644 --- a/engine/pill_renderer/src/pass_overlay_uv.rs +++ b/engine/pill_renderer/src/pass_overlay_uv.rs @@ -1,4 +1,4 @@ -use crate::renderer::{Pass, Renderer}; +use crate::renderer::{Pass, Renderer, WorldQuery}; use crate::resource_manager::ResourceManager; use anyhow::Result; use pill_engine::internal::{BufferDesc, PillRenderer, PipelineV2, PipelineV2Desc, ShaderDesc}; @@ -129,27 +129,31 @@ impl Pass for PassOverlayUV { }); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_rect_bind_group"), + label: Some("overlay_rect_bind_group"), layout: &bgl, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: buffer.as_entire_binding(), - }], - }); + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + }); // Store pipeline handle - self.pipeline = Some(PipelineV2 { pipeline, bind_group_layouts: vec![bgl] }); + self.pipeline = Some(PipelineV2 { + pipeline, + bind_group_layouts: vec![bgl], + }); self.bind_group = Some(bind_group); Ok(()) } fn draw( - &self, + &mut self, encoder: &mut CommandEncoder, - _renderer: &Renderer, + _renderer: &mut Renderer, _frame: &wgpu::SurfaceTexture, view: &wgpu::TextureView, + _world: &WorldQuery, ) -> Result<()> { // Create render pass for this pass using the provided frame let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { diff --git a/engine/pill_renderer/src/pass_scene.rs b/engine/pill_renderer/src/pass_scene.rs new file mode 100644 index 00000000..090a9822 --- /dev/null +++ b/engine/pill_renderer/src/pass_scene.rs @@ -0,0 +1,405 @@ +use crate::renderer::{Pass, Renderer, WorldQuery}; +use crate::resource_manager::ResourceManager; +use anyhow::{Error, Result}; +use glam::{Mat4, Vec3, Vec4}; +use pill_core::{PillSlotMapKey, RendererError}; +use pill_engine::internal::{ + get_renderer_resource_handle_from_camera_component, CameraComponent, ComponentStorage, + EntityHandle, RenderQueueItem, RendererMaterialHandle, RendererMeshHandle, + RendererPipelineHandle, TransformComponent, +}; +use wgpu::CommandEncoder; + +pub struct PassScene { + label: String, + // Per-frame dynamic UBO ring (Milestone 5) + per_draw_stride: u64, + per_draw_capacity: u64, + per_draw_buffer: Option, + per_draw_bind_group: Option, + // Working buffers + visible_pre_draw_buffer: Vec, + groups_buffer: Vec, + staging_buffer: Vec, +} + +impl PassScene { + pub fn new(label: &str) -> Self { + Self { + label: label.to_string(), + per_draw_stride: 256, + per_draw_capacity: 0, + per_draw_buffer: None, + per_draw_bind_group: None, + visible_pre_draw_buffer: Vec::with_capacity(100_000), + groups_buffer: Vec::with_capacity(2000), + staging_buffer: Vec::with_capacity(100_000 * 144), + } + } +} + +impl Pass for PassScene { + fn get_label(&self) -> &str { + &self.label + } + + fn init(&mut self, _device: &wgpu::Device, _res: &mut ResourceManager) -> Result<()> { + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut CommandEncoder, + renderer: &mut Renderer, + _frame: &wgpu::SurfaceTexture, + _view: &wgpu::TextureView, + world: &WorldQuery, + ) -> Result<()> { + let active_camera_entity_handle = world.active_camera; + let camera_component_storage = world.camera_components; + let transform_component_storage = world.transform_components; + + // Update renderer camera UBO from engine camera + transform + let camera_storage = camera_component_storage + .data + .get(active_camera_entity_handle.data().index as usize) + .unwrap(); + let active_camera_component = camera_storage.as_ref().unwrap(); + let camera_transform_storage = transform_component_storage + .data + .get(active_camera_entity_handle.data().index as usize) + .unwrap(); + let active_camera_transform_component = camera_transform_storage.as_ref().unwrap(); + { + let renderer_camera = renderer + .state + .resource_manager + .cameras + .get_mut(get_renderer_resource_handle_from_camera_component( + active_camera_component, + )) + .ok_or(Error::new(RendererError::RendererResourceNotFound))?; + renderer_camera.update( + &renderer.ctx.queue, + active_camera_component, + active_camera_transform_component, + ); + } + let renderer_camera = renderer + .state + .resource_manager + .cameras + .get(get_renderer_resource_handle_from_camera_component( + active_camera_component, + )) + .unwrap(); + let clear_color = active_camera_component.clear_color; + + // View-projection matrix + let vp_mat: Mat4 = + Mat4::from_cols_array_2d(&renderer_camera.uniform.view_projection_matrix); + + // Extract frustum planes + let row3 = vp_mat.row(3); + let row0 = vp_mat.row(0); + let row1 = vp_mat.row(1); + let row2 = vp_mat.row(2); + let make_plane = |plane_vec: Vec4| -> (Vec3, f32) { + let normal = Vec3::new(plane_vec.x, plane_vec.y, plane_vec.z); + let len = normal.length(); + if len > 0.0 { + let normalized = normal / len; + (normalized, plane_vec.w / len) + } else { + (normal, plane_vec.w) + } + }; + let planes = [ + make_plane(row3 + row0), + make_plane(row3 - row0), + make_plane(row3 + row1), + make_plane(row3 - row1), + make_plane(row3 + row2), + make_plane(row3 - row2), + ]; + + // Build visible set + self.visible_pre_draw_buffer.clear(); + for render_queue_item in world.render_queue.iter() { + let key = pill_engine::internal::decompose_render_queue_key(render_queue_item.key); + let mesh_handle = + RendererMeshHandle::from_parts(key.mesh_index as u32, key.mesh_version as u32); + let material_handle = RendererMaterialHandle::from_parts( + key.material_index as u32, + key.material_version as u32, + ); + let material_for_pipeline = renderer + .state + .resource_manager + .materials + .get(material_handle) + .unwrap(); + let pipeline_handle: RendererPipelineHandle = material_for_pipeline.pipeline_handle; + + // Transform + let entity_index = render_queue_item.entity_index as usize; + let transform = transform_component_storage + .data + .get(entity_index) + .unwrap() + .as_ref() + .unwrap(); + let model_arr = pill_engine::internal::get_model_matrix(transform); + let model: Mat4 = Mat4::from_cols_array_2d(&model_arr); + + // Mesh AABB -> world AABB + let mesh = renderer + .state + .resource_manager + .meshes + .get(mesh_handle) + .unwrap(); + let local_min = Vec3::new(mesh.aabb_min[0], mesh.aabb_min[1], mesh.aabb_min[2]); + let local_max = Vec3::new(mesh.aabb_max[0], mesh.aabb_max[1], mesh.aabb_max[2]); + let corners = [ + Vec3::new(local_min.x, local_min.y, local_min.z), + Vec3::new(local_max.x, local_min.y, local_min.z), + Vec3::new(local_min.x, local_max.y, local_min.z), + Vec3::new(local_max.x, local_max.y, local_min.z), + Vec3::new(local_min.x, local_min.y, local_max.z), + Vec3::new(local_max.x, local_min.y, local_max.z), + Vec3::new(local_min.x, local_max.y, local_max.z), + Vec3::new(local_max.x, local_max.y, local_max.z), + ]; + let mut world_min = Vec3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY); + let mut world_max = Vec3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY); + for c in &corners { + let p4 = model * Vec4::new(c.x, c.y, c.z, 1.0); + let p = Vec3::new(p4.x, p4.y, p4.z); + world_min = world_min.min(p); + world_max = world_max.max(p); + } + // Frustum culling + let mut outside = false; + for (normal, d) in &planes { + let p = Vec3::new( + if normal.x >= 0.0 { + world_max.x + } else { + world_min.x + }, + if normal.y >= 0.0 { + world_max.y + } else { + world_min.y + }, + if normal.z >= 0.0 { + world_max.z + } else { + world_min.z + }, + ); + let dist = normal.dot(p) + *d; + if dist < 0.0 { + outside = true; + break; + } + } + if outside { + continue; + } + + let view_proj: Mat4 = + Mat4::from_cols_array_2d(&renderer_camera.uniform.view_projection_matrix); + let mvp: [[f32; 4]; 4] = (view_proj * model).to_cols_array_2d(); + self.visible_pre_draw_buffer + .push(crate::renderer::VisiblePreDraw { + pipeline_handle, + material_handle, + mesh_handle, + entity_index: render_queue_item.entity_index, + mvp, + }); + } + + // Sort and group + self.visible_pre_draw_buffer.sort_by_key(|v| { + ( + v.pipeline_handle.index(), + v.material_handle.index(), + v.mesh_handle.index(), + ) + }); + self.groups_buffer.clear(); + for v in &self.visible_pre_draw_buffer { + if self + .groups_buffer + .last() + .map(|g| { + g.pipeline_handle != v.pipeline_handle || g.material_handle != v.material_handle + }) + .unwrap_or(true) + { + self.groups_buffer.push(crate::renderer::GroupCmd { + pipeline_handle: v.pipeline_handle, + material_handle: v.material_handle, + batches: Vec::new(), + }); + } + let g = self.groups_buffer.last_mut().unwrap(); + if let Some(batch) = g + .batches + .iter_mut() + .find(|b| b.mesh_handle == v.mesh_handle) + { + batch.instances.push(v.mvp); + } else { + g.batches.push(crate::renderer::MeshBatch { + mesh_handle: v.mesh_handle, + instances: vec![v.mvp], + base_offset_u32: 0, + }); + } + } + + // Per-draw ring buffer setup + let needed: u64 = self + .groups_buffer + .iter() + .map(|g| { + g.batches + .iter() + .map(|b| b.instances.len() as u64) + .sum::() + }) + .sum(); + if self.per_draw_capacity < needed { + self.per_draw_capacity = needed.next_power_of_two().max(1); + let size = self.per_draw_stride * self.per_draw_capacity; + self.per_draw_buffer = + Some(renderer.ctx.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("per_draw_dynamic_ubo_ring"), + size, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + })); + self.per_draw_bind_group = Some( + renderer + .ctx + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("per_draw_bind_group"), + layout: &renderer + .state + .resource_manager + .pipeline(renderer.state.master_pipeline_handle) + .per_draw_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: self.per_draw_buffer.as_ref().unwrap(), + offset: 0, + size: Some(std::num::NonZeroU64::new(144).unwrap()), + }), + }], + }), + ); + } + #[repr(C)] + #[derive(Copy, Clone)] + struct PerDrawStd140 { + mvp: [[f32; 4]; 4], + tint: [f32; 4], + } + unsafe impl bytemuck::Zeroable for PerDrawStd140 {} + unsafe impl bytemuck::Pod for PerDrawStd140 {} + let tint: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; + self.staging_buffer.clear(); + let mut next_offset_u32: u32 = 0; + for g in self.groups_buffer.iter_mut() { + for b in g.batches.iter_mut() { + b.base_offset_u32 = next_offset_u32; + for mvp in &b.instances { + let pd = PerDrawStd140 { mvp: *mvp, tint }; + self.staging_buffer + .extend_from_slice(bytemuck::bytes_of(&pd)); + let pad = + (self.per_draw_stride as usize) - std::mem::size_of::(); + self.staging_buffer.extend(std::iter::repeat(0u8).take(pad)); + next_offset_u32 = next_offset_u32.wrapping_add(self.per_draw_stride as u32); + } + } + } + if let Some(buf) = &self.per_draw_buffer { + renderer + .ctx + .queue + .write_buffer(buf, 0, &self.staging_buffer); + } + + // Encode offscreen pass + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("m6_offscreen_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &renderer.state.offscreen_color_texture.texture_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: clear_color.x as f64, + g: clear_color.y as f64, + b: clear_color.z as f64, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &renderer.state.depth_texture.texture_view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + for group in &self.groups_buffer { + rpass.set_pipeline( + &renderer + .state + .resource_manager + .pipeline(renderer.state.master_pipeline_handle) + .render_pipeline, + ); + let material = renderer + .state + .resource_manager + .materials + .get(group.material_handle) + .unwrap(); + rpass.set_bind_group(0, &renderer_camera.bind_group, &[]); + rpass.set_bind_group(1, &material.texture_bind_group, &[]); + rpass.set_bind_group(2, &material.parameter_bind_group, &[]); + for batch in &group.batches { + let mesh = renderer + .state + .resource_manager + .meshes + .get(batch.mesh_handle) + .unwrap(); + rpass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); + rpass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32); + for i in 0..batch.instances.len() { + let offset = batch + .base_offset_u32 + .wrapping_add((i as u32) * (self.per_draw_stride as u32)); + rpass.set_bind_group(3, self.per_draw_bind_group.as_ref().unwrap(), &[offset]); + rpass.draw_indexed(0..mesh.index_count, 0, 0..1); + } + } + } + + Ok(()) + } +} diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 8b864c36..ba04610b 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -7,6 +7,7 @@ use crate::{ pass_overlay_depth::PassOverlayDepth, pass_overlay_logo::PassOverlayLogo, pass_overlay_uv::PassOverlayUV, + pass_scene::PassScene, resource_manager::ResourceManager, resources::{ RendererCamera, RendererMaterial, RendererMesh, RendererPipeline, RendererTexture, Vertex, @@ -116,72 +117,71 @@ pub trait Pass { res: &mut crate::resource_manager::ResourceManager, ) -> Result<()>; fn draw( - &self, + &mut self, encoder: &mut wgpu::CommandEncoder, - renderer: &Renderer, + renderer: &mut Renderer, frame: &wgpu::SurfaceTexture, view: &wgpu::TextureView, + world: &WorldQuery, ) -> Result<()>; } pub struct DeviceContext { - config: config::Config, + pub(crate) config: config::Config, // Window - window_ref: Arc, - window_size: winit::dpi::PhysicalSize, + pub(crate) window_ref: Arc, + pub(crate) window_size: winit::dpi::PhysicalSize, // GPU API - device: wgpu::Device, - queue: wgpu::Queue, - surface: wgpu::Surface<'static>, + pub(crate) device: wgpu::Device, + pub(crate) queue: wgpu::Queue, + pub(crate) surface: wgpu::Surface<'static>, // Framebuffer variables - surface_configuration: wgpu::SurfaceConfiguration, + pub(crate) surface_configuration: wgpu::SurfaceConfiguration, } // Preallocated buffer structs for hot path optimization -struct VisiblePreDraw { - pipeline_handle: RendererPipelineHandle, - material_handle: RendererMaterialHandle, - mesh_handle: RendererMeshHandle, - entity_index: u32, - mvp: [[f32; 4]; 4], +pub(crate) struct VisiblePreDraw { + pub(crate) pipeline_handle: RendererPipelineHandle, + pub(crate) material_handle: RendererMaterialHandle, + pub(crate) mesh_handle: RendererMeshHandle, + pub(crate) entity_index: u32, + pub(crate) mvp: [[f32; 4]; 4], +} + +pub(crate) struct MeshBatch { + pub(crate) mesh_handle: RendererMeshHandle, + pub(crate) instances: Vec<[[f32; 4]; 4]>, + pub(crate) base_offset_u32: u32, // offset into per-draw ring for first instance } -struct MeshBatch { - mesh_handle: RendererMeshHandle, - instances: Vec<[[f32; 4]; 4]>, - base_offset_u32: u32, // offset into per-draw ring for first instance +pub(crate) struct GroupCmd { + pub(crate) pipeline_handle: RendererPipelineHandle, + pub(crate) material_handle: RendererMaterialHandle, + pub(crate) batches: Vec, } -struct GroupCmd { - pipeline_handle: RendererPipelineHandle, - material_handle: RendererMaterialHandle, - batches: Vec, +// Local world query for passes (avoids depending on pill_engine::graphics visibility) +pub struct WorldQuery<'a> { + pub active_camera: EntityHandle, + pub render_queue: &'a Vec, + pub camera_components: &'a ComponentStorage, + pub transform_components: &'a ComponentStorage, } pub struct State { passes: Vec>, egui_renderer: crate::egui::EguiRenderer, // TODO: Separate system adding Pass // Resources and GPU objects moved from ctor into here explicitly - resource_manager: ResourceManager, - color_format: wgpu::TextureFormat, - depth_format: wgpu::TextureFormat, - depth_texture: Arc, - offscreen_color_texture: Arc, - // Per-frame dynamic UBO ring (Milestone 5) - per_draw_stride: u64, - per_draw_capacity: u64, // in elements - per_draw_buffer: Option, - per_draw_bind_group_layout: wgpu::BindGroupLayout, - per_draw_bind_group: Option, + pub(crate) resource_manager: ResourceManager, + pub(crate) color_format: wgpu::TextureFormat, + pub(crate) depth_format: wgpu::TextureFormat, + pub(crate) depth_texture: Arc, + pub(crate) offscreen_color_texture: Arc, // Prebuilt PSO handle // [SIMILAR] Prebuilt once; no per-draw pipeline churn per TALK - master_pipeline_handle: RendererPipelineHandle, + pub(crate) master_pipeline_handle: RendererPipelineHandle, // resources - tex_logo: RendererTextureHandle, - // Preallocated buffers for hot path optimization - visible_pre_draw_buffer: Vec, - groups_buffer: Vec, - staging_buffer: Vec, + pub(crate) tex_logo: RendererTextureHandle, } pub struct Renderer { @@ -422,10 +422,10 @@ impl PillRenderer for Renderer { let master_handle = resource_manager.pipelines.insert(default_pipeline); // Create state - let per_draw_bind_group_layout = + let _per_draw_bind_group_layout = ctx.device .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("per_draw_bind_group_layout"), + label: Some("per_draw_bind_group_layout_unused"), entries: &[wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, @@ -468,21 +468,11 @@ impl PillRenderer for Renderer { depth_format, depth_texture: Arc::new(depth_texture), offscreen_color_texture, - // Per-frame UBO ring init - per_draw_stride: 256, // 80 bytes + 256 byte alignment requirement - per_draw_capacity: 0, - per_draw_buffer: None, - // [SIMILAR] Per-draw dynamic UBO layout with has_dynamic_offset=true per TALK (Milestone 5) - per_draw_bind_group_layout, - per_draw_bind_group: None, + // Scene pass owns per-draw ring and working buffers now // Prebuilt PSO handle master_pipeline_handle: master_handle, // Overlays tex_logo, - // Preallocated buffers for hot path optimization (60k instances capacity) - visible_pre_draw_buffer: Vec::with_capacity(100_000), - groups_buffer: Vec::with_capacity(2000), - staging_buffer: Vec::with_capacity(100_000 * 144), // 60k instances * 144 bytes per instance } }; Self { state, ctx } @@ -491,6 +481,9 @@ impl PillRenderer for Renderer { fn init(&mut self) -> Result<()> { self.state.passes.clear(); + // Scene pass first: renders into offscreen targets + self.state.passes.push(Box::new(PassScene::new("scene"))); + self.state.passes.push(Box::new(PassCompose::new( "compose", self.state.offscreen_color_texture.clone(), @@ -928,430 +921,9 @@ impl PillRenderer for Renderer { .texture .create_view(&wgpu::TextureViewDescriptor::default()); - timer.record("Frame: setup view & camera"); - - // [SIMILAR] Modify resources (camera UBO) before draw; separates data mods from drawing as per TALK - // [SIMILAR] Bind group slot indices same as in TALK's convention (0=globals,1=material,2=shader,3=dynamic); - // Get active camera and update it - let camera_storage = camera_component_storage - .data - .get(active_camera_entity_handle.data().index as usize) - .unwrap(); - let active_camera_component = camera_storage.as_ref().unwrap(); - let renderer_camera = self - .state - .resource_manager - .cameras - .get_mut(get_renderer_resource_handle_from_camera_component( - active_camera_component, - )) - .ok_or(Error::new(RendererError::RendererResourceNotFound))?; - let camera_transform_storage = transform_component_storage - .data - .get(active_camera_entity_handle.data().index as usize) - .unwrap(); - let active_camera_transform_component = camera_transform_storage.as_ref().unwrap(); - renderer_camera.update( - &self.ctx.queue, - active_camera_component, - active_camera_transform_component, - ); - let renderer_camera = self - .state - .resource_manager - .cameras - .get(get_renderer_resource_handle_from_camera_component( - active_camera_component, - )) - .unwrap(); - let clear_color = active_camera_component.clear_color; - - // Record inline draw pass (M2) - // ALLOCATION: CommandEncoder creation (CPU memory) - ~1KB command buffer - let mut encoder = self - .ctx - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("m2_inline_encoder"), - }); - - // [SIMILAR] CPU frustum culling and precomputing per-draw transforms before starting the pass matches TALK's advice - // [DIFF] Only frustum culling here; TALK discusses 2-pass occlusion and broader CPU-driven pipelines - // [API->CLIENT] Culling, model matrix building, and visible set construction should live in client/high-level renderer - // Build view before pass: cull and prepare per-draw MVPs - // SIMD OPTIMIZATION: Use glam for SIMD-accelerated frustum culling - let vp_mat: Mat4 = - Mat4::from_cols_array_2d(&renderer_camera.uniform.view_projection_matrix); - - // Extract frustum planes using SIMD operations - let row3 = vp_mat.row(3); // w row - let row0 = vp_mat.row(0); // x row - let row1 = vp_mat.row(1); // y row - let row2 = vp_mat.row(2); // z row - - // SIMD plane construction: normalize plane equations - let make_plane = |plane_vec: Vec4| -> (Vec3, f32) { - let normal = Vec3::new(plane_vec.x, plane_vec.y, plane_vec.z); - let len = normal.length(); - if len > 0.0 { - let normalized = normal / len; - (normalized, plane_vec.w / len) - } else { - (normal, plane_vec.w) - } - }; - - // Frustum planes using SIMD operations - let planes = [ - make_plane(row3 + row0), // left - make_plane(row3 - row0), // right - make_plane(row3 + row1), // bottom - make_plane(row3 - row1), // top - make_plane(row3 + row2), // near - make_plane(row3 - row2), // far - ]; - - timer.record("Culling & MVP build"); - // REUSE PREALLOCATED BUFFER: Clear and reuse preallocated buffer (no allocation in hot path) - self.state.visible_pre_draw_buffer.clear(); - - for render_queue_item in render_queue.iter() { - let key = pill_engine::internal::decompose_render_queue_key(render_queue_item.key); - let mesh_handle = - RendererMeshHandle::from_parts(key.mesh_index as u32, key.mesh_version as u32); - let material_handle = RendererMaterialHandle::from_parts( - key.material_index as u32, - key.material_version as u32, - ); - let material_for_pipeline = self - .state - .resource_manager - .materials - .get(material_handle) - .unwrap(); - let pipeline_handle = material_for_pipeline.pipeline_handle; - - // Transform and model - let entity_index = render_queue_item.entity_index as usize; - let transform = transform_component_storage - .data - .get(entity_index) - .unwrap() - .as_ref() - .unwrap(); - let model_arr = pill_engine::internal::get_model_matrix(transform); - let model: Mat4 = Mat4::from_cols_array_2d(&model_arr); - - // World AABB from mesh local AABB using SIMD operations - let mesh = self.state.resource_manager.meshes.get(mesh_handle).unwrap(); - let local_min = Vec3::new(mesh.aabb_min[0], mesh.aabb_min[1], mesh.aabb_min[2]); - let local_max = Vec3::new(mesh.aabb_max[0], mesh.aabb_max[1], mesh.aabb_max[2]); - - // SIMD AABB corner transformation - let corners = [ - Vec3::new(local_min.x, local_min.y, local_min.z), - Vec3::new(local_max.x, local_min.y, local_min.z), - Vec3::new(local_min.x, local_max.y, local_min.z), - Vec3::new(local_max.x, local_max.y, local_min.z), - Vec3::new(local_min.x, local_min.y, local_max.z), - Vec3::new(local_max.x, local_min.y, local_max.z), - Vec3::new(local_min.x, local_max.y, local_max.z), - Vec3::new(local_max.x, local_max.y, local_max.z), - ]; - - let mut world_min = Vec3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY); - let mut world_max = Vec3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY); - - // SIMD matrix-vector multiplication for corner transformation - for c in &corners { - let p4 = model * Vec4::new(c.x, c.y, c.z, 1.0); - let p = Vec3::new(p4.x, p4.y, p4.z); - world_min = world_min.min(p); - world_max = world_max.max(p); - } - - // SIMD frustum culling test - let mut outside = false; - for (normal, d) in &planes { - // SIMD select operation for AABB corner selection - let p = Vec3::new( - if normal.x >= 0.0 { - world_max.x - } else { - world_min.x - }, - if normal.y >= 0.0 { - world_max.y - } else { - world_min.y - }, - if normal.z >= 0.0 { - world_max.z - } else { - world_min.z - }, - ); - // SIMD dot product - let dist = normal.dot(p) + *d; - if dist < 0.0 { - outside = true; - break; - } - } - if outside { - continue; - } - - let view_proj: Mat4 = - Mat4::from_cols_array_2d(&renderer_camera.uniform.view_projection_matrix); - let mvp: [[f32; 4]; 4] = (view_proj * model).to_cols_array_2d(); - // REUSE PREALLOCATED BUFFER: Push to preallocated buffer (no allocation in hot path) - self.state.visible_pre_draw_buffer.push(VisiblePreDraw { - pipeline_handle, - material_handle, - mesh_handle, - entity_index: render_queue_item.entity_index, - mvp, - }); - } - - // [SIMILAR] Sort by pipeline/material/mesh to minimize state changes as in TALK - // [API->CLIENT] Sorting/grouping is client responsibility; low-level should accept an already ordered draw stream - // Sort by pipeline -> material -> mesh to minimize state changes - self.state.visible_pre_draw_buffer.sort_by_key(|v| { - ( - v.pipeline_handle.index(), - v.material_handle.index(), - v.mesh_handle.index(), - ) - }); + timer.record("Frame: setup view"); - // Build grouped instancing batches by (pipeline, material, mesh) - timer.record("Sort & group draws"); - // REUSE PREALLOCATED BUFFER: Clear and reuse preallocated groups buffer (no allocation in hot path) - self.state.groups_buffer.clear(); - for v in &self.state.visible_pre_draw_buffer { - if self - .state - .groups_buffer - .last() - .map(|g| { - g.pipeline_handle != v.pipeline_handle || g.material_handle != v.material_handle - }) - .unwrap_or(true) - { - self.state.groups_buffer.push(GroupCmd { - pipeline_handle: v.pipeline_handle, - material_handle: v.material_handle, - batches: Vec::new(), - }); - } - let g = self.state.groups_buffer.last_mut().unwrap(); - if let Some(batch) = g - .batches - .iter_mut() - .find(|b| b.mesh_handle == v.mesh_handle) - { - batch.instances.push(v.mvp); - } else { - // ALLOCATION: Vec<[[f32; 4]; 4]> for instances (CPU memory) - optimistic: 1 instance * 64 bytes, pessimistic: 60k instances * 64 bytes = ~3.8MB per batch - g.batches.push(MeshBatch { - mesh_handle: v.mesh_handle, - instances: vec![v.mvp], - base_offset_u32: 0, - }); - } - } - let draw_call_count: usize = self - .state - .groups_buffer - .iter() - .map(|g| g.batches.len()) - .sum(); - // Expose draw call count via per-frame timer counters for UI/metrics - timer.set_counter("draw_calls", draw_call_count as u64); - - // Milestone 5: Per-frame ring buffer - // [SIMILAR] Batch write dynamic per-draw data once; bind with dynamic offsets - timer.begin_context("Per-draw UBO setup"); - let needed: u64 = self - .state - .groups_buffer - .iter() - .map(|g| { - g.batches - .iter() - .map(|b| b.instances.len() as u64) - .sum::() - }) - .sum(); - // total instances; no direct use beyond sanity, avoid unused warning by not binding - if self.state.per_draw_capacity < needed { - self.state.per_draw_capacity = needed.next_power_of_two().max(1); - let size = self.state.per_draw_stride * self.state.per_draw_capacity; - // ALLOCATION: GPU buffer creation (GPU memory) - optimistic: 1KB, pessimistic: 60k instances * 144 bytes = ~8.6MB - self.state.per_draw_buffer = - Some(self.ctx.device.create_buffer(&wgpu::BufferDescriptor { - label: Some("per_draw_dynamic_ubo_ring"), - size, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - })); - // ALLOCATION: BindGroup creation (GPU memory) - ~128 bytes metadata - self.state.per_draw_bind_group = Some( - self.ctx - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("per_draw_bind_group"), - layout: &self - .state - .resource_manager - .pipeline(self.state.master_pipeline_handle) - .per_draw_bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: self.state.per_draw_buffer.as_ref().unwrap(), - offset: 0, - size: Some(std::num::NonZeroU64::new(144).unwrap()), - }), - }], - }), - ); - } - #[repr(C)] - #[derive(Copy, Clone)] - struct PerDrawStd140 { - mvp: [[f32; 4]; 4], // 4x4 MVP matrix (16 floats) - tint: [f32; 4], // RGBA color (4 floats) - } - unsafe impl bytemuck::Zeroable for PerDrawStd140 {} - unsafe impl bytemuck::Pod for PerDrawStd140 {} - let tint: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; - // REUSE PREALLOCATED BUFFER: Clear and reuse preallocated staging buffer (no allocation in hot path) - self.state.staging_buffer.clear(); - let mut next_offset_u32: u32 = 0; - for g in self.state.groups_buffer.iter_mut() { - for b in g.batches.iter_mut() { - b.base_offset_u32 = next_offset_u32; - for mvp in &b.instances { - let pd = PerDrawStd140 { mvp: *mvp, tint }; - self.state - .staging_buffer - .extend_from_slice(bytemuck::bytes_of(&pd)); - let pad = (self.state.per_draw_stride as usize) - - std::mem::size_of::(); - self.state - .staging_buffer - .extend(std::iter::repeat(0u8).take(pad)); - next_offset_u32 = - next_offset_u32.wrapping_add(self.state.per_draw_stride as u32); - } - } - } - if let Some(buf) = &self.state.per_draw_buffer { - self.ctx - .queue - .write_buffer(buf, 0, &self.state.staging_buffer); - } - timer.record("Per-draw UBO write (ring)"); - timer.end_context()?; - - { - timer.begin_context("Offscreen pass"); - // Render scene into offscreen color target (T0) - let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("m6_offscreen_pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &self.state.offscreen_color_texture.texture_view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: clear_color.x as f64, - g: clear_color.y as f64, - b: clear_color.z as f64, - a: 1.0, - }), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &self.state.depth_texture.texture_view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Clear(1.0), - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }), - timestamp_writes: None, - occlusion_query_set: None, - }); - // [SIMILAR] Bind pipeline/material once per group; change only minimal per-draw state (offsets) - // Instancing: one draw per (pipeline, material, mesh) batch with instance_count - for group in &self.state.groups_buffer { - timer.begin_context(&format!( - "Pipeline {} / Material {}", - group.pipeline_handle.index(), - group.material_handle.index() - )); - rpass.set_pipeline( - &self - .state - .resource_manager - .pipeline(self.state.master_pipeline_handle) - .render_pipeline, - ); - - // HOT SoA (per-frame hot path): - // - RendererMaterial: texture_bind_group, parameter_bind_group - // - RendererCamera : bind_group - // - RendererMesh : vertex_buffer, index_buffer, index_count - // COLD metadata (names/descs) lives in *_cold arrays and is not read in the draw loop. - - // Get material and set bind groups - let material = self - .state - .resource_manager - .materials - .get(group.material_handle) - .unwrap(); - rpass.set_bind_group(0, &renderer_camera.bind_group, &[]); - rpass.set_bind_group(1, &material.texture_bind_group, &[]); - rpass.set_bind_group(2, &material.parameter_bind_group, &[]); - - // Process batches with separate storage borrows to avoid lifetime issues - for batch in &group.batches { - let mesh = self - .state - .resource_manager - .meshes - .get(batch.mesh_handle) - .unwrap(); - // [API->CLIENT] Provide packed mesh atlas + per-draw base offsets so low-level can draw without re-binding buffers - // [DIFF] VB/IB still rebound per batch; packing meshes could avoid rebinding - rpass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); - rpass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32); - // Per-instance draws with dynamic uniform offsets (no shader change required) - for i in 0..batch.instances.len() { - let offset = batch - .base_offset_u32 - .wrapping_add((i as u32) * (self.state.per_draw_stride as u32)); - rpass.set_bind_group( - 3, - self.state.per_draw_bind_group.as_ref().unwrap(), - &[offset], - ); - rpass.draw_indexed(0..mesh.index_count, 0, 0..1); - } - } - timer.end_context()?; // End pipeline/material group - } - timer.record("Encode offscreen pass"); - } - timer.end_context()?; // End "Offscreen pass" - - self.ctx.queue.submit([encoder.finish()]); - timer.record("Submit scene+compose passes"); + // Scene draw is now a Pass; remaining work is handled in user passes encoder below { // User passes with separate encoder @@ -1365,13 +937,22 @@ impl PillRenderer for Renderer { }); // Loop over state.passes, for each - for pass in &self.state.passes { - let label = pass.get_label(); - timer.begin_context(label); - let _ = pass.draw(&mut encoder, self, &frame, &view); - timer.record(label); + // Construct world query bundle for passes + let world_q = WorldQuery { + active_camera: active_camera_entity_handle, + render_queue, + camera_components: camera_component_storage, + transform_components: transform_component_storage, + }; + let mut passes = std::mem::take(&mut self.state.passes); + for pass in passes.iter_mut() { + let label_owned = pass.get_label().to_string(); + timer.begin_context(&label_owned); + let _ = pass.draw(&mut encoder, self, &frame, &view, &world_q); + timer.record(&label_owned); timer.end_context()?; } + self.state.passes = passes; self.ctx.queue.submit([encoder.finish()]); timer.record("User passes submit"); From 32e389dcbaa0eeb75a192d34b108fa8013e484e5 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Mon, 3 Nov 2025 00:53:57 +0100 Subject: [PATCH 52/69] wip: pass scene, local camera --- .../pill_engine/src/graphics/render_queue.rs | 98 +++++++++------- engine/pill_renderer/src/pass_scene.rs | 107 +++++++++++++----- engine/pill_renderer/src/renderer.rs | 1 + engine/pill_renderer/src/resources/mod.rs | 1 + .../src/resources/renderer_camera.rs | 86 ++++++++------ 5 files changed, 187 insertions(+), 106 deletions(-) diff --git a/engine/pill_engine/src/graphics/render_queue.rs b/engine/pill_engine/src/graphics/render_queue.rs index 2c2b3df0..43c62bcb 100644 --- a/engine/pill_engine/src/graphics/render_queue.rs +++ b/engine/pill_engine/src/graphics/render_queue.rs @@ -1,29 +1,22 @@ -use crate::{ - resources::{ - ResourceManager, - MaterialHandle, - TextureHandle, - Material, - MeshHandle, - Mesh, - }, +use crate::{ config::*, + resources::{Material, MaterialHandle, Mesh, MeshHandle, ResourceManager, TextureHandle}, }; use pill_core::Handle; +use anyhow::{Context, Error, Result}; +use core::fmt::{self, Debug}; +use lazy_static::lazy_static; use std::{ cmp::Ordering, - fmt::{Binary, Display}, - ops::{Add, Not, Shl, Sub, Range}, convert::{TryFrom, TryInto}, - path::{Path, PathBuf} + fmt::{Binary, Display}, + ops::{Add, Not, Range, Shl, Sub}, + path::{Path, PathBuf}, }; -use core::fmt::{Debug, self}; -use anyhow::{Result, Context, Error}; -use lazy_static::lazy_static; -// --- Render queue +// --- Render queue pub struct RenderQueue { pub items: Vec, @@ -54,7 +47,7 @@ impl PartialEq for RenderQueueItem { } } -impl Eq for RenderQueueItem { } +impl Eq for RenderQueueItem {} impl Display for RenderQueueItem { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -70,22 +63,34 @@ impl Debug for RenderQueueItem { // --- Render queue field --- -pub struct RenderQueueField { +pub struct RenderQueueField { pub mask_range: core::ops::Range, pub mask_shift: T, pub mask: T, - pub max: T, + pub max: T, } pub trait Pow { fn pow(self, exp: Self) -> Self; } -impl RenderQueueField +impl RenderQueueField where - T: Copy + Default + Pow + Binary + Debug + From + From + Ord + Shl + Sub + Add + Not, + T: Copy + + Default + + Pow + + Binary + + Debug + + From + + From + + Ord + + Shl + + Sub + + Add + + Not, { - pub fn new(mask_range: core::ops::Range) -> Self { // Compile-time evaluable function + pub fn new(mask_range: core::ops::Range) -> Self { + // Compile-time evaluable function let one: T = T::from(1 as u8); let two: T = T::from(2 as u8); let mask_range_length = mask_range.end - mask_range.start + one; //if mask_range.start == zero { mask_range.end + one } else { mask_range.end - mask_range.start }; @@ -104,18 +109,23 @@ where } // Creates pill engine render queue composed from order, material index, material version, mesh index, mesh version -pub fn compose_render_queue_key(resource_manager: &ResourceManager, material_handle: &MaterialHandle, mesh_handle: &MeshHandle) -> Result { +pub fn compose_render_queue_key( + resource_manager: &ResourceManager, + material_handle: &MaterialHandle, + mesh_handle: &MeshHandle, +) -> Result { let material = resource_manager.get_resource::(material_handle)?; let mesh = resource_manager.get_resource::(mesh_handle)?; let h_mat = material.renderer_resource_handle.unwrap(); let h_mesh = mesh.renderer_resource_handle.unwrap(); - let render_queue_key: RenderQueueKey = - ((RENDER_QUEUE_KEY_ORDER.max - material.rendering_order as RenderQueueKey) << RENDER_QUEUE_KEY_ORDER.mask_shift) - | ((h_mat.index() as RenderQueueKey) << RENDER_QUEUE_KEY_MATERIAL_INDEX.mask_shift) - | ((h_mat.generation() as RenderQueueKey) << RENDER_QUEUE_KEY_MATERIAL_VERSION.mask_shift) - | ((h_mesh.index() as RenderQueueKey) << RENDER_QUEUE_KEY_MESH_INDEX.mask_shift) - | ((h_mesh.generation() as RenderQueueKey) << RENDER_QUEUE_KEY_MESH_VERSION.mask_shift); + let render_queue_key: RenderQueueKey = ((RENDER_QUEUE_KEY_ORDER.max + - material.rendering_order as RenderQueueKey) + << RENDER_QUEUE_KEY_ORDER.mask_shift) + | ((h_mat.index() as RenderQueueKey) << RENDER_QUEUE_KEY_MATERIAL_INDEX.mask_shift) + | ((h_mat.generation() as RenderQueueKey) << RENDER_QUEUE_KEY_MATERIAL_VERSION.mask_shift) + | ((h_mesh.index() as RenderQueueKey) << RENDER_QUEUE_KEY_MESH_INDEX.mask_shift) + | ((h_mesh.generation() as RenderQueueKey) << RENDER_QUEUE_KEY_MESH_VERSION.mask_shift); Ok(render_queue_key) } @@ -129,14 +139,18 @@ pub struct RenderQueueKeyFields { } // Decomposes pill engine render queue key into separate fields -pub fn decompose_render_queue_key(render_queue_key: RenderQueueKey) -> RenderQueueKeyFields { - +pub fn decompose_render_queue_key(render_queue_key: RenderQueueKey) -> RenderQueueKeyFields { // [TODO] What if render queue key is not valid - let order: u8 = ((render_queue_key & RENDER_QUEUE_KEY_ORDER.mask as RenderQueueKey) >> RENDER_QUEUE_KEY_ORDER.mask_shift as RenderQueueKey) as u8; - let material_index: u8 = ((render_queue_key & RENDER_QUEUE_KEY_MATERIAL_INDEX.mask) >> RENDER_QUEUE_KEY_MATERIAL_INDEX.mask_shift) as u8; - let material_version: u8 = ((render_queue_key & RENDER_QUEUE_KEY_MATERIAL_VERSION.mask) >> RENDER_QUEUE_KEY_MATERIAL_VERSION.mask_shift) as u8; - let mesh_index: u8 = ((render_queue_key & RENDER_QUEUE_KEY_MESH_INDEX.mask) >> RENDER_QUEUE_KEY_MESH_INDEX.mask_shift) as u8; - let mesh_version: u8 = ((render_queue_key & RENDER_QUEUE_KEY_MESH_VERSION.mask) >> RENDER_QUEUE_KEY_MESH_VERSION.mask_shift) as u8; + let order: u8 = ((render_queue_key & RENDER_QUEUE_KEY_ORDER.mask as RenderQueueKey) + >> RENDER_QUEUE_KEY_ORDER.mask_shift as RenderQueueKey) as u8; + let material_index: u8 = ((render_queue_key & RENDER_QUEUE_KEY_MATERIAL_INDEX.mask) + >> RENDER_QUEUE_KEY_MATERIAL_INDEX.mask_shift) as u8; + let material_version: u8 = ((render_queue_key & RENDER_QUEUE_KEY_MATERIAL_VERSION.mask) + >> RENDER_QUEUE_KEY_MATERIAL_VERSION.mask_shift) as u8; + let mesh_index: u8 = ((render_queue_key & RENDER_QUEUE_KEY_MESH_INDEX.mask) + >> RENDER_QUEUE_KEY_MESH_INDEX.mask_shift) as u8; + let mesh_version: u8 = ((render_queue_key & RENDER_QUEUE_KEY_MESH_VERSION.mask) + >> RENDER_QUEUE_KEY_MESH_VERSION.mask_shift) as u8; RenderQueueKeyFields { order, @@ -153,16 +167,18 @@ pub type RenderQueueKey = crate::config::RenderQueueKeyType; impl Pow for RenderQueueKey { fn pow(self, exp: Self) -> Self { - RenderQueueKey::pow(self, exp.try_into().unwrap()) + RenderQueueKey::pow(self, exp.try_into().unwrap()) } } fn get_render_queue_key_item_range(render_queue_item_index: u8) -> Range { let mut start: RenderQueueKey = 0; let mut end: RenderQueueKey = 0; - for i in 0..render_queue_item_index + 1 - { - start += i.ne(&0).then(|| RENDER_QUEUE_KEY_ITEMS_LENGTH[i as usize - 1]).unwrap_or(0); + for i in 0..render_queue_item_index + 1 { + start += i + .ne(&0) + .then(|| RENDER_QUEUE_KEY_ITEMS_LENGTH[i as usize - 1]) + .unwrap_or(0); end += RENDER_QUEUE_KEY_ITEMS_LENGTH[i as usize]; } start..(end - 1) @@ -195,4 +211,4 @@ pub type RenderQuery<'a> = RenderQueueRefs<'a>; /// Zero-cost factory trait producing borrowed render query refs. pub trait RenderQueueFactory { fn get<'a>(&'a self) -> RenderQuery<'a>; -} \ No newline at end of file +} diff --git a/engine/pill_renderer/src/pass_scene.rs b/engine/pill_renderer/src/pass_scene.rs index 090a9822..3688a37c 100644 --- a/engine/pill_renderer/src/pass_scene.rs +++ b/engine/pill_renderer/src/pass_scene.rs @@ -1,5 +1,6 @@ use crate::renderer::{Pass, Renderer, WorldQuery}; use crate::resource_manager::ResourceManager; +use crate::resources::CameraUniform; use anyhow::{Error, Result}; use glam::{Mat4, Vec3, Vec4}; use pill_core::{PillSlotMapKey, RendererError}; @@ -21,6 +22,15 @@ pub struct PassScene { visible_pre_draw_buffer: Vec, groups_buffer: Vec, staging_buffer: Vec, + // Pass-local state initialized in init(), read every draw + state: Option, +} + +struct PassSceneState { + camera_uniform: CameraUniform, + camera_bgl: wgpu::BindGroupLayout, + camera_buffer: wgpu::Buffer, + camera_bind_group: wgpu::BindGroup, } impl PassScene { @@ -34,16 +44,63 @@ impl PassScene { visible_pre_draw_buffer: Vec::with_capacity(100_000), groups_buffer: Vec::with_capacity(2000), staging_buffer: Vec::with_capacity(100_000 * 144), + state: None, } } } +fn get_state(pass: &mut PassScene) -> &mut PassSceneState { + debug_assert!(pass.state.is_some()); + // SAFETY: initialized once in init(), read every draw + unsafe { pass.state.as_mut().unwrap_unchecked() } +} + impl Pass for PassScene { fn get_label(&self) -> &str { &self.label } fn init(&mut self, _device: &wgpu::Device, _res: &mut ResourceManager) -> Result<()> { + // Pre-create camera bind group layout, buffer and bind group + let device = _device; + + let camera_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("camera_bind_group_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + + let camera_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("camera_buffer"), + size: std::mem::size_of::() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("camera_bind_group"), + layout: &camera_bgl, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: camera_buffer.as_entire_binding(), + }], + }); + + self.state = Some(PassSceneState { + camera_uniform: CameraUniform::new(), + camera_bgl, + camera_buffer, + camera_bind_group, + }); + Ok(()) } @@ -59,7 +116,7 @@ impl Pass for PassScene { let camera_component_storage = world.camera_components; let transform_component_storage = world.transform_components; - // Update renderer camera UBO from engine camera + transform + // Read active camera + transform let camera_storage = camera_component_storage .data .get(active_camera_entity_handle.data().index as usize) @@ -70,34 +127,28 @@ impl Pass for PassScene { .get(active_camera_entity_handle.data().index as usize) .unwrap(); let active_camera_transform_component = camera_transform_storage.as_ref().unwrap(); - { - let renderer_camera = renderer - .state - .resource_manager - .cameras - .get_mut(get_renderer_resource_handle_from_camera_component( - active_camera_component, - )) - .ok_or(Error::new(RendererError::RendererResourceNotFound))?; - renderer_camera.update( - &renderer.ctx.queue, - active_camera_component, - active_camera_transform_component, + + // Update camera uniform and write to GPU buffer (no allocations), + // then release the mutable borrow and keep only cheap copies/ptrs. + let (vp_mat_arr, camera_bg_ptr): ([[f32; 4]; 4], *const wgpu::BindGroup) = { + let state = get_state(self); + state + .camera_uniform + .update_data(active_camera_component, active_camera_transform_component); + renderer.ctx.queue.write_buffer( + &state.camera_buffer, + 0, + bytemuck::bytes_of(&state.camera_uniform), ); - } - let renderer_camera = renderer - .state - .resource_manager - .cameras - .get(get_renderer_resource_handle_from_camera_component( - active_camera_component, - )) - .unwrap(); + ( + state.camera_uniform.view_projection_matrix, + &state.camera_bind_group as *const _, + ) + }; let clear_color = active_camera_component.clear_color; // View-projection matrix - let vp_mat: Mat4 = - Mat4::from_cols_array_2d(&renderer_camera.uniform.view_projection_matrix); + let vp_mat: Mat4 = Mat4::from_cols_array_2d(&vp_mat_arr); // Extract frustum planes let row3 = vp_mat.row(3); @@ -209,8 +260,7 @@ impl Pass for PassScene { continue; } - let view_proj: Mat4 = - Mat4::from_cols_array_2d(&renderer_camera.uniform.view_projection_matrix); + let view_proj: Mat4 = vp_mat; let mvp: [[f32; 4]; 4] = (view_proj * model).to_cols_array_2d(); self.visible_pre_draw_buffer .push(crate::renderer::VisiblePreDraw { @@ -378,7 +428,8 @@ impl Pass for PassScene { .materials .get(group.material_handle) .unwrap(); - rpass.set_bind_group(0, &renderer_camera.bind_group, &[]); + // SAFETY: camera_bg_ptr points to self.state.camera_bind_group, valid for the duration of draw + rpass.set_bind_group(0, unsafe { &*camera_bg_ptr }, &[]); rpass.set_bind_group(1, &material.texture_bind_group, &[]); rpass.set_bind_group(2, &material.parameter_bind_group, &[]); for batch in &group.batches { diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index ba04610b..96ccda28 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -161,6 +161,7 @@ pub(crate) struct GroupCmd { } // Local world query for passes (avoids depending on pill_engine::graphics visibility) +// TODO: Rename to WorldView, SceneView, ... pub struct WorldQuery<'a> { pub active_camera: EntityHandle, pub render_queue: &'a Vec, diff --git a/engine/pill_renderer/src/resources/mod.rs b/engine/pill_renderer/src/resources/mod.rs index 1fe1eb94..3254f541 100644 --- a/engine/pill_renderer/src/resources/mod.rs +++ b/engine/pill_renderer/src/resources/mod.rs @@ -11,6 +11,7 @@ pub use renderer_mesh::{RendererMesh, Vertex}; pub use renderer_texture::RendererTexture; pub use renderer_camera::RendererCamera; +pub use renderer_camera::CameraUniform; pub use renderer_material::RendererMaterial; diff --git a/engine/pill_renderer/src/resources/renderer_camera.rs b/engine/pill_renderer/src/resources/renderer_camera.rs index 17646f66..e828fd90 100644 --- a/engine/pill_renderer/src/resources/renderer_camera.rs +++ b/engine/pill_renderer/src/resources/renderer_camera.rs @@ -1,12 +1,9 @@ use cgmath::{EuclideanSpace, SquareMatrix, Zero}; -use pill_engine::internal::{ - TransformComponent, - CameraComponent -}; +use pill_engine::internal::{CameraComponent, TransformComponent}; -use anyhow::{ Result }; -use wgpu::util::DeviceExt; +use anyhow::Result; use std::f32::consts::FRAC_PI_2; +use wgpu::util::DeviceExt; #[rustfmt::skip] pub const OPENGL_TO_WGPU_MATRIX: cgmath::Matrix4 = cgmath::Matrix4::new( @@ -20,8 +17,8 @@ pub const OPENGL_TO_WGPU_MATRIX: cgmath::Matrix4 = cgmath::Matrix4::new( #[repr(C)] #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -pub(crate) struct CameraUniform { - pub(crate) position: [f32; 4], // Camera position +pub struct CameraUniform { + pub(crate) position: [f32; 4], // Camera position pub(crate) view_projection_matrix: [[f32; 4]; 4], // Perspective manipulation } @@ -33,42 +30,49 @@ impl CameraUniform { } } - pub fn update_data(&mut self, camera_component: &CameraComponent, transform_component: &TransformComponent) { + pub fn update_data( + &mut self, + camera_component: &CameraComponent, + transform_component: &TransformComponent, + ) { // Update position - self.position = cgmath::Vector4:: { - x: transform_component.position.x, - y: transform_component.position.y, - z: transform_component.position.z, - w: 0.0 - }.into(); + self.position = cgmath::Vector4:: { + x: transform_component.position.x, + y: transform_component.position.y, + z: transform_component.position.z, + w: 0.0, + } + .into(); // Update view-projection - self.view_projection_matrix = (CameraUniform::calculate_projection_matrix(camera_component) * CameraUniform::calculate_view_matrix(transform_component)).into(); + self.view_projection_matrix = + (CameraUniform::calculate_projection_matrix(camera_component) + * CameraUniform::calculate_view_matrix(transform_component)) + .into(); } - fn calculate_view_matrix(transform_component: &TransformComponent) -> cgmath::Matrix4:: { + fn calculate_view_matrix(transform_component: &TransformComponent) -> cgmath::Matrix4 { let position = cgmath::Point3::from_vec(transform_component.position); - let roll_matrix = cgmath::Matrix3::from_angle_z(cgmath::Deg(transform_component.rotation.z)); - let yaw_matrix = cgmath::Matrix3::from_angle_y(cgmath::Deg(transform_component.rotation.y)); - let pitch_matrix = cgmath::Matrix3::from_angle_x(cgmath::Deg(transform_component.rotation.x)); + let roll_matrix = + cgmath::Matrix3::from_angle_z(cgmath::Deg(transform_component.rotation.z)); + let yaw_matrix = cgmath::Matrix3::from_angle_y(cgmath::Deg(transform_component.rotation.y)); + let pitch_matrix = + cgmath::Matrix3::from_angle_x(cgmath::Deg(transform_component.rotation.x)); let rotation_matrix = yaw_matrix * pitch_matrix * roll_matrix; - let direction = rotation_matrix * cgmath::Vector3::::unit_z(); + let direction = rotation_matrix * cgmath::Vector3::::unit_z(); - cgmath::Matrix4::look_to_rh( - position, - direction, - cgmath::Vector3::unit_y() - ) + cgmath::Matrix4::look_to_rh(position, direction, cgmath::Vector3::unit_y()) } - fn calculate_projection_matrix(camera_component: &CameraComponent) -> cgmath::Matrix4:: { - OPENGL_TO_WGPU_MATRIX * cgmath::perspective( - cgmath::Deg(camera_component.fov), - camera_component.aspect.get_value(), - camera_component.range.start, - camera_component.range.end - ) + fn calculate_projection_matrix(camera_component: &CameraComponent) -> cgmath::Matrix4 { + OPENGL_TO_WGPU_MATRIX + * cgmath::perspective( + cgmath::Deg(camera_component.fov), + camera_component.aspect.get_value(), + camera_component.range.start, + camera_component.range.end, + ) } } @@ -82,8 +86,10 @@ pub struct RendererCamera { } impl RendererCamera { - pub fn new(device: &wgpu::Device, camera_bind_group_layout: &wgpu::BindGroupLayout) -> Result { - + pub fn new( + device: &wgpu::Device, + camera_bind_group_layout: &wgpu::BindGroupLayout, + ) -> Result { let uniform = CameraUniform::new(); let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { @@ -110,8 +116,14 @@ impl RendererCamera { Ok(camera) } - pub fn update(&mut self, queue: &wgpu::Queue, camera_component: &CameraComponent, transform_component: &TransformComponent) { - self.uniform.update_data(camera_component, transform_component); + pub fn update( + &mut self, + queue: &wgpu::Queue, + camera_component: &CameraComponent, + transform_component: &TransformComponent, + ) { + self.uniform + .update_data(camera_component, transform_component); queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(&[self.uniform])); } } From 300de4e94e8c0ad19281d89334a17ff6e1d8c802 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Mon, 3 Nov 2025 02:18:48 +0100 Subject: [PATCH 53/69] wip: pass scene, pipeline --- engine/pill_renderer/src/pass_compose.rs | 3 +- .../pill_renderer/src/pass_overlay_depth.rs | 3 +- engine/pill_renderer/src/pass_overlay_logo.rs | 7 +- engine/pill_renderer/src/pass_overlay_uv.rs | 3 +- engine/pill_renderer/src/pass_scene.rs | 251 ++++++++++++++---- engine/pill_renderer/src/renderer.rs | 41 +-- 6 files changed, 216 insertions(+), 92 deletions(-) diff --git a/engine/pill_renderer/src/pass_compose.rs b/engine/pill_renderer/src/pass_compose.rs index f125cbaf..4ed467e0 100644 --- a/engine/pill_renderer/src/pass_compose.rs +++ b/engine/pill_renderer/src/pass_compose.rs @@ -36,8 +36,9 @@ impl Pass for PassCompose { &self.label } - fn init(&mut self, device: &wgpu::Device, _res: &mut ResourceManager) -> Result<()> { + fn init(&mut self, renderer: &mut Renderer) -> Result<()> { println!("Initializing pass: {}", self.label); + let device = &renderer.ctx.device; let vs = r#" struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; diff --git a/engine/pill_renderer/src/pass_overlay_depth.rs b/engine/pill_renderer/src/pass_overlay_depth.rs index cff073b1..2843c9b5 100644 --- a/engine/pill_renderer/src/pass_overlay_depth.rs +++ b/engine/pill_renderer/src/pass_overlay_depth.rs @@ -46,8 +46,9 @@ impl Pass for PassOverlayDepth { &self.label } - fn init(&mut self, device: &wgpu::Device, _res: &mut ResourceManager) -> Result<()> { + fn init(&mut self, renderer: &mut Renderer) -> Result<()> { println!("Initializing pass: {}", self.label); + let device = &renderer.ctx.device; // Create buffer for overlay rect UBO let buffer = device.create_buffer(&wgpu::BufferDescriptor { diff --git a/engine/pill_renderer/src/pass_overlay_logo.rs b/engine/pill_renderer/src/pass_overlay_logo.rs index 2f3a7621..ac82cc5d 100644 --- a/engine/pill_renderer/src/pass_overlay_logo.rs +++ b/engine/pill_renderer/src/pass_overlay_logo.rs @@ -46,8 +46,9 @@ impl Pass for PassOverlayLogo { &self.label } - fn init(&mut self, device: &wgpu::Device, _res: &mut ResourceManager) -> Result<()> { + fn init(&mut self, renderer: &mut Renderer) -> Result<()> { println!("Initializing pass: {}", self.label); + let device = &renderer.ctx.device; // Create buffer for overlay rect UBO let buffer = device.create_buffer(&wgpu::BufferDescriptor { @@ -185,7 +186,9 @@ impl Pass for PassOverlayLogo { }], }); - let tex = _res + let tex = renderer + .state + .resource_manager .textures .get(self.tex_logo) .expect("logo texture handle invalid"); diff --git a/engine/pill_renderer/src/pass_overlay_uv.rs b/engine/pill_renderer/src/pass_overlay_uv.rs index 32159a40..a93edf36 100644 --- a/engine/pill_renderer/src/pass_overlay_uv.rs +++ b/engine/pill_renderer/src/pass_overlay_uv.rs @@ -32,8 +32,9 @@ impl Pass for PassOverlayUV { &self.label } - fn init(&mut self, device: &wgpu::Device, _res: &mut ResourceManager) -> Result<()> { + fn init(&mut self, renderer: &mut Renderer) -> Result<()> { println!("Initializing pass: {}", self.label); + let device = &renderer.ctx.device; // Create buffer for overlay rect UBO let aligned_size = 256u64; diff --git a/engine/pill_renderer/src/pass_scene.rs b/engine/pill_renderer/src/pass_scene.rs index 3688a37c..60d2629f 100644 --- a/engine/pill_renderer/src/pass_scene.rs +++ b/engine/pill_renderer/src/pass_scene.rs @@ -9,8 +9,31 @@ use pill_engine::internal::{ EntityHandle, RenderQueueItem, RendererMaterialHandle, RendererMeshHandle, RendererPipelineHandle, TransformComponent, }; +use pill_engine::internal::{PillRenderer, PipelineV2, PipelineV2Desc, ShaderDesc}; use wgpu::CommandEncoder; +// Preallocated buffer structs for hot path optimization +pub(crate) struct VisiblePreDraw { + pub(crate) pipeline_handle: RendererPipelineHandle, + pub(crate) material_handle: RendererMaterialHandle, + pub(crate) mesh_handle: RendererMeshHandle, + pub(crate) entity_index: u32, + pub(crate) mvp: [[f32; 4]; 4], +} + +// M3: Hello Mesh + per-draw MVP (dynamic offsets) +pub(crate) struct MeshBatch { + pub(crate) mesh_handle: RendererMeshHandle, + pub(crate) instances: Vec<[[f32; 4]; 4]>, + pub(crate) base_offset_u32: u32, // offset into per-draw ring for first instance +} + +pub(crate) struct GroupCmd { + pub(crate) pipeline_handle: RendererPipelineHandle, + pub(crate) material_handle: RendererMaterialHandle, + pub(crate) batches: Vec, +} + pub struct PassScene { label: String, // Per-frame dynamic UBO ring (Milestone 5) @@ -19,8 +42,8 @@ pub struct PassScene { per_draw_buffer: Option, per_draw_bind_group: Option, // Working buffers - visible_pre_draw_buffer: Vec, - groups_buffer: Vec, + visible_pre_draw_buffer: Vec, + groups_buffer: Vec, staging_buffer: Vec, // Pass-local state initialized in init(), read every draw state: Option, @@ -28,9 +51,9 @@ pub struct PassScene { struct PassSceneState { camera_uniform: CameraUniform, - camera_bgl: wgpu::BindGroupLayout, camera_buffer: wgpu::Buffer, camera_bind_group: wgpu::BindGroup, + pipeline: PipelineV2, } impl PassScene { @@ -60,13 +83,55 @@ impl Pass for PassScene { &self.label } - fn init(&mut self, _device: &wgpu::Device, _res: &mut ResourceManager) -> Result<()> { - // Pre-create camera bind group layout, buffer and bind group - let device = _device; + fn init(&mut self, renderer: &mut Renderer) -> Result<()> { + // TODO: + // Problem: + // Material is coupled with Pipeline binds, now has handle to update i.e. texture via bind + // User creates Material and sets the params (floats, textures, etc.) that trigger update via Pipeline bind + // User can access Resources only via ResourcePool handles + // Material has to stay in sync with Pipeline and Shader + // Solution?: + // User can access via ResourcesPool only Material or MaterialInstance handles. + // Move PassScene to Engine. Pipeline can be local, Material too? Expose Material by Enum? + // User can create MaterialInstance and select Material/Pipeline via enum? i.e. PBR, UNLIT etc. + // Decouple Material params data from Implementation + // [SIMILAR] Prebuilt PSO used; avoid hot-path pipeline creation per TALK + // Shaders: must match bind group layout indices: 0(camera),1(material textures),2(material params),3(per-draw) + let vertex_wgsl = r#" + @group(0) @binding(0) var UCamera: mat4x4; + struct PerDraw { mvp: mat4x4, tint: vec4, }; + @group(3) @binding(0) var UPerDraw: PerDraw; + struct VSIn { @location(0) pos: vec3, @location(1) uv: vec2, }; + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; + @vertex fn main(input: VSIn) -> VSOut { + var out: VSOut; + out.pos = UPerDraw.mvp * vec4(input.pos, 1.0); + out.uv = input.uv; + return out; + } + "#; + let fragment_wgsl = r#" + @group(1) @binding(0) var tex_diffuse: texture_2d; + @group(1) @binding(1) var smp_diffuse: sampler; + @group(1) @binding(2) var tex_normal: texture_2d; + @group(1) @binding(3) var smp_normal: sampler; + struct MaterialParams { tint_spec: vec4, } + @group(2) @binding(0) var UMaterial: MaterialParams; + struct PerDraw { mvp: mat4x4, tint: vec4, }; + @group(3) @binding(0) var UPerDraw: PerDraw; + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let albedo = textureSample(tex_diffuse, smp_diffuse, uv); + let tinted = vec4(UMaterial.tint_spec.rgb, 1.0) * UPerDraw.tint; + let spec_boost = 0.5 + 0.5 * UMaterial.tint_spec.a; + let color = albedo * tinted * spec_boost; + return vec4(color.rgb, 1.0); + } + "#; - let camera_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("camera_bind_group_layout"), - entries: &[wgpu::BindGroupLayoutEntry { + // Describe bind group layouts for pipeline creation + let bind_groups: Vec> = vec![ + // 0: camera + vec![wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { @@ -76,18 +141,107 @@ impl Pass for PassScene { }, count: None, }], + // 1: material textures + vec![ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + // 2: material params + vec![wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + // 3: per-draw dynamic UBO + vec![wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: Some(std::num::NonZeroU64::new(144).unwrap()), + }, + count: None, + }], + ]; + + let color_target = wgpu::ColorTargetState { + format: renderer.state.color_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + }; + let depth_stencil = Some(wgpu::DepthStencilState { + format: renderer.state.depth_format, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), }); + let desc = PipelineV2Desc { + label: Some("scene_pipeline_v2"), + vs: ShaderDesc { + source: vertex_wgsl, + entry_func: "main", + }, + ps: ShaderDesc { + source: fragment_wgsl, + entry_func: "main", + }, + bind_groups, + targets: &[Some(color_target)], + depth_stencil, + multisample: wgpu::MultisampleState::default(), + }; + + let pipeline = renderer.create_pipeline_v2(desc)?; + + // Camera buffer and bind group using pipeline's camera layout (group 0) + let device = &renderer.ctx.device; let camera_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("camera_buffer"), size: std::mem::size_of::() as u64, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); - let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("camera_bind_group"), - layout: &camera_bgl, + layout: &pipeline.bind_group_layouts[0], entries: &[wgpu::BindGroupEntry { binding: 0, resource: camera_buffer.as_entire_binding(), @@ -96,9 +250,9 @@ impl Pass for PassScene { self.state = Some(PassSceneState { camera_uniform: CameraUniform::new(), - camera_bgl, camera_buffer, camera_bind_group, + pipeline, }); Ok(()) @@ -262,14 +416,13 @@ impl Pass for PassScene { let view_proj: Mat4 = vp_mat; let mvp: [[f32; 4]; 4] = (view_proj * model).to_cols_array_2d(); - self.visible_pre_draw_buffer - .push(crate::renderer::VisiblePreDraw { - pipeline_handle, - material_handle, - mesh_handle, - entity_index: render_queue_item.entity_index, - mvp, - }); + self.visible_pre_draw_buffer.push(VisiblePreDraw { + pipeline_handle, + material_handle, + mesh_handle, + entity_index: render_queue_item.entity_index, + mvp, + }); } // Sort and group @@ -290,7 +443,7 @@ impl Pass for PassScene { }) .unwrap_or(true) { - self.groups_buffer.push(crate::renderer::GroupCmd { + self.groups_buffer.push(GroupCmd { pipeline_handle: v.pipeline_handle, material_handle: v.material_handle, batches: Vec::new(), @@ -304,7 +457,7 @@ impl Pass for PassScene { { batch.instances.push(v.mvp); } else { - g.batches.push(crate::renderer::MeshBatch { + g.batches.push(MeshBatch { mesh_handle: v.mesh_handle, instances: vec![v.mvp], base_offset_u32: 0, @@ -333,27 +486,27 @@ impl Pass for PassScene { usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, })); - self.per_draw_bind_group = Some( - renderer - .ctx - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("per_draw_bind_group"), - layout: &renderer - .state - .resource_manager - .pipeline(renderer.state.master_pipeline_handle) - .per_draw_bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: self.per_draw_buffer.as_ref().unwrap(), - offset: 0, - size: Some(std::num::NonZeroU64::new(144).unwrap()), - }), - }], - }), - ); + let layout_ptr: *const wgpu::BindGroupLayout = { + let state = get_state(self); + &state.pipeline.bind_group_layouts[3] as *const _ + }; + let buf_ptr: *const wgpu::Buffer = self.per_draw_buffer.as_ref().unwrap(); + let new_bg = renderer + .ctx + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("per_draw_bind_group"), + layout: unsafe { &*layout_ptr }, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: unsafe { &*buf_ptr }, + offset: 0, + size: Some(std::num::NonZeroU64::new(144).unwrap()), + }), + }], + }); + self.per_draw_bind_group = Some(new_bg); } #[repr(C)] #[derive(Copy, Clone)] @@ -414,14 +567,12 @@ impl Pass for PassScene { timestamp_writes: None, occlusion_query_set: None, }); + let pipeline_ptr: *const wgpu::RenderPipeline = { + let state = get_state(self); + &state.pipeline.pipeline as *const _ + }; for group in &self.groups_buffer { - rpass.set_pipeline( - &renderer - .state - .resource_manager - .pipeline(renderer.state.master_pipeline_handle) - .render_pipeline, - ); + rpass.set_pipeline(unsafe { &*pipeline_ptr }); let material = renderer .state .resource_manager diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 96ccda28..536f2cf4 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -111,11 +111,7 @@ fn create_render_target( pub trait Pass { fn get_label(&self) -> &str; - fn init( - &mut self, - device: &wgpu::Device, - res: &mut crate::resource_manager::ResourceManager, - ) -> Result<()>; + fn init(&mut self, renderer: &mut Renderer) -> Result<()>; fn draw( &mut self, encoder: &mut wgpu::CommandEncoder, @@ -139,27 +135,6 @@ pub struct DeviceContext { pub(crate) surface_configuration: wgpu::SurfaceConfiguration, } -// Preallocated buffer structs for hot path optimization -pub(crate) struct VisiblePreDraw { - pub(crate) pipeline_handle: RendererPipelineHandle, - pub(crate) material_handle: RendererMaterialHandle, - pub(crate) mesh_handle: RendererMeshHandle, - pub(crate) entity_index: u32, - pub(crate) mvp: [[f32; 4]; 4], -} - -pub(crate) struct MeshBatch { - pub(crate) mesh_handle: RendererMeshHandle, - pub(crate) instances: Vec<[[f32; 4]; 4]>, - pub(crate) base_offset_u32: u32, // offset into per-draw ring for first instance -} - -pub(crate) struct GroupCmd { - pub(crate) pipeline_handle: RendererPipelineHandle, - pub(crate) material_handle: RendererMaterialHandle, - pub(crate) batches: Vec, -} - // Local world query for passes (avoids depending on pill_engine::graphics visibility) // TODO: Rename to WorldView, SceneView, ... pub struct WorldQuery<'a> { @@ -518,7 +493,7 @@ impl PillRenderer for Renderer { // Temporarily move passes out to avoid borrowing conflicts let mut passes = std::mem::take(&mut self.state.passes); for pass in passes.iter_mut() { - let _ = pass.init(&self.ctx.device, &mut self.state.resource_manager); + let _ = pass.init(self); } self.state.passes = passes; @@ -631,7 +606,7 @@ impl PillRenderer for Renderer { vertex: wgpu::VertexState { module: &vs, entry_point: desc.vs.entry_func, - buffers: &[], + buffers: &[crate::resources::RendererMesh::data_layout_descriptor()], compilation_options: Default::default(), }, fragment: Some(wgpu::FragmentState { @@ -641,7 +616,7 @@ impl PillRenderer for Renderer { compilation_options: Default::default(), }), primitive: wgpu::PrimitiveState { - cull_mode: None, + cull_mode: Some(wgpu::Face::Back), ..wgpu::PrimitiveState::default() }, depth_stencil: desc.depth_stencil, @@ -904,13 +879,6 @@ impl PillRenderer for Renderer { egui_ui: Box, timer: &mut Timer, ) -> Result<()> { - // [SIMILAR] Prebuilt PSO used; avoid hot-path pipeline creation per TALK - let _per_draw_bind_group_layout = &self - .state - .resource_manager - .pipeline(self.state.master_pipeline_handle) - .per_draw_bind_group_layout; - // M3: Hello Mesh + per-draw MVP (dynamic offsets) timer.record("Frame: acquire"); // Get frame or return mapped error if failed @@ -951,7 +919,6 @@ impl PillRenderer for Renderer { timer.begin_context(&label_owned); let _ = pass.draw(&mut encoder, self, &frame, &view, &world_q); timer.record(&label_owned); - timer.end_context()?; } self.state.passes = passes; From f2eaae8659ea86febf074e571e27373b8e8d038a Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Mon, 10 Nov 2025 02:56:31 +0100 Subject: [PATCH 54/69] wip: pass scene, materialfull --- engine/pill_engine/src/config.rs | 21 +- .../src/ecs/components/camera_component.rs | 2 +- .../components/deferred_update_component.rs | 2 +- .../components/mesh_rendering_component.rs | 23 +- .../src/ecs/systems/rendering_system.rs | 63 +- engine/pill_engine/src/engine.rs | 2070 ++++++++--------- engine/pill_engine/src/graphics/mod.rs | 6 +- .../pill_engine/src/graphics/render_queue.rs | 36 +- engine/pill_engine/src/graphics/renderer.rs | 47 +- engine/pill_engine/src/lib.rs | 21 +- engine/pill_engine/src/resources/material.rs | 550 +---- engine/pill_engine/src/resources/mod.rs | 47 +- .../src/resources/resource_manager.rs | 198 +- .../src/resources/resource_storage.rs | 18 +- engine/pill_engine/src/resources/sound.rs | 2 +- engine/pill_engine/src/resources/texture.rs | 71 +- engine/pill_renderer/src/pass_scene.rs | 402 ++-- engine/pill_renderer/src/renderer.rs | 480 +++- engine/pill_renderer/src/resource_manager.rs | 8 +- engine/pill_renderer/src/resource_snapshot.rs | 11 +- engine/pill_renderer/src/resources/mod.rs | 6 +- .../src/resources/renderer_material.rs | 272 +-- .../src/resources/renderer_pipeline.rs | 38 +- examples/pill_demo/src/game.rs | 8 +- 24 files changed, 2170 insertions(+), 2232 deletions(-) diff --git a/engine/pill_engine/src/config.rs b/engine/pill_engine/src/config.rs index 4750962f..ac0b6681 100644 --- a/engine/pill_engine/src/config.rs +++ b/engine/pill_engine/src/config.rs @@ -4,8 +4,8 @@ use crate::{ AudioManagerComponent, DeferredUpdateComponent, EguiManagerComponent, InputComponent, RenderStateComponent, SystemFunction, TimeComponent, UpdatePhase, }, - graphics::{RendererMaterialHandle, RendererTextureHandle}, - resources::{MaterialHandle, TextureHandle, TextureType}, + graphics::RendererTextureHandle, + resources::{TextureHandle, TextureType}, }; use pill_core::{Handle, PillSlotMapKeyData}; @@ -116,30 +116,17 @@ pub fn get_default_texture_handles( texture_type: TextureType, ) -> (TextureHandle, RendererTextureHandle) { match texture_type { - TextureType::Color => ( + TextureType::Gamma => ( DEFAULT_COLOR_TEXTURE_HANDLE, DEFAULT_RENDERER_COLOR_TEXTURE_HANDLE, ), - TextureType::Normal => ( + TextureType::Linear => ( DEFAULT_NORMAL_TEXTURE_HANDLE, DEFAULT_RENDERER_NORMAL_TEXTURE_HANDLE, ), } } -// Default resource handle - Material -pub const DEFAULT_MATERIAL_HANDLE: MaterialHandle = MaterialHandle { - 0: PillSlotMapKeyData { - index: 1, - version: unsafe { std::num::NonZeroU32::new_unchecked(1) }, - }, -}; -pub const DEFAULT_RENDERER_MATERIAL_HANDLE: RendererMaterialHandle = Handle::from_parts(1, 1); - -pub fn get_default_material_handles() -> (MaterialHandle, RendererMaterialHandle) { - (DEFAULT_MATERIAL_HANDLE, DEFAULT_RENDERER_MATERIAL_HANDLE) -} - lazy_static! { pub static ref ENGINE_GLOBAL_COMPONENTS: Vec = vec!( TypeId::of::(), diff --git a/engine/pill_engine/src/ecs/components/camera_component.rs b/engine/pill_engine/src/ecs/components/camera_component.rs index 377e4ade..44a032c8 100644 --- a/engine/pill_engine/src/ecs/components/camera_component.rs +++ b/engine/pill_engine/src/ecs/components/camera_component.rs @@ -1,7 +1,7 @@ use crate::{ engine::Engine, graphics::{ RenderQueueKey, compose_render_queue_key, RendererCameraHandle }, - resources::{ Material, MaterialHandle, Mesh, MeshHandle }, + resources::{ PBRMaterial, PBRMaterialHandle, Mesh, MeshHandle }, ecs::{ Component, ComponentStorage, EntityHandle, SceneHandle, DeferredUpdateManagerPointer, DeferredUpdateComponentRequest }, }; diff --git a/engine/pill_engine/src/ecs/components/deferred_update_component.rs b/engine/pill_engine/src/ecs/components/deferred_update_component.rs index e5246b5c..6bf71d25 100644 --- a/engine/pill_engine/src/ecs/components/deferred_update_component.rs +++ b/engine/pill_engine/src/ecs/components/deferred_update_component.rs @@ -2,7 +2,7 @@ use crate::{ engine::Engine, - resources::{ MeshHandle, Material, Resource, ResourceStorage }, + resources::{ MeshHandle, PBRMaterial, Resource, ResourceStorage }, ecs::{ Component, EntityHandle, ComponentStorage, SceneHandle, GlobalComponentStorage, GlobalComponent } }; diff --git a/engine/pill_engine/src/ecs/components/mesh_rendering_component.rs b/engine/pill_engine/src/ecs/components/mesh_rendering_component.rs index 131aa833..358698f1 100644 --- a/engine/pill_engine/src/ecs/components/mesh_rendering_component.rs +++ b/engine/pill_engine/src/ecs/components/mesh_rendering_component.rs @@ -1,12 +1,11 @@ use crate::{ - config::DEFAULT_MATERIAL_HANDLE, ecs::{ Component, ComponentStorage, DeferredUpdateComponent, DeferredUpdateComponentRequest, DeferredUpdateManagerPointer, EntityHandle, SceneHandle, }, engine::Engine, graphics::{compose_render_queue_key, RenderQueueKey}, - resources::{Material, MaterialHandle, Mesh, MeshHandle, ResourceManager}, + resources::{Mesh, MeshHandle, PBRMaterial, PBRMaterialHandle, ResourceManager}, }; use cgmath::num_traits::Float; @@ -36,7 +35,7 @@ impl MeshRenderingComponentBuilder { self } - pub fn material(mut self, material_handle: &MaterialHandle) -> Self { + pub fn material(mut self, material_handle: &PBRMaterialHandle) -> Self { self.component.material_handle = Some(material_handle.clone()); self } @@ -53,7 +52,7 @@ pub struct MeshRenderingComponent { #[readonly] pub mesh_handle: Option, #[readonly] - pub material_handle: Option, + pub material_handle: Option, // [SIMILAR] Precomputes/stores a key for sorting by pipeline/material/mesh outside hot draw loop (per TALK separation of data setup vs draw) // [API->CLIENT] Key encodes pipeline/material/mesh generational handles used by low-level to minimize state changes pub(crate) render_queue_key: Option, @@ -79,7 +78,7 @@ impl MeshRenderingComponent { } } - pub fn set_material(&mut self, material_handle: &MaterialHandle) { + pub fn set_material(&mut self, material_handle: &PBRMaterialHandle) { self.material_handle = Some(material_handle.clone()); // [SIMILAR] Mutations post a deferred update; avoids work in the hot path self.post_deferred_update_request(DEFERRED_REQUEST_VARIANT_SET_MATERIAL); @@ -101,7 +100,7 @@ impl MeshRenderingComponent { self.post_deferred_update_request(DEFERRED_REQUEST_VARIANT_UPDATE_RENDER_QUEUE); } - pub(crate) fn set_material_handle(&mut self, material_handle: Option) { + pub(crate) fn set_material_handle(&mut self, material_handle: Option) { self.material_handle = material_handle; } @@ -114,17 +113,11 @@ impl MeshRenderingComponent { resource_manager: &ResourceManager, ) -> Result<()> { if self.mesh_handle.is_some() { - // Use default material if no material is set - let material_handle = match self.material_handle { - Some(v) => v, - None => DEFAULT_MATERIAL_HANDLE, - }; - // Compose render queue key and set it if let Ok(render_queue_key) = compose_render_queue_key( resource_manager, - &material_handle, &self.mesh_handle.unwrap(), + &self.material_handle, ) { // [SIMILAR] Precompute render binning key when data changes (not per draw) // [RECOMMENDED] Keep key up-to-date on any mesh/material change @@ -177,7 +170,7 @@ impl Component for MeshRenderingComponent { // Check if material handle is valid if self.material_handle.is_some() { engine - .get_resource::(&self.material_handle.unwrap()) + .get_resource::(&self.material_handle.unwrap()) .context(format!( "Creating {} {} failed", "Component".gobj_style(), @@ -213,7 +206,7 @@ impl Component for MeshRenderingComponent { DEFERRED_REQUEST_VARIANT_SET_MATERIAL => { // Check if material handle is valid engine - .get_resource::(&self.material_handle.unwrap()) + .get_resource::(&self.material_handle.unwrap()) .context(format!( "Setting {} {} failed", "Resource".gobj_style(), diff --git a/engine/pill_engine/src/ecs/systems/rendering_system.rs b/engine/pill_engine/src/ecs/systems/rendering_system.rs index 9a3bb4fe..6fcaa178 100644 --- a/engine/pill_engine/src/ecs/systems/rendering_system.rs +++ b/engine/pill_engine/src/ecs/systems/rendering_system.rs @@ -9,7 +9,7 @@ use crate::{ graphics::{ compose_render_queue_key, RenderQuery, RenderQueueFactory, RenderQueueItem, RenderQueueKey, }, - resources::{Material, MaterialHandle, Mesh, MeshHandle, ResourceManager}, + resources::{Mesh, MeshHandle, PBRMaterial, PBRMaterialHandle, ResourceManager}, }; use pill_core::{EngineError, PillSlotMapKey, PillStyle, RendererError, Timer}; @@ -24,7 +24,7 @@ use crate::config::{ MAX_MESHES, MAX_SOUNDS, MAX_TEXTURES, }; use crate::ecs::components::render_state_component::RenderStateComponent; -use crate::resources::{Resource, ResourceLoadType, Texture, TextureType}; +use crate::resources::{Resource, ResourceLoadType, Texture, TextureHandle, TextureType}; // Constants for hot path optimization const MAX_RENDERABLES_CAPACITY: usize = 100000; // Maximum expected renderables per frame @@ -101,6 +101,57 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { timer.record("Prepare render queue"); + // Prepass: deferred material updates (CPU -> GPU) for dirty PBR materials + { + // Collect updates immutably to avoid borrow conflicts + let mut updates: Vec<( + PBRMaterialHandle, + crate::graphics::RendererMaterialHandle, + crate::graphics::MaterialDesc, + )> = Vec::new(); + if let Ok(storage) = engine + .resource_manager + .get_resource_storage::() + { + for (h, opt_mat) in storage.data.iter() { + if let Some(mat) = opt_mat { + if mat.is_dirty { + if let Some(rm_handle) = mat.renderer_resource_handle { + let map_tex = |th: &Option| -> Option { + th.as_ref().and_then(|hh| { + engine + .resource_manager + .get_resource::(hh) + .ok() + .and_then(|t| t.renderer_resource_handle) + }) + }; + let desc = crate::graphics::MaterialDesc { + label: "upd", + albedo: [mat.albedo.x, mat.albedo.y, mat.albedo.z], + metallic: mat.metallic, + roughness: mat.roughness, + emissive: [mat.emissive.x, mat.emissive.y, mat.emissive.z], + albedo_tex: map_tex(&mat.albedo_texture), + normal_tex: map_tex(&mat.normal_texture), + metallic_roughness_tex: map_tex(&mat.metallic_roughness_texture), + emissive_tex: map_tex(&mat.emissive_texture), + }; + updates.push((h, rm_handle, desc)); + } + } + } + } + } + // Apply updates and clear dirty flags + for (ph, rmh, desc) in updates { + let _ = engine.renderer.update_material(rmh, desc); + if let Ok(m) = engine.resource_manager.get_resource_mut::(&ph) { + m.is_dirty = false; + } + } + } + // Use static preallocated dirty_entities Vec unsafe { DIRTY_ENTITIES.clear(); // Clear and reuse preallocated Vec @@ -248,27 +299,27 @@ fn init_default_resources(engine: &mut Engine) -> Result<(), Error> { engine.register_resource_type::(max_texture_count)?; engine.register_resource_type::(max_mesh_count)?; - engine.register_resource_type::(max_material_count)?; + engine.register_resource_type::(max_material_count)?; engine.register_resource_type::(max_sound_count)?; let default_color = Box::new(*include_bytes!("../../../res/textures/default_color.png")); let default_normal = Box::new(*include_bytes!("../../../res/textures/default_normal.png")); let mut color = Texture::new( DEFAULT_COLOR_TEXTURE_NAME, - TextureType::Color, + TextureType::Gamma, ResourceLoadType::Bytes(default_color), ); color.initialize(engine)?; engine.resource_manager.add_resource(color)?; let mut normal = Texture::new( DEFAULT_NORMAL_TEXTURE_NAME, - TextureType::Normal, + TextureType::Linear, ResourceLoadType::Bytes(default_normal), ); normal.initialize(engine)?; engine.resource_manager.add_resource(normal)?; - let mut mat = Material::new(DEFAULT_MATERIAL_NAME); + let mut mat = PBRMaterial::new(DEFAULT_MATERIAL_NAME); mat.initialize(engine)?; engine.resource_manager.add_resource(mat)?; diff --git a/engine/pill_engine/src/engine.rs b/engine/pill_engine/src/engine.rs index 3e9dc5e4..5122eb12 100644 --- a/engine/pill_engine/src/engine.rs +++ b/engine/pill_engine/src/engine.rs @@ -1,1035 +1,1035 @@ -use crate::{config::*, ecs::*, graphics::*, resources::*}; - -use pill_core::{ - get_enum_variant_type_name, get_game_error_message, get_type_name, get_value_type_name, - EngineError, PillSlotMapKey, PillStyle, PillTypeMap, Timer, Vector2f, -}; - -use anyhow::{Context, Error, Result}; -use boolinator::Boolinator; -use log::{debug, error, info}; -use std::{ - any::type_name, any::Any, any::TypeId, cell::RefCell, collections::VecDeque, ops::RangeBounds, -}; -use winit::{dpi::PhysicalPosition, event::KeyEvent}; - -// ------------------------------------------------------------------------------- - -pub type Game = Box; -pub type KeyboardKey = winit::keyboard::KeyCode; -pub type MouseButton = winit::event::MouseButton; - -/// Engine <-> Game interface -/// -/// Entry point of the game project. Mandatory to implement. -pub trait PillGame { - fn start(&self, engine: &mut Engine) -> Result<()>; -} - -/// Heart of Pill Engine -pub struct Engine { - pub(crate) config: config::Config, - pub(crate) game: Option, - pub(crate) renderer: Renderer, - pub(crate) scene_manager: SceneManager, - pub(crate) system_manager: SystemManager, - pub(crate) resource_manager: ResourceManager, - pub(crate) global_components: PillTypeMap, - pub(crate) input_queue: VecDeque, - pub(crate) render_queue: Vec, - pub(crate) window_size: winit::dpi::PhysicalSize, - pub(crate) game_resources_directory_path: std::path::PathBuf, - pub(crate) frame_delta_time: f32, // In milliseconds -} - -// ---- INTERNAL API ----------------------------------------------------------------- -struct EguiUI<'a> { - frame_delta_time: &'a f32, // Use a reference to frame_delta_time -} - -impl<'a> EguiUI<'a> { - fn render(&self, ui: &egui::Context) { - egui::Window::new("PillEngine") - .default_open(true) - .resizable(true) - .anchor(egui::Align2::LEFT_TOP, [0.0, 0.0]) - .show(ui, |ui| { - if ui.add(egui::Button::new("Click me")).clicked() { - println!("PRESSED"); - println!("{}", self.frame_delta_time); - } - }); - } -} -/// Pill Engine internal API -#[cfg(feature = "internal")] -impl Engine { - pub fn new( - game: Box, - game_resources_directory_path: std::path::PathBuf, - renderer: Box, - config: config::Config, - ) -> Self { - let max_entity_count = config - .get_int("MAX_ENTITIES") - .unwrap_or(MAX_ENTITIES as i64) as usize; - - Self { - config, - game: Some(game), - renderer, - scene_manager: SceneManager::new(max_entity_count), - system_manager: SystemManager::new(), - resource_manager: ResourceManager::new(), - global_components: PillTypeMap::new(), - input_queue: VecDeque::new(), - render_queue: Vec::::with_capacity(max_entity_count), - window_size: winit::dpi::PhysicalSize::::default(), - game_resources_directory_path, - frame_delta_time: 0.0.into(), - } - } - - fn start_game(&mut self) -> Result<()> { - info!("Starting {}", "Game".mobj_style()); - - let game = self - .game - .take() - .ok_or(EngineError::Other("Cannot get game".to_string()))?; - let stop_on_game_errors = self - .config - .get_bool("PANIC_ON_GAME_ERRORS") - .unwrap_or(PANIC_ON_GAME_ERRORS); - let result = game.start(self); - match stop_on_game_errors { - true => result.context(format!("{} error", "Game".mobj_style()))?, - false => { - if let Some(message) = get_game_error_message(result) { - error!("{}", message); - } - } - } - self.game = Some(game); - - Ok(()) - } - - /// Initializes Pill Engine - /// - /// Creates default global components, adds default systems, creates default resources, initializes game - pub fn initialize(&mut self, window_size: winit::dpi::PhysicalSize) -> Result<()> { - info!("Initializing {}", "Engine".mobj_style()); - - // Set window size - self.window_size = window_size; - - // Register global components - self.add_global_component(InputComponent::new())?; - self.add_global_component(TimeComponent::new())?; - self.add_global_component(DeferredUpdateComponent::new())?; - self.add_global_component(EguiManagerComponent::new())?; - self.add_global_component(RenderStateComponent::new())?; - - let max_ambient_sink_count = - self.config - .get_int("MAX_CONCURRENT_2D_SOUNDS") - .unwrap_or(MAX_CONCURRENT_2D_SOUNDS as i64) as usize; - let max_spatial_sink_count = - self.config - .get_int("MAX_CONCURRENT_3D_SOUNDS") - .unwrap_or(MAX_CONCURRENT_3D_SOUNDS as i64) as usize; - self.add_global_component(AudioManagerComponent::new( - max_ambient_sink_count, - max_spatial_sink_count, - ))?; - - // Add built-in systems - self.system_manager.add_system( - INPUT_SYSTEM.name, - INPUT_SYSTEM.system_function, - INPUT_SYSTEM.update_phase, - )?; - self.system_manager.add_system( - TIME_SYSTEM.name, - TIME_SYSTEM.system_function, - TIME_SYSTEM.update_phase, - )?; - self.system_manager.add_system( - AUDIO_SYSTEM.name, - AUDIO_SYSTEM.system_function, - AUDIO_SYSTEM.update_phase, - )?; - self.system_manager.add_system( - DEFERRED_UPDATE_SYSTEM.name, - DEFERRED_UPDATE_SYSTEM.system_function, - DEFERRED_UPDATE_SYSTEM.update_phase, - )?; - self.system_manager.add_system( - RENDERING_SYSTEM.name, - RENDERING_SYSTEM.system_function, - RENDERING_SYSTEM.update_phase, - )?; - - self.renderer.init(); - self.init_renderer(); - - // Start game - self.start_game()?; - - Ok(()) - } - - fn init_renderer(&mut self) { - // KISS: Run only rendering system once for bootstrap (no active scene yet) - // TODO: Loop over PostGame systems to give them a change to init before game starts - { - let update_phase = UpdatePhase::PostGame; - let (system_name, system_function) = { - let sys = self - .system_manager - .get_system(RENDERING_SYSTEM.name, update_phase.clone()) - .unwrap(); - (sys.name.clone(), sys.system_function) - }; - let mut timer = Timer::new(); - timer.begin_context(&format!("{} system bootstrap", system_name)); - self.system_manager - .update_system_timer(system_name.as_str(), update_phase.clone(), timer) - .unwrap(); - (system_function)(self).unwrap(); - let mut timer = self - .system_manager - .get_system_timer(system_name.as_str(), update_phase.clone()) - .unwrap() - .unwrap(); - timer.end_context().unwrap(); - self.system_manager - .update_system_timer(system_name.as_str(), update_phase.clone(), timer) - .unwrap(); - } - } - - /// Main engine update function - /// - /// Runs all systems in order: PreGame -> Game -> PostGame - pub fn update(&mut self, delta_time: std::time::Duration) { - let stop_on_game_errors = self - .config - .get_bool("PANIC_ON_GAME_ERRORS") - .unwrap_or(PANIC_ON_GAME_ERRORS); - - // Run systems - for update_phase_index in 0..self.system_manager.update_phases.len() { - for system_index in 0..self.system_manager.update_phases[update_phase_index].len() { - let (system_name, update_phase, system_function); - { - let system = - &mut self.system_manager.update_phases[update_phase_index][system_index]; - if !system.enabled { - continue; - } - system_name = system.name.to_string(); - update_phase = system.update_phase.clone(); - system_function = system.system_function; - } - - // Create new time and asign it to system so it can be accessed inside the system function - // For rendering system we can't clean its timer here, - // because it has to render its own timer data in the UI - // (and since the frame in which it renders is not yet finished when it renders UI, it has to use previous frame timer data) - if system_name != RENDERING_SYSTEM.name { - let mut timer = Timer::new(); - timer.begin_context(&format!("{} system update", system_name)); - self.system_manager - .update_system_timer(system_name.as_str(), update_phase.clone(), timer) - .unwrap(); - } - - { - // Run system update and handle errors based on configuration - let result = (system_function)(self).context(EngineError::SystemUpdateFailed( - system_name.clone(), - format!("{:?}", update_phase.clone()), - )); - - if update_phase == UpdatePhase::Game && stop_on_game_errors { - result.unwrap(); // Panic on error if configured - } else if let Err(err) = result { - if let Some(message) = get_game_error_message(Err(err)) { - error!("{}", message); - } - } - } - - // Update system timer with the final timer state - let mut timer = match self - .system_manager - .get_system_timer(system_name.as_str(), update_phase.clone()) - { - Ok(Some(timer)) => timer, - Ok(None) => { - panic!( - "{}", - Error::new(EngineError::NonReturnedSystemTimer(system_name.clone())) - ); - } - Err(e) => { - panic!("{}", Error::new(EngineError::Other(e.to_string()))); - } - }; - - timer - .end_context() - .context(format!( - "Failed to end timer context for {}", - system_name.clone() - )) - .unwrap(); // End system update context - self.system_manager - .update_system_timer(system_name.as_str(), update_phase.clone(), timer) - .unwrap(); - } - } - - // Update FPS counter - let new_frame_time = delta_time.as_secs_f32() * 1000.0; - self.frame_delta_time = new_frame_time.into(); - } - - pub fn shutdown(&mut self) { - info!("Shutting down {}", "Engine".mobj_style()); - } - - pub fn resize(&mut self, new_window_size: winit::dpi::PhysicalSize) { - debug!( - "{} resized to {}x{}", - "Window".mobj_style(), - new_window_size.width, - new_window_size.height - ); - self.window_size = new_window_size; - self.renderer.resize(new_window_size); - } - - pub fn pass_keyboard_key_input(&mut self, keyboard_input: &KeyEvent) { - let state: winit::event::ElementState = keyboard_input.state; - match keyboard_input.physical_key { - winit::keyboard::PhysicalKey::Code(key_code) => { - let input_event = InputEvent::KeyboardKey { - key: key_code, - state: state, - }; - self.input_queue.push_back(input_event); - debug!("Got new keyboard key input: {:?} {:?}", key_code, state); - } - winit::keyboard::PhysicalKey::Unidentified(_) => { - debug!("Unidentified key input: {:?}", keyboard_input.physical_key); - } - } - } - - pub fn pass_mouse_key_input(&mut self, key: &MouseButton, state: &winit::event::ElementState) { - let input_event = InputEvent::MouseButton { - key: *key, - state: *state, - }; - self.input_queue.push_back(input_event); - debug!("Got new mouse key input"); - } - - pub fn pass_mouse_wheel_input(&mut self, delta: &winit::event::MouseScrollDelta) { - let input_event = InputEvent::MouseWheel { delta: *delta }; - self.input_queue.push_back(input_event); - debug!("Got new mouse wheel input"); - } - - pub fn pass_mouse_delta_input(&mut self, delta: &(f64, f64)) { - let input_event = InputEvent::MouseDelta { - delta: Vector2f::new(delta.0 as f32, delta.1 as f32), - }; - self.input_queue.push_back(input_event); - debug!("Got new mouse motion input"); - } - - pub fn pass_mouse_position_input(&mut self, position: &PhysicalPosition) { - let input_event = InputEvent::MousePosition { - position: Vector2f::new(position.x as f32, position.y as f32), - }; - self.input_queue.push_back(input_event); - debug!("Got new mouse position input"); - } - - pub fn pass_input_to_egui(&mut self, event: &winit::event::WindowEvent) { - self.renderer.pass_input_to_egui(event).unwrap(); - } - - pub fn get_input_queue(&self) -> &VecDeque { - &self.input_queue - } -} - -// --- API ------------------------------------------------------------------ - -/// Pill Engine game API -impl Engine { - // --- System API --- - - /// Adds game-defined system to the game update phase - pub fn add_system( - &mut self, - name: &str, - system_function: fn(engine: &mut Engine) -> Result<()>, - ) -> Result<()> { - debug!( - "Adding {} {} to {} {}", - "System".gobj_style(), - name.name_style(), - "UpdatePhase".sobj_style(), - "Game".name_style() - ); - - self.system_manager - .add_system(name, system_function, UpdatePhase::Game) - .context(format!("Adding {} failed", "System".gobj_style())) - } - - /// Removes game-defined system - pub fn remove_system(&mut self, name: &str) -> Result<()> { - debug!( - "Removing {} {} from {} {}", - "System".gobj_style(), - name.name_style(), - "UpdatePhase".sobj_style(), - "Game".name_style() - ); - - self.system_manager - .remove_system(name, UpdatePhase::Game) - .context(format!("Removing {} failed", "System".gobj_style())) - } - - /// Toggles game-defined system - pub fn toggle_system( - &mut self, - name: &str, - update_phase: UpdatePhase, - enabled: bool, - ) -> Result<()> { - debug!( - "Toggling {} {} from {} {} to {} state", - "System".gobj_style(), - name.name_style(), - "UpdatePhase".sobj_style(), - update_phase.to_string().name_style(), - if enabled { "Enabled" } else { "Disabled" } - ); - - self.system_manager - .toggle_system(name, update_phase, enabled) - .context(format!("Toggling {} failed", "System".gobj_style())) - } - - /// Returns system timer. It has to be returned back using update_system_timer function, otherwise engine will panic. - pub fn get_system_timer(&mut self, name: &str) -> Timer { - debug!( - "Getting {} {} timer from {} {}", - "System".gobj_style(), - name.name_style(), - "UpdatePhase".sobj_style(), - "Game".name_style() - ); - - self.system_manager - .get_system_timer(name, UpdatePhase::Game) - .unwrap() - .unwrap() - } - - // --- Entity API --- - - /// Returns EntityBuilder, allowing for handy entity creation - pub fn build_entity(&mut self, scene_handle: SceneHandle) -> EntityBuilder<'_> { - let entity_handle = self.create_entity(scene_handle).unwrap(); - EntityBuilder { - engine: self, - entity_handle, - scene_handle, - } - } - - // Creates new entity to scene specified with scene handle - pub fn create_entity(&mut self, scene_handle: SceneHandle) -> Result { - debug!( - "Creating {} in {} {}", - "Entity".gobj_style(), - "Scene".gobj_style(), - self.scene_manager - .get_scene(scene_handle) - .unwrap() - .name - .name_style() - ); - - self.scene_manager - .create_entity(scene_handle) - .context(format!("Creating {} failed", "Entity".gobj_style())) - } - - // Removes entity specified with entity handle from scene specified with scene handle - pub fn remove_entity( - &mut self, - entity_handle: EntityHandle, - scene_handle: SceneHandle, - ) -> Result<()> { - debug!( - "Removing {} from {} {}", - "Entity".gobj_style(), - "Scene".gobj_style(), - self.scene_manager - .get_scene(scene_handle) - .unwrap() - .name - .name_style() - ); - - let component_destroyers = self - .scene_manager - .remove_entity(scene_handle, entity_handle) - .context(format!("Creating {} failed", "Entity".gobj_style()))?; - - // Destroy components using destroyers - for mut component_destroyer in component_destroyers { - component_destroyer.destroy(self, scene_handle, entity_handle)?; - } - - Ok(()) - } - - // --- Component API --- - - /// Registers new component type in scene specified with scene handle - pub fn register_component(&mut self, scene_handle: SceneHandle) -> Result<()> - where - T: Component>, - { - debug!( - "Registering {} {} in {} {}", - "Component".gobj_style(), - get_type_name::().sobj_style(), - "Scene".sobj_style(), - self.scene_manager - .get_scene(scene_handle) - .unwrap() - .name - .name_style() - ); - - self.scene_manager - .register_component::(scene_handle) - .context(format!("Registering {} failed", "Component".gobj_style())) - } - - /// Adds new component to the entity specified with scene and entity handle - pub fn add_component_to_entity( - &mut self, - scene_handle: SceneHandle, - entity_handle: EntityHandle, - mut component: T, - ) -> Result<()> - where - T: Component>, - { - debug!( - "Adding {} {} to {} {} in {} {}", - "Component".gobj_style(), - get_type_name::().sobj_style(), - "Entity".gobj_style(), - entity_handle.data().index, - "Scene".gobj_style(), - self.scene_manager - .get_scene(scene_handle) - .unwrap() - .name - .name_style() - ); - - // Check if already added - let target_scene = self.scene_manager.get_scene(scene_handle)?; - - if target_scene.entity_has_component::(entity_handle)? { - return Err(Error::new(EngineError::ComponentAlreadyExists( - get_type_name::(), - ))); - } - - // Initialize component - component.initialize(self).context(format!( - "Adding {} {} failed", - "Component".gobj_style(), - get_type_name::().sobj_style() - ))?; - - // Add component - self.scene_manager - .add_component_to_entity::(scene_handle, entity_handle, component) - .context(format!( - "Adding {} to {} failed", - "Component".gobj_style(), - "Entity".gobj_style() - ))?; - let component = self - .scene_manager - .get_entity_component::(entity_handle, scene_handle)?; - - // Pass handles to entity and scene to this component so it can store it if needed - component.pass_handles(scene_handle, entity_handle); - - Ok(()) - } - - /// Removes component from the entity specified with scene and entity handle - pub fn remove_component_from_entity( - &mut self, - scene_handle: SceneHandle, - entity_handle: EntityHandle, - ) -> Result<()> - where - T: Component>, - { - debug!( - "Removing {} {} from {} {} in {} {}", - "Component".gobj_style(), - get_type_name::().sobj_style(), - "Entity".gobj_style(), - entity_handle.data().index, - "Scene".gobj_style(), - self.scene_manager - .get_scene(scene_handle) - .unwrap() - .name - .name_style() - ); - - let mut component = self - .scene_manager - .remove_component_from_entity::(scene_handle, entity_handle) - .context("Removing component from entity failed") - .unwrap(); - - // Destroy component - component.destroy(self, scene_handle, entity_handle)?; - - Ok(()) - } - - // --- Global Component API --- - - /// Adds global component to engine - pub fn add_global_component(&mut self, mut component: T) -> Result<()> - where - T: GlobalComponent>, - { - // Check if component of this type is not already added - if self.global_components.contains_key::() { - return Err(Error::new(EngineError::GlobalComponentAlreadyExists( - get_type_name::(), - ))); - } - - // Initialize component - component.initialize(self)?; - - // Add component - self.global_components - .insert::(GlobalComponentStorage::::new(component)); - - Ok(()) - } - - /// Returns global component - pub fn get_global_component(&self) -> Result<&T> - where - T: GlobalComponent>, - { - // Get component - let component = self - .global_components - .get::() - .ok_or(Error::new(EngineError::GlobalComponentNotFound( - get_type_name::(), - )))? - .data - .as_ref() - .unwrap(); - - Ok(component) - } - - /// Returns global mutable component - pub fn get_global_component_mut(&mut self) -> Result<&mut T> - where - T: GlobalComponent>, - { - // Get component - let component = self - .global_components - .get_mut::() - .ok_or(Error::new(EngineError::GlobalComponentNotFound( - get_type_name::(), - )))? - .data - .as_mut() - .unwrap(); - - Ok(component) - } - - /// Removes global component from the engine - pub fn remove_global_component(&mut self) -> Result<()> - where - T: GlobalComponent>, - { - // Check if the type of the component is the same as of the ones, which cannot be removed - if ENGINE_GLOBAL_COMPONENTS.contains(&TypeId::of::()) { - return Err(Error::new(EngineError::GlobalComponentCannotBeRemoved( - get_type_name::(), - ))); - } - - // Remove and destroy component - let global_component_storage = self - .global_components - .remove::() - .ok_or(EngineError::GlobalComponentNotFound(get_type_name::()))?; - let mut global_component = global_component_storage.data.unwrap(); - global_component.destroy(self)?; - - Ok(()) - } - - // --- Iterator API --- - - /// Returns iterator for specified component - /// - /// Additionally returns entity handle to matching entities - pub fn iterate_one_component(&self) -> Result> - where - A: Component>, - { - // Get scene handle and iterator - let scene_handle = self.scene_manager.get_active_scene_handle()?; - self.scene_manager - .get_one_component_iterator::(scene_handle) - } - - /// Returns iterator for specified component mutable - /// - /// Additionally returns entity handle to matching entities - pub fn iterate_one_component_mut( - &mut self, - ) -> Result> - where - A: Component>, - { - // Get scene handle and iterator - let scene_handle = self.scene_manager.get_active_scene_handle()?; - self.scene_manager - .get_one_component_iterator_mut::(scene_handle) - } - - /// Returns iterator for specified component pair - /// - /// Iterator fetches specified components only for those entities which have them all - /// Additionally returns entity handle to matching entities - pub fn iterate_two_components( - &self, - ) -> Result> - where - A: Component>, - B: Component>, - { - // Get scene handle and iterator - let scene_handle = self.scene_manager.get_active_scene_handle()?; - self.scene_manager - .get_two_component_iterator::(scene_handle) - } - - /// Returns iterator for specified component pair mutable - /// - /// Iterator fetches specified components only for those entities which have them all - /// Additionally returns entity handle to matching entities - pub fn iterate_two_components_mut( - &mut self, - ) -> Result> - where - A: Component>, - B: Component>, - { - // Get scene handle and iterator - let scene_handle = self.scene_manager.get_active_scene_handle()?; - self.scene_manager - .get_two_component_iterator_mut::(scene_handle) - } - - /// Returns iterator for specified component triple - /// - /// Iterator fetches specified components only for those entities which have them all - /// Additionally returns entity handle to matching entities - pub fn iterate_three_components( - &self, - ) -> Result> - where - A: Component>, - B: Component>, - C: Component>, - { - // Get scene handle and iterator - let scene_handle = self.scene_manager.get_active_scene_handle()?; - self.scene_manager - .get_three_component_iterator::(scene_handle) - } - - /// Returns iterator for specified component triple mutable - /// - /// Iterator fetches specified components only for those entities which have them all - /// Additionally returns entity handle to matching entities - pub fn iterate_three_components_mut( - &mut self, - ) -> Result> - where - A: Component>, - B: Component>, - C: Component>, - { - // Get scene handle and iterator - let scene_handle = self.scene_manager.get_active_scene_handle()?; - self.scene_manager - .get_three_component_iterator_mut::(scene_handle) - } - - // --- Scene API --- - - // Creates scene - pub fn create_scene(&mut self, name: &str) -> Result { - info!("Creating scene: {}", name); - self.scene_manager - .create_scene(name) - .context(format!("Creating new {} failed", "Scene".gobj_style())) - } - - /// Returns handle to the scene specified by its name - pub fn get_scene_handle(&self, name: &str) -> Result { - self.scene_manager - .get_scene_handle(name) - .context(format!("Getting {} failed", "SceneHandle".sobj_style())) - } - - pub fn set_active_scene(&mut self, scene_handle: SceneHandle) -> Result<()> { - self.scene_manager - .set_active_scene(scene_handle) - .context(format!("Setting active {} failed", "Scene".gobj_style())) - } - - /// Returns handle to the active scene - pub fn get_active_scene_handle(&self) -> Result { - self.scene_manager - .get_active_scene_handle() - .context(format!( - "Getting {} of active {} failed", - "SceneHandle".sobj_style(), - "Scene".gobj_style() - )) - } - - // Removes scene deleting all data in it - pub fn remove_scene(&mut self, scene_handle: SceneHandle) -> Result<()> { - // Get scene - let scene = self.scene_manager.get_scene(scene_handle)?; - - // Get entity handles - let mut entity_handles = Vec::::new(); - for (entity_handle, _) in scene.entities.iter() { - entity_handles.push(entity_handle.clone()); - } - - // Remove entities - for entity_handle in entity_handles { - self.remove_entity(entity_handle, scene_handle)?; - } - - // Remove scene - self.scene_manager - .remove_scene(scene_handle) - .context(format!( - "Removing {} with usage of {} failed", - "Scene".sobj_style(), - "SceneHandle".gobj_style() - ))?; - - Ok(()) - } - - // --- Resource API --- - - // Registers new resource type in the engine - pub fn register_resource_type(&mut self, max_resource_count: usize) -> Result<()> - where - T: Resource>, - { - self.resource_manager - .register_resource_type::(max_resource_count) - } - - // Adds resource to the engine - pub fn add_resource(&mut self, mut resource: T) -> Result - where - T: Resource>, - { - debug!( - "Adding {} {} {}", - "Resource".gobj_style(), - get_type_name::().sobj_style(), - resource.get_name().name_style() - ); - - // Check if resource has proper name - let resource_name = resource.get_name(); - if resource_name.starts_with(DEFAULT_RESOURCE_PREFIX) { - return Err(Error::new(EngineError::WrongResourceName( - resource_name.clone(), - ))); - } - - // Initialize resource - resource.initialize(self).context(format!( - "Adding {} {} failed", - "Resource".gobj_style(), - get_type_name::().sobj_style() - ))?; - - // Add resource and get it back - let add_result = self.resource_manager.add_resource(resource)?; - let resource_handle = add_result.0; - let resource = add_result.1; - - // Pass handle to this resource so it can store it if needed - resource.pass_handle(resource_handle); - - Ok(resource_handle) - } - - // Returns resource associated with resource handle - pub fn get_resource<'a, T>(&'a self, resource_handle: &'a T::Handle) -> Result<&'a T> - where - T: Resource>, - { - Ok(self.resource_manager.get_resource::(resource_handle)?) - } - - /// Returns resource specified by its name - pub fn get_resource_by_name(&self, name: &str) -> Result<&T> - where - T: Resource>, - { - Ok(self.resource_manager.get_resource_by_name::(name)?) - } - - /// Returns handle to resource specified by the name of this resource - pub fn get_resource_handle(&self, name: &str) -> Result - where - T: Resource>, - { - Ok(self.resource_manager.get_resource_handle::(name)?) - } - - // Returns mutable resource associated with resource handle - pub fn get_resource_mut<'a, T>( - &'a mut self, - resource_handle: &'a T::Handle, - ) -> Result<&'a mut T> - where - T: Resource>, - { - Ok(self - .resource_manager - .get_resource_mut::(resource_handle)?) - } - - /// Returns mutable resource specified by its name - pub fn get_resource_by_name_mut(&mut self, name: &str) -> Result<&mut T> - where - T: Resource>, - { - Ok(self.resource_manager.get_resource_by_name_mut::(name)?) - } - - // Removes resource associated with resource handle from the engine - pub fn remove_resource(&mut self, resource_handle: &T::Handle) -> Result<()> - where - T: Resource>, - { - let error_message = format!( - "Removing {} {} failed", - "Resource".gobj_style(), - get_type_name::().sobj_style() - ); - - // Check if resource is not default - let resource_name = self - .resource_manager - .get_resource::(resource_handle) - .context(error_message.to_string())? - .get_name(); - if resource_name.starts_with(DEFAULT_RESOURCE_PREFIX) { - return Err(Error::new(EngineError::RemoveDefaultResource( - resource_name.clone(), - ))) - .context(error_message.to_string()); - } - - // Remove and destroy resource - let mut remove_result = self - .resource_manager - .remove_resource::(resource_handle) - .context(error_message.to_string())?; - remove_result.1.destroy(self, *resource_handle)?; - - Ok(()) - } - - // Removes resource specified with its name from the engine - pub fn remove_resource_by_name(&mut self, name: &str) -> Result<()> - where - T: Resource>, - { - let error_message = format!( - "Removing {} {} {} failed", - "Resource".gobj_style(), - get_type_name::().sobj_style(), - name.to_string().name_style() - ); - - // Check if resource exists - self.resource_manager - .get_resource_by_name::(name) - .context(error_message.to_string())?; - - // Check if resource is not default - if name.starts_with(DEFAULT_RESOURCE_PREFIX) { - return Err(Error::new(EngineError::RemoveDefaultResource( - name.to_string(), - ))) - .context(error_message.to_string()); - } - - // Remove resource - let mut remove_result = self - .resource_manager - .remove_resource_by_name::(name) - .context(error_message.to_string())?; - remove_result.1.destroy(self, remove_result.0)?; - - Ok(()) - } -} +use crate::{config::*, ecs::*, graphics::*, resources::*}; + +use pill_core::{ + get_enum_variant_type_name, get_game_error_message, get_type_name, get_value_type_name, + EngineError, PillSlotMapKey, PillStyle, PillTypeMap, Timer, Vector2f, +}; + +use anyhow::{Context, Error, Result}; +use boolinator::Boolinator; +use log::{debug, error, info}; +use std::{ + any::type_name, any::Any, any::TypeId, cell::RefCell, collections::VecDeque, ops::RangeBounds, +}; +use winit::{dpi::PhysicalPosition, event::KeyEvent}; + +// ------------------------------------------------------------------------------- + +pub type Game = Box; +pub type KeyboardKey = winit::keyboard::KeyCode; +pub type MouseButton = winit::event::MouseButton; + +/// Engine <-> Game interface +/// +/// Entry point of the game project. Mandatory to implement. +pub trait PillGame { + fn start(&self, engine: &mut Engine) -> Result<()>; +} + +/// Heart of Pill Engine +pub struct Engine { + pub(crate) config: config::Config, + pub(crate) game: Option, + pub(crate) renderer: Renderer, + pub(crate) scene_manager: SceneManager, + pub(crate) system_manager: SystemManager, + pub(crate) resource_manager: ResourceManager, + pub(crate) global_components: PillTypeMap, + pub(crate) input_queue: VecDeque, + pub(crate) render_queue: Vec, + pub(crate) window_size: winit::dpi::PhysicalSize, + pub(crate) game_resources_directory_path: std::path::PathBuf, + pub(crate) frame_delta_time: f32, // In milliseconds +} + +// ---- INTERNAL API ----------------------------------------------------------------- +struct EguiUI<'a> { + frame_delta_time: &'a f32, // Use a reference to frame_delta_time +} + +impl<'a> EguiUI<'a> { + fn render(&self, ui: &egui::Context) { + egui::Window::new("PillEngine") + .default_open(true) + .resizable(true) + .anchor(egui::Align2::LEFT_TOP, [0.0, 0.0]) + .show(ui, |ui| { + if ui.add(egui::Button::new("Click me")).clicked() { + println!("PRESSED"); + println!("{}", self.frame_delta_time); + } + }); + } +} +/// Pill Engine internal API +#[cfg(feature = "internal")] +impl Engine { + pub fn new( + game: Box, + game_resources_directory_path: std::path::PathBuf, + renderer: Box, + config: config::Config, + ) -> Self { + let max_entity_count = config + .get_int("MAX_ENTITIES") + .unwrap_or(MAX_ENTITIES as i64) as usize; + + Self { + config, + game: Some(game), + renderer, + scene_manager: SceneManager::new(max_entity_count), + system_manager: SystemManager::new(), + resource_manager: ResourceManager::new(), + global_components: PillTypeMap::new(), + input_queue: VecDeque::new(), + render_queue: Vec::::with_capacity(max_entity_count), + window_size: winit::dpi::PhysicalSize::::default(), + game_resources_directory_path, + frame_delta_time: 0.0.into(), + } + } + + fn start_game(&mut self) -> Result<()> { + info!("Starting {}", "Game".mobj_style()); + + let game = self + .game + .take() + .ok_or(EngineError::Other("Cannot get game".to_string()))?; + let stop_on_game_errors = self + .config + .get_bool("PANIC_ON_GAME_ERRORS") + .unwrap_or(PANIC_ON_GAME_ERRORS); + let result = game.start(self); + match stop_on_game_errors { + true => result.context(format!("{} error", "Game".mobj_style()))?, + false => { + if let Some(message) = get_game_error_message(result) { + error!("{}", message); + } + } + } + self.game = Some(game); + + Ok(()) + } + + /// Initializes Pill Engine + /// + /// Creates default global components, adds default systems, creates default resources, initializes game + pub fn initialize(&mut self, window_size: winit::dpi::PhysicalSize) -> Result<()> { + info!("Initializing {}", "Engine".mobj_style()); + + // Set window size + self.window_size = window_size; + + // Register global components + self.add_global_component(InputComponent::new())?; + self.add_global_component(TimeComponent::new())?; + self.add_global_component(DeferredUpdateComponent::new())?; + self.add_global_component(EguiManagerComponent::new())?; + self.add_global_component(RenderStateComponent::new())?; + + let max_ambient_sink_count = + self.config + .get_int("MAX_CONCURRENT_2D_SOUNDS") + .unwrap_or(MAX_CONCURRENT_2D_SOUNDS as i64) as usize; + let max_spatial_sink_count = + self.config + .get_int("MAX_CONCURRENT_3D_SOUNDS") + .unwrap_or(MAX_CONCURRENT_3D_SOUNDS as i64) as usize; + self.add_global_component(AudioManagerComponent::new( + max_ambient_sink_count, + max_spatial_sink_count, + ))?; + + // Add built-in systems + self.system_manager.add_system( + INPUT_SYSTEM.name, + INPUT_SYSTEM.system_function, + INPUT_SYSTEM.update_phase, + )?; + self.system_manager.add_system( + TIME_SYSTEM.name, + TIME_SYSTEM.system_function, + TIME_SYSTEM.update_phase, + )?; + self.system_manager.add_system( + AUDIO_SYSTEM.name, + AUDIO_SYSTEM.system_function, + AUDIO_SYSTEM.update_phase, + )?; + self.system_manager.add_system( + DEFERRED_UPDATE_SYSTEM.name, + DEFERRED_UPDATE_SYSTEM.system_function, + DEFERRED_UPDATE_SYSTEM.update_phase, + )?; + self.system_manager.add_system( + RENDERING_SYSTEM.name, + RENDERING_SYSTEM.system_function, + RENDERING_SYSTEM.update_phase, + )?; + + self.renderer.init(); + self.init_renderer(); + + // Start game + self.start_game()?; + + Ok(()) + } + + fn init_renderer(&mut self) { + // KISS: Run only rendering system once for bootstrap (no active scene yet) + // TODO: Loop over PostGame systems to give them a change to init before game starts + { + let update_phase = UpdatePhase::PostGame; + let (system_name, system_function) = { + let sys = self + .system_manager + .get_system(RENDERING_SYSTEM.name, update_phase.clone()) + .unwrap(); + (sys.name.clone(), sys.system_function) + }; + let mut timer = Timer::new(); + timer.begin_context(&format!("{} system bootstrap", system_name)); + self.system_manager + .update_system_timer(system_name.as_str(), update_phase.clone(), timer) + .unwrap(); + (system_function)(self).unwrap(); + let mut timer = self + .system_manager + .get_system_timer(system_name.as_str(), update_phase.clone()) + .unwrap() + .unwrap(); + timer.end_context().unwrap(); + self.system_manager + .update_system_timer(system_name.as_str(), update_phase.clone(), timer) + .unwrap(); + } + } + + /// Main engine update function + /// + /// Runs all systems in order: PreGame -> Game -> PostGame + pub fn update(&mut self, delta_time: std::time::Duration) { + let stop_on_game_errors = self + .config + .get_bool("PANIC_ON_GAME_ERRORS") + .unwrap_or(PANIC_ON_GAME_ERRORS); + + // Run systems + for update_phase_index in 0..self.system_manager.update_phases.len() { + for system_index in 0..self.system_manager.update_phases[update_phase_index].len() { + let (system_name, update_phase, system_function); + { + let system = + &mut self.system_manager.update_phases[update_phase_index][system_index]; + if !system.enabled { + continue; + } + system_name = system.name.to_string(); + update_phase = system.update_phase.clone(); + system_function = system.system_function; + } + + // Create new time and asign it to system so it can be accessed inside the system function + // For rendering system we can't clean its timer here, + // because it has to render its own timer data in the UI + // (and since the frame in which it renders is not yet finished when it renders UI, it has to use previous frame timer data) + if system_name != RENDERING_SYSTEM.name { + let mut timer = Timer::new(); + timer.begin_context(&format!("{} system update", system_name)); + self.system_manager + .update_system_timer(system_name.as_str(), update_phase.clone(), timer) + .unwrap(); + } + + { + // Run system update and handle errors based on configuration + let result = (system_function)(self).context(EngineError::SystemUpdateFailed( + system_name.clone(), + format!("{:?}", update_phase.clone()), + )); + + if update_phase == UpdatePhase::Game && stop_on_game_errors { + result.unwrap(); // Panic on error if configured + } else if let Err(err) = result { + if let Some(message) = get_game_error_message(Err(err)) { + error!("{}", message); + } + } + } + + // Update system timer with the final timer state + let mut timer = match self + .system_manager + .get_system_timer(system_name.as_str(), update_phase.clone()) + { + Ok(Some(timer)) => timer, + Ok(None) => { + panic!( + "{}", + Error::new(EngineError::NonReturnedSystemTimer(system_name.clone())) + ); + } + Err(e) => { + panic!("{}", Error::new(EngineError::Other(e.to_string()))); + } + }; + + timer + .end_context() + .context(format!( + "Failed to end timer context for {}", + system_name.clone() + )) + .unwrap(); // End system update context + self.system_manager + .update_system_timer(system_name.as_str(), update_phase.clone(), timer) + .unwrap(); + } + } + + // Update FPS counter + let new_frame_time = delta_time.as_secs_f32() * 1000.0; + self.frame_delta_time = new_frame_time.into(); + } + + pub fn shutdown(&mut self) { + info!("Shutting down {}", "Engine".mobj_style()); + } + + pub fn resize(&mut self, new_window_size: winit::dpi::PhysicalSize) { + debug!( + "{} resized to {}x{}", + "Window".mobj_style(), + new_window_size.width, + new_window_size.height + ); + self.window_size = new_window_size; + self.renderer.resize(new_window_size); + } + + pub fn pass_keyboard_key_input(&mut self, keyboard_input: &KeyEvent) { + let state: winit::event::ElementState = keyboard_input.state; + match keyboard_input.physical_key { + winit::keyboard::PhysicalKey::Code(key_code) => { + let input_event = InputEvent::KeyboardKey { + key: key_code, + state: state, + }; + self.input_queue.push_back(input_event); + debug!("Got new keyboard key input: {:?} {:?}", key_code, state); + } + winit::keyboard::PhysicalKey::Unidentified(_) => { + debug!("Unidentified key input: {:?}", keyboard_input.physical_key); + } + } + } + + pub fn pass_mouse_key_input(&mut self, key: &MouseButton, state: &winit::event::ElementState) { + let input_event = InputEvent::MouseButton { + key: *key, + state: *state, + }; + self.input_queue.push_back(input_event); + debug!("Got new mouse key input"); + } + + pub fn pass_mouse_wheel_input(&mut self, delta: &winit::event::MouseScrollDelta) { + let input_event = InputEvent::MouseWheel { delta: *delta }; + self.input_queue.push_back(input_event); + debug!("Got new mouse wheel input"); + } + + pub fn pass_mouse_delta_input(&mut self, delta: &(f64, f64)) { + let input_event = InputEvent::MouseDelta { + delta: Vector2f::new(delta.0 as f32, delta.1 as f32), + }; + self.input_queue.push_back(input_event); + debug!("Got new mouse motion input"); + } + + pub fn pass_mouse_position_input(&mut self, position: &PhysicalPosition) { + let input_event = InputEvent::MousePosition { + position: Vector2f::new(position.x as f32, position.y as f32), + }; + self.input_queue.push_back(input_event); + debug!("Got new mouse position input"); + } + + pub fn pass_input_to_egui(&mut self, event: &winit::event::WindowEvent) { + self.renderer.pass_input_to_egui(event).unwrap(); + } + + pub fn get_input_queue(&self) -> &VecDeque { + &self.input_queue + } +} + +// --- API ------------------------------------------------------------------ + +/// Pill Engine game API +impl Engine { + // --- System API --- + + /// Adds game-defined system to the game update phase + pub fn add_system( + &mut self, + name: &str, + system_function: fn(engine: &mut Engine) -> Result<()>, + ) -> Result<()> { + debug!( + "Adding {} {} to {} {}", + "System".gobj_style(), + name.name_style(), + "UpdatePhase".sobj_style(), + "Game".name_style() + ); + + self.system_manager + .add_system(name, system_function, UpdatePhase::Game) + .context(format!("Adding {} failed", "System".gobj_style())) + } + + /// Removes game-defined system + pub fn remove_system(&mut self, name: &str) -> Result<()> { + debug!( + "Removing {} {} from {} {}", + "System".gobj_style(), + name.name_style(), + "UpdatePhase".sobj_style(), + "Game".name_style() + ); + + self.system_manager + .remove_system(name, UpdatePhase::Game) + .context(format!("Removing {} failed", "System".gobj_style())) + } + + /// Toggles game-defined system + pub fn toggle_system( + &mut self, + name: &str, + update_phase: UpdatePhase, + enabled: bool, + ) -> Result<()> { + debug!( + "Toggling {} {} from {} {} to {} state", + "System".gobj_style(), + name.name_style(), + "UpdatePhase".sobj_style(), + update_phase.to_string().name_style(), + if enabled { "Enabled" } else { "Disabled" } + ); + + self.system_manager + .toggle_system(name, update_phase, enabled) + .context(format!("Toggling {} failed", "System".gobj_style())) + } + + /// Returns system timer. It has to be returned back using update_system_timer function, otherwise engine will panic. + pub fn get_system_timer(&mut self, name: &str) -> Timer { + debug!( + "Getting {} {} timer from {} {}", + "System".gobj_style(), + name.name_style(), + "UpdatePhase".sobj_style(), + "Game".name_style() + ); + + self.system_manager + .get_system_timer(name, UpdatePhase::Game) + .unwrap() + .unwrap() + } + + // --- Entity API --- + + /// Returns EntityBuilder, allowing for handy entity creation + pub fn build_entity(&mut self, scene_handle: SceneHandle) -> EntityBuilder<'_> { + let entity_handle = self.create_entity(scene_handle).unwrap(); + EntityBuilder { + engine: self, + entity_handle, + scene_handle, + } + } + + // Creates new entity to scene specified with scene handle + pub fn create_entity(&mut self, scene_handle: SceneHandle) -> Result { + debug!( + "Creating {} in {} {}", + "Entity".gobj_style(), + "Scene".gobj_style(), + self.scene_manager + .get_scene(scene_handle) + .unwrap() + .name + .name_style() + ); + + self.scene_manager + .create_entity(scene_handle) + .context(format!("Creating {} failed", "Entity".gobj_style())) + } + + // Removes entity specified with entity handle from scene specified with scene handle + pub fn remove_entity( + &mut self, + entity_handle: EntityHandle, + scene_handle: SceneHandle, + ) -> Result<()> { + debug!( + "Removing {} from {} {}", + "Entity".gobj_style(), + "Scene".gobj_style(), + self.scene_manager + .get_scene(scene_handle) + .unwrap() + .name + .name_style() + ); + + let component_destroyers = self + .scene_manager + .remove_entity(scene_handle, entity_handle) + .context(format!("Creating {} failed", "Entity".gobj_style()))?; + + // Destroy components using destroyers + for mut component_destroyer in component_destroyers { + component_destroyer.destroy(self, scene_handle, entity_handle)?; + } + + Ok(()) + } + + // --- Component API --- + + /// Registers new component type in scene specified with scene handle + pub fn register_component(&mut self, scene_handle: SceneHandle) -> Result<()> + where + T: Component>, + { + debug!( + "Registering {} {} in {} {}", + "Component".gobj_style(), + get_type_name::().sobj_style(), + "Scene".sobj_style(), + self.scene_manager + .get_scene(scene_handle) + .unwrap() + .name + .name_style() + ); + + self.scene_manager + .register_component::(scene_handle) + .context(format!("Registering {} failed", "Component".gobj_style())) + } + + /// Adds new component to the entity specified with scene and entity handle + pub fn add_component_to_entity( + &mut self, + scene_handle: SceneHandle, + entity_handle: EntityHandle, + mut component: T, + ) -> Result<()> + where + T: Component>, + { + debug!( + "Adding {} {} to {} {} in {} {}", + "Component".gobj_style(), + get_type_name::().sobj_style(), + "Entity".gobj_style(), + entity_handle.data().index, + "Scene".gobj_style(), + self.scene_manager + .get_scene(scene_handle) + .unwrap() + .name + .name_style() + ); + + // Check if already added + let target_scene = self.scene_manager.get_scene(scene_handle)?; + + if target_scene.entity_has_component::(entity_handle)? { + return Err(Error::new(EngineError::ComponentAlreadyExists( + get_type_name::(), + ))); + } + + // Initialize component + component.initialize(self).context(format!( + "Adding {} {} failed", + "Component".gobj_style(), + get_type_name::().sobj_style() + ))?; + + // Add component + self.scene_manager + .add_component_to_entity::(scene_handle, entity_handle, component) + .context(format!( + "Adding {} to {} failed", + "Component".gobj_style(), + "Entity".gobj_style() + ))?; + let component = self + .scene_manager + .get_entity_component::(entity_handle, scene_handle)?; + + // Pass handles to entity and scene to this component so it can store it if needed + component.pass_handles(scene_handle, entity_handle); + + Ok(()) + } + + /// Removes component from the entity specified with scene and entity handle + pub fn remove_component_from_entity( + &mut self, + scene_handle: SceneHandle, + entity_handle: EntityHandle, + ) -> Result<()> + where + T: Component>, + { + debug!( + "Removing {} {} from {} {} in {} {}", + "Component".gobj_style(), + get_type_name::().sobj_style(), + "Entity".gobj_style(), + entity_handle.data().index, + "Scene".gobj_style(), + self.scene_manager + .get_scene(scene_handle) + .unwrap() + .name + .name_style() + ); + + let mut component = self + .scene_manager + .remove_component_from_entity::(scene_handle, entity_handle) + .context("Removing component from entity failed") + .unwrap(); + + // Destroy component + component.destroy(self, scene_handle, entity_handle)?; + + Ok(()) + } + + // --- Global Component API --- + + /// Adds global component to engine + pub fn add_global_component(&mut self, mut component: T) -> Result<()> + where + T: GlobalComponent>, + { + // Check if component of this type is not already added + if self.global_components.contains_key::() { + return Err(Error::new(EngineError::GlobalComponentAlreadyExists( + get_type_name::(), + ))); + } + + // Initialize component + component.initialize(self)?; + + // Add component + self.global_components + .insert::(GlobalComponentStorage::::new(component)); + + Ok(()) + } + + /// Returns global component + pub fn get_global_component(&self) -> Result<&T> + where + T: GlobalComponent>, + { + // Get component + let component = self + .global_components + .get::() + .ok_or(Error::new(EngineError::GlobalComponentNotFound( + get_type_name::(), + )))? + .data + .as_ref() + .unwrap(); + + Ok(component) + } + + /// Returns global mutable component + pub fn get_global_component_mut(&mut self) -> Result<&mut T> + where + T: GlobalComponent>, + { + // Get component + let component = self + .global_components + .get_mut::() + .ok_or(Error::new(EngineError::GlobalComponentNotFound( + get_type_name::(), + )))? + .data + .as_mut() + .unwrap(); + + Ok(component) + } + + /// Removes global component from the engine + pub fn remove_global_component(&mut self) -> Result<()> + where + T: GlobalComponent>, + { + // Check if the type of the component is the same as of the ones, which cannot be removed + if ENGINE_GLOBAL_COMPONENTS.contains(&TypeId::of::()) { + return Err(Error::new(EngineError::GlobalComponentCannotBeRemoved( + get_type_name::(), + ))); + } + + // Remove and destroy component + let global_component_storage = self + .global_components + .remove::() + .ok_or(EngineError::GlobalComponentNotFound(get_type_name::()))?; + let mut global_component = global_component_storage.data.unwrap(); + global_component.destroy(self)?; + + Ok(()) + } + + // --- Iterator API --- + + /// Returns iterator for specified component + /// + /// Additionally returns entity handle to matching entities + pub fn iterate_one_component(&self) -> Result> + where + A: Component>, + { + // Get scene handle and iterator + let scene_handle = self.scene_manager.get_active_scene_handle()?; + self.scene_manager + .get_one_component_iterator::(scene_handle) + } + + /// Returns iterator for specified component mutable + /// + /// Additionally returns entity handle to matching entities + pub fn iterate_one_component_mut( + &mut self, + ) -> Result> + where + A: Component>, + { + // Get scene handle and iterator + let scene_handle = self.scene_manager.get_active_scene_handle()?; + self.scene_manager + .get_one_component_iterator_mut::(scene_handle) + } + + /// Returns iterator for specified component pair + /// + /// Iterator fetches specified components only for those entities which have them all + /// Additionally returns entity handle to matching entities + pub fn iterate_two_components( + &self, + ) -> Result> + where + A: Component>, + B: Component>, + { + // Get scene handle and iterator + let scene_handle = self.scene_manager.get_active_scene_handle()?; + self.scene_manager + .get_two_component_iterator::(scene_handle) + } + + /// Returns iterator for specified component pair mutable + /// + /// Iterator fetches specified components only for those entities which have them all + /// Additionally returns entity handle to matching entities + pub fn iterate_two_components_mut( + &mut self, + ) -> Result> + where + A: Component>, + B: Component>, + { + // Get scene handle and iterator + let scene_handle = self.scene_manager.get_active_scene_handle()?; + self.scene_manager + .get_two_component_iterator_mut::(scene_handle) + } + + /// Returns iterator for specified component triple + /// + /// Iterator fetches specified components only for those entities which have them all + /// Additionally returns entity handle to matching entities + pub fn iterate_three_components( + &self, + ) -> Result> + where + A: Component>, + B: Component>, + C: Component>, + { + // Get scene handle and iterator + let scene_handle = self.scene_manager.get_active_scene_handle()?; + self.scene_manager + .get_three_component_iterator::(scene_handle) + } + + /// Returns iterator for specified component triple mutable + /// + /// Iterator fetches specified components only for those entities which have them all + /// Additionally returns entity handle to matching entities + pub fn iterate_three_components_mut( + &mut self, + ) -> Result> + where + A: Component>, + B: Component>, + C: Component>, + { + // Get scene handle and iterator + let scene_handle = self.scene_manager.get_active_scene_handle()?; + self.scene_manager + .get_three_component_iterator_mut::(scene_handle) + } + + // --- Scene API --- + + // Creates scene + pub fn create_scene(&mut self, name: &str) -> Result { + info!("Creating scene: {}", name); + self.scene_manager + .create_scene(name) + .context(format!("Creating new {} failed", "Scene".gobj_style())) + } + + /// Returns handle to the scene specified by its name + pub fn get_scene_handle(&self, name: &str) -> Result { + self.scene_manager + .get_scene_handle(name) + .context(format!("Getting {} failed", "SceneHandle".sobj_style())) + } + + pub fn set_active_scene(&mut self, scene_handle: SceneHandle) -> Result<()> { + self.scene_manager + .set_active_scene(scene_handle) + .context(format!("Setting active {} failed", "Scene".gobj_style())) + } + + /// Returns handle to the active scene + pub fn get_active_scene_handle(&self) -> Result { + self.scene_manager + .get_active_scene_handle() + .context(format!( + "Getting {} of active {} failed", + "SceneHandle".sobj_style(), + "Scene".gobj_style() + )) + } + + // Removes scene deleting all data in it + pub fn remove_scene(&mut self, scene_handle: SceneHandle) -> Result<()> { + // Get scene + let scene = self.scene_manager.get_scene(scene_handle)?; + + // Get entity handles + let mut entity_handles = Vec::::new(); + for (entity_handle, _) in scene.entities.iter() { + entity_handles.push(entity_handle.clone()); + } + + // Remove entities + for entity_handle in entity_handles { + self.remove_entity(entity_handle, scene_handle)?; + } + + // Remove scene + self.scene_manager + .remove_scene(scene_handle) + .context(format!( + "Removing {} with usage of {} failed", + "Scene".sobj_style(), + "SceneHandle".gobj_style() + ))?; + + Ok(()) + } + + // --- Resource API --- + + // Registers new resource type in the engine + pub fn register_resource_type(&mut self, max_resource_count: usize) -> Result<()> + where + T: Resource>, + { + self.resource_manager + .register_resource_type::(max_resource_count) + } + + // Adds resource to the engine + pub fn add_resource(&mut self, mut resource: T) -> Result + where + T: Resource>, + { + debug!( + "Adding {} {} {}", + "Resource".gobj_style(), + get_type_name::().sobj_style(), + resource.get_name().name_style() + ); + + // Check if resource has proper name + let resource_name = resource.get_name(); + if resource_name.starts_with(DEFAULT_RESOURCE_PREFIX) { + return Err(Error::new(EngineError::WrongResourceName( + resource_name.clone(), + ))); + } + + // Initialize resource + resource.initialize(self).context(format!( + "Adding {} {} failed", + "Resource".gobj_style(), + get_type_name::().sobj_style() + ))?; + + // Add resource and get it back + let add_result = self.resource_manager.add_resource(resource)?; + let resource_handle = add_result.0; + let resource = add_result.1; + + // Pass handle to this resource so it can store it if needed + resource.pass_handle(resource_handle); + + Ok(resource_handle) + } + + // Returns resource associated with resource handle + pub fn get_resource<'a, T>(&'a self, resource_handle: &'a T::Handle) -> Result<&'a T> + where + T: Resource>, + { + Ok(self.resource_manager.get_resource::(resource_handle)?) + } + + /// Returns resource specified by its name + pub fn get_resource_by_name(&self, name: &str) -> Result<&T> + where + T: Resource>, + { + Ok(self.resource_manager.get_resource_by_name::(name)?) + } + + /// Returns handle to resource specified by the name of this resource + pub fn get_resource_handle(&self, name: &str) -> Result + where + T: Resource>, + { + Ok(self.resource_manager.get_resource_handle::(name)?) + } + + // Returns mutable resource associated with resource handle + pub fn get_resource_mut<'a, T>( + &'a mut self, + resource_handle: &'a T::Handle, + ) -> Result<&'a mut T> + where + T: Resource>, + { + Ok(self + .resource_manager + .get_resource_mut::(resource_handle)?) + } + + /// Returns mutable resource specified by its name + pub fn get_resource_by_name_mut(&mut self, name: &str) -> Result<&mut T> + where + T: Resource>, + { + Ok(self.resource_manager.get_resource_by_name_mut::(name)?) + } + + // Removes resource associated with resource handle from the engine + pub fn remove_resource(&mut self, resource_handle: &T::Handle) -> Result<()> + where + T: Resource>, + { + let error_message = format!( + "Removing {} {} failed", + "Resource".gobj_style(), + get_type_name::().sobj_style() + ); + + // Check if resource is not default + let resource_name = self + .resource_manager + .get_resource::(resource_handle) + .context(error_message.to_string())? + .get_name(); + if resource_name.starts_with(DEFAULT_RESOURCE_PREFIX) { + return Err(Error::new(EngineError::RemoveDefaultResource( + resource_name.clone(), + ))) + .context(error_message.to_string()); + } + + // Remove and destroy resource + let mut remove_result = self + .resource_manager + .remove_resource::(resource_handle) + .context(error_message.to_string())?; + remove_result.1.destroy(self, *resource_handle)?; + + Ok(()) + } + + // Removes resource specified with its name from the engine + pub fn remove_resource_by_name(&mut self, name: &str) -> Result<()> + where + T: Resource>, + { + let error_message = format!( + "Removing {} {} {} failed", + "Resource".gobj_style(), + get_type_name::().sobj_style(), + name.to_string().name_style() + ); + + // Check if resource exists + self.resource_manager + .get_resource_by_name::(name) + .context(error_message.to_string())?; + + // Check if resource is not default + if name.starts_with(DEFAULT_RESOURCE_PREFIX) { + return Err(Error::new(EngineError::RemoveDefaultResource( + name.to_string(), + ))) + .context(error_message.to_string()); + } + + // Remove resource + let mut remove_result = self + .resource_manager + .remove_resource_by_name::(name) + .context(error_message.to_string())?; + remove_result.1.destroy(self, remove_result.0)?; + + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/mod.rs b/engine/pill_engine/src/graphics/mod.rs index 6dee7492..80b005ef 100644 --- a/engine/pill_engine/src/graphics/mod.rs +++ b/engine/pill_engine/src/graphics/mod.rs @@ -11,9 +11,9 @@ pub use pill_core::{ }; pub use renderer::{render_with_factory, WorldView, WorldViewFactory}; pub use renderer::{ - BufferDesc, PillRenderer, PipelineV2, PipelineV2Desc, Renderer, RendererBufferHandle, - RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, RendererPipelineHandle, - RendererPipelineV2Handle, RendererTextureHandle, ShaderDesc, + BufferDesc, MaterialDesc, PillRenderer, PipelineV2, PipelineV2Desc, Renderer, + RendererBufferHandle, RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, + RendererPipelineHandle, RendererPipelineV2Handle, RendererTextureHandle, ShaderDesc, }; pub use render_queue::{ diff --git a/engine/pill_engine/src/graphics/render_queue.rs b/engine/pill_engine/src/graphics/render_queue.rs index 43c62bcb..2c68dc60 100644 --- a/engine/pill_engine/src/graphics/render_queue.rs +++ b/engine/pill_engine/src/graphics/render_queue.rs @@ -1,6 +1,6 @@ use crate::{ config::*, - resources::{Material, MaterialHandle, Mesh, MeshHandle, ResourceManager, TextureHandle}, + resources::{Mesh, MeshHandle, PBRMaterial, PBRMaterialHandle, ResourceManager, TextureHandle}, }; use pill_core::Handle; @@ -111,21 +111,35 @@ where // Creates pill engine render queue composed from order, material index, material version, mesh index, mesh version pub fn compose_render_queue_key( resource_manager: &ResourceManager, - material_handle: &MaterialHandle, mesh_handle: &MeshHandle, + material_handle: &Option, ) -> Result { - let material = resource_manager.get_resource::(material_handle)?; let mesh = resource_manager.get_resource::(mesh_handle)?; - let h_mat = material.renderer_resource_handle.unwrap(); let h_mesh = mesh.renderer_resource_handle.unwrap(); - let render_queue_key: RenderQueueKey = ((RENDER_QUEUE_KEY_ORDER.max - - material.rendering_order as RenderQueueKey) - << RENDER_QUEUE_KEY_ORDER.mask_shift) - | ((h_mat.index() as RenderQueueKey) << RENDER_QUEUE_KEY_MATERIAL_INDEX.mask_shift) - | ((h_mat.generation() as RenderQueueKey) << RENDER_QUEUE_KEY_MATERIAL_VERSION.mask_shift) - | ((h_mesh.index() as RenderQueueKey) << RENDER_QUEUE_KEY_MESH_INDEX.mask_shift) - | ((h_mesh.generation() as RenderQueueKey) << RENDER_QUEUE_KEY_MESH_VERSION.mask_shift); + // Material (optional) + let (mat_index, mat_gen) = if let Some(mh) = material_handle { + if let Ok(mat) = resource_manager.get_resource::(mh) { + if let Some(rh) = mat.renderer_resource_handle { + ( + rh.index() as RenderQueueKey, + rh.generation() as RenderQueueKey, + ) + } else { + (0, 0) + } + } else { + (0, 0) + } + } else { + (0, 0) + }; + + let render_queue_key: RenderQueueKey = ((h_mesh.index() as RenderQueueKey) + << RENDER_QUEUE_KEY_MESH_INDEX.mask_shift) + | ((h_mesh.generation() as RenderQueueKey) << RENDER_QUEUE_KEY_MESH_VERSION.mask_shift) + | (mat_index << RENDER_QUEUE_KEY_MATERIAL_INDEX.mask_shift) + | (mat_gen << RENDER_QUEUE_KEY_MATERIAL_VERSION.mask_shift); Ok(render_queue_key) } diff --git a/engine/pill_engine/src/graphics/renderer.rs b/engine/pill_engine/src/graphics/renderer.rs index 67c9304a..75a04e66 100644 --- a/engine/pill_engine/src/graphics/renderer.rs +++ b/engine/pill_engine/src/graphics/renderer.rs @@ -2,10 +2,7 @@ use crate::{ ecs::{CameraComponent, ComponentStorage, EntityHandle, TransformComponent}, engine::Engine, graphics::{RenderQuery, RenderQueueFactory, RenderQueueItem}, - resources::{ - MaterialHandle, MaterialParameterMap, MaterialTextureMap, MeshData, MeshHandle, - TextureHandle, TextureType, - }, + resources::{MeshData, MeshHandle, TextureHandle, TextureType}, }; use pill_core::PillStyle; @@ -19,12 +16,12 @@ use std::{path::PathBuf, sync::Arc}; use thiserror::Error; // --- Renderer resource handles (typed generational handles) --- -pub type RendererMaterialHandle = Handle; - pub type RendererMeshHandle = Handle; pub type RendererPipelineHandle = Handle; +pub type RendererMaterialHandle = Handle; + pub type RendererCameraHandle = Handle; pub type RendererTextureHandle = Handle; @@ -60,6 +57,20 @@ pub struct PipelineV2Desc<'a> { pub multisample: wgpu::MultisampleState, } +pub struct MaterialDesc<'a> { + pub label: &'a str, + // Factors + pub albedo: [f32; 3], + pub metallic: f32, + pub roughness: f32, + pub emissive: [f32; 3], + // Textures (optional; renderer will fallback to defaults if None) + pub albedo_tex: Option, + pub normal_tex: Option, + pub metallic_roughness_tex: Option, + pub emissive_tex: Option, +} + // --- Renderer trait definition --- pub struct PipelineV2 { @@ -78,6 +89,12 @@ pub trait PillRenderer { // Creates a 256B-aligned uniform buffer (COPY_DST) and returns its handle fn create_buffer(&mut self, desc: BufferDesc) -> Result; fn create_pipeline_v2(&mut self, desc: PipelineV2Desc) -> Result; + fn create_material(&mut self, desc: MaterialDesc) -> Result; + fn update_material( + &mut self, + renderer_material_handle: RendererMaterialHandle, + desc: MaterialDesc, + ) -> Result; fn create_mesh(&mut self, name: &str, mesh_data: &MeshData) -> Result; fn create_texture( &mut self, @@ -85,28 +102,10 @@ pub trait PillRenderer { image_data: &image::DynamicImage, texture_type: TextureType, ) -> Result; - fn create_material( - &mut self, - name: &str, - textures: &MaterialTextureMap, - parameters: &MaterialParameterMap, - ) -> Result; fn create_camera(&mut self) -> Result; - fn update_material_textures( - &mut self, - renderer_material_handle: RendererMaterialHandle, - textures: &MaterialTextureMap, - ) -> Result<()>; - fn update_material_parameters( - &mut self, - renderer_material_handle: RendererMaterialHandle, - parameters: &MaterialParameterMap, - ) -> Result<()>; - fn destroy_mesh(&mut self, renderer_mesh_handle: RendererMeshHandle) -> Result<()>; fn destroy_texture(&mut self, renderer_texture_handle: RendererTextureHandle) -> Result<()>; - fn destroy_material(&mut self, renderer_material_handle: RendererMaterialHandle) -> Result<()>; fn destroy_camera(&mut self, renderer_camera_handle: RendererCameraHandle) -> Result<()>; fn pass_input_to_egui(&mut self, event: &winit::event::WindowEvent) -> Result<()>; diff --git a/engine/pill_engine/src/lib.rs b/engine/pill_engine/src/lib.rs index 0ee6df15..afadeeff 100644 --- a/engine/pill_engine/src/lib.rs +++ b/engine/pill_engine/src/lib.rs @@ -62,7 +62,7 @@ pub mod game { }, engine::{Engine, KeyboardKey, MouseButton, PillGame}, resources::{ - Material, MaterialHandle, Mesh, MeshHandle, Resource, ResourceLoadType, + Mesh, MeshHandle, PBRMaterial, PBRMaterialHandle, Resource, ResourceLoadType, ResourceStorage, Sound, Texture, TextureHandle, TextureType, }, }; @@ -90,18 +90,17 @@ pub mod internal { }, engine::{Engine, PillGame}, graphics::{ - decompose_render_queue_key, BufferDesc, PillRenderer, PipelineV2, PipelineV2Desc, - RenderQueueItem, RenderQueueKey, RenderQueueKeyFields, RendererBufferHandle, - RendererBufferTag, RendererCameraHandle, RendererCameraTag, RendererMaterialHandle, - RendererMaterialTag, RendererMeshHandle, RendererMeshTag, RendererPipelineHandle, - RendererPipelineTag, RendererPipelineV2Handle, RendererPipelineV2Tag, - RendererTextureHandle, RendererTextureTag, ShaderDesc, RENDER_QUEUE_KEY_ORDER, + decompose_render_queue_key, BufferDesc, MaterialDesc, PillRenderer, PipelineV2, + PipelineV2Desc, RenderQueueItem, RenderQueueKey, RenderQueueKeyFields, + RendererBufferHandle, RendererBufferTag, RendererCameraHandle, RendererCameraTag, + RendererMaterialHandle, RendererMaterialTag, RendererMeshHandle, RendererMeshTag, + RendererPipelineHandle, RendererPipelineTag, RendererPipelineV2Handle, + RendererPipelineV2Tag, RendererTextureHandle, RendererTextureTag, ShaderDesc, + RENDER_QUEUE_KEY_ORDER, }, resources::{ - get_renderer_texture_handle_from_material_texture, Material, MaterialHandle, - MaterialParameter, MaterialParameterMap, MaterialTexture, MaterialTextureMap, Mesh, - MeshData, MeshHandle, MeshVertex, ResourceLoadType, ResourceManager, Texture, - TextureHandle, TextureType, + Mesh, MeshData, MeshHandle, MeshVertex, PBRMaterial, PBRMaterialHandle, + ResourceLoadType, ResourceManager, Texture, TextureHandle, TextureType, }, }; } diff --git a/engine/pill_engine/src/resources/material.rs b/engine/pill_engine/src/resources/material.rs index a60000cf..db710eb6 100644 --- a/engine/pill_engine/src/resources/material.rs +++ b/engine/pill_engine/src/resources/material.rs @@ -1,494 +1,150 @@ use crate::{ - engine::Engine, - graphics::{ RendererTextureHandle, RendererMaterialHandle, RENDER_QUEUE_KEY_ORDER }, - resources::{ TextureHandle, TextureType, Texture, ResourceStorage, Resource }, - ecs::{ DeferredUpdateManagerPointer, DeferredUpdateResourceRequest, MeshRenderingComponent, DeferredUpdateComponent }, config::*, + engine::Engine, + graphics::{ + MaterialDesc, RendererMaterialHandle, RendererTextureHandle, RENDER_QUEUE_KEY_ORDER, + }, + resources::{Resource, ResourceStorage, Texture, TextureHandle, TextureType}, }; -use pill_core::{ Color, EngineError, PillSlotMapKey, PillTypeMapKey, PillStyle, enum_variant_eq, get_enum_variant_type_name, get_type_name }; - -use anyhow::{ Result, Context, Error }; -use boolinator::*; -use std::{ - path::{ Path, PathBuf }, - collections::HashMap, - ops::{Range, RangeInclusive} -}; - - -const DEFERRED_REQUEST_VARIANT_RENDERING_ORDER: usize = 0; -const DEFERRED_REQUEST_VARIANT_PARAMETER: usize = 1; - -const DEFERRED_REQUEST_VARIANT_TEXTURE_START: usize = 2; -const DEFERRED_REQUEST_VARIANT_TEXTURE_END: usize = 10; - -// --- Material parameters --- - -#[derive(Debug)] -pub enum MaterialParameter { - Scalar(Option), - Bool(Option), - Color(Option), -} - -impl MaterialParameter { - pub fn is_some(&self) -> bool { - match self { - MaterialParameter::Scalar(v) => v.is_some(), - MaterialParameter::Bool(v) => v.is_some(), - MaterialParameter::Color(v) => v.is_some(), - } - } -} - -pub struct MaterialParameterMap { - pub data: HashMap, - pub(crate) mapping: Vec, // Maps index to slot name -} - -impl MaterialParameterMap { - pub fn new() -> Self { - Self { - data: HashMap::::new(), - mapping: Vec::::new(), - } - } - - pub fn get_scalar(&self, parameter_name: &str) -> Result { - let error = EngineError::MaterialParameterSlotNotFound(parameter_name.to_string(), "Scalar".to_string()); - match self.data.get(parameter_name).context(error.clone())? { - MaterialParameter::Scalar(v) => match v { - Some(vv) => Ok(vv.clone()), - None => panic!(), - }, - _ => Err(Error::new(error)) - } - } - - pub fn get_bool(&self, parameter_name: &str) -> Result { - let error = EngineError::MaterialParameterSlotNotFound(parameter_name.to_string(), "Bool".to_string()); - match self.data.get(parameter_name).context(error.clone())? { - MaterialParameter::Bool(v) => match v { - Some(vv) => Ok(vv.clone()), - None => panic!(), - }, - _ => Err(Error::new(error)) - } - } - - pub fn get_color(&self, parameter_name: &str) -> Result { - let error = EngineError::MaterialParameterSlotNotFound(parameter_name.to_string(), "Color".to_string()); - match self.data.get(parameter_name).context(error.clone())? { - MaterialParameter::Color(v) => match v { - Some(vv) => Ok(vv.clone()), - None => panic!(), - }, - _ => Err(Error::new(error)) - } - } - - pub fn set_parameter(&mut self, parameter_name: &str, value: MaterialParameter) -> Result<()> { - let error = Error::new(EngineError::MaterialParameterSlotNotFound(parameter_name.to_string(), pill_core::get_enum_variant_type_name(&value).to_string())); - let parameter = self.data.get_mut(parameter_name).context(error)?; - - if pill_core::enum_variant_eq::(¶meter, &value) { - *parameter = value; - } - - Ok(()) - } -} - -// --- Material textures --- +use pill_core::{get_type_name, Color, EngineError, PillSlotMapKey, PillStyle, PillTypeMapKey}; -pub struct MaterialTexture { - pub texture_type: TextureType, - pub texture_handle: Option, - pub(crate) renderer_texture_handle: Option, -} - -impl MaterialTexture { - pub fn new(texture_type: TextureType) -> Self { - Self { - texture_type, - texture_handle: None, - renderer_texture_handle: None, - } - } -} - -// This needed so that renderer can get renderer texture handle from material texture while it is still hidden in game API -pub fn get_renderer_texture_handle_from_material_texture(material_texture: &MaterialTexture) -> &Option { - &material_texture.renderer_texture_handle -} - -pub struct MaterialTextureMap { - pub data: HashMap, - pub(crate) mapping: Vec, // Maps index to slot name -} +use anyhow::{Context, Error, Result}; -impl MaterialTextureMap { - pub fn new() -> Self { - Self { - data: HashMap::::new(), - mapping: Vec::::new(), - } - } +// --- Material (PBR POD) --- - pub fn get(&self, name: &str) -> Option<&MaterialTexture> { - self.data.get(name) - } -} - -// --- Builder --- - -pub struct MaterialBuilder { - material: Material, -} - -impl MaterialBuilder { - pub fn new(name: &str) -> Self { - Self { - material: Material::new(name), - } - } - - pub fn texture(mut self, slot_name: &str, texture_handle: TextureHandle) -> Result { - self.material.set_texture(slot_name, texture_handle)?; - Ok(self) - } - - pub fn scalar(mut self, slot_name: &str, value: f32) -> Result { - self.material.set_scalar(slot_name, value)?; - Ok(self) - } - - pub fn bool(mut self, slot_name: &str, value: bool) -> Result { - self.material.set_bool(slot_name, value)?; - Ok(self) - } - - pub fn color(mut self, slot_name: &str, value: Color) -> Result { - self.material.set_color(slot_name, value)?; - Ok(self) - } - - pub fn rendering_order(mut self, order: u8) -> Result { - self.material.set_rendering_order(order)?; - Ok(self) - } - - pub fn build(self) -> Material { - self.material - } -} - -// --- Material --- - -pill_core::define_new_pill_slotmap_key! { - pub struct MaterialHandle; +pill_core::define_new_pill_slotmap_key! { + pub struct PBRMaterialHandle; } #[readonly::make] -pub struct Material { +pub struct PBRMaterial { #[readonly] pub name: String, #[readonly] - textures: MaterialTextureMap, - #[readonly] - parameters: MaterialParameterMap, - #[readonly] - pub rendering_order: u8, - pub renderer_resource_handle: Option, - - handle: Option, - deferred_update_manager: Option, + // PBR factors (no alpha) + pub albedo: Color, // vec3 + pub metallic: f32, + pub roughness: f32, + pub emissive: Color, // vec3 + + // PBR textures + pub albedo_texture: Option, // Color + pub normal_texture: Option, // Normal + pub metallic_roughness_texture: Option, // Color (G=roughness, B=metallic) + pub emissive_texture: Option, // Color + + // Dirty flag to trigger GPU updates + pub is_dirty: bool, + pub(crate) renderer_resource_handle: Option, } -impl Material { - pub fn builder(name: &str) -> MaterialBuilder { - MaterialBuilder::new(name) - } - - pub fn new(name: &str) -> Self { - let mut textures = MaterialTextureMap::new(); - textures.data.insert(MASTER_SHADER_COLOR_TEXTURE_SLOT.to_string(), MaterialTexture::new(TextureType::Color)); - textures.mapping.push(MASTER_SHADER_COLOR_TEXTURE_SLOT.to_string()); - textures.data.insert(MASTER_SHADER_NORMAL_TEXTURE_SLOT.to_string(), MaterialTexture::new(TextureType::Normal)); - textures.mapping.push(MASTER_SHADER_NORMAL_TEXTURE_SLOT.to_string()); - - let mut parameters = MaterialParameterMap::new(); - parameters.data.insert(MASTER_SHADER_TINT_PARAMETER_SLOT.to_string(), MaterialParameter::Color(None)); - textures.mapping.push(MASTER_SHADER_TINT_PARAMETER_SLOT.to_string()); - parameters.data.insert(MASTER_SHADER_SPECULARITY_PARAMETER_SLOT.to_string(), MaterialParameter::Scalar(None)); - textures.mapping.push(MASTER_SHADER_SPECULARITY_PARAMETER_SLOT.to_string()); - +impl PBRMaterial { + pub fn new(name: &str) -> Self { Self { - name: name.to_string(), - textures, - parameters, - rendering_order: RENDER_QUEUE_KEY_ORDER.max as u8, - renderer_resource_handle: None, - handle: None, - deferred_update_manager: None, + name: name.to_string(), + albedo: Color::new(1.0, 1.0, 1.0), + metallic: 0.0, + roughness: 0.5, + emissive: Color::new(0.0, 0.0, 0.0), + albedo_texture: None, + normal_texture: None, + metallic_roughness_texture: None, + emissive_texture: None, + is_dirty: true, + renderer_resource_handle: None, } } - pub fn set_texture(&mut self, slot_name: &str, texture_handle: TextureHandle) -> Result<()> { - // Get texture slot - let texture_slot = self.textures.data.get_mut(slot_name) - .ok_or( Error::new(EngineError::MaterialTextureSlotNotFound(slot_name.to_string())))?; - - // Get texture slot index - let texture_slot_index = self.textures.mapping.iter().position(|v| v == slot_name).expect("Critical: No mapping"); - - // Set new handle but not renderer resource handle (it will be set by deferred update system) - let _ = texture_slot.texture_handle.insert(texture_handle.clone()); - - // Post deferred update request (only if renderer resource handle is set (it means that material is initialized)) - if self.renderer_resource_handle.is_some() { - self.post_deferred_update_request(DEFERRED_REQUEST_VARIANT_TEXTURE_START + texture_slot_index); - } - - Ok(()) + // Factor setters (mark dirty) + pub fn set_base_color_factor(&mut self, value: Color) { + self.albedo = Color::new( + value.x.clamp(0.0, 1.0), + value.y.clamp(0.0, 1.0), + value.z.clamp(0.0, 1.0), + ); + self.is_dirty = true; } - pub fn remove_texture(&mut self, slot_name: &str) -> Result<()> { - // Get texture slot - let texture_slot = self.textures.data.get_mut(slot_name) - .ok_or( Error::new(EngineError::MaterialTextureSlotNotFound(slot_name.to_string())))?; - - // Get texture slot index - let texture_slot_index = self.textures.mapping.iter().position(|v| v == slot_name).expect("Critical: No mapping"); - - // Set new handle and renderer resource handle - texture_slot.texture_handle = None; - texture_slot.renderer_texture_handle = None; - - // Post deferred update request (only if renderer resource handle is set (it means that material is initialized)) - if self.renderer_resource_handle.is_some() { - self.post_deferred_update_request(DEFERRED_REQUEST_VARIANT_TEXTURE_START + texture_slot_index); - } - - Ok(()) - } - - pub fn set_rendering_order(&mut self, order: u8) -> Result<()> { - if order < RENDER_QUEUE_KEY_ORDER.max as u8 { - // Set new order - self.rendering_order = order; - - // Post deferred update request (only if renderer resource handle is set (it means that material is initialized)) - if self.renderer_resource_handle.is_some() { - self.post_deferred_update_request(DEFERRED_REQUEST_VARIANT_RENDERING_ORDER); - } - } - else { - return Err(Error::new(EngineError::WrongRenderingOrder(order.to_string(), format!("{}-{}", 0, RENDER_QUEUE_KEY_ORDER.max.to_string())))); - } - - Ok(()) + pub fn set_metallic_factor(&mut self, value: f32) { + self.metallic = value.clamp(0.0, 1.0); + self.is_dirty = true; } - pub fn get_scalar(&self, parameter_name: &str) -> Result { - self.parameters.get_scalar(parameter_name) + pub fn set_roughness_factor(&mut self, value: f32) { + self.roughness = value.clamp(0.0, 1.0); + self.is_dirty = true; } - pub fn get_bool(&self, parameter_name: &str) -> Result { - self.parameters.get_bool(parameter_name) + pub fn set_emissive_factor(&mut self, value: Color) { + self.emissive = Color::new( + value.x.clamp(0.0, 1.0), + value.y.clamp(0.0, 1.0), + value.z.clamp(0.0, 1.0), + ); + self.is_dirty = true; } - pub fn get_color(&self, parameter_name: &str) -> Result { - self.parameters.get_color(parameter_name) + // Texture setters (mark dirty) + pub fn set_albedo_texture(&mut self, handle: TextureHandle) { + self.albedo_texture = Some(handle); + self.is_dirty = true; } - pub fn set_scalar(&mut self, parameter_name: &str, value: f32) -> Result<()> { - self.set_parameter(parameter_name, MaterialParameter::Scalar(Some(value))) + pub fn set_normal_texture(&mut self, handle: TextureHandle) { + self.normal_texture = Some(handle); + self.is_dirty = true; } - pub fn set_bool(&mut self, parameter_name: &str, value: bool) -> Result<()> { - self.set_parameter(parameter_name, MaterialParameter::Bool(Some(value))) - } - - pub fn set_color(&mut self, parameter_name: &str, value: Color) -> Result<()> { - // Clamp color channel values between 0.0 and 1.0 - let valid_color = Color::new(value.x.clamp(0.0, 1.0), value.y.clamp(0.0, 1.0), value.z.clamp(0.0, 1.0)); - self.set_parameter(parameter_name, MaterialParameter::Color(Some(valid_color))) - } - - fn set_parameter(&mut self, parameter_name: &str, value: MaterialParameter) -> Result<()> { - // Set parameter - self.parameters.set_parameter(parameter_name, value)?; - - // Post deferred update request (only if renderer resource handle is set (it means that material is initialized)) - if self.renderer_resource_handle.is_some() { - self.post_deferred_update_request(DEFERRED_REQUEST_VARIANT_PARAMETER); - } - - Ok(()) + pub fn set_metallic_roughness_texture(&mut self, handle: TextureHandle) { + self.metallic_roughness_texture = Some(handle); + self.is_dirty = true; } - pub(crate) fn get_textures(&mut self) -> &mut MaterialTextureMap { - &mut self.textures - } - - fn post_deferred_update_request(&mut self, request_variant: usize) { - let handle = self.handle.expect("Critical: Cannot post deferred update request. No Handle set in Resource"); - let request = DeferredUpdateResourceRequest::::new(handle, request_variant); - self.deferred_update_manager.as_mut().expect("Critical: No DeferredUpdateManager").post_update_request(request); + pub fn set_emissive_texture(&mut self, handle: TextureHandle) { + self.emissive_texture = Some(handle); + self.is_dirty = true; } } -impl PillTypeMapKey for Material { - type Storage = ResourceStorage; +impl PillTypeMapKey for PBRMaterial { + type Storage = ResourceStorage; } -impl Resource for Material { - type Handle = MaterialHandle; +impl Resource for PBRMaterial { + type Handle = PBRMaterialHandle; fn get_name(&self) -> String { self.name.clone() } fn initialize(&mut self, engine: &mut Engine) -> Result<()> { - let error_message = format!("Initializing {} {} failed", "Resource".gobj_style(), get_type_name::().sobj_style()); - - // This resource is using DeferredUpdateSystem so keep DeferredUpdateManager - let deferred_update_component = engine.get_global_component_mut::().expect("Critical: No DeferredUpdateComponent"); - self.deferred_update_manager = Some(deferred_update_component.borrow_deferred_update_manager()); - - // Check if assigned textures are of correct type - for texture_slot in self.textures.data.iter_mut() { - if let Some(texture_handle) = texture_slot.1.texture_handle { - // Get texture to be set - let texture = engine.get_resource::(&texture_handle) - .context(error_message.clone()).context(format!("Invalid {} for {} in slot {}", "Handle".sobj_style(), "Texture".sobj_style(), texture_slot.0.name_style()))?; - - // Check if slots are of same type - if !enum_variant_eq(&texture.texture_type,&texture_slot.1.texture_type) { - return Err(Error::new(EngineError::WrongTextureType( - get_enum_variant_type_name(&texture.texture_type), - texture_slot.0.to_string(), - get_enum_variant_type_name(&texture_slot.1.texture_type) - ))); - } - - // Set renderer resource handle - texture_slot.1.renderer_texture_handle = texture.renderer_resource_handle; - } - } - - // Set default parameters if not already set - let parameter_values = vec![ - (MASTER_SHADER_TINT_PARAMETER_SLOT, MaterialParameter::Color(Some(Color::new(1.0, 1.0, 1.0)))), - (MASTER_SHADER_SPECULARITY_PARAMETER_SLOT, MaterialParameter::Scalar(Some(0.0))) - ]; - for parameter_value in parameter_values { - let parameter = self.parameters.data.get_mut(parameter_value.0); - match parameter { - Some(v) => { - if pill_core::enum_variant_eq::(¶meter_value.1, v) { - if !v.is_some() { - *v = parameter_value.1; - } - } - }, - None => panic!("Critical: Wrong parameters setup"), - } - } - - // Create new renderer material resource - let renderer_resource_handle = engine.renderer.create_material(&self.name, &self.textures, &self.parameters).context(error_message)?; - self.renderer_resource_handle = Some(renderer_resource_handle); - - Ok(()) - } - - fn pass_handle(&mut self, self_handle: H) { - self.handle = Some(MaterialHandle::from(self_handle.data())); - } - - fn deferred_update(&mut self, engine: &mut Engine, request: usize) -> Result<()> { - match request { - DEFERRED_REQUEST_VARIANT_RENDERING_ORDER => - { - // Find mesh rendering components that use this material and update them - for (scene_handle, scene) in engine.scene_manager.scenes.iter_mut() { - for (entity_handle, mesh_rendering_component) in scene.get_one_component_iterator_mut::()? { - if let Some(material_handle) = mesh_rendering_component.material_handle { - // If mesh rendering component has handle to this material - if material_handle.data() == self.handle.unwrap().data() { - mesh_rendering_component.update_render_queue_key(&engine.resource_manager).unwrap(); - } - } - } - } - }, - DEFERRED_REQUEST_VARIANT_PARAMETER => - { - // Update renderer counterpart - engine.renderer.update_material_parameters(self.renderer_resource_handle.unwrap(), &self.parameters)?; - }, - DEFERRED_REQUEST_VARIANT_TEXTURE_START..=DEFERRED_REQUEST_VARIANT_TEXTURE_END => - { - // Check if assigned texture is of correct type - let texture_slot_name = self.textures.mapping.get(request - DEFERRED_REQUEST_VARIANT_TEXTURE_START).unwrap(); - let texture_slot = self.textures.data.get_mut(texture_slot_name).unwrap(); - if let Some(texture_handle) = texture_slot.texture_handle { - // Get texture to be set - let texture = engine.get_resource::(&texture_handle) - .context(format!("Cannot set {}. Invalid {} for {} in slot {}. ", "Texture".sobj_style(), "Handle".sobj_style(), "Texture".sobj_style(), texture_slot_name.name_style()))?; - - // Check if slots are of same type - if !enum_variant_eq(&texture.texture_type,&texture_slot.texture_type) { - return Err(Error::new(EngineError::WrongTextureType( - get_enum_variant_type_name(&texture.texture_type), - texture_slot_name.to_string(), - get_enum_variant_type_name(&texture_slot.texture_type) - ))); - } - - // Set renderer resource handle - let _ = texture_slot.renderer_texture_handle.insert(texture.renderer_resource_handle.unwrap().clone()); - } - - // Update renderer counterpart - engine.renderer.update_material_textures(self.renderer_resource_handle.unwrap(), &self.textures)?; - }, - _ => - { - panic!("Critical: Processing deferred update request with value {} in {} failed. Handling is not implemented", request, get_type_name::().sobj_style()); + // Build initial descriptor + let desc = { + // Resolve renderer texture handles if available + let to_renderer = |th: &Option| -> Option { + th.as_ref().and_then(|h| { + engine + .resource_manager + .get_resource::(h) + .ok() + .and_then(|t| t.renderer_resource_handle) + }) + }; + MaterialDesc { + label: &self.name, + albedo: [self.albedo.x, self.albedo.y, self.albedo.z], + metallic: self.metallic, + roughness: self.roughness, + emissive: [self.emissive.x, self.emissive.y, self.emissive.z], + albedo_tex: to_renderer(&self.albedo_texture), + normal_tex: to_renderer(&self.normal_texture), + metallic_roughness_tex: to_renderer(&self.metallic_roughness_texture), + emissive_tex: to_renderer(&self.emissive_texture), } - } - + }; + let h = engine.renderer.create_material(desc)?; + self.renderer_resource_handle = Some(h); Ok(()) } - - fn destroy(&mut self, engine: &mut Engine, self_handle: H) -> Result<()> { - // Destroy renderer resource - if let Some(v) = self.renderer_resource_handle { - engine.renderer.destroy_material(v).unwrap(); - } - - // Find mesh rendering components that use this material and update them - - - - for (scene_handle, scene) in engine.scene_manager.scenes.iter_mut() { - let x = &engine.resource_manager; - - // for (entity_handle, mesh_rendering_component) in engine.iterate_one_component::()? { - // if let Some(material_handle) = mesh_rendering_component.material_handle { - // // If mesh rendering component has handle to this material - // if material_handle.data() == self_handle.data() { - // mesh_rendering_component.set_material_handle(Option::::None); - // mesh_rendering_component.update_render_queue_key(&engine.resource_manager).unwrap(); - // } - // } - // } - } - - Ok(()) - } -} \ No newline at end of file +} diff --git a/engine/pill_engine/src/resources/mod.rs b/engine/pill_engine/src/resources/mod.rs index c965146c..018a0c50 100644 --- a/engine/pill_engine/src/resources/mod.rs +++ b/engine/pill_engine/src/resources/mod.rs @@ -1,48 +1,25 @@ #![cfg_attr(debug_assertions, allow(dead_code, unused_imports, unused_variables))] -mod resource_manager; -mod mesh; -mod texture; -mod resource_storage; mod material; +mod mesh; mod resource; +mod resource_manager; +mod resource_storage; mod sound; +mod texture; // --- Use --- pub use resource_manager::ResourceManager; -pub use resource::{ - Resource, - ResourceLoadType, -}; +pub use resource::{Resource, ResourceLoadType}; pub use resource_storage::ResourceStorage; -pub use sound::{ - Sound, - SoundHandle, -}; - -pub use mesh::{ - Mesh, - MeshData, - MeshVertex, - MeshHandle -}; - -pub use texture::{ - Texture, - TextureType, - TextureHandle -}; - -pub use material::{ - Material, - MaterialTextureMap, - MaterialParameter, - MaterialTexture, - MaterialParameterMap, - MaterialHandle, - get_renderer_texture_handle_from_material_texture, -}; +pub use sound::{Sound, SoundHandle}; + +pub use mesh::{Mesh, MeshData, MeshHandle, MeshVertex}; + +pub use texture::{Texture, TextureHandle, TextureType}; + +pub use material::{PBRMaterial, PBRMaterialHandle}; diff --git a/engine/pill_engine/src/resources/resource_manager.rs b/engine/pill_engine/src/resources/resource_manager.rs index caa6ada7..474e84e1 100644 --- a/engine/pill_engine/src/resources/resource_manager.rs +++ b/engine/pill_engine/src/resources/resource_manager.rs @@ -1,19 +1,15 @@ use crate::{ - graphics::{ RendererMaterialHandle, RendererTextureHandle }, - resources::{ ResourceStorage, Resource, MaterialHandle, Material, TextureHandle, Texture, TextureType }, + graphics::RendererTextureHandle, + resources::{ + PBRMaterial, PBRMaterialHandle, Resource, ResourceStorage, Texture, TextureHandle, TextureType, + }, }; -use pill_core::{ EngineError, get_type_name, PillSlotMapKey, PillTypeMap, PillTypeMapKey }; +use pill_core::{get_type_name, EngineError, PillSlotMapKey, PillTypeMap, PillTypeMapKey}; -use std::{ - collections::{HashMap}, - convert::TryInto, - env, - num::NonZeroU32, - path::PathBuf -}; +use anyhow::{Context, Error, Result}; use boolinator::Boolinator; -use anyhow::{Result, Context, Error}; +use std::{collections::HashMap, convert::TryInto, env, num::NonZeroU32, path::PathBuf}; pub struct ResourceManager { resources: PillTypeMap, @@ -21,35 +17,52 @@ pub struct ResourceManager { impl ResourceManager { pub fn new() -> Self { - Self { + Self { resources: PillTypeMap::new(), } } // --- Slots --- - pub(crate) fn get_resource_slot<'a, T>(&'a self, resource_handle: &T::Handle) -> Result<&'a Option> - where T: Resource> + pub(crate) fn get_resource_slot<'a, T>( + &'a self, + resource_handle: &T::Handle, + ) -> Result<&'a Option> + where + T: Resource>, { // Get resource storage let resource_storage = self.get_resource_storage::()?; // Get resource slot - let resource_slot = resource_storage.data.get(resource_handle.clone()) - .ok_or(Error::new(EngineError::InvalidResourceHandle(get_type_name::())))?; + let resource_slot = + resource_storage + .data + .get(resource_handle.clone()) + .ok_or(Error::new(EngineError::InvalidResourceHandle( + get_type_name::(), + )))?; Ok(resource_slot) } - pub(crate) fn get_resource_slot_mut<'a, T>(&'a mut self, resource_handle: &T::Handle) -> Result<&'a mut Option> - where T: Resource> + pub(crate) fn get_resource_slot_mut<'a, T>( + &'a mut self, + resource_handle: &T::Handle, + ) -> Result<&'a mut Option> + where + T: Resource>, { // Get resource storage let resource_storage = self.get_resource_storage_mut::()?; // Get resource slot - let resource_slot = resource_storage.data.get_mut(resource_handle.clone()) - .ok_or(Error::new(EngineError::InvalidResourceHandle(get_type_name::())))?; + let resource_slot = resource_storage + .data + .get_mut(resource_handle.clone()) + .ok_or(Error::new(EngineError::InvalidResourceHandle( + get_type_name::(), + )))?; Ok(resource_slot) } @@ -57,30 +70,42 @@ impl ResourceManager { // --- Storages --- pub(crate) fn get_resource_storage(&self) -> Result<&ResourceStorage> - where T: Resource> + where + T: Resource>, { - self.resources.get::().ok_or(Error::new(EngineError::ResourceNotRegistered(get_type_name::()))) + self.resources + .get::() + .ok_or(Error::new(EngineError::ResourceNotRegistered( + get_type_name::(), + ))) } pub(crate) fn get_resource_storage_mut(&mut self) -> Result<&mut ResourceStorage> - where T: Resource> + where + T: Resource>, { - - self.resources.get_mut::().ok_or(Error::new(EngineError::ResourceNotRegistered(get_type_name::()))) + self.resources + .get_mut::() + .ok_or(Error::new(EngineError::ResourceNotRegistered( + get_type_name::(), + ))) } // --- Register - Add - Remove --- pub fn register_resource_type(&mut self, max_resource_count: usize) -> Result<()> - where T: Resource> + where + T: Resource>, { - self.resources.insert::(ResourceStorage::::new(max_resource_count)); + self.resources + .insert::(ResourceStorage::::new(max_resource_count)); Ok(()) } pub fn add_resource(&mut self, resource: T) -> Result<(T::Handle, &mut T)> - where T: Resource> + where + T: Resource>, { // Get resource storage let resource_storage = self.get_resource_storage_mut::()?; @@ -88,33 +113,49 @@ impl ResourceManager { // Check if there is space for resource if resource_storage.data.len() >= resource_storage.max_resource_count { - return Err(Error::new(EngineError::ResourceLimitReached(get_type_name::()))); + return Err(Error::new(EngineError::ResourceLimitReached( + get_type_name::(), + ))); } // Check if resource already exists if resource_storage.mapping.contains_key(&resource_name) { - return Err(Error::new(EngineError::ResourceAlreadyExists(get_type_name::(), resource_name.clone()))); + return Err(Error::new(EngineError::ResourceAlreadyExists( + get_type_name::(), + resource_name.clone(), + ))); } // Insert new resource let resource_handle = resource_storage.data.insert(Some(resource)); - let resource = resource_storage.data.get_mut(resource_handle).unwrap().as_mut().unwrap(); + let resource = resource_storage + .data + .get_mut(resource_handle) + .unwrap() + .as_mut() + .unwrap(); // Insert new mapping - resource_storage.mapping.insert(&resource_name, &resource_handle); + resource_storage + .mapping + .insert(&resource_name, &resource_handle); Ok((resource_handle, resource)) } pub fn remove_resource(&mut self, resource_handle: &T::Handle) -> Result<(T::Handle, T)> - where T: Resource> + where + T: Resource>, { // Get resource storage let resource_storage = self.get_resource_storage_mut::()?; // Remove resource - let resource = resource_storage.data.remove(*resource_handle) - .ok_or(EngineError::InvalidResourceHandle(get_type_name::()))?.expect("Critical: Resource is None"); + let resource = resource_storage + .data + .remove(*resource_handle) + .ok_or(EngineError::InvalidResourceHandle(get_type_name::()))? + .expect("Critical: Resource is None"); // Remove mapping resource_storage.mapping.remove_by_value(resource_handle); @@ -123,17 +164,28 @@ impl ResourceManager { } pub fn remove_resource_by_name(&mut self, name: &str) -> Result<(T::Handle, T)> - where T: Resource> + where + T: Resource>, { // Get resource storage let resource_storage = self.get_resource_storage_mut::()?; // Get handle by name - let resource_handle = resource_storage.mapping.get_value(&name.to_string()) - .ok_or(EngineError::InvalidResourceName(name.to_string(), get_type_name::()))?.clone(); + let resource_handle = resource_storage + .mapping + .get_value(&name.to_string()) + .ok_or(EngineError::InvalidResourceName( + name.to_string(), + get_type_name::(), + ))? + .clone(); // Remove resource - let resource = resource_storage.data.remove(resource_handle).unwrap().expect("Critical: Resource is None"); + let resource = resource_storage + .data + .remove(resource_handle) + .unwrap() + .expect("Critical: Resource is None"); // Remove mapping resource_storage.mapping.remove_by_key(&name.to_string()); @@ -144,65 +196,99 @@ impl ResourceManager { // --- Get --- pub fn get_resource_handle(&self, name: &str) -> Result - where T: Resource> + where + T: Resource>, { // Get resource storage let resource_storage = self.get_resource_storage::()?; // Get resource handle - let resource_handle = resource_storage.mapping.get_value(&name.to_string()) - .ok_or(EngineError::InvalidSceneName(name.to_string()))?.clone(); + let resource_handle = resource_storage + .mapping + .get_value(&name.to_string()) + .ok_or(EngineError::InvalidSceneName(name.to_string()))? + .clone(); Ok(resource_handle) } pub fn get_resource<'a, T>(&'a self, resource_handle: &'a T::Handle) -> Result<&'a T> - where T: Resource> + where + T: Resource>, { // Get resource - let resource = self.get_resource_slot::(resource_handle)?.as_ref().expect("Critical: Resource is None"); + let resource = self + .get_resource_slot::(resource_handle)? + .as_ref() + .expect("Critical: Resource is None"); Ok(resource) } pub fn get_resource_by_name<'a, T>(&'a self, name: &str) -> Result<&'a T> - where T: Resource> + where + T: Resource>, { // Get resource storage let resource_storage = self.get_resource_storage::()?; // Get handle by name - let resource_handle = resource_storage.mapping.get_value(&name.to_string()) - .ok_or(EngineError::InvalidResourceName(name.to_string(), get_type_name::()))?; + let resource_handle = resource_storage + .mapping + .get_value(&name.to_string()) + .ok_or(EngineError::InvalidResourceName( + name.to_string(), + get_type_name::(), + ))?; // Get resource - let resource = self.get_resource_slot::(resource_handle)?.as_ref().expect("Critical: Resource is None"); + let resource = self + .get_resource_slot::(resource_handle)? + .as_ref() + .expect("Critical: Resource is None"); Ok(resource) } - pub fn get_resource_mut<'a, T>(&'a mut self, resource_handle: &'a T::Handle) -> Result<&'a mut T> - where T: Resource> + pub fn get_resource_mut<'a, T>( + &'a mut self, + resource_handle: &'a T::Handle, + ) -> Result<&'a mut T> + where + T: Resource>, { // Get resource - let resource = self.get_resource_slot_mut::(resource_handle)?.as_mut().expect("Critical: Resource is None"); + let resource = self + .get_resource_slot_mut::(resource_handle)? + .as_mut() + .expect("Critical: Resource is None"); Ok(resource) } pub fn get_resource_by_name_mut<'a, T>(&'a mut self, name: &str) -> Result<&'a mut T> - where T: Resource> + where + T: Resource>, { // Get resource storage let resource_storage = self.get_resource_storage_mut::()?; // Get handle by name - let resource_handle = resource_storage.mapping.get_value(&name.to_string()) - .ok_or(EngineError::InvalidResourceName(name.to_string(), get_type_name::()))?.clone(); + let resource_handle = resource_storage + .mapping + .get_value(&name.to_string()) + .ok_or(EngineError::InvalidResourceName( + name.to_string(), + get_type_name::(), + ))? + .clone(); // Get resource - let resource = self.get_resource_slot_mut::(&resource_handle)?.as_mut().expect("Critical: Resource is None"); + let resource = self + .get_resource_slot_mut::(&resource_handle)? + .as_mut() + .expect("Critical: Resource is None"); Ok(resource) } -} \ No newline at end of file +} diff --git a/engine/pill_engine/src/resources/resource_storage.rs b/engine/pill_engine/src/resources/resource_storage.rs index 8c76bd04..bcb027e7 100644 --- a/engine/pill_engine/src/resources/resource_storage.rs +++ b/engine/pill_engine/src/resources/resource_storage.rs @@ -1,21 +1,25 @@ -use crate::{resources::Resource, config::RESOURCE_VERSION_LIMIT}; +use crate::{config::RESOURCE_VERSION_LIMIT, resources::Resource}; -use pill_core::{ PillSlotMap, PillSlotMapKey, PillTwinMap }; +use pill_core::{PillSlotMap, PillSlotMapKey, PillTwinMap}; use std::collections::HashMap; pub struct ResourceStorage { - pub data: PillSlotMap>, + pub data: PillSlotMap>, pub mapping: PillTwinMap, pub max_resource_count: usize, } impl ResourceStorage { - pub fn new(max_resource_count: usize) -> Self { - Self { - data: PillSlotMap::>::with_capacity_and_key_and_version_limit(max_resource_count, RESOURCE_VERSION_LIMIT as u32).unwrap(), + pub fn new(max_resource_count: usize) -> Self { + Self { + data: PillSlotMap::>::with_capacity_and_key_and_version_limit( + max_resource_count, + RESOURCE_VERSION_LIMIT as u32, + ) + .unwrap(), mapping: PillTwinMap::::new(), max_resource_count, } } -} \ No newline at end of file +} diff --git a/engine/pill_engine/src/resources/sound.rs b/engine/pill_engine/src/resources/sound.rs index 9eca3c2d..5e96836d 100644 --- a/engine/pill_engine/src/resources/sound.rs +++ b/engine/pill_engine/src/resources/sound.rs @@ -1,7 +1,7 @@ use crate::{ engine::Engine, graphics::{ RendererTextureHandle }, - resources::{ ResourceStorage, Resource, ResourceLoadType, Material }, + resources::{ ResourceStorage, Resource, ResourceLoadType, PBRMaterial }, ecs::{ DeferredUpdateManagerPointer, AudioSourceComponent, SoundType, AudioManagerComponent }, config::*, }; diff --git a/engine/pill_engine/src/resources/texture.rs b/engine/pill_engine/src/resources/texture.rs index 056c833d..36aa393d 100644 --- a/engine/pill_engine/src/resources/texture.rs +++ b/engine/pill_engine/src/resources/texture.rs @@ -1,26 +1,26 @@ use crate::{ - engine::Engine, - graphics::{ RendererTextureHandle }, - resources::{ ResourceStorage, Resource, ResourceLoadType, Material }, - ecs::{ DeferredUpdateManagerPointer }, config::*, + ecs::DeferredUpdateManagerPointer, + engine::Engine, + graphics::RendererTextureHandle, + resources::{PBRMaterial, Resource, ResourceLoadType, ResourceStorage}, }; -use pill_core::{ PillSlotMapKey, PillTypeMapKey, PillStyle, get_type_name }; +use pill_core::{get_type_name, PillSlotMapKey, PillStyle, PillTypeMapKey}; -use std::collections::HashSet; -use std::path::{ Path, PathBuf }; -use anyhow::{ Result, Context, Error }; +use anyhow::{Context, Error, Result}; use bytemuck::{Pod, Zeroable}; // For derive macros to work -use readonly::make; // For #[readonly::make] to resolve +use readonly::make; +use std::collections::HashSet; +use std::path::{Path, PathBuf}; // For #[readonly::make] to resolve pill_core::define_new_pill_slotmap_key! { pub struct TextureHandle; } #[derive(Clone, Copy, Debug)] pub enum TextureType { - Color, - Normal, + Gamma, // sRGB + Linear, } #[readonly::make] @@ -35,7 +35,11 @@ pub struct Texture { } impl Texture { - pub fn new(name: &str, texture_type: TextureType, resource_load_type: ResourceLoadType) -> Self { + pub fn new( + name: &str, + texture_type: TextureType, + resource_load_type: ResourceLoadType, + ) -> Self { Self { name: name.to_string(), load_type: resource_load_type, @@ -57,7 +61,11 @@ impl Resource for Texture { } fn initialize(&mut self, engine: &mut Engine) -> Result<()> { - let error_message = format!("Initializing {} {} failed", "Resource".gobj_style(), get_type_name::().sobj_style()); + let error_message = format!( + "Initializing {} {} failed", + "Resource".gobj_style(), + get_type_name::().sobj_style() + ); // Create new renderer texture resource let image_data = match &self.load_type { @@ -68,15 +76,18 @@ impl Resource for Texture { // Load data image::open(&resource_file_path)? - }, + } ResourceLoadType::Bytes(bytes) => { // Load data image::load_from_memory(bytes)? - }, + } }; // Create renderer texture resource - let renderer_resource_handle = engine.renderer.create_texture(&self.name, &image_data, self.texture_type).context(error_message.clone())?; + let renderer_resource_handle = engine + .renderer + .create_texture(&self.name, &image_data, self.texture_type) + .context(error_message.clone())?; self.renderer_resource_handle = Some(renderer_resource_handle); Ok(()) @@ -88,32 +99,6 @@ impl Resource for Texture { engine.renderer.destroy_texture(v).unwrap(); } - // Take resource storage from engine - let resource_storage = engine.resource_manager.get_resource_storage_mut::().expect("Critical: Resource not registered"); - let materials = &mut resource_storage.data; - - // Find materials that use this texture and update them - for material_slot in materials.iter_mut() { - let material = material_slot.1.as_mut().expect("Critical: Resource is None"); - - // Update texture slots - let mut material_updated = false; - for texture_slot in material.get_textures().data.iter_mut() { - if let Some(texture_handle) = texture_slot.1.texture_handle { - // If material texture has handle to this texture - if texture_handle.data() == self_handle.data() { - texture_slot.1.texture_handle = None; - texture_slot.1.renderer_texture_handle = None; - material_updated = true; - } - } - } - - if material_updated { - engine.renderer.update_material_textures(material.renderer_resource_handle.unwrap(), &material.textures).unwrap(); - } - } - Ok(()) } -} \ No newline at end of file +} diff --git a/engine/pill_renderer/src/pass_scene.rs b/engine/pill_renderer/src/pass_scene.rs index 60d2629f..2873d0bf 100644 --- a/engine/pill_renderer/src/pass_scene.rs +++ b/engine/pill_renderer/src/pass_scene.rs @@ -1,20 +1,58 @@ use crate::renderer::{Pass, Renderer, WorldQuery}; use crate::resource_manager::ResourceManager; use crate::resources::CameraUniform; +use crate::resources::RendererTexture; +use std::sync::Arc; + use anyhow::{Error, Result}; use glam::{Mat4, Vec3, Vec4}; use pill_core::{PillSlotMapKey, RendererError}; use pill_engine::internal::{ get_renderer_resource_handle_from_camera_component, CameraComponent, ComponentStorage, - EntityHandle, RenderQueueItem, RendererMaterialHandle, RendererMeshHandle, - RendererPipelineHandle, TransformComponent, + EntityHandle, MaterialDesc, RenderQueueItem, RendererMaterialHandle, RendererMeshHandle, + TransformComponent, }; -use pill_engine::internal::{PillRenderer, PipelineV2, PipelineV2Desc, ShaderDesc}; +use pill_engine::internal::{BufferDesc, PillRenderer, PipelineV2, PipelineV2Desc, ShaderDesc}; +use wgpu::util::DeviceExt; use wgpu::CommandEncoder; +#[repr(C)] +#[derive(Copy, Clone)] +struct PerDrawStd140 { + mvp: [[f32; 4]; 4], +} +unsafe impl bytemuck::Zeroable for PerDrawStd140 {} +unsafe impl bytemuck::Pod for PerDrawStd140 {} + +#[repr(C)] +#[derive(Copy, Clone)] +struct MaterialParamsStd140 { + base_color_factor: [f32; 3], + _pad0: f32, + metallic: f32, + roughness: f32, + _pad1: [f32; 2], + emissive_factor: [f32; 3], + _pad2: f32, +} +unsafe impl bytemuck::Zeroable for MaterialParamsStd140 {} +unsafe impl bytemuck::Pod for MaterialParamsStd140 {} + +// Static configuration for per-draw buffering (derive capacities, avoid magic numbers) +const MAX_EXPECTED_PER_DRAW_INSTANCES: usize = 100_000; +const UNIFORM_OFFSET_ALIGNMENT: usize = 256; +const PER_DRAW_STRIDE_BYTES: usize = ((std::mem::size_of::() + + (UNIFORM_OFFSET_ALIGNMENT - 1)) + / UNIFORM_OFFSET_ALIGNMENT) + * UNIFORM_OFFSET_ALIGNMENT; + +pub const MATERIAL_BIND_GROUP_GLOBALS: usize = 0; +pub const MATERIAL_BIND_GROUP_TEXTURES: usize = 1; +pub const MATERIAL_BIND_GROUP_PARAMS: usize = 2; +pub const MATERIAL_BIND_GROUP_PERDRAW: usize = 3; + // Preallocated buffer structs for hot path optimization pub(crate) struct VisiblePreDraw { - pub(crate) pipeline_handle: RendererPipelineHandle, pub(crate) material_handle: RendererMaterialHandle, pub(crate) mesh_handle: RendererMeshHandle, pub(crate) entity_index: u32, @@ -29,18 +67,19 @@ pub(crate) struct MeshBatch { } pub(crate) struct GroupCmd { - pub(crate) pipeline_handle: RendererPipelineHandle, + pub(crate) pipeline: *const wgpu::RenderPipeline, pub(crate) material_handle: RendererMaterialHandle, pub(crate) batches: Vec, } pub struct PassScene { label: String, + // Resources + offscreen_color_texture: Arc, + depth_texture: Arc, // Per-frame dynamic UBO ring (Milestone 5) per_draw_stride: u64, per_draw_capacity: u64, - per_draw_buffer: Option, - per_draw_bind_group: Option, // Working buffers visible_pre_draw_buffer: Vec, groups_buffer: Vec, @@ -54,19 +93,28 @@ struct PassSceneState { camera_buffer: wgpu::Buffer, camera_bind_group: wgpu::BindGroup, pipeline: PipelineV2, + // Per-draw resources + per_draw_buffer: wgpu::Buffer, + per_draw_bind_group: wgpu::BindGroup, } impl PassScene { - pub fn new(label: &str) -> Self { + pub fn new( + label: &str, + offscreen_color_texture: Arc, + depth_texture: Arc, + ) -> Self { Self { label: label.to_string(), - per_draw_stride: 256, + offscreen_color_texture: offscreen_color_texture, + depth_texture: depth_texture, + per_draw_stride: PER_DRAW_STRIDE_BYTES as u64, per_draw_capacity: 0, - per_draw_buffer: None, - per_draw_bind_group: None, - visible_pre_draw_buffer: Vec::with_capacity(100_000), + visible_pre_draw_buffer: Vec::with_capacity(MAX_EXPECTED_PER_DRAW_INSTANCES), groups_buffer: Vec::with_capacity(2000), - staging_buffer: Vec::with_capacity(100_000 * 144), + staging_buffer: Vec::with_capacity( + MAX_EXPECTED_PER_DRAW_INSTANCES * PER_DRAW_STRIDE_BYTES, + ), state: None, } } @@ -84,22 +132,11 @@ impl Pass for PassScene { } fn init(&mut self, renderer: &mut Renderer) -> Result<()> { - // TODO: - // Problem: - // Material is coupled with Pipeline binds, now has handle to update i.e. texture via bind - // User creates Material and sets the params (floats, textures, etc.) that trigger update via Pipeline bind - // User can access Resources only via ResourcePool handles - // Material has to stay in sync with Pipeline and Shader - // Solution?: - // User can access via ResourcesPool only Material or MaterialInstance handles. - // Move PassScene to Engine. Pipeline can be local, Material too? Expose Material by Enum? - // User can create MaterialInstance and select Material/Pipeline via enum? i.e. PBR, UNLIT etc. - // Decouple Material params data from Implementation // [SIMILAR] Prebuilt PSO used; avoid hot-path pipeline creation per TALK // Shaders: must match bind group layout indices: 0(camera),1(material textures),2(material params),3(per-draw) let vertex_wgsl = r#" @group(0) @binding(0) var UCamera: mat4x4; - struct PerDraw { mvp: mat4x4, tint: vec4, }; + struct PerDraw { mvp: mat4x4, }; @group(3) @binding(0) var UPerDraw: PerDraw; struct VSIn { @location(0) pos: vec3, @location(1) uv: vec2, }; struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; @@ -111,20 +148,35 @@ impl Pass for PassScene { } "#; let fragment_wgsl = r#" - @group(1) @binding(0) var tex_diffuse: texture_2d; - @group(1) @binding(1) var smp_diffuse: sampler; - @group(1) @binding(2) var tex_normal: texture_2d; - @group(1) @binding(3) var smp_normal: sampler; - struct MaterialParams { tint_spec: vec4, } + // PBR material textures (set 1) + @group(1) @binding(0) var texBaseColor: texture_2d; + @group(1) @binding(1) var smpBaseColor: sampler; + @group(1) @binding(2) var texNormal: texture_2d; + @group(1) @binding(3) var smpNormal: sampler; + @group(1) @binding(4) var texMetallicRoughness: texture_2d; + @group(1) @binding(5) var smpMetallicRoughness: sampler; + @group(1) @binding(6) var texEmissive: texture_2d; + @group(1) @binding(7) var smpEmissive: sampler; + // PBR params UBO (set 2) + struct MaterialParams { + baseColorFactor: vec3, _pad0: f32, + metallicFactor: f32, roughnessFactor: f32, _pad1: vec2, + emissiveFactor: vec3, _pad2: f32, + } @group(2) @binding(0) var UMaterial: MaterialParams; - struct PerDraw { mvp: mat4x4, tint: vec4, }; + // Per-draw + struct PerDraw { mvp: mat4x4, }; @group(3) @binding(0) var UPerDraw: PerDraw; @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { - let albedo = textureSample(tex_diffuse, smp_diffuse, uv); - let tinted = vec4(UMaterial.tint_spec.rgb, 1.0) * UPerDraw.tint; - let spec_boost = 0.5 + 0.5 * UMaterial.tint_spec.a; - let color = albedo * tinted * spec_boost; - return vec4(color.rgb, 1.0); + let bc_tex = textureSample(texBaseColor, smpBaseColor, uv).rgb; + let mr = textureSample(texMetallicRoughness, smpMetallicRoughness, uv).gb; + let roughness = clamp(mr.x * UMaterial.roughnessFactor, 0.0, 1.0); + let metallic = clamp(mr.y * UMaterial.metallicFactor, 0.0, 1.0); + let base = bc_tex * UMaterial.baseColorFactor; + let emissive = textureSample(texEmissive, smpEmissive, uv).rgb * UMaterial.emissiveFactor; + // Simple unlit-ish output; lighting model TBD + let color = base + emissive; + return vec4(color, 1.0); } "#; @@ -141,7 +193,7 @@ impl Pass for PassScene { }, count: None, }], - // 1: material textures + // 1: material textures (PBR) vec![ wgpu::BindGroupLayoutEntry { binding: 0, @@ -175,8 +227,40 @@ impl Pass for PassScene { ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, + wgpu::BindGroupLayoutEntry { + binding: 4, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 5, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 6, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 7, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, ], - // 2: material params + // 2: material params (PBR) vec![wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::FRAGMENT, @@ -187,32 +271,19 @@ impl Pass for PassScene { }, count: None, }], - // 3: per-draw dynamic UBO + // 3: per-draw dynamic UBO (MVP) vec![wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: true, - min_binding_size: Some(std::num::NonZeroU64::new(144).unwrap()), + min_binding_size: Some(std::num::NonZeroU64::new(64).unwrap()), }, count: None, }], ]; - let color_target = wgpu::ColorTargetState { - format: renderer.state.color_format, - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - }; - let depth_stencil = Some(wgpu::DepthStencilState { - format: renderer.state.depth_format, - depth_write_enabled: true, - depth_compare: wgpu::CompareFunction::Less, - stencil: wgpu::StencilState::default(), - bias: wgpu::DepthBiasState::default(), - }); - let desc = PipelineV2Desc { label: Some("scene_pipeline_v2"), vs: ShaderDesc { @@ -224,8 +295,18 @@ impl Pass for PassScene { entry_func: "main", }, bind_groups, - targets: &[Some(color_target)], - depth_stencil, + targets: &[Some(wgpu::ColorTargetState { + format: self.offscreen_color_texture.texture.format(), + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + depth_stencil: Some(wgpu::DepthStencilState { + format: self.depth_texture.texture.format(), + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), multisample: wgpu::MultisampleState::default(), }; @@ -241,18 +322,48 @@ impl Pass for PassScene { }); let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("camera_bind_group"), - layout: &pipeline.bind_group_layouts[0], + layout: &pipeline.bind_group_layouts[MATERIAL_BIND_GROUP_GLOBALS], entries: &[wgpu::BindGroupEntry { binding: 0, resource: camera_buffer.as_entire_binding(), }], }); + // Pre-create per-draw dynamic UBO ring and its bind group with large preallocation + // Creation happens in init; growth/recreation only when capacity is insufficient during draw + self.per_draw_capacity = MAX_EXPECTED_PER_DRAW_INSTANCES as u64; + let size = self.per_draw_stride * self.per_draw_capacity; + let per_draw_buffer = renderer.create_buffer(BufferDesc { + label: Some("per_draw_dynamic_ubo_ring"), + byte_size: size, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + })?; + let layout_ptr: *const wgpu::BindGroupLayout = + &pipeline.bind_group_layouts[MATERIAL_BIND_GROUP_PERDRAW as usize] as *const _; + let per_draw_bind_group = + renderer + .ctx + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("per_draw_bind_group"), + layout: unsafe { &*layout_ptr }, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &per_draw_buffer, + offset: 0, + size: Some(std::num::NonZeroU64::new(64).unwrap()), + }), + }], + }); + self.state = Some(PassSceneState { camera_uniform: CameraUniform::new(), camera_buffer, camera_bind_group, pipeline, + per_draw_buffer, + per_draw_bind_group, }); Ok(()) @@ -270,6 +381,10 @@ impl Pass for PassScene { let camera_component_storage = world.camera_components; let transform_component_storage = world.transform_components; + // TODO: Update all isDirty=true materials, for each: + // - Color3f -> Update material uniform + // - Texture2D -> Update material bind groups + // Read active camera + transform let camera_storage = camera_component_storage .data @@ -282,9 +397,8 @@ impl Pass for PassScene { .unwrap(); let active_camera_transform_component = camera_transform_storage.as_ref().unwrap(); - // Update camera uniform and write to GPU buffer (no allocations), - // then release the mutable borrow and keep only cheap copies/ptrs. - let (vp_mat_arr, camera_bg_ptr): ([[f32; 4]; 4], *const wgpu::BindGroup) = { + // Update camera uniform and write to GPU buffer (no allocations) + let vp_mat_arr: [[f32; 4]; 4] = { let state = get_state(self); state .camera_uniform @@ -294,10 +408,7 @@ impl Pass for PassScene { 0, bytemuck::bytes_of(&state.camera_uniform), ); - ( - state.camera_uniform.view_projection_matrix, - &state.camera_bind_group as *const _, - ) + state.camera_uniform.view_projection_matrix }; let clear_color = active_camera_component.clear_color; @@ -338,13 +449,6 @@ impl Pass for PassScene { key.material_index as u32, key.material_version as u32, ); - let material_for_pipeline = renderer - .state - .resource_manager - .materials - .get(material_handle) - .unwrap(); - let pipeline_handle: RendererPipelineHandle = material_for_pipeline.pipeline_handle; // Transform let entity_index = render_queue_item.entity_index as usize; @@ -417,7 +521,6 @@ impl Pass for PassScene { let view_proj: Mat4 = vp_mat; let mvp: [[f32; 4]; 4] = (view_proj * model).to_cols_array_2d(); self.visible_pre_draw_buffer.push(VisiblePreDraw { - pipeline_handle, material_handle, mesh_handle, entity_index: render_queue_item.entity_index, @@ -425,26 +528,25 @@ impl Pass for PassScene { }); } - // Sort and group + // Sort and group (Pipeline -> Material -> Mesh) self.visible_pre_draw_buffer.sort_by_key(|v| { - ( - v.pipeline_handle.index(), - v.material_handle.index(), - v.mesh_handle.index(), - ) + ((v.material_handle.generation() as u64) << 32) | (v.material_handle.index() as u64) }); self.groups_buffer.clear(); + // Resolve pipeline pointer for this frame (single pipeline for now) + let pipeline_ptr: *const wgpu::RenderPipeline = { + let state = get_state(self); + &state.pipeline.pipeline as *const _ + }; for v in &self.visible_pre_draw_buffer { - if self + let need_new_group = self .groups_buffer .last() - .map(|g| { - g.pipeline_handle != v.pipeline_handle || g.material_handle != v.material_handle - }) - .unwrap_or(true) - { + .map(|g| g.material_handle != v.material_handle) + .unwrap_or(true); + if need_new_group { self.groups_buffer.push(GroupCmd { - pipeline_handle: v.pipeline_handle, + pipeline: pipeline_ptr, material_handle: v.material_handle, batches: Vec::new(), }); @@ -477,53 +579,24 @@ impl Pass for PassScene { }) .sum(); if self.per_draw_capacity < needed { - self.per_draw_capacity = needed.next_power_of_two().max(1); - let size = self.per_draw_stride * self.per_draw_capacity; - self.per_draw_buffer = - Some(renderer.ctx.device.create_buffer(&wgpu::BufferDescriptor { - label: Some("per_draw_dynamic_ubo_ring"), - size, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - })); - let layout_ptr: *const wgpu::BindGroupLayout = { - let state = get_state(self); - &state.pipeline.bind_group_layouts[3] as *const _ - }; - let buf_ptr: *const wgpu::Buffer = self.per_draw_buffer.as_ref().unwrap(); - let new_bg = renderer - .ctx - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("per_draw_bind_group"), - layout: unsafe { &*layout_ptr }, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: unsafe { &*buf_ptr }, - offset: 0, - size: Some(std::num::NonZeroU64::new(144).unwrap()), - }), - }], - }); - self.per_draw_bind_group = Some(new_bg); - } - #[repr(C)] - #[derive(Copy, Clone)] - struct PerDrawStd140 { - mvp: [[f32; 4]; 4], - tint: [f32; 4], + #[cfg(debug_assertions)] + { + log::error!( + "Per-draw capacity exceeded: needed={} capacity={}", + needed, + self.per_draw_capacity + ); + } + // Release: proceed; only first (capacity) entries will be used by draws } - unsafe impl bytemuck::Zeroable for PerDrawStd140 {} - unsafe impl bytemuck::Pod for PerDrawStd140 {} - let tint: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; + self.staging_buffer.clear(); let mut next_offset_u32: u32 = 0; for g in self.groups_buffer.iter_mut() { for b in g.batches.iter_mut() { b.base_offset_u32 = next_offset_u32; for mvp in &b.instances { - let pd = PerDrawStd140 { mvp: *mvp, tint }; + let pd = PerDrawStd140 { mvp: *mvp }; self.staging_buffer .extend_from_slice(bytemuck::bytes_of(&pd)); let pad = @@ -533,18 +606,19 @@ impl Pass for PassScene { } } } - if let Some(buf) = &self.per_draw_buffer { + { + let state_ref: &PassSceneState = unsafe { self.state.as_ref().unwrap_unchecked() }; renderer .ctx .queue - .write_buffer(buf, 0, &self.staging_buffer); + .write_buffer(&state_ref.per_draw_buffer, 0, &self.staging_buffer); } // Encode offscreen pass let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("m6_offscreen_pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &renderer.state.offscreen_color_texture.texture_view, + view: &self.offscreen_color_texture.texture_view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { @@ -557,7 +631,7 @@ impl Pass for PassScene { }, })], depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &renderer.state.depth_texture.texture_view, + view: &self.depth_texture.texture_view, depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: wgpu::StoreOp::Store, @@ -567,36 +641,66 @@ impl Pass for PassScene { timestamp_writes: None, occlusion_query_set: None, }); - let pipeline_ptr: *const wgpu::RenderPipeline = { - let state = get_state(self); - &state.pipeline.pipeline as *const _ - }; + // Immutable view of pass state for binding (no aliasing with groups_buffer immutable borrow) + let state_ref: &PassSceneState = unsafe { self.state.as_ref().unwrap_unchecked() }; for group in &self.groups_buffer { - rpass.set_pipeline(unsafe { &*pipeline_ptr }); - let material = renderer - .state - .resource_manager - .materials - .get(group.material_handle) - .unwrap(); - // SAFETY: camera_bg_ptr points to self.state.camera_bind_group, valid for the duration of draw - rpass.set_bind_group(0, unsafe { &*camera_bg_ptr }, &[]); - rpass.set_bind_group(1, &material.texture_bind_group, &[]); - rpass.set_bind_group(2, &material.parameter_bind_group, &[]); - for batch in &group.batches { - let mesh = renderer + rpass.set_pipeline(unsafe { &*group.pipeline }); + rpass.set_bind_group( + MATERIAL_BIND_GROUP_GLOBALS as u32, + &state_ref.camera_bind_group, + &[], + ); + // Bind group-specific material (HOT-ish path; debug-validated, unchecked in release) + let material = unsafe { + let m = renderer .state .resource_manager - .meshes - .get(batch.mesh_handle) - .unwrap(); + .materials + .get(group.material_handle); + debug_assert!( + m.is_some(), + "stale RendererMaterialHandle {:?} in render queue", + group.material_handle + ); + m.unwrap_unchecked() + }; + rpass.set_bind_group( + MATERIAL_BIND_GROUP_TEXTURES as u32, + &material.texture_bind_group, + &[], + ); + rpass.set_bind_group( + MATERIAL_BIND_GROUP_PARAMS as u32, + &material.param_bind_group, + &[], + ); + for batch in &group.batches { + // Hot path: assume valid handles (render queue built from alive resources). + // Debug-only validation; no branches in release. + let mesh = unsafe { + let m = renderer + .state + .resource_manager + .meshes + .get(batch.mesh_handle); + debug_assert!( + m.is_some(), + "stale RendererMeshHandle {:?} in render queue", + batch.mesh_handle + ); + m.unwrap_unchecked() + }; rpass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); rpass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32); for i in 0..batch.instances.len() { let offset = batch .base_offset_u32 .wrapping_add((i as u32) * (self.per_draw_stride as u32)); - rpass.set_bind_group(3, self.per_draw_bind_group.as_ref().unwrap(), &[offset]); + rpass.set_bind_group( + MATERIAL_BIND_GROUP_PERDRAW as u32, + &state_ref.per_draw_bind_group, + &[offset], + ); rpass.draw_indexed(0..mesh.index_count, 0, 0..1); } } diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 536f2cf4..7ef6baa6 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -10,7 +10,8 @@ use crate::{ pass_scene::PassScene, resource_manager::ResourceManager, resources::{ - RendererCamera, RendererMaterial, RendererMesh, RendererPipeline, RendererTexture, Vertex, + RendererCamera, RendererMaterial, RendererMaterialParamsStd140, RendererMaterialTextures, + RendererMesh, RendererPipeline, RendererTexture, Vertex, }, }; @@ -18,11 +19,10 @@ use std::sync::Arc; use pill_engine::internal::{ get_renderer_resource_handle_from_camera_component, BufferDesc, CameraComponent, - ComponentStorage, EntityHandle, MaterialParameterMap, MaterialTextureMap, MeshData, - PillRenderer, PipelineV2, PipelineV2Desc, RenderQueueItem, RendererBufferHandle, - RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, RendererPipelineHandle, - RendererPipelineV2Handle, RendererTextureHandle, ShaderDesc, TextureType, TransformComponent, - RENDER_QUEUE_KEY_ORDER, + ComponentStorage, EntityHandle, MaterialDesc, MeshData, PillRenderer, PipelineV2, + PipelineV2Desc, RenderQueueItem, RendererBufferHandle, RendererCameraHandle, + RendererMaterialHandle, RendererMeshHandle, RendererPipelineHandle, RendererPipelineV2Handle, + RendererTextureHandle, ShaderDesc, TextureType, TransformComponent, RENDER_QUEUE_KEY_ORDER, }; use pill_core::{Handle, PillSlotMapKey, PillStyle, RendererError, Timer}; @@ -332,10 +332,7 @@ impl PillRenderer for Renderer { struct Camera { position: vec4, view_projection_matrix: mat4x4, }; @group(0) @binding(0) var GCamera: Camera; - struct PerDraw { - mvp: mat4x4, - tint: vec4, - }; + struct PerDraw { mvp: mat4x4, }; @group(3) @binding(0) var UPerDraw: PerDraw; struct VSIn { @location(0) pos: vec3, @location(1) uv: vec2, }; @@ -359,16 +356,13 @@ impl PillRenderer for Renderer { struct MaterialParams { tint_spec: vec4, } @group(2) @binding(0) var UMaterial: MaterialParams; - // Per-draw (set 3): supports per-entity tint for M4 - struct PerDraw { - mvp: mat4x4, - tint: vec4, - }; + // Per-draw (set 3): MVP only + struct PerDraw { mvp: mat4x4, }; @group(3) @binding(0) var UPerDraw: PerDraw; @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { let albedo = textureSample(tex_diffuse, smp_diffuse, uv); - let tinted = vec4(UMaterial.tint_spec.rgb, 1.0) * UPerDraw.tint; + let tinted = vec4(UMaterial.tint_spec.rgb, 1.0); let spec_boost = 0.5 + 0.5 * UMaterial.tint_spec.a; let color = albedo * tinted * spec_boost; return vec4(color.rgb, 1.0); @@ -427,7 +421,7 @@ impl PillRenderer for Renderer { &ctx.queue, Some("overlay_logo"), &logo_image, - TextureType::Color, + TextureType::Gamma, ) .expect("failed to create overlay logo texture"); resource_manager.textures.insert(tex) @@ -457,8 +451,17 @@ impl PillRenderer for Renderer { fn init(&mut self) -> Result<()> { self.state.passes.clear(); + // Ensure default textures are created before any pass init that may need them + self.state + .resource_manager + .ensure_default_textures(&self.ctx.device, &self.ctx.queue); + // Scene pass first: renders into offscreen targets - self.state.passes.push(Box::new(PassScene::new("scene"))); + self.state.passes.push(Box::new(PassScene::new( + "scene", + self.state.offscreen_color_texture.clone(), + self.state.depth_texture.clone(), + ))); self.state.passes.push(Box::new(PassCompose::new( "compose", @@ -501,7 +504,7 @@ impl PillRenderer for Renderer { } fn create_buffer(&mut self, desc: BufferDesc) -> Result { - let aligned_size = ((desc.byte_size + 255) / 256) * 256; // 256B for Metal UBOs + let aligned_size = ((desc.byte_size + 64) / 64) * 64; // 64B for Metal UBOs let buffer = self.ctx.device.create_buffer(&wgpu::BufferDescriptor { label: desc.label, size: aligned_size, @@ -630,43 +633,31 @@ impl PillRenderer for Renderer { }) } - fn create_mesh(&mut self, name: &str, mesh_data: &MeshData) -> Result { - let mesh = RendererMesh::new(&self.ctx.device, name, mesh_data)?; - let handle = self.state.resource_manager.meshes.insert(mesh); - - Ok(handle) - } - - fn create_texture( - &mut self, - name: &str, - image_data: &image::DynamicImage, - texture_type: TextureType, - ) -> Result { - let texture = RendererTexture::new_texture( - &self.ctx.device, - &self.ctx.queue, - Some(name), - image_data, - texture_type, - )?; - let handle = self.state.resource_manager.textures.insert(texture); - - Ok(handle) - } + fn create_material(&mut self, desc: MaterialDesc) -> Result { + // Ensure default textures exist + let (def_color_h, def_normal_h) = self + .state + .resource_manager + .ensure_default_textures(&self.ctx.device, &self.ctx.queue); + let def_color = self + .state + .resource_manager + .textures + .get(def_color_h) + .expect("default color"); + let def_normal = self + .state + .resource_manager + .textures + .get(def_normal_h) + .expect("default normal"); - fn create_material( - &mut self, - name: &str, - textures: &MaterialTextureMap, - parameters: &MaterialParameterMap, - ) -> Result { - // Create bind group layouts inline (avoid pipeline storage dependency) - let material_texture_bind_group_layout = + // Create bind group layouts matching PassScene + let textures_bgl = self.ctx .device .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("material_texture_bind_group_layout"), + label: Some("material_textures_bgl"), entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, @@ -700,13 +691,45 @@ impl PillRenderer for Renderer { ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, + wgpu::BindGroupLayoutEntry { + binding: 4, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 5, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 6, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 7, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, ], }); - let material_parameter_bind_group_layout = + let params_bgl = self.ctx .device .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("material_parameter_bind_group_layout"), + label: Some("material_params_bgl"), entries: &[wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::FRAGMENT, @@ -719,25 +742,308 @@ impl PillRenderer for Renderer { }], }); - // ensure default textures exist for material fallback - self.state + // Build params from desc + let params = RendererMaterialParamsStd140 { + albedo: desc.albedo, + _pad0: 0.0, + metallic: desc.metallic, + roughness: desc.roughness, + _pad1: [0.0, 0.0], + emissive: desc.emissive, + _pad2: 0.0, + }; + + // Build params buffer + bind group + let param_buffer = self + .ctx + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("{}_material_params", desc.label)), + contents: bytemuck::bytes_of(¶ms), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + let param_bind_group = self + .ctx + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + layout: ¶ms_bgl, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: param_buffer.as_entire_binding(), + }], + label: Some(&format!("{}_material_params_bg", desc.label)), + }); + // Resolve textures (use defaults if None) + let albedo_tex = desc + .albedo_tex + .and_then(|h| self.state.resource_manager.textures.get(h)) + .unwrap_or(def_color); + let normal_tex = desc + .normal_tex + .and_then(|h| self.state.resource_manager.textures.get(h)) + .unwrap_or(def_normal); + let mr_tex = desc + .metallic_roughness_tex + .and_then(|h| self.state.resource_manager.textures.get(h)) + .unwrap_or(def_color); + let emissive_tex = desc + .emissive_tex + .and_then(|h| self.state.resource_manager.textures.get(h)) + .unwrap_or(def_color); + + // Build texture bind group + let texture_bind_group = self + .ctx + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + layout: &textures_bgl, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&albedo_tex.texture_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&albedo_tex.sampler), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::TextureView(&normal_tex.texture_view), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::Sampler(&normal_tex.sampler), + }, + wgpu::BindGroupEntry { + binding: 4, + resource: wgpu::BindingResource::TextureView(&mr_tex.texture_view), + }, + wgpu::BindGroupEntry { + binding: 5, + resource: wgpu::BindingResource::Sampler(&mr_tex.sampler), + }, + wgpu::BindGroupEntry { + binding: 6, + resource: wgpu::BindingResource::TextureView(&emissive_tex.texture_view), + }, + wgpu::BindGroupEntry { + binding: 7, + resource: wgpu::BindingResource::Sampler(&emissive_tex.sampler), + }, + ], + label: Some(&format!("{}_material_textures_bg", desc.label)), + }); + let mat = RendererMaterial { + name: desc.label.to_string(), + texture_bind_group, + param_buffer, + param_bind_group, + }; + let handle = self.state.resource_manager.materials.insert(mat); + Ok(handle) + } + + fn update_material( + &mut self, + renderer_material_handle: RendererMaterialHandle, + desc: MaterialDesc, + ) -> Result { + // Prepare params struct + let params = RendererMaterialParamsStd140 { + albedo: desc.albedo, + _pad0: 0.0, + metallic: desc.metallic, + roughness: desc.roughness, + _pad1: [0.0, 0.0], + emissive: desc.emissive, + _pad2: 0.0, + }; + + // Resolve textures without holding a mutable borrow to materials + let (def_color_h, def_normal_h) = self + .state .resource_manager .ensure_default_textures(&self.ctx.device, &self.ctx.queue); + let def_color = self + .state + .resource_manager + .textures + .get(def_color_h) + .unwrap(); + let def_normal = self + .state + .resource_manager + .textures + .get(def_normal_h) + .unwrap(); + let albedo_tex = desc + .albedo_tex + .and_then(|h| self.state.resource_manager.textures.get(h)) + .unwrap_or(def_color); + let normal_tex = desc + .normal_tex + .and_then(|h| self.state.resource_manager.textures.get(h)) + .unwrap_or(def_normal); + let mr_tex = desc + .metallic_roughness_tex + .and_then(|h| self.state.resource_manager.textures.get(h)) + .unwrap_or(def_color); + let emissive_tex = desc + .emissive_tex + .and_then(|h| self.state.resource_manager.textures.get(h)) + .unwrap_or(def_color); + + // Create a compatible layout and new texture bind group + let textures_bgl = + self.ctx + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("material_textures_bgl_update"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 4, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 5, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 6, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 7, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + let new_texture_bg = self + .ctx + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + layout: &textures_bgl, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&albedo_tex.texture_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&albedo_tex.sampler), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::TextureView(&normal_tex.texture_view), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::Sampler(&normal_tex.sampler), + }, + wgpu::BindGroupEntry { + binding: 4, + resource: wgpu::BindingResource::TextureView(&mr_tex.texture_view), + }, + wgpu::BindGroupEntry { + binding: 5, + resource: wgpu::BindingResource::Sampler(&mr_tex.sampler), + }, + wgpu::BindGroupEntry { + binding: 6, + resource: wgpu::BindingResource::TextureView(&emissive_tex.texture_view), + }, + wgpu::BindGroupEntry { + binding: 7, + resource: wgpu::BindingResource::Sampler(&emissive_tex.sampler), + }, + ], + label: Some(&format!("{}_material_textures_bg", desc.label)), + }); + + // Mutate material once + if let Some(mat) = self + .state + .resource_manager + .materials + .get_mut(renderer_material_handle) + { + self.ctx + .queue + .write_buffer(&mat.param_buffer, 0, bytemuck::bytes_of(¶ms)); + mat.texture_bind_group = new_texture_bg; + } + Ok(renderer_material_handle) + } - let material = RendererMaterial::new( + fn create_mesh(&mut self, name: &str, mesh_data: &MeshData) -> Result { + let mesh = RendererMesh::new(&self.ctx.device, name, mesh_data)?; + let handle = self.state.resource_manager.meshes.insert(mesh); + + Ok(handle) + } + + fn create_texture( + &mut self, + name: &str, + image_data: &image::DynamicImage, + texture_type: TextureType, + ) -> Result { + let texture = RendererTexture::new_texture( &self.ctx.device, &self.ctx.queue, - &self.state.resource_manager, - name, - self.state.master_pipeline_handle, - &material_texture_bind_group_layout, - textures, - &material_parameter_bind_group_layout, - parameters, - ) - .unwrap(); - - let handle = self.state.resource_manager.materials.insert(material); + Some(name), + image_data, + texture_type, + )?; + let handle = self.state.resource_manager.textures.insert(texture); Ok(handle) } @@ -765,34 +1071,6 @@ impl PillRenderer for Renderer { Ok(handle) } - fn update_material_textures( - &mut self, - renderer_material_handle: RendererMaterialHandle, - textures: &MaterialTextureMap, - ) -> Result<()> { - RendererMaterial::update_textures( - &self.ctx.device, - &self.ctx.queue, - renderer_material_handle, - &mut self.state.resource_manager, - textures, - ) - } - - fn update_material_parameters( - &mut self, - renderer_material_handle: RendererMaterialHandle, - parameters: &MaterialParameterMap, - ) -> Result<()> { - RendererMaterial::update_parameters( - &self.ctx.device, - &self.ctx.queue, - renderer_material_handle, - &mut self.state.resource_manager, - parameters, - ) - } - fn destroy_mesh(&mut self, renderer_mesh_handle: RendererMeshHandle) -> Result<()> { self.state .resource_manager @@ -813,16 +1091,6 @@ impl PillRenderer for Renderer { Ok(()) } - fn destroy_material(&mut self, renderer_material_handle: RendererMaterialHandle) -> Result<()> { - self.state - .resource_manager - .materials - .remove(renderer_material_handle) - .unwrap(); - - Ok(()) - } - fn destroy_camera(&mut self, renderer_camera_handle: RendererCameraHandle) -> Result<()> { self.state .resource_manager diff --git a/engine/pill_renderer/src/resource_manager.rs b/engine/pill_renderer/src/resource_manager.rs index 512b42ed..0df7a5e2 100644 --- a/engine/pill_renderer/src/resource_manager.rs +++ b/engine/pill_renderer/src/resource_manager.rs @@ -5,8 +5,8 @@ use pill_core::{ RendererPipelineTag, RendererTextureTag, ResourcePool, }; use pill_engine::internal::{ - RendererBufferHandle, RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, - RendererPipelineHandle, RendererTextureHandle, + RendererBufferHandle, RendererCameraHandle, RendererMeshHandle, RendererPipelineHandle, + RendererTextureHandle, }; use crate::resources::{ @@ -89,7 +89,7 @@ impl ResourceManager { queue, Some("default_color"), &dyn_img, - pill_engine::internal::TextureType::Color, + pill_engine::internal::TextureType::Gamma, ) .expect("create default color tex"); let h = self.textures.insert(tex); @@ -105,7 +105,7 @@ impl ResourceManager { queue, Some("default_normal"), &dyn_img, - pill_engine::internal::TextureType::Normal, + pill_engine::internal::TextureType::Linear, ) .expect("create default normal tex"); let h = self.textures.insert(tex); diff --git a/engine/pill_renderer/src/resource_snapshot.rs b/engine/pill_renderer/src/resource_snapshot.rs index 175352e9..d067b1aa 100644 --- a/engine/pill_renderer/src/resource_snapshot.rs +++ b/engine/pill_renderer/src/resource_snapshot.rs @@ -1,7 +1,12 @@ -use pill_core::{ResourcePool, RendererBufferTag, RendererCameraTag, RendererMaterialTag, RendererMeshTag, RendererPipelineTag, RendererTextureTag}; +use pill_core::{ + RendererBufferTag, RendererCameraTag, RendererMaterialTag, RendererMeshTag, + RendererPipelineTag, RendererTextureTag, ResourcePool, +}; -use crate::resources::{RendererCamera, RendererMaterial, RendererMesh, RendererTexture, RendererPipeline}; use crate::resource_manager::ResourceManager; +use crate::resources::{ + RendererCamera, RendererMaterial, RendererMesh, RendererPipeline, RendererTexture, +}; pub struct ResourceSnapshot { pub buffers: ResourcePool, @@ -24,5 +29,3 @@ impl From for ResourceSnapshot { } } } - - diff --git a/engine/pill_renderer/src/resources/mod.rs b/engine/pill_renderer/src/resources/mod.rs index 3254f541..0d36b0c8 100644 --- a/engine/pill_renderer/src/resources/mod.rs +++ b/engine/pill_renderer/src/resources/mod.rs @@ -10,9 +10,9 @@ pub use renderer_mesh::{RendererMesh, Vertex}; pub use renderer_texture::RendererTexture; -pub use renderer_camera::RendererCamera; pub use renderer_camera::CameraUniform; +pub use renderer_camera::RendererCamera; -pub use renderer_material::RendererMaterial; - +pub use renderer_material::RendererMaterialTextures; +pub use renderer_material::{RendererMaterial, RendererMaterialParamsStd140}; pub use renderer_pipeline::RendererPipeline; diff --git a/engine/pill_renderer/src/resources/renderer_material.rs b/engine/pill_renderer/src/resources/renderer_material.rs index 3f2d1c9d..6b869283 100644 --- a/engine/pill_renderer/src/resources/renderer_material.rs +++ b/engine/pill_renderer/src/resources/renderer_material.rs @@ -1,255 +1,33 @@ -use crate::resource_manager::ResourceManager; - -use pill_core::RendererError; -use pill_engine::internal::{ - get_default_texture_handles, get_renderer_texture_handle_from_material_texture, - MaterialParameterMap, MaterialTextureMap, RendererMaterialHandle, RendererPipelineHandle, - MASTER_SHADER_COLOR_TEXTURE_SLOT, MASTER_SHADER_NORMAL_TEXTURE_SLOT, - MASTER_SHADER_SPECULARITY_PARAMETER_SLOT, MASTER_SHADER_TINT_PARAMETER_SLOT, -}; - -use anyhow::{Error, Result}; -use wgpu::util::DeviceExt; - -// --- Material Uniform --- - -#[repr(C)] -#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -pub(crate) struct MaterialUniform { - pub(crate) tint: [f32; 3], - pub(crate) specularity: f32, +use crate::resources::RendererTexture; + +// COLD: construction-only helper; no allocations, just references bundled for factory use. +pub struct RendererMaterialTextures<'a> { + pub albedo: &'a RendererTexture, + pub normal: &'a RendererTexture, + pub metallic_roughness: &'a RendererTexture, + pub emissive: &'a RendererTexture, } -impl MaterialUniform { - pub fn new() -> Self { - Self { - tint: cgmath::Vector3::::new(0.0, 0.0, 0.0).into(), - specularity: 0.0, - } - } +#[repr(C)] +#[derive(Copy, Clone)] +// HOT(ish): per-material UBO bound during draws; keep compact (<= 64B ideal). +// Layout is 3x vec3+pads → 48 bytes total, 16-byte aligned to match WGSL/std140 rules. +pub struct RendererMaterialParamsStd140 { + pub albedo: [f32; 3], + pub _pad0: f32, + pub metallic: f32, + pub roughness: f32, + pub _pad1: [f32; 2], + pub emissive: [f32; 3], + pub _pad2: f32, } +unsafe impl bytemuck::Zeroable for RendererMaterialParamsStd140 {} +unsafe impl bytemuck::Pod for RendererMaterialParamsStd140 {} -// --- Material --- - +// COLD: GPU handles (bind groups/buffer) created infrequently and reused across many draws. pub struct RendererMaterial { pub name: String, - pub pipeline_handle: RendererPipelineHandle, pub texture_bind_group: wgpu::BindGroup, - pub parameter_bind_group: wgpu::BindGroup, - pub(crate) uniform: MaterialUniform, - buffer: wgpu::Buffer, -} - -impl RendererMaterial { - pub fn new( - device: &wgpu::Device, - queue: &wgpu::Queue, - resources: &ResourceManager, - name: &str, - pipeline_handle: RendererPipelineHandle, - texture_bind_group_layout: &wgpu::BindGroupLayout, - textures: &MaterialTextureMap, - parameter_bind_group_layout: &wgpu::BindGroupLayout, - parameters: &MaterialParameterMap, - ) -> Result { - // Create parameter buffer and write data to it - let mut uniform = MaterialUniform::new(); - let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("material_buffer"), - contents: bytemuck::cast_slice(&[uniform]), - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - }); - uniform.tint = parameters - .get_color(MASTER_SHADER_TINT_PARAMETER_SLOT) - .unwrap() - .into(); - uniform.specularity = parameters - .get_scalar(MASTER_SHADER_SPECULARITY_PARAMETER_SLOT) - .unwrap() - .into(); - queue.write_buffer(&buffer, 0, bytemuck::cast_slice(&[uniform])); - - // Create texture binding group - let texture_bind_group = Self::create_texture_bind_group( - device, - resources, - &texture_bind_group_layout, - &(name.to_owned() + "_textures"), - textures, - )?; - - // Create parameter binding group - let parameter_bind_group = Self::create_parameter_bind_group( - device, - ¶meter_bind_group_layout, - &(name.to_owned() + "_parameters"), - &buffer, - )?; - - let renderer_material = Self { - name: name.to_string(), - pipeline_handle, - texture_bind_group, - parameter_bind_group, - uniform, - buffer, - }; - - Ok(renderer_material) - } - - pub fn update_textures( - device: &wgpu::Device, - queue: &wgpu::Queue, - material_renderer_handle: RendererMaterialHandle, - resources: &mut ResourceManager, - textures: &MaterialTextureMap, - ) -> Result<()> { - // Ensure defaults are available for fallback - let _ = resources.ensure_default_textures(device, queue); - let material = resources - .materials - .get(material_renderer_handle) - .ok_or(Error::new(RendererError::RendererResourceNotFound))?; - let material_name = material.name.clone(); - let texture_bind_group = { - let pipeline = resources - .pipelines - .get(material.pipeline_handle) - .ok_or(Error::new(RendererError::RendererResourceNotFound))?; - RendererMaterial::create_texture_bind_group( - &device, - resources, - &pipeline.material_texture_bind_group_layout, - &(material_name.to_owned() + "_textures"), - textures, - )? - }; - - let material = resources - .materials - .get_mut(material_renderer_handle) - .ok_or(Error::new(RendererError::RendererResourceNotFound))?; - material.texture_bind_group = texture_bind_group; - Ok(()) - } - - pub fn create_texture_bind_group( - device: &wgpu::Device, - resources: &ResourceManager, - texture_bind_group_layout: &wgpu::BindGroupLayout, - name: &str, - textures: &MaterialTextureMap, - ) -> Result { - // Get texture renderer handles, if is it None use default texture for this type of slot - let color_texture = textures.data.get(MASTER_SHADER_COLOR_TEXTURE_SLOT).unwrap(); - let def_color = resources - .default_color_tex - .expect("default color tex must exist"); - let def_normal = resources - .default_normal_tex - .expect("default normal tex must exist"); - let color_renderer_texture_handle = - get_renderer_texture_handle_from_material_texture(color_texture).unwrap_or(def_color); - - let normal_texture = textures - .data - .get(MASTER_SHADER_NORMAL_TEXTURE_SLOT) - .unwrap(); - let normal_renderer_textur_handle = - get_renderer_texture_handle_from_material_texture(normal_texture).unwrap_or(def_normal); - - let color_texture = resources - .textures - .get(color_renderer_texture_handle) - .ok_or(Error::new(RendererError::RendererResourceNotFound))?; - let normal_texture = resources - .textures - .get(normal_renderer_textur_handle) - .ok_or(Error::new(RendererError::RendererResourceNotFound))?; - - // Set texture resources to the bind group - let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - layout: &texture_bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&color_texture.texture_view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&color_texture.sampler), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::TextureView(&normal_texture.texture_view), - }, - wgpu::BindGroupEntry { - binding: 3, - resource: wgpu::BindingResource::Sampler(&normal_texture.sampler), - }, - ], - label: Some(name), - }); - - Ok(bind_group) - } - - pub fn update_parameters( - device: &wgpu::Device, - queue: &wgpu::Queue, - material_renderer_handle: RendererMaterialHandle, - resources: &mut ResourceManager, - parameters: &MaterialParameterMap, - ) -> Result<()> { - let material = resources - .materials - .get_mut(material_renderer_handle) - .ok_or(Error::new(RendererError::RendererResourceNotFound))?; - let pipeline = resources - .pipelines - .get(material.pipeline_handle) - .ok_or(Error::new(RendererError::RendererResourceNotFound))?; - - material.uniform.tint = parameters - .get_color(MASTER_SHADER_TINT_PARAMETER_SLOT) - .unwrap() - .into(); - material.uniform.specularity = parameters - .get_scalar(MASTER_SHADER_SPECULARITY_PARAMETER_SLOT) - .unwrap() - .into(); - queue.write_buffer( - &material.buffer, - 0, - bytemuck::cast_slice(&[material.uniform]), - ); - - material.parameter_bind_group = RendererMaterial::create_parameter_bind_group( - device, - &pipeline.material_parameter_bind_group_layout, - &(material.name.to_owned() + "_parameter"), - &material.buffer, - )?; - - Ok(()) - } - - fn create_parameter_bind_group( - device: &wgpu::Device, - parameter_bind_group_layout: &wgpu::BindGroupLayout, - name: &str, - buffer: &wgpu::Buffer, - ) -> Result { - let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - layout: ¶meter_bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: buffer.as_entire_binding(), - }], - label: Some(name), - }); - - Ok(bind_group) - } + pub param_buffer: wgpu::Buffer, + pub param_bind_group: wgpu::BindGroup, } diff --git a/engine/pill_renderer/src/resources/renderer_pipeline.rs b/engine/pill_renderer/src/resources/renderer_pipeline.rs index ef27e171..39be9c9b 100644 --- a/engine/pill_renderer/src/resources/renderer_pipeline.rs +++ b/engine/pill_renderer/src/resources/renderer_pipeline.rs @@ -60,6 +60,40 @@ impl RendererPipeline { ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, + // MetallicRoughness texture + sampler + wgpu::BindGroupLayoutEntry { + binding: 4, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 5, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + // Emissive texture + sampler + wgpu::BindGroupLayoutEntry { + binding: 6, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 7, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, ], }); @@ -106,8 +140,8 @@ impl RendererPipeline { ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: true, - // mvp(64) + model(64) + tint(vec4=16) = 144 bytes minimum - min_binding_size: Some(std::num::NonZeroU64::new(144).unwrap()), + // Per-draw UBO: MVP only (mat4 = 64 bytes) + min_binding_size: Some(std::num::NonZeroU64::new(64).unwrap()), }, count: None, }], diff --git a/examples/pill_demo/src/game.rs b/examples/pill_demo/src/game.rs index 3393b0a8..d549add7 100644 --- a/examples/pill_demo/src/game.rs +++ b/examples/pill_demo/src/game.rs @@ -55,16 +55,16 @@ impl PillGame for Game { let mut materials = Vec::new(); for j in 0..10 { let mut mat = Material::new(&format!("PillMat{}", j)); - mat.set_texture("color", pill_color_texture_handle.clone())?; - mat.set_texture("normal", pill_normal_texture_handle.clone())?; + mat.set_albedo_texture(pill_color_texture_handle.clone()); + mat.set_normal_texture(pill_normal_texture_handle.clone()); let tint = Color::new( rng.gen_range(0.2..=0.8), rng.gen_range(0.2..=0.8), rng.gen_range(0.2..=0.8), ); let spec = rng.gen_range(0.0..=1.0); - mat.set_color("tint", tint)?; - mat.set_scalar("specularity", spec)?; + mat.set_base_color_factor(tint); + mat.set_metallic_factor(spec); let handle = engine.add_resource::(mat)?; materials.push(handle); } From efdc248175de9d5c8774ea2d1a09a50c42cf8f8d Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Mon, 10 Nov 2025 03:58:22 +0100 Subject: [PATCH 55/69] feat: pbr basic --- engine/Cargo.toml | 2 +- engine/pill_renderer/src/pass_scene.rs | 169 ++++++++-- examples/pill_demo/game.log | 10 + examples/pill_demo/res/config.ini | 6 +- examples/pill_demo/res/textures/pill_logo.png | Bin 0 -> 9434 bytes examples/pill_demo/src/game.rs | 309 +++++++++--------- 6 files changed, 311 insertions(+), 185 deletions(-) create mode 100644 examples/pill_demo/game.log create mode 100644 examples/pill_demo/res/textures/pill_logo.png diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 0332a48f..5a432d79 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -5,7 +5,7 @@ members = [ "pill_engine", "pill_renderer", "pill_standalone", - "/Users/mk/dev/demo/Pill-Demo", ### Game project crate (This will be changed by Pill Launcher on build to allow proper compilation of game project) + "/Users/mk/dev/demo/pill_demo", ### Game project crate (This will be changed by Pill Launcher on build to allow proper compilation of game project) ] exclude = [ diff --git a/engine/pill_renderer/src/pass_scene.rs b/engine/pill_renderer/src/pass_scene.rs index 2873d0bf..71e36318 100644 --- a/engine/pill_renderer/src/pass_scene.rs +++ b/engine/pill_renderer/src/pass_scene.rs @@ -18,8 +18,9 @@ use wgpu::CommandEncoder; #[repr(C)] #[derive(Copy, Clone)] -struct PerDrawStd140 { +pub(crate) struct PerDrawStd140 { mvp: [[f32; 4]; 4], + model: [[f32; 4]; 4], } unsafe impl bytemuck::Zeroable for PerDrawStd140 {} unsafe impl bytemuck::Pod for PerDrawStd140 {} @@ -57,12 +58,13 @@ pub(crate) struct VisiblePreDraw { pub(crate) mesh_handle: RendererMeshHandle, pub(crate) entity_index: u32, pub(crate) mvp: [[f32; 4]; 4], + pub(crate) model: [[f32; 4]; 4], } // M3: Hello Mesh + per-draw MVP (dynamic offsets) pub(crate) struct MeshBatch { pub(crate) mesh_handle: RendererMeshHandle, - pub(crate) instances: Vec<[[f32; 4]; 4]>, + pub(crate) instances: Vec, pub(crate) base_offset_u32: u32, // offset into per-draw ring for first instance } @@ -135,15 +137,33 @@ impl Pass for PassScene { // [SIMILAR] Prebuilt PSO used; avoid hot-path pipeline creation per TALK // Shaders: must match bind group layout indices: 0(camera),1(material textures),2(material params),3(per-draw) let vertex_wgsl = r#" - @group(0) @binding(0) var UCamera: mat4x4; - struct PerDraw { mvp: mat4x4, }; + struct Camera { + position: vec4, + viewProjection: mat4x4, + }; + @group(0) @binding(0) var UCamera: Camera; + struct PerDraw { mvp: mat4x4, model: mat4x4, }; @group(3) @binding(0) var UPerDraw: PerDraw; - struct VSIn { @location(0) pos: vec3, @location(1) uv: vec2, }; - struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; + struct VSIn { + @location(0) pos: vec3, + @location(1) uv: vec2, + @location(2) normal: vec3, + }; + struct VSOut { + @builtin(position) pos: vec4, + @location(0) uv: vec2, + @location(1) worldPos: vec3, + @location(2) worldNormal: vec3, + }; @vertex fn main(input: VSIn) -> VSOut { var out: VSOut; + let worldPos4 = UPerDraw.model * vec4(input.pos, 1.0); + // TODO: Use a proper normal matrix (inverse-transpose of model) if non-uniform scaling is used. + let n = normalize((UPerDraw.model * vec4(input.normal, 0.0)).xyz); out.pos = UPerDraw.mvp * vec4(input.pos, 1.0); out.uv = input.uv; + out.worldPos = worldPos4.xyz; + out.worldNormal = n; return out; } "#; @@ -165,17 +185,110 @@ impl Pass for PassScene { } @group(2) @binding(0) var UMaterial: MaterialParams; // Per-draw - struct PerDraw { mvp: mat4x4, }; + struct PerDraw { mvp: mat4x4, model: mat4x4, }; @group(3) @binding(0) var UPerDraw: PerDraw; - @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { - let bc_tex = textureSample(texBaseColor, smpBaseColor, uv).rgb; + struct Camera { + position: vec4, + viewProjection: mat4x4, + }; + @group(0) @binding(0) var UCamera: Camera; + + const PI: f32 = 3.14159265359; + + // TODO: Move lights to engine-provided uniforms; support directional/spot/area lights. + const LIGHT_POS0: vec3 = vec3(-10.0, 10.0, 10.0); + const LIGHT_POS1: vec3 = vec3( 10.0, 10.0, 10.0); + const LIGHT_POS2: vec3 = vec3(-10.0, -10.0, 10.0); + const LIGHT_POS3: vec3 = vec3( 10.0, -10.0, 10.0); + const LIGHT_COL0: vec3 = vec3(300.0, 300.0, 300.0); + const LIGHT_COL1: vec3 = vec3(300.0, 300.0, 300.0); + const LIGHT_COL2: vec3 = vec3(300.0, 300.0, 300.0); + const LIGHT_COL3: vec3 = vec3(300.0, 300.0, 300.0); + + fn DistributionGGX(N: vec3, H: vec3, roughness: f32) -> f32 { + // Add epsilon to avoid singularities at very low roughness. + let a = max(roughness * roughness, 0.0025); + let a2 = a * a; + let NdotH = max(dot(N, H), 0.0); + let NdotH2 = NdotH * NdotH; + let denom = (NdotH2 * (a2 - 1.0) + 1.0); + return a2 / (PI * denom * denom + 1e-7); + } + + fn GeometrySchlickGGX(NdotV: f32, roughness: f32) -> f32 { + // Heitz's k for direct lighting approximation + let r = roughness + 1.0; + let k = (r * r) / 8.0; + let denom = NdotV * (1.0 - k) + k; + return NdotV / denom; + } + + fn GeometrySmith(N: vec3, V: vec3, L: vec3, roughness: f32) -> f32 { + let NdotV = max(dot(N, V), 0.0); + let NdotL = max(dot(N, L), 0.0); + let ggx2 = GeometrySchlickGGX(NdotV, roughness); + let ggx1 = GeometrySchlickGGX(NdotL, roughness); + return ggx1 * ggx2; + } + + fn fresnelSchlick(cosTheta: f32, F0: vec3) -> vec3 { + return F0 + (vec3(1.0, 1.0, 1.0) - F0) * pow(1.0 - cosTheta, 5.0); + } + + fn accumulateLight( + WorldPos: vec3, N: vec3, V: vec3, F0: vec3, + albedo: vec3, roughness: f32, metallic: f32, + lightPos: vec3, lightColor: vec3 + ) -> vec3 { + let L = normalize(lightPos - WorldPos); + let H = normalize(V + L); + let distance = length(lightPos - WorldPos); + let attenuation = 1.0 / (distance * distance); + let radiance = lightColor * attenuation; + + let NDF = DistributionGGX(N, H, roughness); + let G = GeometrySmith(N, V, L, roughness); + let F = fresnelSchlick(max(dot(H, V), 0.0), F0); + + let kS = F; + var kD = vec3(1.0, 1.0, 1.0) - kS; + kD = kD * (1.0 - metallic); + + let numerator = NDF * G * F; + let denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.0001; + let specular = numerator / vec3(denominator, denominator, denominator); + + let NdotL = max(dot(N, L), 0.0); + return (kD * (albedo / PI) + specular) * radiance * NdotL; + } + + @fragment fn main( + @location(0) uv: vec2, + @location(1) WorldPos: vec3, + @location(2) NormalIn: vec3 + ) -> @location(0) vec4 { + // Surface parameters + var albedo = textureSample(texBaseColor, smpBaseColor, uv).rgb * UMaterial.baseColorFactor; let mr = textureSample(texMetallicRoughness, smpMetallicRoughness, uv).gb; - let roughness = clamp(mr.x * UMaterial.roughnessFactor, 0.0, 1.0); + var roughness = clamp(mr.x * UMaterial.roughnessFactor, 0.0, 1.0); + // Robustness: keep roughness in a sane range to preserve highlight and stability. + roughness = clamp(roughness, 0.045, 0.99); let metallic = clamp(mr.y * UMaterial.metallicFactor, 0.0, 1.0); - let base = bc_tex * UMaterial.baseColorFactor; - let emissive = textureSample(texEmissive, smpEmissive, uv).rgb * UMaterial.emissiveFactor; - // Simple unlit-ish output; lighting model TBD - let color = base + emissive; + // TODO: Support normal mapping (tangent space) and AO texture. + let ao: f32 = 1.0; + let N = normalize(NormalIn); + let V = normalize(UCamera.position.xyz - WorldPos); + + var F0 = vec3(0.04, 0.04, 0.04); + F0 = mix(F0, albedo, vec3(metallic, metallic, metallic)); + + var Lo = vec3(0.0, 0.0, 0.0); + Lo = Lo + accumulateLight(WorldPos, N, V, F0, albedo, roughness, metallic, LIGHT_POS0, LIGHT_COL0); + Lo = Lo + accumulateLight(WorldPos, N, V, F0, albedo, roughness, metallic, LIGHT_POS1, LIGHT_COL1); + Lo = Lo + accumulateLight(WorldPos, N, V, F0, albedo, roughness, metallic, LIGHT_POS2, LIGHT_COL2); + Lo = Lo + accumulateLight(WorldPos, N, V, F0, albedo, roughness, metallic, LIGHT_POS3, LIGHT_COL3); + + var color = Lo; return vec4(color, 1.0); } "#; @@ -278,7 +391,10 @@ impl Pass for PassScene { ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: true, - min_binding_size: Some(std::num::NonZeroU64::new(64).unwrap()), + min_binding_size: Some( + std::num::NonZeroU64::new(std::mem::size_of::() as u64) + .unwrap(), + ), }, count: None, }], @@ -352,7 +468,12 @@ impl Pass for PassScene { resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &per_draw_buffer, offset: 0, - size: Some(std::num::NonZeroU64::new(64).unwrap()), + size: Some( + std::num::NonZeroU64::new( + std::mem::size_of::() as u64 + ) + .unwrap(), + ), }), }], }); @@ -525,6 +646,7 @@ impl Pass for PassScene { mesh_handle, entity_index: render_queue_item.entity_index, mvp, + model: model.to_cols_array_2d(), }); } @@ -557,11 +679,17 @@ impl Pass for PassScene { .iter_mut() .find(|b| b.mesh_handle == v.mesh_handle) { - batch.instances.push(v.mvp); + batch.instances.push(PerDrawStd140 { + mvp: v.mvp, + model: v.model, + }); } else { g.batches.push(MeshBatch { mesh_handle: v.mesh_handle, - instances: vec![v.mvp], + instances: vec![PerDrawStd140 { + mvp: v.mvp, + model: v.model, + }], base_offset_u32: 0, }); } @@ -595,10 +723,9 @@ impl Pass for PassScene { for g in self.groups_buffer.iter_mut() { for b in g.batches.iter_mut() { b.base_offset_u32 = next_offset_u32; - for mvp in &b.instances { - let pd = PerDrawStd140 { mvp: *mvp }; + for pd in &b.instances { self.staging_buffer - .extend_from_slice(bytemuck::bytes_of(&pd)); + .extend_from_slice(bytemuck::bytes_of(pd)); let pad = (self.per_draw_stride as usize) - std::mem::size_of::(); self.staging_buffer.extend(std::iter::repeat(0u8).take(pad)); diff --git a/examples/pill_demo/game.log b/examples/pill_demo/game.log new file mode 100644 index 00000000..42f7f6e8 --- /dev/null +++ b/examples/pill_demo/game.log @@ -0,0 +1,10 @@ +Building game project from /Users/mk/dev/demo/Pill-Demo... + Finished `dev` profile [optimized + debuginfo] target(s) in 0.57s +Game built successfully! +Running game project from /Users/mk/dev/demo/Pill-Demo/build/dev... +[INFO] 2025-10-17T01:19:27 pill_standalone/src/main.rs:439: Initializing Standalone +[INFO] 2025-10-17T01:19:27 pill_renderer/src/renderer.rs:701: Initializing Renderer +[INFO] 2025-10-17T01:19:27 pill_renderer/src/renderer.rs:732: Using GPU: Apple M1 Pro (Metal) +[INFO] 2025-10-17T01:19:27 pill_engine/src/engine.rs:122: Initializing Engine +[INFO] 2025-10-17T01:19:27 pill_engine/src/engine.rs:94: Starting Game +[INFO] 2025-10-17T01:19:46 pill_renderer/src/renderer.rs:1493: Resizing Renderer resources diff --git a/examples/pill_demo/res/config.ini b/examples/pill_demo/res/config.ini index 02a80df0..aad642f1 100644 --- a/examples/pill_demo/res/config.ini +++ b/examples/pill_demo/res/config.ini @@ -8,14 +8,14 @@ WINDOW_HEIGHT=1200 WINDOW_FULLSCREEN=false # ENGINE GENERAL -MAX_ENTITIES=10000000 -MAX_CAMERAS=1 +MAX_ENTITIES=100000 +MAX_CAMERAS=10 MAX_CONCURRENT_2D_SOUNDS=10 MAX_CONCURRENT_3D_SOUNDS=10 # ENGINE RESOURCES MAX_TEXTURES=10 -MAX_MATERIALS=32 +MAX_MATERIALS=20000 MAX_MESHES=10 MAX_SOUNDS=10 diff --git a/examples/pill_demo/res/textures/pill_logo.png b/examples/pill_demo/res/textures/pill_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d4e5bc353398f8fc570b1d93cb403cc45ad579e2 GIT binary patch literal 9434 zcmd5?c|26@+dngwu~V`WrAUY#g|f_~lwB!=7)2>dwva3{)?_aal_ey3D!T@S5VB=U zjO_c8ofyl^-0v~-{CX2Kd64MSZk)=V@|D3-PO24hP?`y6ISWLJ&Le-hqIUQw8A;a+eLRX~Pcz z0Q585C@Cp{&-4$S7!?&27#K*W#K*_OjiRC=`0VTJODDqT&d$#K{Cv3aUp{4JWpKN$ zt_~(PH#b*RRq^ofynp|mot>RNN>o&ofq@}0F%iB+p-_^NlFG`;^ha4)S*fY1aNF70 z`NWA6o}Qj?J1i`0cz9SqKmcw_OH0G&Teoh(=jrL`%avui-Q9iT#tjmQ1XGrlmf$n}V{UE^H^^l2e+d8kDk35RD~v!OgoK3n`1lxOj>EV? z_76;quEMDCC<2VTZ6p$a#SB0Kzd)fljMMjkcS*npMA9@7V1X}_@Nwx10P^tl9( zHC4&;fX4m~UZPt;Qr^mZvxx)cfjH;#65y zL5b-XJqf3)t#NTc6RhnLDmyuwbqTVf)egn2 z=B)NEzqhyZsL$QoK+*`4R^ z!jeC$g*8%-g|~M65av4a<80Bha?Ag+%5IqR>@KN*onE1)Bo9W1)^VYM>m_4bL&swV z?T-t^Uz`6WQq{62xMh2LH*-T^xzt>Lqt&lJR;A3mVY@9V4F$eFtznrJ)9=k&yMC;d z8g;WXdD$plpDv?id+U%QX?)nR%Gzo8@0}CD4nbMfi~(ND_TzzxO%`$A=OmT$7>YpT z?dJvioBFSSb}gLPaNUxBXc?wzW~`1DHb@P|6` zh@FxB1?X`vpBT+H$L*O4(!6*#Rau2{nglkaXNV+4(g*wDDD0)_&g7^PUkmT;<3YKG zGUzx_N#hP_r=Fd(6bM`X#ikntwLBR;ia3#H_nYP*z3t!qTRRf&CElMV@K|;B;2crP z@pMFHchC(>&olvPd%B@(ITK?qPbYa-Re4JzRh3ccs}FMh*X)qMd+Xz?;|kcL@dTOr zH0pq=gLB_GAN`t3y=>}Jzlo;=`IW=!-+5uYX!;z=2g|SdsaY?Rz4zlcgfL|q(4y+T zQ(U7z&*pxng#s55q^KMEeVIK1O{n9)UBx4UHxt$z7f(Hxub_3{Ac6sZL%)i#?+l7A ztin?Z#E(3yCah(>IgT`YTHGk;rh!rw{g0{3a$eWRC~F^iR(NXi2z3TY3u~rGi4;%*V`H0oE8g{{}sXFPL-al@l_dhTC?Pc*xLEK$(yNNc4kH=O0M4be@is8A3dM`KIfG`*~vgCG?OwAmKq?E~-7nqO*IzEl}!Z8MCKT z+dKI^fV1N`K!T^`^y5&|W*aD8sn<$+i}0TQDE0v0{b8(cxMhN|aXb3MHpu-j9y`yQ zy*FP=Kkh?QeO}&vfjVIGI=N2N@v%XK*n!=N^(Av#zT2e z|HJm(;d|7MiLWQroA(9%;>1s~N?@EO7qVzBuu9h1L<_x^W;pUtLA4GMk2UJsLVmpX zrTaL|$4Q4x(t%6~YZ=2GRY*s3tN|eiEns?+4z5v_7npPuIbTdc5}zm++Tsv3T=p*S zo(2P5jj_N{0~91=^szLWM!okm91Pv7*|Nhsqo6ao9(p{?kQ$6(=)yMod9A$yqrEz$ zbLJekEmX*0VRiP+B+etzSxjOcf!Qy|L?<49GlkRQ=UrrK@rNeImlbLuTU2rBDIEv0 z@h78Gtnu5BghNpdLJ{f<%9Up_`GbCG{83187r_I{EjS==38w_(WU9|FYU`lSeh35A zQ(M38aZsLnqe3nFlxM-Z#1_T_@t#D{?UPD*KrwF0r?`M`xuT&^gc?f?t$XvEMVv>f zJwML{sNTPeg8?tr)Ia&`_f~qb18!vfMNh~+Xw?i!)57rh-0sLH35liCL5fI;;a?C9 zI4NExs`P+vFCvJsMk`gej^6d~yEx#&%*g?n7+sQOhA=K`m&b7q(w7H`K^cq~&2O=` zbQ{8jL5dJ$FTs}wS5`u^R>RhaR2O@#EfW7gQ;dbC&5_I&@TQ~w45VfAHj|G5%1}OY zY#l9h(oLxI?S!}>4S3yiX~1xh<-*(+3!dhnzb>Bwm(HAWl`TA<69&;`;q^cQ=Y$Qj z$IHJ9Xnadgai{NhZuZv&j*I594W?T z3C;|7K8U?bf$7$sYg23w?2h;ft#p(VnE0T{m(DaZo@mdJ)7zd=HqaAh0%p*KWz;ts z=geayP@AfO65RE`Tu1O|a7NH{J$z*e=fEaG-KZ%<;+=JSBN5Q$6wI|a@Wf9+XD|S- zb#}7>bJ*rps{9J>9*e^>!Yv5;DaFM}#@$;+M&OiQyp9d18c{m)9kjVT%)TsZpMdF= z;@~7NM?=F61tni&hr|=&5gKd+EoHcLy}pd5y1G4eX3QwJORpLQJ9mr@IEFdH^@g!) z0hpv!Hwb1t53x!gCt7+nE>%w!)SO@2mlHxaS1_3}Vwy7UdXjnbSsr4xv>(>%$73 z&&#r>EL&Z|iC1fVtNO0R2eN#U=x0`m%@n+{@B?R8m{IdkJ~3#Y#S@!r&mjk#F)U~(fcb;@$>5=XwN;EOoO{OT?@k9I*U--$X8+;NJNgH~;KfB_ufGTFlrvfVqWb;mt_r$;IazCkKbY*bz>6qg z3O&IqO~4u^8GcvU^hk@+_!kQ6cl7K<@a1`MxXE zozP(}`m}SdU#Y&$xvFC2FgOG3t?(Kj=dUAj7(&I^!)baNoaz8!TO<9WuBEZ%R1jtO z*jsF%4Vzggi=<7mn6QV`vfL9??|CR4Y|QrsP}vgs?)Bu$Ga)TR7~CleU|&Xh2N3p_ z10sl<0--l)`PBo)(P@lYroyb#uz_2sz=&sJ8`skJ3zzcJQ^@}A-(aXMd4|-`c5iv2 z{VnBD{$Ln%A2{_Dk^O?C4&3W0kZ(X(SeaN-(4^5rqj&>faoNd1YMcu-i9O7d?3#sb zeC1(&%vn}~mTr!|@g<{OEeL0SZQj7L6=(dmuT8DYs%6~5*2M<2SZAgl&f6hmtHL$x znVRMb}lV{VwcvoGRGIjDHCCxgY|3w0@{dER(B4!o^yxGb<10 z372Lo!jItTgiZo^LNd#Ys~zCVsfFuQQV^_ep?nyuF7i5A!GTAj37m!TxCm)mo_5y1 ziRtA}zXQ0x{i%*-uhZVfHpWhIHiFT zeg}}k1}G=HZxN9VdqW$}K%8*-krIWyVolv-=b6QcsU?9MnE%R(>sGlymbsy=qXt$daI1$R((&rUg<=hzcctXSLUm&2D>rhI5l~Y-*e~qczY0ODg ziH9*i8-@-@{iJ#IB}Nb$hb405QOmGvLaTwl6+)P5*GKQy92Vk6j@laiG{Ilr^IJ!` z?8gk94}mT+TuuGAI@PH;YCt7;hT1TH2FFsUG7*qEJg}WR}7Lkbz zW33pNeV&pFxg!|Q$g_y!+%spia5spCPo88jS;6*vyxX3L)xWc0tcSgpo*m=~!rLKIt?HNed;;GqUzllUV zL3=$%2Uzv49`$5sS*Z>blO1);xUX$VaMBNgU8P$Bc6}+X78SW=%u(X7mc2z)WLIQ7 zpSGo!lm1hcQDa33oQx-ROcX1B80#$L{lGKNprE?tn z`4eu4%5{KbFFB9~aoWJ?)7&bOskl^8^K8J%jCcupeRfPzp>N+4>ml*Yy(3()vwK#S;iKyo8}cc{_i@p|r1X_#QU?2AKa@G(ZT1B`xRN_{uyv==RvLnc>IMOuJ#P}E-vnOpqYDZ z&krxi2LIaVw5z+li%p4LOq(o-sC)5O-sg|m91>1Wtv&-j?G|5Bu1xdQw+~;dn1SY~ z#B4k4lb4{x_k+O)DHjz4@#$Ek0nBHElvkZmy=DQxtWbZnq<=U7sB5o)3;Y=`5k4nXS z5SA0%WO72KyqR@j`XV)9^GbAtSfy_9xiVYmq+j{H$FaML@xg#F+h@)|PFy*2CCBt} z>EBGSac)Uaq&Hz#Tm&Fb{JzDACRVA8Qd#G>`OU3;gHx3PnN#NkTU1?`7wdKT9 zqD-rRz2QDfab9HEnuL;*<(B^(2=PJrrw-J2uU3!8E^6>Og9vL#5K_nVZVEo)sEO8) z^g-`7r3aV)6D?5j`ECANZ!2n$r?agd)x5uG*A5vA?q%lE*ISz}TuVut3wq}Ax}xTK z=dC%zk2g#^K#kyCG;q1*wU98irU=Z@(=WC1;ti1&19r14cq2qhKf~nScG28WLHO+g zZl9Q1Z$d=gQc*Ln%(V2=C^&@obYc0-YTIZNRq<>Id^?sQ+vgDf8E8MqH8RknEfohO zsl`J+4nzyG3=|6*8Gw(MX>eoo+f|$}^)Wv*HUKr;ZGfPE7WX|6;IU@7o0h>l1!R7M zub1_qH)`Ng5Fd1t7|hnax9qf%Mb93nIkS2Z^5m|xV%OB{Yg9`cwSyO)?*DXd#f|@b zy(`u}7V>UK{5o`D0l7bGZyjA~Hmh7?QPC2xnKSfgXrKk{=QX;>_UhH=4R59nQw7S0 zuP6nIb*uCk;Snh8*qQG2VrK~}V*tsP^i4`LPnONY0VZvzsk>j~yc@lyg zou;q@_{ZLfbDM)sTIx6~V+k^<_T)LG&;uGDMiM-i?b2>M64$Ra8$G9`XSy?4{pdbU z^mwB8+_Q-jly4s?+_KQuI3!Q@zZ$yOO2Vv8WV|H!c2p6*oCY@o?R!QfKbmjMuyL_d z@zmF&%V6lAon0JJp@Y4B5S#hXP4n8ZE+PEOZLg!%pav=7!3M8B;c0HUeaouz6~iCg z_Xcc{c=40agjMswZ zQS%D0pZ~*&=K(oq!6gJQl$He5(;_O5Z{W!TmQltU=j=(bUni|aN#0CEvyLkN7b3a4 zAz@EHqumS5Nz(hc3nH&-H-hZeN5uF=VD02n>10FzwTze?4Ji2I-fpJ1<>}(1T@$y) zF~3^p(JZxC}N!$Mq5|r z^&u2p#93y^`Nlqcm6U3w58AF^dC=DF#fVO`xZHv%`EgKP%PW%m%C`|<^*N85@3{8v zxm%^>FUPk>4neH=rVtFHBh6)ee&RJZCZ6}>P!Z2JP`fzxu=n%0R+DuqRtetql8zju zZSe?N1x&f894ik_&xTT~k855BB5dRWDoI=q`t#;LF&Bi1x#{QDDo_B#<-kBAq_T}2 z+ZtiM097tmG9J^R`=RrEE$T^zQmqPM6CD9H#k_q>k66v_k;RKKjkmX|ThlHsWW~Jh z2NKAP34Lgsju=~&K+_kYtvhGG_mZjNZ(Lq`lU*4kpj%uJJ$okNoQ;1()s~ZxaUXYZ zI!b_n1z*kpZ-oy^IQcPZ@YHDTdxQSo$4bE->%3i)>ApzjmVlDFf9|D$0m)gpjH*jz z_P7`MW|SC=ll5@^sO8Ad*URfDks!U=iy7~FOu7aro~DHq>?1Wds;)TrDJ?r^k1v9>SHh%C(EGl&6|)W>6fYcD?yM>UWtN}b39wC=su`X5 zwtISq(%}!uT^M1MxIx1;ZhI-FTi?ut>=e4A-w<34%33$8)#5vE0xYJKK_Y!Nj*Yez zJb2y}MQDWVSBn}ZiUN8}S>6JX4)6-3ZQ!iQ>8ntLI}83g&E*>5He7dNRE}$OEP&|^ z^a%LVekFjMz6eEpVY?hon~VLJM#wKl4_my*ZaF3Z(=^Z%E{DRL382SKP(H)= zzDprWE=+gTfeN6OZB+l>M(c$$Nc0Lp!L+S?Z>;1%=B63G_2uTlxYr75)DAbuK`>E1%j-UCrIPH%5ErGi7|h z^0{mb1?!oQ!e-}Xz{?I{$S#r|di*o{M*H4<_E8lU}7amaLrT=1Lo zlfr@25g_AX*Vg^)R5I{svWF%o#1K;FXntADObN<8Q88^k>`H0x2`U3VZy(Grf;>}K zN-H$YeSE32`cWWOf#X*-*yKYD7*dIL)VOC2P?N@0y7@c{bwOzpwIK;X(+6`|)%y+= zu8-oX4wAfqAzyg@bXr$kYWO+;8GhRP#lNj01vYjyx4jsoD z0|z;%PaH7a+sicaqMyY(EkBO1>J&@5Jqqx8Up}4B@x4=;MZOUzTd?786qn&{)AFo5 z(m~jkXu~F9P2j06%T%5hJN)sXcEkK{Ajeb_btDJq&=oxJ2XE^XqR9~LT5k-+sU?dI zuU7|BJB*-7)jOb`E|ksZuv?d=MEv03;>PZ!Kx@*rA2Jq2iGlP(s6tv5F>&o{={~Lh z+@HXu1BlBK2mj= znb%`#Cs694Vfm)5N6oHkg|!cr5?pb6tHe=ku^9tZju7c@64SQ+w2O_zsc9Ye6C*Ns z@1{1RJtnw7Ucr0Q(2M|xgWY#WfS!S4$y|p=Db^|Mi0_A6_Nv0h0lS5QhRHS0!#{Fy$n9LzjTR{E-VkC`fEav(jQE zs*Wg0Y!%SPcAmSrd*7~CoXcR@Cbz(JiGsT^@+XO-`dxkiqc93{-{AXTx(*G!hVcXy zj=~e~xlOHKnZNxU9v+#!8X`q7pC48Z%EFlvHP~DjY%{H93#ujP{U=LEJhqGX(=X}6 zQR~W1_(IP5JpS~qFytVMY@{M-X@dW`G1lL_!wkS5gaor(ksP6LSd5Tc))Kq#ua?HpmCL1#DGO;lL^`W^RYcl%P$MpPU#A|9Qh zp13utaE+#fGkI%kdw`VGRnt|V&;fR_J*V|Ue?U!!m7;0^k~eFOlWBRjZ+mrhhKQ~X z*j^i}d#o_>3P)W=R}skrhcfI&s&&4#1_d=2Exnpj`H{ZeKwF8m-8J}qo%RAtNjTvw zKMkAcZ*O87XfI~7PQJ6F?f?Iuc2nYkG&)Si=DJm~nvLv3bKCAJLiP80Mk6*Z$Qe`~ zBh{<_EmW+WRSWUtoOY6OZ>pMovKqo_k)gMC?j^*J={*1Mh=C*aN?9nH z{3|seA+#g${J&}<6`LcU(1(~oQbI~s=$+VVj$|s`jwc9lGUHYAAOnv(ujwNB@VeoK zKj~^A6?NM$(6y3<65W`j2jjOnGC!}G7Y$?fr!ECp% zs-5F4RxUaD&ib~~lmhll=DqySw~$TW6-?i+l*uyV%hwWlcOtTAwCkr$*9I4p)1dgg z#A^Sk&}8_B>#Sz?GAC~%%Tc1RMqEAii-#7x(@W{%@Pw&I+jGTcRG5Jy{GvVjPH=^F zNl9f(!RfQmR;zit2rogXjn%be+Eg~8!*8iNb5{Q?OgEZ6g0gqHs8lnME7X5;`5r7X%bhwxLsng7`F*#0 zF+~%HCI)UF=F@_8yTPt5q;`8sCaza-UhKxy2Wbyo0oXadwZrd)`k5wcR+p~kdf6}e zeE566hQVGOZ@zr~a9zj`8r>0!^7>@|7AK{a>j7J-Y?Yg5X=c4X8C~S45f#YyqQalE z@2~4AzZBb-4eZ@+5uUr(@RDvmCirs-F8xf4C%iONxQgU(SbG~rbiQO&|Hb-Mkw53G zdNVwq#uO_T&pg!yD%41!_R^;G7I`dc+#mrCSXI}ArHvB~q)bQIx!f7IVwuVMsd>zq zVNuwDce*PQCr6112I+;W$#)e~6r=;jWGuGN#z$3Uixex@`3~`}%SfQ(w|SG&zXc|> z85gG>6{z+XYf4N=UF5PznOlhS@>f$Ot)0?$g&wcHSGzbkW&G%R!?)vDcB#^5mT>4z zZ2S2xXg9eW`c%^VN(CQ&HA{nGAL;dEM~kfg6#?rn{Bo6px76fd1SjK$yUKE`?53I# zDb3AmeZ4IM0X%4O9+~pzf%BUEE!7!=`%|9m?7Rw6tyZmBx1Qx^`x`dRgo_-nc)ra6 lzlHxl|AU172b4-OdNa`e%g9|h;k|#|=xQ5X$k(zB{vWIbV>kc+ literal 0 HcmV?d00001 diff --git a/examples/pill_demo/src/game.rs b/examples/pill_demo/src/game.rs index d549add7..c6aee56f 100644 --- a/examples/pill_demo/src/game.rs +++ b/examples/pill_demo/src/game.rs @@ -18,176 +18,163 @@ pub struct Game {} impl PillGame for Game { fn start(&self, engine: &mut Engine) -> Result<()> { - // Create scene - let active_scene = engine.create_scene("Default")?; - engine.set_active_scene(active_scene)?; - - // Register components - engine.register_component::(active_scene)?; - engine.register_component::(active_scene)?; - engine.register_component::(active_scene)?; - engine.register_component::(active_scene)?; - engine.register_component::(active_scene)?; - engine.register_component::(active_scene)?; - - // Add systems - engine.add_system("PillRotation", pill_rotation_system)?; - - // Add meshes - let pill_mesh = Mesh::new("Pill", "models/pill.obj".into()); - let pill_mesh_handle = engine.add_resource(pill_mesh)?; - // Add textures - let pill_color_texture = Texture::new( - "PillColor", - TextureType::Color, - ResourceLoadType::Path("textures/pill_color.png".into()), - ); - let pill_color_texture_handle = engine.add_resource::(pill_color_texture)?; - let pill_normal_texture = Texture::new( - "PillNormal", - TextureType::Normal, - ResourceLoadType::Path("textures/pill_normal.png".into()), + let _scene_benchmark = create_scene_benchmark(engine, "Benchmark 60k pills")?; + let scene_pbr_grid: SceneHandle = create_scene_pbr_grid(engine, "PBR Grid")?; + engine.set_active_scene(scene_pbr_grid)?; + Ok(()) + } +} + +fn create_scene_benchmark(engine: &mut Engine, name: &str) -> Result { + let scene = engine.create_scene(name)?; + + // Register components + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + + // Add systems + engine.add_system("PillRotation", pill_rotation_system)?; + + // Add meshes + let pill_mesh_handle = get_or_add_mesh(engine, "Pill", "models/pill.obj")?; + // Add textures + let pill_color_texture = Texture::new( + "PillColor", + TextureType::Gamma, + ResourceLoadType::Path("textures/pill_color.png".into()), + ); + let pill_color_texture_handle = engine.add_resource::(pill_color_texture)?; + let pill_normal_texture = Texture::new( + "PillNormal", + TextureType::Linear, + ResourceLoadType::Path("textures/pill_normal.png".into()), + ); + let pill_normal_texture_handle = engine.add_resource::(pill_normal_texture)?; + + // Material properties showcase: create a small set of materials to reuse + let mut rng = rand::thread_rng(); + let mut materials = Vec::new(); + for j in 0..10 { + let mut mat: PBRMaterial = PBRMaterial::new(&format!("PillMat{}", j)); + mat.set_albedo_texture(pill_color_texture_handle.clone()); + mat.set_normal_texture(pill_normal_texture_handle.clone()); + let tint = Color::new( + rng.gen_range(0.2..=0.8), + rng.gen_range(0.2..=0.8), + rng.gen_range(0.2..=0.8), ); - let pill_normal_texture_handle = engine.add_resource::(pill_normal_texture)?; - - // Material properties showcase: create a small set of materials to reuse - let mut rng = rand::thread_rng(); - let mut materials = Vec::new(); - for j in 0..10 { - let mut mat = Material::new(&format!("PillMat{}", j)); - mat.set_albedo_texture(pill_color_texture_handle.clone()); - mat.set_normal_texture(pill_normal_texture_handle.clone()); - let tint = Color::new( - rng.gen_range(0.2..=0.8), - rng.gen_range(0.2..=0.8), - rng.gen_range(0.2..=0.8), - ); - let spec = rng.gen_range(0.0..=1.0); - mat.set_base_color_factor(tint); - mat.set_metallic_factor(spec); - let handle = engine.add_resource::(mat)?; - materials.push(handle); - } + let metallic = rng.gen_range(0.0..=1.0); + let roughness = rng.gen_range(0.2..=1.0); + mat.set_base_color_factor(tint); + mat.set_metallic_factor(metallic); + mat.set_roughness_factor(roughness); + let handle = engine.add_resource::(mat)?; + materials.push(handle); + } - // Create camera entity - let camera = engine.create_entity(active_scene)?; + // Create camera entity + let camera = engine.create_entity(scene)?; + let transform_component = TransformComponent::builder() + .position(Vector3f::new(0.0, 0.0, -16.0)) + .rotation(Vector3f::new(0.0, 0.0, -20.0)) + .build(); + engine.add_component_to_entity(scene, camera, transform_component)?; + let camera_component = CameraComponent::builder().enabled(true).build(); + engine.add_component_to_entity(scene, camera, camera_component)?; + + // Create pill entity + for i in 0..60000 { + let pill = engine.create_entity(scene)?; + let posx = rng.gen_range(-10.0..=20.0); + let posy = rng.gen_range(-10.0..=10.0); + let posz = rng.gen_range(-1.0..=1.0); + let rotx = rng.gen_range(-180.0..=180.0); let transform_component = TransformComponent::builder() - .position(Vector3f::new(0.0, 0.0, -16.0)) - .rotation(Vector3f::new(0.0, 0.0, -20.0)) + .rotation(Vector3f::new(rotx, 0.0, 0.0)) + .position(Vector3f::new(posx, posy, posz)) .build(); - engine.add_component_to_entity(active_scene, camera, transform_component)?; - let camera_component = CameraComponent::builder().enabled(true).build(); - engine.add_component_to_entity(active_scene, camera, camera_component)?; - - // Create pill entity - for i in 0..60000 { - let pill = engine.create_entity(active_scene)?; - let posx = rng.gen_range(-10.0..=20.0); - let posy = rng.gen_range(-10.0..=10.0); - let posz = rng.gen_range(-1.0..=1.0); - let rotx = rng.gen_range(-180.0..=180.0); - let transform_component = TransformComponent::builder() - .rotation(Vector3f::new(rotx, 0.0, 0.0)) - .position(Vector3f::new(posx, posy, posz)) + engine.add_component_to_entity(scene, pill, transform_component)?; + let mesh_rendering_component = MeshRenderingComponent::builder() + .mesh(&pill_mesh_handle) + .material(&materials[i as usize % materials.len()]) + .build(); + engine.add_component_to_entity(scene, pill, mesh_rendering_component)?; + // engine.add_component_to_entity(scene, pill, PillComponent {})?; + } + + Ok(scene) +} + +fn create_scene_pbr_grid(engine: &mut Engine, name: &str) -> Result { + let scene = engine.create_scene(name)?; + + // Register components + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + engine.register_component::(scene)?; + + // Camera + let camera = engine.create_entity(scene)?; + let camera_transform = TransformComponent::builder() + .position(Vector3f::new(0.0, 0.0, -16.0)) + .rotation(Vector3f::new(0.0, 0.0, -20.0)) + .build(); + engine.add_component_to_entity(scene, camera, camera_transform)?; + let camera_component = CameraComponent::builder().enabled(true).build(); + engine.add_component_to_entity(scene, camera, camera_component)?; + + // Mesh (use pill as sphere substitute) + let pill_mesh_handle = get_or_add_mesh(engine, "Pill", "models/pill.obj")?; + + // Create 5x5 grid where: + // - X axis: roughness [0.0 .. 1.0] + // - Y axis: metallic [0.0 .. 1.0] + // - Base color: (0.35, 0.35, 0.35) + let grid_size = 5; + let spacing = 2.5f32; + for y in 0..grid_size { + for x in 0..grid_size { + let roughness = (x as f32) / ((grid_size - 1) as f32); + let metallic = (y as f32) / ((grid_size - 1) as f32); + + let mut mat: PBRMaterial = PBRMaterial::new(&format!("PBR_{}_{}", x, y)); + mat.set_base_color_factor(Color::new(0.35, 0.35, 0.35)); + mat.set_metallic_factor(metallic); + mat.set_roughness_factor(roughness); + let mat_handle = engine.add_resource::(mat)?; + + let entity = engine.create_entity(scene)?; + let pos_x = (x as f32 - ((grid_size as f32 - 1.0) * 0.5)) * spacing; + let pos_y = (y as f32 - ((grid_size as f32 - 1.0) * 0.5)) * spacing; + let transform = TransformComponent::builder() + .position(Vector3f::new(pos_x, pos_y, 0.0)) + .rotation(Vector3f::new(45.0, 45.0, 45.0)) .build(); - engine.add_component_to_entity(active_scene, pill, transform_component)?; - let mesh_rendering_component = MeshRenderingComponent::builder() + engine.add_component_to_entity(scene, entity, transform)?; + + let mesh_render = MeshRenderingComponent::builder() .mesh(&pill_mesh_handle) - .material(&materials[i as usize % materials.len()]) + .material(&mat_handle) .build(); - engine.add_component_to_entity(active_scene, pill, mesh_rendering_component)?; - engine.add_component_to_entity(active_scene, pill, PillComponent {})?; + engine.add_component_to_entity(scene, entity, mesh_render)?; } + } - /* --- Overlay Logo Pass (userland draft using pill_renderer vNext API) --- - { - // Acquire renderer and its resource manager - let renderer = engine.renderer_mut()?; // Draft API accessor - let mut rm = renderer.resources(); - - // Create logo texture + sampler → material bind group (set 1) - let logo_tex = - rm.create_texture(pr::TextureDesc::from_path("textures/pill_logo.png").srgb(true)); - let logo_smp = rm.create_sampler(pr::SamplerDesc::linear_clamp()); - - // Overlay pipeline (fixed-function screen space quad shader) - let overlay_pso = rm.create_pipeline(pr::GraphicsPipelineDesc::overlay_logo( - renderer.surface_format(), - )); - - // Material BG uses @group(1): texture_2d + sampler - let overlay_material_bg = rm.create_bind_group(pr::BindGroupDesc { - debug_name: Some("overlay_logo_material"), - layout: overlay_pso.material_layout(), - textures: vec![logo_tex], - samplers: vec![logo_smp], - ..Default::default() - }); - - // Per-pass globals (@group(0)) with screen-space rect in pixels and tint - #[repr(C)] - #[derive(Copy, Clone)] - struct OverlayGlobals { - rect_px: [f32; 4], - tint: [f32; 4], - } - let globals = OverlayGlobals { - rect_px: [16.0, 16.0, 160.0, 64.0], - tint: [1.0, 1.0, 1.0, 1.0], - }; - let globals_bg = { - // 256B-aligned uniform buffer with initial data - let globals_buf = rm.create_buffer(pr::BufferDesc::uniform_init_aligned( - "overlay_logo_globals", - pr::bytes_of(&globals), - )); - rm.create_bind_group(pr::BindGroupDesc { - debug_name: Some("overlay_logo_globals"), - layout: overlay_pso.globals_layout(), - buffers: vec![pr::BufferBinding { - buffer: globals_buf, - byte_offset: 0, - }], - ..Default::default() - }) - }; - - // Tiny quad mesh (two triangles in NDC via VS or a unit quad mesh) - let quad_mesh = rm.upload_mesh(pr::CpuMesh::unit_quad()); - - // Add pass to master pipeline that draws the logo over the swapchain - let pass = pr::RenderPassDesc { - name: "overlay_logo", - target: pr::TargetDesc::Swapchain, - clear: pr::ClearDesc { - color: None, - depth: None, - stencil: None, - }, - subpipeline: pr::Subpipeline { - pipeline: overlay_pso, - globals: globals_bg, - draws: pr::DrawRecipe::Inline(Box::new( - move |dlb: &mut pr::DrawListBuilder| { - dlb.set_pipeline(overlay_pso); - dlb.set_bind_groups_with_offsets( - globals_bg, - Some(overlay_material_bg), - None, - None, - None, - None, - ); - dlb.set_mesh(quad_mesh, 0, 0); - dlb.draw_indexed(6, 0); - }, - )), - }, - }; - renderer.master().add_pass(pass); - } */ + Ok(scene) +} - Ok(()) +fn get_or_add_mesh(engine: &mut Engine, name: &str, path: &str) -> Result { + match engine.get_resource_handle::(name) { + Ok(handle) => Ok(handle), + Err(_) => { + let mesh = Mesh::new(name, path.into()); + engine.add_resource(mesh) + } } } @@ -197,6 +184,8 @@ fn pill_rotation_system(engine: &mut Engine) -> Result<()> { // Exit on Escape if input_component.get_key_pressed(KeyboardKey::Escape) { + // TODO: engine.request_shutdown(); + // TODO: standalone handle request_shutdown std::process::exit(0); } From 394922df4c2f562a24b4f3fc291ff3e55849b380 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Mon, 10 Nov 2025 03:58:51 +0100 Subject: [PATCH 56/69] feat: gltf basic (.glb) --- engine/pill_engine/Cargo.toml | 1 + engine/pill_engine/src/resources/mesh.rs | 193 +++++++++++++++++++++-- 2 files changed, 182 insertions(+), 12 deletions(-) diff --git a/engine/pill_engine/Cargo.toml b/engine/pill_engine/Cargo.toml index ea8ea801..8f62b31a 100644 --- a/engine/pill_engine/Cargo.toml +++ b/engine/pill_engine/Cargo.toml @@ -20,6 +20,7 @@ log = "0.4" # Loaders image = "0.23" tobj = "3.0" +gltf = { version = "1.4", features = ["import", "names"] } # Audio rodio = { version = "0.20", features = ["wav", "mp3"] } diff --git a/engine/pill_engine/src/resources/mesh.rs b/engine/pill_engine/src/resources/mesh.rs index aa043c1f..95f0ff68 100644 --- a/engine/pill_engine/src/resources/mesh.rs +++ b/engine/pill_engine/src/resources/mesh.rs @@ -13,6 +13,7 @@ use pill_core::{ use anyhow::{Context, Error, Result}; use boolinator::Boolinator; use cgmath::InnerSpace; +use gltf::import as gltf_import; use std::path::{Path, PathBuf}; use tobj::LoadOptions; @@ -73,19 +74,50 @@ impl Resource for Mesh { get_type_name::().sobj_style() ); - // Check if path to asset is correct + // Resolve absolute path let resource_file_path = engine.game_resources_directory_path.join(&self.path); - pill_core::validate_asset_path(&resource_file_path, &["obj"]) - .context(error_message.clone())?; - - // Create mesh data - let mesh_data = MeshData::new(&resource_file_path, self.flip_uv_y) - .context(error_message.clone()) - .context(format!( - "Failed to create mesh data from {} file", - resource_file_path.file_name().unwrap().to_string_lossy() - ))?; - self.mesh_data = Some(mesh_data); + let ext = resource_file_path + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("") + .to_ascii_lowercase(); + + // Route by extension + match ext.as_str() { + "obj" => { + // Validate and load OBJ + pill_core::validate_asset_path(&resource_file_path, &["obj"]) + .context(error_message.clone())?; + let mesh_data = MeshData::new(&resource_file_path, self.flip_uv_y) + .context(error_message.clone()) + .context(format!( + "Failed to create mesh data from {} file", + resource_file_path.file_name().unwrap().to_string_lossy() + ))?; + self.mesh_data = Some(mesh_data); + } + "gltf" | "glb" => { + // Validate and load glTF/GLB + pill_core::validate_asset_path(&resource_file_path, &["gltf", "glb"]) + .context(error_message.clone())?; + let mesh_data = load_meshdata_from_gltf(&resource_file_path) + .context(error_message.clone()) + .context(format!( + "Failed to create mesh data from {} file", + resource_file_path.file_name().unwrap().to_string_lossy() + ))?; + self.mesh_data = Some(mesh_data); + } + _ => { + return Err(Error::new(EngineError::InvalidModelFile( + resource_file_path + .clone() + .into_os_string() + .into_string() + .unwrap_or_else(|_| "unknown".to_string()), + ))); + } + } // Create new renderer mesh resource let renderer_resource_handle = engine @@ -277,3 +309,140 @@ impl MeshData { Ok(mesh_data) } } + +fn load_meshdata_from_gltf(path: &PathBuf) -> Result { + // Import glTF file (supports .gltf and .glb) + let (doc, buffers, _images) = gltf_import(path)?; + + // Take the first mesh + let mesh = doc + .meshes() + .next() + .ok_or_else(|| Error::msg("glTF contains no meshes"))?; + + // Accumulate primitives into a single vertex/index stream + let mut positions: Vec<[f32; 3]> = Vec::new(); + let mut normals: Vec<[f32; 3]> = Vec::new(); + let mut texcoords: Vec<[f32; 2]> = Vec::new(); + let mut indices: Vec = Vec::new(); + + for primitive in mesh.primitives() { + let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()])); + + let prim_positions: Vec<[f32; 3]> = reader + .read_positions() + .ok_or_else(|| Error::msg("Missing POSITION in glTF primitive"))? + .collect(); + let prim_normals: Vec<[f32; 3]> = reader + .read_normals() + .ok_or_else(|| Error::msg("Missing NORMAL in glTF primitive"))? + .collect(); + let prim_texcoords: Vec<[f32; 2]> = reader + .read_tex_coords(0) + .map(|tc| tc.into_f32().collect()) + .unwrap_or_else(|| vec![[0.0, 0.0]; prim_positions.len()]); + + let index_base = positions.len() as u32; + positions.extend_from_slice(&prim_positions); + normals.extend_from_slice(&prim_normals); + texcoords.extend_from_slice(&prim_texcoords); + + if let Some(read_indices) = reader.read_indices() { + indices.extend(read_indices.into_u32().map(|i| i + index_base)); + } else { + // Non-indexed: generate a sequential index buffer + indices.extend((0..prim_positions.len() as u32).map(|i| i + index_base)); + } + } + + if positions.is_empty() { + return Err(Error::msg("No vertex data found in glTF mesh")); + } + + // Build vertices + let mut vertices: Vec = Vec::with_capacity(positions.len()); + let mut min_v = cgmath::Vector3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY); + let mut max_v = cgmath::Vector3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY); + + for i in 0..positions.len() { + let p = positions[i]; + let n = normals[i]; + let uv = texcoords.get(i).copied().unwrap_or([0.0, 0.0]); // glTF UVs are used as-is + + let pp = cgmath::Vector3::new(p[0], p[1], p[2]); + min_v = cgmath::Vector3::new(min_v.x.min(pp.x), min_v.y.min(pp.y), min_v.z.min(pp.z)); + max_v = cgmath::Vector3::new(max_v.x.max(pp.x), max_v.y.max(pp.y), max_v.z.max(pp.z)); + + vertices.push(MeshVertex { + position: p, + texture_coordinates: uv, + normal: n, + tangent: [0.0; 3].into(), + bitangent: [0.0; 3].into(), + }); + } + + // Compute tangents/bitangents (same approach as OBJ path) + let mut triangles_included = vec![0usize; vertices.len()]; + for c in indices.chunks(3) { + if c.len() < 3 { + continue; + } + let v0 = vertices[c[0] as usize]; + let v1 = vertices[c[1] as usize]; + let v2 = vertices[c[2] as usize]; + + let pos0: cgmath::Vector3<_> = v0.position.into(); + let pos1: cgmath::Vector3<_> = v1.position.into(); + let pos2: cgmath::Vector3<_> = v2.position.into(); + + let uv0: cgmath::Vector2<_> = v0.texture_coordinates.into(); + let uv1: cgmath::Vector2<_> = v1.texture_coordinates.into(); + let uv2: cgmath::Vector2<_> = v2.texture_coordinates.into(); + + let delta_pos1 = pos1 - pos0; + let delta_pos2 = pos2 - pos0; + let delta_uv1 = uv1 - uv0; + let delta_uv2 = uv2 - uv0; + + let r = 1.0 / (delta_uv1.x * delta_uv2.y - delta_uv1.y * delta_uv2.x); + let tangent = (delta_pos1 * delta_uv2.y - delta_pos2 * delta_uv1.y) * r; + let bitangent = (delta_pos2 * delta_uv1.x - delta_pos1 * delta_uv2.x) * r; + + vertices[c[0] as usize].tangent = + (tangent + cgmath::Vector3::from(vertices[c[0] as usize].tangent)).into(); + vertices[c[1] as usize].tangent = + (tangent + cgmath::Vector3::from(vertices[c[1] as usize].tangent)).into(); + vertices[c[2] as usize].tangent = + (tangent + cgmath::Vector3::from(vertices[c[2] as usize].tangent)).into(); + vertices[c[0] as usize].bitangent = + (bitangent + cgmath::Vector3::from(vertices[c[0] as usize].bitangent)).into(); + vertices[c[1] as usize].bitangent = + (bitangent + cgmath::Vector3::from(vertices[c[1] as usize].bitangent)).into(); + vertices[c[2] as usize].bitangent = + (bitangent + cgmath::Vector3::from(vertices[c[2] as usize].bitangent)).into(); + + triangles_included[c[0] as usize] += 1; + triangles_included[c[1] as usize] += 1; + triangles_included[c[2] as usize] += 1; + } + + for (i, n) in triangles_included.into_iter().enumerate() { + if n == 0 { + continue; + } + let denom = 1.0 / n as f32; + let vertex = &mut vertices[i]; + vertex.tangent = (Vector3f::from(vertex.tangent) * denom).normalize().into(); + vertex.bitangent = (Vector3f::from(vertex.bitangent) * denom) + .normalize() + .into(); + } + + Ok(MeshData { + vertices, + indices, + aabb_min: [min_v.x, min_v.y, min_v.z], + aabb_max: [max_v.x, max_v.y, max_v.z], + }) +} From 1eb239227945aa954246a3bf51cc1c6dc3d4e04f Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Mon, 10 Nov 2025 06:23:45 +0100 Subject: [PATCH 57/69] feat: gltf model sponza --- engine/pill_engine/src/config.rs | 15 +- .../src/ecs/systems/rendering_system.rs | 9 +- engine/pill_engine/src/lib.rs | 4 +- engine/pill_engine/src/resources/mesh.rs | 115 ++-- engine/pill_engine/src/resources/mod.rs | 3 + engine/pill_engine/src/resources/model.rs | 530 ++++++++++++++++++ engine/pill_renderer/src/pass_scene.rs | 43 +- .../src/resources/renderer_mesh.rs | 44 +- .../src/resources/renderer_texture.rs | 80 ++- 9 files changed, 718 insertions(+), 125 deletions(-) create mode 100644 engine/pill_engine/src/resources/model.rs diff --git a/engine/pill_engine/src/config.rs b/engine/pill_engine/src/config.rs index ac0b6681..f084fa6d 100644 --- a/engine/pill_engine/src/config.rs +++ b/engine/pill_engine/src/config.rs @@ -61,14 +61,21 @@ pub const RENDERING_SYSTEM: SystemConfig = SystemConfig { }; // --- Resources --- - +// +// Diagnostics (glTF import): +// - Set `LOG_GLTF_IMPORT = true` in runtime config to log imported meshes/materials/texture bindings. +// - Run with `RUST_LOG=info RUST_BACKTRACE=1` to see logs. +// - If materials show no textures bound, the asset may use KTX2/Basis images; use PNG/JPG variants +// (e.g., glTF-Sample-Models “glTF-Binary”) or add Basis/KTX2 transcode support. +// pub const RESOURCE_VERSION_LIMIT: usize = 255; pub const MAX_PIPELINES: usize = 10; -pub const MAX_TEXTURES: usize = 10; -pub const MAX_MATERIALS: usize = 10; -pub const MAX_MESHES: usize = 10; +pub const MAX_TEXTURES: usize = 1000; +pub const MAX_MATERIALS: usize = 1000; +pub const MAX_MESHES: usize = 1000; pub const MAX_SOUNDS: usize = 10; +pub const MAX_MODELS: usize = 1000; // Convention: All resource names starting with "pill_default" are restricted, cannot be added and removed from game pub const DEFAULT_RESOURCE_PREFIX: &str = "pill_default"; diff --git a/engine/pill_engine/src/ecs/systems/rendering_system.rs b/engine/pill_engine/src/ecs/systems/rendering_system.rs index 6fcaa178..a5c1e27e 100644 --- a/engine/pill_engine/src/ecs/systems/rendering_system.rs +++ b/engine/pill_engine/src/ecs/systems/rendering_system.rs @@ -9,7 +9,7 @@ use crate::{ graphics::{ compose_render_queue_key, RenderQuery, RenderQueueFactory, RenderQueueItem, RenderQueueKey, }, - resources::{Mesh, MeshHandle, PBRMaterial, PBRMaterialHandle, ResourceManager}, + resources::{Mesh, MeshHandle, Model, PBRMaterial, PBRMaterialHandle, ResourceManager}, }; use pill_core::{EngineError, PillSlotMapKey, PillStyle, RendererError, Timer}; @@ -21,7 +21,7 @@ use std::{ops::Range, time::Instant}; use crate::config::{ DEFAULT_COLOR_TEXTURE_NAME, DEFAULT_MATERIAL_NAME, DEFAULT_NORMAL_TEXTURE_NAME, MAX_MATERIALS, - MAX_MESHES, MAX_SOUNDS, MAX_TEXTURES, + MAX_MESHES, MAX_MODELS, MAX_SOUNDS, MAX_TEXTURES, }; use crate::ecs::components::render_state_component::RenderStateComponent; use crate::resources::{Resource, ResourceLoadType, Texture, TextureHandle, TextureType}; @@ -291,6 +291,10 @@ fn init_default_resources(engine: &mut Engine) -> Result<(), Error> { .config .get_int("MAX_MATERIALS") .unwrap_or(MAX_MATERIALS as i64) as usize; + let max_model_count = engine + .config + .get_int("MAX_MODELS") + .unwrap_or(MAX_MODELS as i64) as usize; // TODO: Move to SoundSystem init let max_sound_count = engine .config @@ -300,6 +304,7 @@ fn init_default_resources(engine: &mut Engine) -> Result<(), Error> { engine.register_resource_type::(max_texture_count)?; engine.register_resource_type::(max_mesh_count)?; engine.register_resource_type::(max_material_count)?; + engine.register_resource_type::(max_model_count)?; engine.register_resource_type::(max_sound_count)?; let default_color = Box::new(*include_bytes!("../../../res/textures/default_color.png")); diff --git a/engine/pill_engine/src/lib.rs b/engine/pill_engine/src/lib.rs index afadeeff..27ffc601 100644 --- a/engine/pill_engine/src/lib.rs +++ b/engine/pill_engine/src/lib.rs @@ -62,8 +62,8 @@ pub mod game { }, engine::{Engine, KeyboardKey, MouseButton, PillGame}, resources::{ - Mesh, MeshHandle, PBRMaterial, PBRMaterialHandle, Resource, ResourceLoadType, - ResourceStorage, Sound, Texture, TextureHandle, TextureType, + Mesh, MeshHandle, Model, ModelHandle, PBRMaterial, PBRMaterialHandle, Resource, + ResourceLoadType, ResourceStorage, Sound, Texture, TextureHandle, TextureType, }, }; diff --git a/engine/pill_engine/src/resources/mesh.rs b/engine/pill_engine/src/resources/mesh.rs index 95f0ff68..7463675f 100644 --- a/engine/pill_engine/src/resources/mesh.rs +++ b/engine/pill_engine/src/resources/mesh.rs @@ -46,6 +46,16 @@ impl Mesh { } } + pub fn from_mesh_data(name: &str, data: MeshData) -> Self { + Self { + name: name.to_string(), + path: PathBuf::new(), + renderer_resource_handle: None, + mesh_data: Some(data), + flip_uv_y: false, + } + } + pub fn with_uv_flip(mut self, flip: bool) -> Self { self.flip_uv_y = flip; self @@ -74,48 +84,51 @@ impl Resource for Mesh { get_type_name::().sobj_style() ); - // Resolve absolute path - let resource_file_path = engine.game_resources_directory_path.join(&self.path); - let ext = resource_file_path - .extension() - .and_then(|e| e.to_str()) - .unwrap_or("") - .to_ascii_lowercase(); - - // Route by extension - match ext.as_str() { - "obj" => { - // Validate and load OBJ - pill_core::validate_asset_path(&resource_file_path, &["obj"]) - .context(error_message.clone())?; - let mesh_data = MeshData::new(&resource_file_path, self.flip_uv_y) - .context(error_message.clone()) - .context(format!( - "Failed to create mesh data from {} file", - resource_file_path.file_name().unwrap().to_string_lossy() - ))?; - self.mesh_data = Some(mesh_data); - } - "gltf" | "glb" => { - // Validate and load glTF/GLB - pill_core::validate_asset_path(&resource_file_path, &["gltf", "glb"]) - .context(error_message.clone())?; - let mesh_data = load_meshdata_from_gltf(&resource_file_path) - .context(error_message.clone()) - .context(format!( - "Failed to create mesh data from {} file", - resource_file_path.file_name().unwrap().to_string_lossy() - ))?; - self.mesh_data = Some(mesh_data); - } - _ => { - return Err(Error::new(EngineError::InvalidModelFile( - resource_file_path - .clone() - .into_os_string() - .into_string() - .unwrap_or_else(|_| "unknown".to_string()), - ))); + // If mesh data is not pre-populated, load from path + if self.mesh_data.is_none() { + // Resolve absolute path + let resource_file_path = engine.game_resources_directory_path.join(&self.path); + let ext = resource_file_path + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("") + .to_ascii_lowercase(); + + // Route by extension + match ext.as_str() { + "obj" => { + // Validate and load OBJ + pill_core::validate_asset_path(&resource_file_path, &["obj"]) + .context(error_message.clone())?; + let mesh_data = MeshData::new(&resource_file_path, self.flip_uv_y) + .context(error_message.clone()) + .context(format!( + "Failed to create mesh data from {} file", + resource_file_path.file_name().unwrap().to_string_lossy() + ))?; + self.mesh_data = Some(mesh_data); + } + "gltf" | "glb" => { + // Validate and load glTF/GLB + pill_core::validate_asset_path(&resource_file_path, &["gltf", "glb"]) + .context(error_message.clone())?; + let mesh_data = load_meshdata_from_gltf(&resource_file_path) + .context(error_message.clone()) + .context(format!( + "Failed to create mesh data from {} file", + resource_file_path.file_name().unwrap().to_string_lossy() + ))?; + self.mesh_data = Some(mesh_data); + } + _ => { + return Err(Error::new(EngineError::InvalidModelFile( + resource_file_path + .clone() + .into_os_string() + .into_string() + .unwrap_or_else(|_| "unknown".to_string()), + ))); + } } } @@ -175,6 +188,24 @@ pub struct MeshData { pub aabb_max: [f32; 3], } +impl MeshVertex { + pub fn new( + position: [f32; 3], + texture_coordinates: [f32; 2], + normal: [f32; 3], + tangent: [f32; 3], + bitangent: [f32; 3], + ) -> Self { + Self { + position, + texture_coordinates, + normal, + tangent, + bitangent, + } + } +} + impl MeshData { pub fn new(path: &PathBuf, flip_uv_y: bool) -> Result { // Load model from path using tinyobjloader crate diff --git a/engine/pill_engine/src/resources/mod.rs b/engine/pill_engine/src/resources/mod.rs index 018a0c50..e83c266a 100644 --- a/engine/pill_engine/src/resources/mod.rs +++ b/engine/pill_engine/src/resources/mod.rs @@ -2,6 +2,7 @@ mod material; mod mesh; +mod model; mod resource; mod resource_manager; mod resource_storage; @@ -23,3 +24,5 @@ pub use mesh::{Mesh, MeshData, MeshHandle, MeshVertex}; pub use texture::{Texture, TextureHandle, TextureType}; pub use material::{PBRMaterial, PBRMaterialHandle}; + +pub use model::{Model, ModelHandle, ModelMaterialSlot}; diff --git a/engine/pill_engine/src/resources/model.rs b/engine/pill_engine/src/resources/model.rs new file mode 100644 index 00000000..5d5c484f --- /dev/null +++ b/engine/pill_engine/src/resources/model.rs @@ -0,0 +1,530 @@ +use crate::{ + engine::Engine, + resources::{ + material::PBRMaterialHandle, texture::TextureHandle, Mesh, MeshData, MeshHandle, + MeshVertex, PBRMaterial, Resource, ResourceLoadType, ResourceStorage, Texture, TextureType, + }, +}; +use anyhow::{anyhow, Context, Result}; +use cgmath::InnerSpace; +use gltf::import as gltf_import; +use image::{DynamicImage, ImageBuffer, ImageOutputFormat, Luma, Rgb, Rgba}; +use log::info; +use pill_core::{get_type_name, PillSlotMapKey, PillTypeMapKey, Vector3f}; +use std::collections::HashMap; +use std::env; +use std::io::Cursor; +use std::path::{Path, PathBuf}; + +pill_core::define_new_pill_slotmap_key! { + pub struct ModelHandle; +} + +#[readonly::make] +pub struct ModelMaterialSlot { + #[readonly] + pub mesh: MeshHandle, + #[readonly] + pub material: PBRMaterialHandle, + #[readonly] + pub translation: Vector3f, + #[readonly] + pub rotation_euler_deg: Vector3f, // XYZ degrees, matches TransformComponent expectations + #[readonly] + pub scale: Vector3f, +} + +#[readonly::make] +pub struct Model { + #[readonly] + pub name: String, + #[readonly] + pub path: PathBuf, + #[readonly] + pub meshes: Vec, + #[readonly] + pub materials: Vec, + #[readonly] + pub material_slots: Vec, +} + +impl Model { + pub fn new(name: &str, path: PathBuf) -> Self { + Self { + name: name.to_string(), + path, + meshes: Vec::new(), + materials: Vec::new(), + material_slots: Vec::new(), + } + } +} + +impl PillTypeMapKey for Model { + type Storage = ResourceStorage; +} + +impl Resource for Model { + type Handle = ModelHandle; + + fn get_name(&self) -> String { + self.name.clone() + } + + fn initialize(&mut self, engine: &mut Engine) -> Result<()> { + // Resolve absolute path + let model_path = engine.game_resources_directory_path.join(&self.path); + let (doc, buffers, images) = gltf_import(&model_path) + .with_context(|| format!("Importing glTF '{}' failed", model_path.display()))?; + + let base_dir = model_path + .parent() + .map(|p| p.to_path_buf()) + .unwrap_or_else(|| PathBuf::from(".")); + + // Diagnostics: + // - To enable verbose glTF import logging, set the runtime config key: + // LOG_GLTF_IMPORT = true + // and run with environment variables: + // RUST_LOG=info RUST_BACKTRACE=1 + // - The logs will print scenes/nodes/meshes/materials/images and, for each + // material, whether baseColor/mr/normal/emissive textures are bound. + // - If all textures show as false, your asset may use KTX2/Basis textures. + // Use a PNG/JPG variant (e.g., glTF-Sample-Models “glTF-Binary”) or add + // Basis/KTX2 transcode support before import. + // Logging flag: enabled if LOG_GLTF_IMPORT=true in config or environment + let log_import = engine.config.get_bool("LOG_GLTF_IMPORT").unwrap_or(false) + || env::var("LOG_GLTF_IMPORT") + .map(|v| { + v == "1" || v.eq_ignore_ascii_case("true") || v.eq_ignore_ascii_case("yes") + }) + .unwrap_or(false); + if log_import { + info!( + "glTF import '{}': scenes={}, nodes={}, meshes={}, materials={}, images={}", + self.name, + doc.scenes().len(), + doc.nodes().len(), + doc.meshes().len(), + doc.materials().len(), + images.len() + ); + println!( + "glTF import '{}': scenes={}, nodes={}, meshes={}, materials={}, images={}", + self.name, + doc.scenes().len(), + doc.nodes().len(), + doc.meshes().len(), + doc.materials().len(), + images.len() + ); + // Print textures and their sources up-front + for (ti, tex) in doc.textures().enumerate() { + let src = tex.source(); + let src_desc = match src.source() { + gltf::image::Source::Uri { uri, .. } => format!("uri '{}'", uri), + gltf::image::Source::View { .. } => "embedded view".to_string(), + }; + info!(" texture[{}] -> image[{}] ({})", ti, src.index(), src_desc); + println!(" texture[{}] -> image[{}] ({})", ti, src.index(), src_desc); + } + // Print materials with bound textures and factors + for (i, m) in doc.materials().enumerate() { + let p = m.pbr_metallic_roughness(); + let albedo_tex = p.base_color_texture().map(|t| t.texture().source().index()); + let mr_tex = p + .metallic_roughness_texture() + .map(|t| t.texture().source().index()); + let normal_tex = m.normal_texture().map(|t| t.texture().source().index()); + let emissive_tex = m.emissive_texture().map(|t| t.texture().source().index()); + let name = m.name().unwrap_or(""); + let bc = p.base_color_factor(); + let ef = m.emissive_factor(); + info!( + " material[{}] '{}': baseColorTex={:?}, mrTex={:?}, normalTex={:?}, emissiveTex={:?}, baseColorFactor=[{:.3},{:.3},{:.3}], metal={:.3}, rough={:.3}, emissive=[{:.3},{:.3},{:.3}]", + i, name, albedo_tex, mr_tex, normal_tex, emissive_tex, bc[0], bc[1], bc[2], p.metallic_factor(), p.roughness_factor(), ef[0], ef[1], ef[2] + ); + println!( + " material[{}] '{}': baseColorTex={:?}, mrTex={:?}, normalTex={:?}, emissiveTex={:?}, baseColorFactor=[{:.3},{:.3},{:.3}], metal={:.3}, rough={:.3}, emissive=[{:.3},{:.3},{:.3}]", + i, name, albedo_tex, mr_tex, normal_tex, emissive_tex, bc[0], bc[1], bc[2], p.metallic_factor(), p.roughness_factor(), ef[0], ef[1], ef[2] + ); + } + // Print meshes and primitive attribute overview + for (mi, m) in doc.meshes().enumerate() { + let mname = m.name().unwrap_or(""); + info!( + " mesh[{}] '{}': {} primitives", + mi, + mname, + m.primitives().len() + ); + println!( + " mesh[{}] '{}': {} primitives", + mi, + mname, + m.primitives().len() + ); + for (pi, prim) in m.primitives().enumerate() { + let attrs: Vec<_> = prim.attributes().map(|(s, _)| s.to_string()).collect(); + info!( + " prim[{}]: mode={:?}, attrs={:?}", + pi, + prim.mode(), + attrs + ); + println!( + " prim[{}]: mode={:?}, attrs={:?}", + pi, + prim.mode(), + attrs + ); + } + } + // Print nodes referencing meshes + for (ni, node) in doc.nodes().enumerate() { + if let Some(mesh) = node.mesh() { + let nname = node.name().unwrap_or(""); + let (t, r, s) = node.transform().decomposed(); + info!( + " node[{}] '{}': mesh={}, T=[{:.3},{:.3},{:.3}]", + ni, + nname, + mesh.index(), + t[0], + t[1], + t[2] + ); + println!( + " node[{}] '{}': mesh={}, T=[{:.3},{:.3},{:.3}]", + ni, + nname, + mesh.index(), + t[0], + t[1], + t[2] + ); + } + } + } + + // --- Textures and Materials --- + // Cache textures by image index to avoid duplicates (works for URI/data/embedded) + let mut image_index_to_tex: HashMap = HashMap::new(); + + let mut materials: Vec = Vec::new(); + for (mat_index, mat) in doc.materials().enumerate() { + let mat_name_owned = mat + .name() + .map(|s| s.to_string()) + .unwrap_or(format!("Mat{}", mat_index)); + let mut pbr = PBRMaterial::new(&format!("{}_{}", self.name, mat_name_owned)); + + // Factors + let pbrmr = mat.pbr_metallic_roughness(); + let bc = pbrmr.base_color_factor(); + pbr.set_base_color_factor(pill_core::Color::new(bc[0], bc[1], bc[2])); + pbr.set_metallic_factor(pbrmr.metallic_factor()); + pbr.set_roughness_factor(pbrmr.roughness_factor()); + let ef = mat.emissive_factor(); + pbr.set_emissive_factor(pill_core::Color::new(ef[0], ef[1], ef[2])); + + // Helper to load a texture from a glTF texture reference via imported images + let mut load_tex = + |tex: gltf::Texture, tex_type: TextureType| -> Result { + let img = tex.source(); + let idx = img.index(); + if let Some(h) = image_index_to_tex.get(&idx) { + if log_import { + info!(" reuse image[{}] as {:?} texture", idx, tex_type); + } + println!(" reuse image[{}] as {:?} texture", idx, tex_type); + return Ok(h.clone()); + } + let img_data = images.get(idx).ok_or_else(|| { + anyhow!("Image index {} not found in imported images", idx) + })?; + // Encode to PNG bytes for Texture::Bytes + let buf = encode_gltf_image_to_png_bytes(img_data)?; + let tex_name = format!("{}_image_{}", self.name, idx); + let texture = Texture::new(&tex_name, tex_type, ResourceLoadType::Bytes(buf)); + let h = engine.add_resource::(texture)?; + image_index_to_tex.insert(idx, h.clone()); + let src_desc = match tex.source().source() { + gltf::image::Source::Uri { uri, .. } => format!("uri '{}'", uri), + gltf::image::Source::View { .. } => "embedded view".to_string(), + }; + if log_import { + info!( + " created Texture '{}' for image[{}] ({}) as {:?}", + tex_name, idx, src_desc, tex_type + ); + } + println!( + " created Texture '{}' for image[{}] ({}) as {:?}", + tex_name, idx, src_desc, tex_type + ); + Ok(h) + }; + + // Base color texture + if let Some(ti) = pbrmr.base_color_texture() { + let th = load_tex(ti.texture(), TextureType::Gamma)?; + pbr.set_albedo_texture(th); + } + // Metallic-roughness combined texture + if let Some(ti) = pbrmr.metallic_roughness_texture() { + let th = load_tex(ti.texture(), TextureType::Linear)?; + pbr.set_metallic_roughness_texture(th); + } + // Normal map + if let Some(ti) = mat.normal_texture() { + let th = load_tex(ti.texture(), TextureType::Linear)?; + pbr.set_normal_texture(th); + } + // Emissive map + if let Some(ti) = mat.emissive_texture() { + let th = load_tex(ti.texture(), TextureType::Gamma)?; + pbr.set_emissive_texture(th); + } + + let mh = engine.add_resource::(pbr)?; + materials.push(mh); + if log_import { + let mref = engine.get_resource::(&mh)?; + info!( + "material[{}] '{}': baseColorTex={}, mrTex={}, normalTex={}, emissiveTex={}", + mat_index, + mref.name, + mref.albedo_texture.is_some(), + mref.metallic_roughness_texture.is_some(), + mref.normal_texture.is_some(), + mref.emissive_texture.is_some() + ); + println!( + "material[{}] '{}': baseColorTex={}, mrTex={}, normalTex={}, emissiveTex={}", + mat_index, + mref.name, + mref.albedo_texture.is_some(), + mref.metallic_roughness_texture.is_some(), + mref.normal_texture.is_some(), + mref.emissive_texture.is_some() + ); + } + } + + // --- Meshes (per primitive) --- + // Map (mesh_index, prim_index) -> MeshHandle + let mut prim_to_mesh: HashMap<(usize, usize), MeshHandle> = HashMap::new(); + let mut mesh_handles: Vec = Vec::new(); + for (m_i, m) in doc.meshes().enumerate() { + for (p_i, primitive) in m.primitives().enumerate() { + let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()])); + + let positions: Vec<[f32; 3]> = reader + .read_positions() + .ok_or_else(|| anyhow!("Missing POSITION in mesh {} primitive {}", m_i, p_i))? + .collect(); + let normals: Vec<[f32; 3]> = reader + .read_normals() + .ok_or_else(|| anyhow!("Missing NORMAL in mesh {} primitive {}", m_i, p_i))? + .collect(); + let texcoords: Vec<[f32; 2]> = reader + .read_tex_coords(0) + .map(|tc| tc.into_f32().collect()) + .unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]); + + // Indices + let indices: Vec = if let Some(read_indices) = reader.read_indices() { + read_indices.into_u32().collect() + } else { + (0..positions.len() as u32).collect() + }; + + // Build MeshData with tangents/bitangents and AABB + let mut vertices: Vec = Vec::with_capacity(positions.len()); + let mut min_v = cgmath::Vector3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY); + let mut max_v = + cgmath::Vector3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY); + for i in 0..positions.len() { + let p = positions[i]; + let n = normals[i]; + let uv = texcoords.get(i).copied().unwrap_or([0.0, 0.0]); + let pp = cgmath::Vector3::new(p[0], p[1], p[2]); + min_v = cgmath::Vector3::new( + min_v.x.min(pp.x), + min_v.y.min(pp.y), + min_v.z.min(pp.z), + ); + max_v = cgmath::Vector3::new( + max_v.x.max(pp.x), + max_v.y.max(pp.y), + max_v.z.max(pp.z), + ); + vertices.push(MeshDataVertexTemp { + position: p, + normal: n, + uv, + tangent: [0.0; 3], + bitangent: [0.0; 3], + }); + } + + // Tangents + let mut counts = vec![0usize; vertices.len()]; + for tri in indices.chunks(3) { + if tri.len() < 3 { + continue; + } + let v0 = vertices[tri[0] as usize]; + let v1 = vertices[tri[1] as usize]; + let v2 = vertices[tri[2] as usize]; + let pos0: cgmath::Vector3<_> = v0.position.into(); + let pos1: cgmath::Vector3<_> = v1.position.into(); + let pos2: cgmath::Vector3<_> = v2.position.into(); + let uv0: cgmath::Vector2<_> = v0.uv.into(); + let uv1: cgmath::Vector2<_> = v1.uv.into(); + let uv2: cgmath::Vector2<_> = v2.uv.into(); + let dp1 = pos1 - pos0; + let dp2 = pos2 - pos0; + let duv1 = uv1 - uv0; + let duv2 = uv2 - uv0; + let r = 1.0 / (duv1.x * duv2.y - duv1.y * duv2.x); + let t = (dp1 * duv2.y - dp2 * duv1.y) * r; + let b = (dp2 * duv1.x - dp1 * duv2.x) * r; + for &idx in tri { + let v = &mut vertices[idx as usize]; + v.tangent = (cgmath::Vector3::from(v.tangent) + t).into(); + v.bitangent = (cgmath::Vector3::from(v.bitangent) + b).into(); + counts[idx as usize] += 1; + } + } + let mut final_vertices: Vec = Vec::with_capacity(vertices.len()); + for (i, c) in counts.into_iter().enumerate() { + let denom = if c == 0 { 1.0 } else { 1.0 / c as f32 }; + let v = &mut vertices[i]; + let tan = (pill_core::Vector3f::from(v.tangent) * denom).normalize(); + let bitan = (pill_core::Vector3f::from(v.bitangent) * denom).normalize(); + final_vertices.push(MeshVertex::new( + v.position, + v.uv, + v.normal, + tan.into(), + bitan.into(), + )); + } + let data = MeshData { + vertices: final_vertices, + indices: indices.clone(), + aabb_min: [min_v.x, min_v.y, min_v.z], + aabb_max: [max_v.x, max_v.y, max_v.z], + }; + + let mesh_name = format!("{}_m{}_p{}", self.name, m_i, p_i); + let mh = engine.add_resource::(Mesh::from_mesh_data(&mesh_name, data))?; + prim_to_mesh.insert((m_i, p_i), mh.clone()); + mesh_handles.push(mh); + if log_import { + info!( + "mesh[{}] prim[{}] -> '{}' (vtx={}, idx={})", + m_i, + p_i, + mesh_name, + positions.len(), + indices.len() + ); + } + } + } + + // --- Material slots from scene nodes (local TRS) --- + let mut slots: Vec = Vec::new(); + for node in doc.nodes() { + if let Some(m) = node.mesh() { + // Node local TRS + let (t, r, s) = node.transform().decomposed(); + // Convert quat -> Euler ZYX to match TransformComponent (Rz * Ry * Rx) + let q = glam::Quat::from_xyzw(r[0], r[1], r[2], r[3]); + let (z, y, x) = q.to_euler(glam::EulerRot::ZYX); + let euler_deg = Vector3f::new(x.to_degrees(), y.to_degrees(), z.to_degrees()); + + for (p_i, primitive) in m.primitives().enumerate() { + let mesh_h = prim_to_mesh + .get(&(m.index(), p_i)) + .ok_or_else(|| anyhow!("Missing mesh handle for primitive"))? + .clone(); + // Material mapping + let mat_index = primitive.material().index().unwrap_or(0); + let mat_h = materials + .get(mat_index) + .ok_or_else(|| anyhow!("Material index out of bounds"))? + .clone(); + + slots.push(ModelMaterialSlot { + mesh: mesh_h, + material: mat_h, + translation: Vector3f::new(t[0], t[1], t[2]), + rotation_euler_deg: euler_deg, + scale: Vector3f::new(s[0], s[1], s[2]), + }); + } + } + } + + self.meshes = mesh_handles; + self.materials = materials; + self.material_slots = slots; + + if log_import { + info!("model '{}' slots: {}", self.name, self.material_slots.len()); + } + + Ok(()) + } +} + +// Internal: temp vertex used to accumulate tangents +#[derive(Clone, Copy)] +struct MeshDataVertexTemp { + position: [f32; 3], + normal: [f32; 3], + uv: [f32; 2], + tangent: [f32; 3], + bitangent: [f32; 3], +} + +fn encode_gltf_image_to_png_bytes(data: &gltf::image::Data) -> Result> { + let (w, h) = (data.width, data.height); + let dyn_img: DynamicImage = match data.format { + gltf::image::Format::R8G8B8A8 => { + let img: ImageBuffer, _> = ImageBuffer::from_raw(w, h, data.pixels.clone()) + .ok_or_else(|| anyhow!("Invalid RGBA8 image buffer"))?; + DynamicImage::ImageRgba8(img) + } + gltf::image::Format::R8G8B8 => { + let img: ImageBuffer, _> = ImageBuffer::from_raw(w, h, data.pixels.clone()) + .ok_or_else(|| anyhow!("Invalid RGB8 image buffer"))?; + DynamicImage::ImageRgb8(img) + } + gltf::image::Format::R8 => { + let img: ImageBuffer, _> = ImageBuffer::from_raw(w, h, data.pixels.clone()) + .ok_or_else(|| anyhow!("Invalid L8 image buffer"))?; + DynamicImage::ImageLuma8(img) + } + // Fallback: expand to RGBA8 + _ => { + // Try to interpret as RGBA8 by padding/truncation if length matches + let expected = (w as usize) * (h as usize) * 4; + let mut pixels = data.pixels.clone(); + pixels.resize(expected, 255u8); + let img: ImageBuffer, _> = ImageBuffer::from_raw(w, h, pixels) + .ok_or_else(|| anyhow!("Invalid fallback image buffer"))?; + DynamicImage::ImageRgba8(img) + } + }; + let mut buf = Vec::new(); + { + let mut writer = Cursor::new(&mut buf); + dyn_img.write_to(&mut writer, ImageOutputFormat::Png)?; + } + Ok(buf.into_boxed_slice()) +} diff --git a/engine/pill_renderer/src/pass_scene.rs b/engine/pill_renderer/src/pass_scene.rs index 71e36318..ae8d12a1 100644 --- a/engine/pill_renderer/src/pass_scene.rs +++ b/engine/pill_renderer/src/pass_scene.rs @@ -195,15 +195,14 @@ impl Pass for PassScene { const PI: f32 = 3.14159265359; - // TODO: Move lights to engine-provided uniforms; support directional/spot/area lights. - const LIGHT_POS0: vec3 = vec3(-10.0, 10.0, 10.0); - const LIGHT_POS1: vec3 = vec3( 10.0, 10.0, 10.0); - const LIGHT_POS2: vec3 = vec3(-10.0, -10.0, 10.0); - const LIGHT_POS3: vec3 = vec3( 10.0, -10.0, 10.0); - const LIGHT_COL0: vec3 = vec3(300.0, 300.0, 300.0); - const LIGHT_COL1: vec3 = vec3(300.0, 300.0, 300.0); - const LIGHT_COL2: vec3 = vec3(300.0, 300.0, 300.0); - const LIGHT_COL3: vec3 = vec3(300.0, 300.0, 300.0); + // Directional lights: vec4(direction.xyz, intensity) + // Direction points toward the surface (light-to-surface). + const LIGHT_DIR0: vec3 = vec3(-0.5, -1.0, -0.2); + const LIGHT_DIR1: vec3 = vec3( 0.5, -0.5, 0.2); + const LIGHT_DIR2: vec3 = vec3( 0.0, 1.0, 0.0); + const LIGHT_COL0: vec4 = vec4( 1.0, 0.5, 0.5, 10.0); // Key + const LIGHT_COL1: vec4 = vec4( 0.5, 0.5, 1.0, 3.0); // Fill + const LIGHT_COL2: vec4 = vec4( 0.1, 0.1, 1.0, 0.2); // Rim fn DistributionGGX(N: vec3, H: vec3, roughness: f32) -> f32 { // Add epsilon to avoid singularities at very low roughness. @@ -235,16 +234,16 @@ impl Pass for PassScene { return F0 + (vec3(1.0, 1.0, 1.0) - F0) * pow(1.0 - cosTheta, 5.0); } - fn accumulateLight( - WorldPos: vec3, N: vec3, V: vec3, F0: vec3, + // Directional light accumulator (white light scaled by intensity). + fn accumulateDirLight( + N: vec3, V: vec3, F0: vec3, albedo: vec3, roughness: f32, metallic: f32, - lightPos: vec3, lightColor: vec3 + lightDir: vec3, lightColor: vec4 ) -> vec3 { - let L = normalize(lightPos - WorldPos); + // lightDir is direction from light to surface; incoming L is opposite + let L = normalize(-lightDir.xyz); let H = normalize(V + L); - let distance = length(lightPos - WorldPos); - let attenuation = 1.0 / (distance * distance); - let radiance = lightColor * attenuation; + let radiance = lightColor.w * lightColor.xyz; let NDF = DistributionGGX(N, H, roughness); let G = GeometrySmith(N, V, L, roughness); @@ -275,7 +274,6 @@ impl Pass for PassScene { roughness = clamp(roughness, 0.045, 0.99); let metallic = clamp(mr.y * UMaterial.metallicFactor, 0.0, 1.0); // TODO: Support normal mapping (tangent space) and AO texture. - let ao: f32 = 1.0; let N = normalize(NormalIn); let V = normalize(UCamera.position.xyz - WorldPos); @@ -283,12 +281,15 @@ impl Pass for PassScene { F0 = mix(F0, albedo, vec3(metallic, metallic, metallic)); var Lo = vec3(0.0, 0.0, 0.0); - Lo = Lo + accumulateLight(WorldPos, N, V, F0, albedo, roughness, metallic, LIGHT_POS0, LIGHT_COL0); - Lo = Lo + accumulateLight(WorldPos, N, V, F0, albedo, roughness, metallic, LIGHT_POS1, LIGHT_COL1); - Lo = Lo + accumulateLight(WorldPos, N, V, F0, albedo, roughness, metallic, LIGHT_POS2, LIGHT_COL2); - Lo = Lo + accumulateLight(WorldPos, N, V, F0, albedo, roughness, metallic, LIGHT_POS3, LIGHT_COL3); + Lo = Lo + accumulateDirLight(N, V, F0, albedo, roughness, metallic, LIGHT_DIR0, LIGHT_COL0); + Lo = Lo + accumulateDirLight(N, V, F0, albedo, roughness, metallic, LIGHT_DIR1, LIGHT_COL1); + Lo = Lo + accumulateDirLight(N, V, F0, albedo, roughness, metallic, LIGHT_DIR2, LIGHT_COL2); var color = Lo; + // color = vec3(uv, 0.0); // DBG uv + // color = vec3(N*0.5+0.5); // DBG Normal + // color = vec3(WorldPos); // DBG WorldPos + // color = vec3(roughness); // DBG roughness return vec4(color, 1.0); } "#; diff --git a/engine/pill_renderer/src/resources/renderer_mesh.rs b/engine/pill_renderer/src/resources/renderer_mesh.rs index 68546df3..3d527e82 100644 --- a/engine/pill_renderer/src/resources/renderer_mesh.rs +++ b/engine/pill_renderer/src/resources/renderer_mesh.rs @@ -48,42 +48,18 @@ impl RendererMesh { impl Vertex for RendererMesh { fn data_layout_descriptor<'a>() -> wgpu::VertexBufferLayout<'a> { - use std::mem; + // Use a static attribute array to satisfy lifetime requirements + const ATTRS: [wgpu::VertexAttribute; 5] = wgpu::vertex_attr_array![ + 0 => Float32x3, // position + 1 => Float32x2, // uv + 2 => Float32x3, // normal + 3 => Float32x3, // tangent + 4 => Float32x3, // bitangent + ]; wgpu::VertexBufferLayout { - array_stride: mem::size_of::() as wgpu::BufferAddress, + array_stride: std::mem::size_of::() as wgpu::BufferAddress, step_mode: wgpu::VertexStepMode::Vertex, - attributes: &[ - wgpu::VertexAttribute { - // Vertex position - offset: 0, - shader_location: 0, - format: wgpu::VertexFormat::Float32x3, - }, - wgpu::VertexAttribute { - // Vertex texture coordinates - offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, - shader_location: 1, - format: wgpu::VertexFormat::Float32x2, - }, - wgpu::VertexAttribute { - // Vertex normal - offset: mem::size_of::<[f32; 5]>() as wgpu::BufferAddress, - shader_location: 2, - format: wgpu::VertexFormat::Float32x3, - }, - wgpu::VertexAttribute { - // Vertex tangent - offset: mem::size_of::<[f32; 8]>() as wgpu::BufferAddress, - shader_location: 3, - format: wgpu::VertexFormat::Float32x3, - }, - wgpu::VertexAttribute { - // Vertex bitangent - offset: mem::size_of::<[f32; 11]>() as wgpu::BufferAddress, - shader_location: 4, - format: wgpu::VertexFormat::Float32x3, - }, - ], + attributes: &ATTRS, } } } diff --git a/engine/pill_renderer/src/resources/renderer_texture.rs b/engine/pill_renderer/src/resources/renderer_texture.rs index 6341f095..ca681d05 100644 --- a/engine/pill_renderer/src/resources/renderer_texture.rs +++ b/engine/pill_renderer/src/resources/renderer_texture.rs @@ -21,7 +21,7 @@ impl RendererTexture { queue: &wgpu::Queue, name: Option<&str>, image_data: &image::DynamicImage, - _texture_type: TextureType, + texture_type: TextureType, ) -> Result { let dimensions = image_data.dimensions(); let rgba = image_data.to_rgba8(); @@ -31,38 +31,78 @@ impl RendererTexture { depth_or_array_layers: 1, }; + let format = match texture_type { + TextureType::Gamma => wgpu::TextureFormat::Rgba8UnormSrgb, + TextureType::Linear => wgpu::TextureFormat::Rgba8Unorm, + }; + let texture = device.create_texture(&wgpu::TextureDescriptor { label: name, size, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8UnormSrgb, + format, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, view_formats: &[], }); - queue.write_texture( - wgpu::ImageCopyTexture { - texture: &texture, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - &rgba, - wgpu::ImageDataLayout { - offset: 0, - bytes_per_row: Some(4 * dimensions.0), - rows_per_image: Some(dimensions.1), - }, - size, - ); + // Write with proper row alignment to avoid stretching/scrambling + // WebGPU requires bytes_per_row to be COPY_BYTES_PER_ROW_ALIGNMENT (256) aligned. + let bpp: u32 = 4; + let unpadded_bytes_per_row = (dimensions.0 * bpp) as usize; + let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; + let padded_bytes_per_row = ((unpadded_bytes_per_row + align - 1) / align) * align; + if padded_bytes_per_row == unpadded_bytes_per_row { + // Fast path: already aligned + queue.write_texture( + wgpu::ImageCopyTexture { + texture: &texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + &rgba, + wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(unpadded_bytes_per_row as u32), + rows_per_image: Some(dimensions.1), + }, + size, + ); + } else { + // Pad each row up to alignment + let height = dimensions.1 as usize; + let mut padded = vec![0u8; padded_bytes_per_row * height]; + let src = rgba.as_raw(); + for y in 0..height { + let src_offset = y * unpadded_bytes_per_row; + let dst_offset = y * padded_bytes_per_row; + padded[dst_offset..dst_offset + unpadded_bytes_per_row] + .copy_from_slice(&src[src_offset..src_offset + unpadded_bytes_per_row]); + } + queue.write_texture( + wgpu::ImageCopyTexture { + texture: &texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + &padded, + wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(padded_bytes_per_row as u32), + rows_per_image: Some(dimensions.1), + }, + size, + ); + } let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); let sampler = device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, + address_mode_u: wgpu::AddressMode::Repeat, + address_mode_v: wgpu::AddressMode::Repeat, + address_mode_w: wgpu::AddressMode::Repeat, mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Nearest, mipmap_filter: wgpu::FilterMode::Nearest, From 20acb099b9bd8492122679f568fc744e48c75842 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Sat, 15 Nov 2025 03:17:25 +0100 Subject: [PATCH 58/69] move passes creation to engine --- .../src/ecs/systems/rendering_system.rs | 85 ++ engine/pill_engine/src/graphics/mod.rs | 16 +- .../pill_engine/src/graphics/pass_compose.rs | 198 +++++ engine/pill_engine/src/graphics/pass_logo.rs | 289 +++++++ .../src/graphics/pass_overlay_depth.rs | 263 ++++++ .../src/graphics/pass_overlay_uv.rs | 181 ++++ engine/pill_engine/src/graphics/pass_scene.rs | 795 ++++++++++++++++++ engine/pill_engine/src/graphics/renderer.rs | 41 + engine/pill_engine/src/lib.rs | 8 +- engine/pill_renderer/src/lib.rs | 7 - engine/pill_renderer/src/pass_scene.rs | 2 +- engine/pill_renderer/src/renderer.rs | 360 +++----- 12 files changed, 2012 insertions(+), 233 deletions(-) create mode 100644 engine/pill_engine/src/graphics/pass_compose.rs create mode 100644 engine/pill_engine/src/graphics/pass_logo.rs create mode 100644 engine/pill_engine/src/graphics/pass_overlay_depth.rs create mode 100644 engine/pill_engine/src/graphics/pass_overlay_uv.rs create mode 100644 engine/pill_engine/src/graphics/pass_scene.rs diff --git a/engine/pill_engine/src/ecs/systems/rendering_system.rs b/engine/pill_engine/src/ecs/systems/rendering_system.rs index a5c1e27e..a32a4053 100644 --- a/engine/pill_engine/src/ecs/systems/rendering_system.rs +++ b/engine/pill_engine/src/ecs/systems/rendering_system.rs @@ -1,3 +1,4 @@ +use crate::graphics::RendererTargetDesc; use crate::{ config::RENDERING_SYSTEM, ecs::{ @@ -24,6 +25,7 @@ use crate::config::{ MAX_MESHES, MAX_MODELS, MAX_SOUNDS, MAX_TEXTURES, }; use crate::ecs::components::render_state_component::RenderStateComponent; +use crate::graphics::{Pass, PassCompose, PassLogo, PassOverlayDepth, PassOverlayUV}; use crate::resources::{Resource, ResourceLoadType, Texture, TextureHandle, TextureType}; // Constants for hot path optimization @@ -51,6 +53,77 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { DIRTY_ENTITIES.reserve(MAX_RENDERABLES_CAPACITY); // Reserve space for up to 100k entities } + // Install renderer passes once during bootstrap + { + // Resolve logo texture created in init_default_resources + let tex_logo = engine + .resource_manager + .get_resource_by_name::("pill_logo_horizontal_white")?; + let fmt = engine.renderer.get_surface_format(); + + let offscreen_color_texture = + engine.renderer.create_render_target(RendererTargetDesc { + name: "offscreen_color".to_string(), + format: fmt, + width: engine.window_size.width, + height: engine.window_size.height, + })?; + + // Create depth and color texture + let depth_texture = engine.renderer.create_depth_texture("depth_texture")?; + + // Build passes + let mut passes: Vec> = Vec::new(); + + // Scene (renders into offscreen targets) + passes.push(Box::new(crate::graphics::pass_scene::PassScene::new( + "scene", + offscreen_color_texture, + depth_texture, + fmt, + ))); + + // Compose (tone map) from offscreen to swapchain (last) + passes.push(Box::new(crate::graphics::PassCompose::new( + "compose", + offscreen_color_texture, + fmt, + ))); + + // UV overlay + passes.push(Box::new(crate::graphics::PassOverlayUV::new( + "overlay_uv", + [0.75, 0.75, 0.95, 0.95], + fmt, + ))); + + // Depth overlay + passes.push(Box::new(crate::graphics::PassOverlayDepth::new( + "overlay_depth", + [0.75, 0.50, 0.95, 0.70], + [1.0, 1.0, 1.0, 1.0], + fmt, + depth_texture, + ))); + + // Logo overlay + let h: f32 = 0.04; + let rect_logo = [0.98 - 3.0 * h, 0.02, 0.98, 0.02 + h]; + // Convert engine Texture -> renderer texture handle + let tex_logo_rt = tex_logo + .renderer_resource_handle + .expect("renderer handle for logo texture"); + passes.push(Box::new(PassLogo::new( + "overlay_logo", + rect_logo, + [1.0, 1.0, 1.0, 1.0], + tex_logo_rt, + fmt, + ))); + + engine.renderer.set_passes(passes)?; + } + if let Ok(rs) = engine.get_global_component_mut::() { rs.boot_done = true; } @@ -324,6 +397,18 @@ fn init_default_resources(engine: &mut Engine) -> Result<(), Error> { normal.initialize(engine)?; engine.resource_manager.add_resource(normal)?; + // Pill logo (overlay) texture + let pill_logo = Box::new(*include_bytes!( + "../../../../pill_renderer/res/pill_logo_horizontal_white.png" + )); + let mut tex_logo = Texture::new( + "pill_logo_horizontal_white", + TextureType::Gamma, + ResourceLoadType::Bytes(pill_logo), + ); + tex_logo.initialize(engine)?; + engine.resource_manager.add_resource(tex_logo)?; + let mut mat = PBRMaterial::new(DEFAULT_MATERIAL_NAME); mat.initialize(engine)?; engine.resource_manager.add_resource(mat)?; diff --git a/engine/pill_engine/src/graphics/mod.rs b/engine/pill_engine/src/graphics/mod.rs index 80b005ef..4b7fe666 100644 --- a/engine/pill_engine/src/graphics/mod.rs +++ b/engine/pill_engine/src/graphics/mod.rs @@ -13,11 +13,25 @@ pub use renderer::{render_with_factory, WorldView, WorldViewFactory}; pub use renderer::{ BufferDesc, MaterialDesc, PillRenderer, PipelineV2, PipelineV2Desc, Renderer, RendererBufferHandle, RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, - RendererPipelineHandle, RendererPipelineV2Handle, RendererTextureHandle, ShaderDesc, + RendererPipelineHandle, RendererPipelineV2Handle, RendererTargetDesc, RendererTextureHandle, + ShaderDesc, }; +pub use renderer::{Pass, WorldQuery}; pub use render_queue::{ compose_render_queue_key, decompose_render_queue_key, RenderQueueItem, RenderQueueKey, RenderQueueKeyFields, RENDER_QUEUE_KEY_ORDER, }; pub use render_queue::{RenderQuery, RenderQueueFactory}; + +// Passes owned by engine +pub mod pass_logo; +pub use pass_logo::PassLogo; +pub mod pass_overlay_uv; +pub use pass_overlay_uv::PassOverlayUV; +pub mod pass_overlay_depth; +pub use pass_overlay_depth::PassOverlayDepth; +pub mod pass_compose; +pub use pass_compose::PassCompose; +pub mod pass_scene; +pub use pass_scene::PassScene; diff --git a/engine/pill_engine/src/graphics/pass_compose.rs b/engine/pill_engine/src/graphics/pass_compose.rs new file mode 100644 index 00000000..ca13fdf0 --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_compose.rs @@ -0,0 +1,198 @@ +use anyhow::Result; +use wgpu::CommandEncoder; + +use crate::graphics::renderer::{ + Pass, PillRenderer as EnginePillRenderer, PipelineV2, RendererTextureHandle, WorldQuery, +}; + +pub struct PassCompose { + label: String, + offscreen_color_texture: RendererTextureHandle, + target_format: wgpu::TextureFormat, + pipeline: Option, + bind_group: Option, +} + +impl PassCompose { + pub fn new( + label: &str, + offscreen_color_texture: RendererTextureHandle, + target_format: wgpu::TextureFormat, + ) -> Self { + Self { + label: label.to_string(), + offscreen_color_texture, + target_format, + pipeline: None, + bind_group: None, + } + } +} + +impl Pass for PassCompose { + fn get_label(&self) -> &str { + &self.label + } + + fn init(&mut self, renderer: &mut dyn EnginePillRenderer) -> Result<()> { + let device = renderer.get_device(); + + let vs = r#" + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var pos = array, 3>( + vec2(-1.0, -3.0), + vec2( 3.0, 1.0), + vec2(-1.0, 1.0) + ); + var tuv = array, 3>( + vec2(0.0, 2.0), + vec2(2.0, 0.0), + vec2(0.0, 0.0) + ); + var o: VSOut; + o.pos = vec4(pos[vi], 0.0, 1.0); + o.uv = tuv[vi]; + return o; + } + "#; + + let fs = r#" + @group(0) @binding(0) var t_src: texture_2d; + @group(0) @binding(1) var s_src: sampler; + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let hdr = textureSample(t_src, s_src, uv).rgb; + // Reinhard tone mapping; output remains in linear space. The sRGB swapchain will encode. + let ldr = hdr / (1.0 + hdr); + return vec4(ldr, 1.0); + } + "#; + + // Build bind group layout + let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("compose_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + + let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("compose_pl"), + bind_group_layouts: &[&bgl], + push_constant_ranges: &[], + }); + + let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("compose_vs"), + source: wgpu::ShaderSource::Wgsl(vs.into()), + }); + let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("compose_fs"), + source: wgpu::ShaderSource::Wgsl(fs.into()), + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("compose_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fs_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: self.target_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + // Bind offscreen color texture from renderer (offscreen target) + // let (view, sampler) = renderer.get_offscreen_color_view_and_sampler(); + let view = renderer + .get_texture(self.offscreen_color_texture) + .create_view(&wgpu::TextureViewDescriptor::default()); + let sampler = renderer + .get_device() + .create_sampler(&wgpu::SamplerDescriptor::default()); + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("compose_bind_group"), + layout: &bgl, // Group 0: Texture and sampler bindings + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + ], + }); + + // Store pipeline handle + self.pipeline = Some(PipelineV2 { + pipeline, + bind_group_layouts: vec![bgl], + }); + self.bind_group = Some(bind_group); + + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut CommandEncoder, + _renderer: &mut dyn EnginePillRenderer, + _frame: &wgpu::SurfaceTexture, + view: &wgpu::TextureView, + _world: &WorldQuery, + ) -> Result<()> { + // Create render pass for this pass using the provided frame + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.label), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + // Draw fullscreen triangle for composition + pass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); + pass.set_bind_group(0, &self.bind_group.as_ref().unwrap(), &[]); + pass.draw(0..3, 0..1); + + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/pass_logo.rs b/engine/pill_engine/src/graphics/pass_logo.rs new file mode 100644 index 00000000..fefffb77 --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_logo.rs @@ -0,0 +1,289 @@ +use anyhow::Result; +use wgpu::CommandEncoder; + +use crate::graphics::renderer::{ + Pass, PillRenderer as EnginePillRenderer, PipelineV2, RendererTextureHandle, WorldQuery, +}; + +pub struct PassLogo { + label: String, + tex_logo: RendererTextureHandle, + rect: [f32; 4], + tint: [f32; 4], + target_format: wgpu::TextureFormat, + state: Option, +} + +struct PassLogoState { + buffer: wgpu::Buffer, + pipeline: PipelineV2, + tex_logo_view: wgpu::TextureView, + tex_logo_sampler: wgpu::Sampler, + tint_buffer: wgpu::Buffer, + bind_group_rect: wgpu::BindGroup, + bind_group_material: wgpu::BindGroup, +} + +impl PassLogo { + pub fn new( + label: &str, + rect: [f32; 4], + tint: [f32; 4], + tex_logo: RendererTextureHandle, + target_format: wgpu::TextureFormat, + ) -> Self { + Self { + label: label.to_string(), + tex_logo, + rect, + tint, + target_format, + state: None, + } + } +} + +impl Pass for PassLogo { + fn get_label(&self) -> &str { + &self.label + } + + fn init(&mut self, renderer: &mut dyn EnginePillRenderer) -> Result<()> { + let device = renderer.get_device(); + + // Create mapped buffer for overlay rect UBO + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_logo_ubo"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM, + mapped_at_creation: true, + }); + { + let mut view = buffer.slice(..).get_mapped_range_mut(); + let src = bytemuck::bytes_of(&self.rect); + view[..src.len()].copy_from_slice(src); + } + buffer.unmap(); + + let vs = r#" + @group(1) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] + + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var unit = array, 6>( + vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), + vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) + ); + let p = unit[vi]; // [-1,1] NDC space + let s = p*0.5+0.5; // [0,1] screen space + let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] + let ndc = r*2.0-1.0; // [-1,1] NDC space + var out: VSOut; + out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); + out.uv = vec2(s.x, 1.0 - s.y); + return out; + } + "#; + + let fs = r#" + @group(0) @binding(0) var tex: texture_2d; + @group(0) @binding(1) var smp: sampler; + @group(0) @binding(2) var UTint: vec4; + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let c = textureSample(tex, smp, uv); + return c * UTint; + } + "#; + + // Build bind group layouts + let bgl_material = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_logo_material_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }, + ], + }); + let bgl_rect = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_logo_rect_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }], + }); + let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("overlay_logo_pl"), + bind_group_layouts: &[&bgl_material, &bgl_rect], + push_constant_ranges: &[], + }); + let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_logo_vs"), + source: wgpu::ShaderSource::Wgsl(vs.into()), + }); + let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_logo_fs"), + source: wgpu::ShaderSource::Wgsl(fs.into()), + }); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("overlay_logo_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fs_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: self.target_format, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + let bind_group_rect = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_rect_bind_group"), + layout: &bgl_rect, // Group 1: Rect UBO + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + }); + + let tex_logo_view = renderer + .get_texture(self.tex_logo) + .create_view(&wgpu::TextureViewDescriptor::default()); + let tex_logo_sampler = renderer + .get_device() + .create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::Repeat, + address_mode_v: wgpu::AddressMode::Repeat, + address_mode_w: wgpu::AddressMode::Repeat, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + + let tint_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_logo_tint"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM, + mapped_at_creation: true, + }); + { + let mut view = tint_buffer.slice(..).get_mapped_range_mut(); + let src = bytemuck::bytes_of(&self.tint); + view[..src.len()].copy_from_slice(src); + } + tint_buffer.unmap(); + + let bind_group_material = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_logo_material_bg"), + layout: &bgl_material, // Group 0: Material bindings + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&tex_logo_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&tex_logo_sampler), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &tint_buffer, + offset: 0, + size: Some(std::num::NonZeroU64::new(16).unwrap()), + }), + }, + ], + }); + + self.state = Some(PassLogoState { + buffer, + pipeline: PipelineV2 { + pipeline, + bind_group_layouts: vec![bgl_material, bgl_rect], + }, + tex_logo_view, + tex_logo_sampler, + tint_buffer, + bind_group_rect, + bind_group_material, + }); + + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut CommandEncoder, + _renderer: &mut dyn EnginePillRenderer, + _frame: &wgpu::SurfaceTexture, + view: &wgpu::TextureView, + _world: &WorldQuery, + ) -> Result<()> { + // Load over the existing frame and draw quad + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.label), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + pass.set_pipeline(&self.state.as_ref().unwrap().pipeline.pipeline); + let state = self.state.as_ref().expect("PassLogo not initialized"); + pass.set_bind_group(0, &state.bind_group_material, &[]); + pass.set_bind_group(1, &state.bind_group_rect, &[]); + pass.draw(0..6, 0..1); + + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/pass_overlay_depth.rs b/engine/pill_engine/src/graphics/pass_overlay_depth.rs new file mode 100644 index 00000000..66e828cd --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_overlay_depth.rs @@ -0,0 +1,263 @@ +use anyhow::Result; +use wgpu::CommandEncoder; + +use crate::graphics::renderer::{ + Pass, PillRenderer as EnginePillRenderer, PipelineV2, RendererTextureHandle, WorldQuery, +}; + +pub struct PassOverlayDepth { + label: String, + pipeline: Option, + bind_group_rect: Option, + bind_group_material: Option, + tint_buffer: Option, + rect: [f32; 4], + tint: [f32; 4], + target_format: wgpu::TextureFormat, + depth_texture: RendererTextureHandle, +} + +impl PassOverlayDepth { + pub fn new( + label: &str, + rect: [f32; 4], + tint: [f32; 4], + target_format: wgpu::TextureFormat, + depth_texture: RendererTextureHandle, + ) -> Self { + Self { + label: label.to_string(), + pipeline: None, + bind_group_rect: None, + bind_group_material: None, + tint_buffer: None, + rect, + tint, + target_format, + depth_texture, + } + } +} + +impl Pass for PassOverlayDepth { + fn get_label(&self) -> &str { + &self.label + } + + fn init(&mut self, renderer: &mut dyn EnginePillRenderer) -> Result<()> { + let device = renderer.get_device(); + + // Create buffer for overlay rect UBO + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_depth_rect_ubo"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM, + mapped_at_creation: true, + }); + { + let mut view = buffer.slice(..).get_mapped_range_mut(); + let src = bytemuck::bytes_of(&self.rect); + view[..src.len()].copy_from_slice(src); + } + buffer.unmap(); + + let vs = r#" + @group(1) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] + + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var unit = array, 6>( + vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), + vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) + ); + let p = unit[vi]; // [-1,1] NDC space + let s = p*0.5+0.5; // [0,1] screen space + let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] + let ndc = r*2.0-1.0; // [-1,1] NDC space + var out: VSOut; + out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); + out.uv = vec2(s.x, 1.0 - s.y); + return out; + } + "#; + + let fs = r#" + @group(0) @binding(0) var tex: texture_depth_2d; + @group(0) @binding(1) var UTint: vec4; + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let dims = textureDimensions(tex, 0u); + let coord = vec2(uv * vec2(dims)); + let d = textureLoad(tex, coord, 0); + let vis = fract(1000.0*d); + return vec4(vis, vis, vis, 1.0) * UTint; + } + "#; + + // Build bind group layouts + let bgl_material = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_depth_material_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Depth, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }, + ], + }); + let bgl_rect = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_depth_rect_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }], + }); + let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("overlay_depth_pl"), + bind_group_layouts: &[&bgl_material, &bgl_rect], + push_constant_ranges: &[], + }); + let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_depth_vs"), + source: wgpu::ShaderSource::Wgsl(vs.into()), + }); + let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_depth_fs"), + source: wgpu::ShaderSource::Wgsl(fs.into()), + }); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("overlay_depth_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fs_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: self.target_format, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + let bind_group_rect = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_depth_rect_bind_group"), + layout: &bgl_rect, // Group 1: Rect UBO + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + }); + + // Create tint buffer + let tint_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_depth_tint"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM, + mapped_at_creation: true, + }); + { + let mut view = tint_buffer.slice(..).get_mapped_range_mut(); + let src = bytemuck::bytes_of(&self.tint); + view[..src.len()].copy_from_slice(src); + } + tint_buffer.unmap(); + + // Create material bind group using renderer-provided depth texture view + let depth_view = renderer + .get_texture(self.depth_texture) + .create_view(&wgpu::TextureViewDescriptor::default()); + let bind_group_material = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_depth_material_bg"), + layout: &bgl_material, // Group 0: Material bindings + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&depth_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &tint_buffer, + offset: 0, + size: Some(std::num::NonZeroU64::new(16).unwrap()), + }), + }, + ], + }); + + // Store pipeline handle + self.pipeline = Some(PipelineV2 { + pipeline, + bind_group_layouts: vec![bgl_material, bgl_rect], + }); + self.bind_group_rect = Some(bind_group_rect); + self.bind_group_material = Some(bind_group_material); + self.tint_buffer = Some(tint_buffer); + + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut CommandEncoder, + _renderer: &mut dyn EnginePillRenderer, + _frame: &wgpu::SurfaceTexture, + view: &wgpu::TextureView, + _world: &WorldQuery, + ) -> Result<()> { + // Create render pass for this pass using the provided frame + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.label), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + // Draw depth overlay (bind material at group 0 and rect at group 1) + pass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); + pass.set_bind_group(0, &self.bind_group_material.as_ref().unwrap(), &[]); + pass.set_bind_group(1, &self.bind_group_rect.as_ref().unwrap(), &[]); + pass.draw(0..6, 0..1); + + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/pass_overlay_uv.rs b/engine/pill_engine/src/graphics/pass_overlay_uv.rs new file mode 100644 index 00000000..136c8868 --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_overlay_uv.rs @@ -0,0 +1,181 @@ +use anyhow::Result; +use wgpu::CommandEncoder; + +use crate::graphics::renderer::{ + Pass, PillRenderer as EnginePillRenderer, PipelineV2, RendererTextureHandle, WorldQuery, +}; + +pub struct PassOverlayUV { + label: String, + buffer: Option, + pipeline: Option, + bind_group: Option, + rect: [f32; 4], + target_format: wgpu::TextureFormat, +} + +impl PassOverlayUV { + pub fn new(label: &str, rect: [f32; 4], target_format: wgpu::TextureFormat) -> Self { + Self { + label: label.to_string(), + buffer: None, + pipeline: None, + bind_group: None, + rect, + target_format, + } + } +} + +impl Pass for PassOverlayUV { + fn get_label(&self) -> &str { + &self.label + } + + fn init(&mut self, renderer: &mut dyn EnginePillRenderer) -> Result<()> { + let device = renderer.get_device(); + + // Create buffer for overlay rect UBO + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_uv_rect_ubo"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM, + mapped_at_creation: true, + }); + { + let mut view = buffer.slice(..).get_mapped_range_mut(); + let src = bytemuck::bytes_of(&self.rect); + view[..src.len()].copy_from_slice(src); + } + buffer.unmap(); + + let vs = r#" + @group(0) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] + + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var unit = array, 6>( + vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), + vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) + ); + let p = unit[vi]; // [-1,1] NDC space + let s = p*0.5+0.5; // [0,1] screen space + let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] + let ndc = r*2.0-1.0; // [-1,1] NDC space + var out: VSOut; + out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); + out.uv = s; + return out; + } + "#; + + let fs = r#" + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + return vec4(uv, 0.0, 0.5); + } + "#; + + // Build bind group layout and pipeline + let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_uv_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }], + }); + let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("overlay_uv_pl"), + bind_group_layouts: &[&bgl], + push_constant_ranges: &[], + }); + let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_uv_vs"), + source: wgpu::ShaderSource::Wgsl(vs.into()), + }); + let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_uv_fs"), + source: wgpu::ShaderSource::Wgsl(fs.into()), + }); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("overlay_uv_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fs_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: self.target_format, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_uv_rect_bind_group"), + layout: &bgl, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + }); + + // Store pipeline handle + self.pipeline = Some(PipelineV2 { + pipeline, + bind_group_layouts: vec![bgl], + }); + self.bind_group = Some(bind_group); + self.buffer = Some(buffer); + + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut CommandEncoder, + _renderer: &mut dyn EnginePillRenderer, + _frame: &wgpu::SurfaceTexture, + view: &wgpu::TextureView, + _world: &WorldQuery, + ) -> Result<()> { + // Create render pass for this pass using the provided frame + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.label), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + // Draw small overlay UV quad in normalized rect via UBO + pass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); + pass.set_bind_group(0, self.bind_group.as_ref().unwrap(), &[]); + pass.draw(0..6, 0..1); + + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/pass_scene.rs b/engine/pill_engine/src/graphics/pass_scene.rs new file mode 100644 index 00000000..d70f96e2 --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_scene.rs @@ -0,0 +1,795 @@ +use crate::graphics::renderer::{ + Pass, PillRenderer as EnginePillRenderer, PipelineV2, PipelineV2Desc, ShaderDesc, WorldQuery, +}; +use crate::graphics::{ + BufferDesc, RendererMaterialHandle, RendererMeshHandle, RendererTextureHandle, +}; +use crate::internal; +use anyhow::Result; +use glam::{EulerRot, Mat4, Quat, Vec3, Vec4}; +use pill_core::PillSlotMapKey; +use wgpu::CommandEncoder; + +// Minimal camera uniform used by this pass (matches WGSL layout: position + viewProjection) +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct CameraUniform { + position: [f32; 4], + view_projection_matrix: [[f32; 4]; 4], +} + +impl CameraUniform { + fn new() -> Self { + Self { + position: [0.0; 4], + view_projection_matrix: Mat4::IDENTITY.to_cols_array_2d(), + } + } + + // Matches renderer CameraUniform math (OPENGL_TO_WGPU * perspective * view) + const OPENGL_TO_WGPU_MATRIX: Mat4 = Mat4::from_cols_array(&[ + 1.0, 0.0, 0.0, 0.0, // + 0.0, 1.0, 0.0, 0.0, // + 0.0, 0.0, 0.5, 0.5, // + 0.0, 0.0, 0.0, 1.0, // + ]); + + fn update_data( + &mut self, + camera_component: &crate::ecs::CameraComponent, + transform_component: &crate::ecs::TransformComponent, + ) { + // Position + self.position = [ + transform_component.position.x, + transform_component.position.y, + transform_component.position.z, + 0.0, + ]; + + // View matrix (Yaw-Pitch-Roll: Y then X then Z; forward = +Z) + let yaw = transform_component.rotation.y.to_radians(); + let pitch = transform_component.rotation.x.to_radians(); + let roll = transform_component.rotation.z.to_radians(); + let q = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll); + let eye = Vec3::new( + transform_component.position.x, + transform_component.position.y, + transform_component.position.z, + ); + let dir = q * Vec3::Z; + let view = Mat4::look_to_rh(eye, dir, Vec3::Y); + + // Projection matrix (with OpenGL->WGPU depth transform) + let fov_y = camera_component.fov.to_radians(); + let aspect = camera_component.aspect.get_value(); + let z_near = camera_component.range.start; + let z_far = camera_component.range.end; + let proj = Self::OPENGL_TO_WGPU_MATRIX * Mat4::perspective_rh(fov_y, aspect, z_near, z_far); + + self.view_projection_matrix = (proj * view).to_cols_array_2d(); + } +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct PerDrawStd140 { + mvp: [[f32; 4]; 4], + model: [[f32; 4]; 4], +} +unsafe impl bytemuck::Zeroable for PerDrawStd140 {} +unsafe impl bytemuck::Pod for PerDrawStd140 {} + +#[repr(C)] +#[derive(Copy, Clone)] +struct MaterialParamsStd140 { + base_color_factor: [f32; 3], + _pad0: f32, + metallic: f32, + roughness: f32, + _pad1: [f32; 2], + emissive_factor: [f32; 3], + _pad2: f32, +} +unsafe impl bytemuck::Zeroable for MaterialParamsStd140 {} +unsafe impl bytemuck::Pod for MaterialParamsStd140 {} + +// Static configuration for per-draw buffering (derive capacities, avoid magic numbers) +const MAX_EXPECTED_PER_DRAW_INSTANCES: usize = 100_000; +const UNIFORM_OFFSET_ALIGNMENT: usize = 256; +const PER_DRAW_STRIDE_BYTES: usize = ((std::mem::size_of::() + + (UNIFORM_OFFSET_ALIGNMENT - 1)) + / UNIFORM_OFFSET_ALIGNMENT) + * UNIFORM_OFFSET_ALIGNMENT; + +pub const MATERIAL_BIND_GROUP_GLOBALS: usize = 0; +pub const MATERIAL_BIND_GROUP_TEXTURES: usize = 1; +pub const MATERIAL_BIND_GROUP_PARAMS: usize = 2; +pub const MATERIAL_BIND_GROUP_PERDRAW: usize = 3; + +// Preallocated buffer structs for hot path optimization +pub(crate) struct VisiblePreDraw { + pub(crate) material_handle: RendererMaterialHandle, + pub(crate) mesh_handle: RendererMeshHandle, + pub(crate) entity_index: u32, + pub(crate) mvp: [[f32; 4]; 4], + pub(crate) model: [[f32; 4]; 4], +} + +// M3: Hello Mesh + per-draw MVP (dynamic offsets) +pub(crate) struct MeshBatch { + pub(crate) mesh_handle: RendererMeshHandle, + pub(crate) instances: Vec, + pub(crate) base_offset_u32: u32, // offset into per-draw ring for first instance +} + +pub(crate) struct GroupCmd { + pub(crate) pipeline: *const wgpu::RenderPipeline, + pub(crate) material_handle: RendererMaterialHandle, + pub(crate) batches: Vec, +} + +pub struct PassScene { + label: String, + offscreen_color_texture: RendererTextureHandle, + depth_texture: RendererTextureHandle, + color_format: wgpu::TextureFormat, + // Per-frame dynamic UBO ring (Milestone 5) + per_draw_stride: u64, + per_draw_capacity: u64, + // Working buffers + visible_pre_draw_buffer: Vec, + groups_buffer: Vec, + staging_buffer: Vec, + // Pass-local state initialized in init(), read every draw + state: Option, +} + +struct PassSceneState { + camera_uniform: CameraUniform, + camera_buffer: wgpu::Buffer, + camera_bind_group: wgpu::BindGroup, + pipeline: PipelineV2, + // Per-draw resources + per_draw_buffer: wgpu::Buffer, + per_draw_bind_group: wgpu::BindGroup, +} + +impl PassScene { + pub fn new( + label: &str, + offscreen_color_texture: RendererTextureHandle, + depth_texture: RendererTextureHandle, + color_format: wgpu::TextureFormat, + ) -> Self { + Self { + label: label.to_string(), + offscreen_color_texture, + depth_texture, + color_format, + per_draw_stride: PER_DRAW_STRIDE_BYTES as u64, + per_draw_capacity: 0, + visible_pre_draw_buffer: Vec::with_capacity(MAX_EXPECTED_PER_DRAW_INSTANCES), + groups_buffer: Vec::with_capacity(2000), + staging_buffer: Vec::with_capacity( + MAX_EXPECTED_PER_DRAW_INSTANCES * PER_DRAW_STRIDE_BYTES, + ), + state: None, + } + } +} + +fn get_state(pass: &mut PassScene) -> &mut PassSceneState { + debug_assert!(pass.state.is_some()); + // SAFETY: initialized once in init(), read every draw + unsafe { pass.state.as_mut().unwrap_unchecked() } +} + +impl Pass for PassScene { + fn get_label(&self) -> &str { + &self.label + } + + fn init(&mut self, renderer: &mut dyn EnginePillRenderer) -> Result<()> { + // [SIMILAR] Prebuilt PSO used; avoid hot-path pipeline creation per TALK + // Shaders: must match bind group layout indices: 0(camera),1(material textures),2(material params),3(per-draw) + let vertex_wgsl = r#" + struct Camera { + position: vec4, + viewProjection: mat4x4, + }; + @group(0) @binding(0) var UCamera: Camera; + struct PerDraw { mvp: mat4x4, model: mat4x4, }; + @group(3) @binding(0) var UPerDraw: PerDraw; + struct VSIn { + @location(0) pos: vec3, + @location(1) uv: vec2, + @location(2) normal: vec3, + }; + struct VSOut { + @builtin(position) pos: vec4, + @location(0) uv: vec2, + @location(1) worldPos: vec3, + @location(2) worldNormal: vec3, + }; + @vertex fn main(input: VSIn) -> VSOut { + var out: VSOut; + let worldPos4 = UPerDraw.model * vec4(input.pos, 1.0); + // TODO: Use a proper normal matrix (inverse-transpose of model) if non-uniform scaling is used. + let n = normalize((UPerDraw.model * vec4(input.normal, 0.0)).xyz); + out.pos = UPerDraw.mvp * vec4(input.pos, 1.0); + out.uv = input.uv; + out.worldPos = worldPos4.xyz; + out.worldNormal = n; + return out; + } + "#; + let fragment_wgsl = r#" + // PBR material textures (set 1) + @group(1) @binding(0) var texBaseColor: texture_2d; + @group(1) @binding(1) var smpBaseColor: sampler; + @group(1) @binding(2) var texNormal: texture_2d; + @group(1) @binding(3) var smpNormal: sampler; + @group(1) @binding(4) var texMetallicRoughness: texture_2d; + @group(1) @binding(5) var smpMetallicRoughness: sampler; + @group(1) @binding(6) var texEmissive: texture_2d; + @group(1) @binding(7) var smpEmissive: sampler; + // PBR params UBO (set 2) + struct MaterialParams { + baseColorFactor: vec3, _pad0: f32, + metallicFactor: f32, roughnessFactor: f32, _pad1: vec2, + emissiveFactor: vec3, _pad2: f32, + } + @group(2) @binding(0) var UMaterial: MaterialParams; + // Per-draw + struct PerDraw { mvp: mat4x4, model: mat4x4, }; + @group(3) @binding(0) var UPerDraw: PerDraw; + struct Camera { + position: vec4, + viewProjection: mat4x4, + }; + @group(0) @binding(0) var UCamera: Camera; + + const PI: f32 = 3.14159265359; + + // Directional lights: vec4(direction.xyz, intensity) + // Direction points toward the surface (light-to-surface). + const LIGHT_DIR0: vec3 = vec3(-0.5, -1.0, -0.2); + const LIGHT_DIR1: vec3 = vec3( 0.5, -0.5, 0.2); + const LIGHT_DIR2: vec3 = vec3( 0.0, 1.0, 0.0); + const LIGHT_COL0: vec4 = vec4( 1.0, 0.5, 0.5, 10.0); // Key + const LIGHT_COL1: vec4 = vec4( 0.5, 0.5, 1.0, 3.0); // Fill + const LIGHT_COL2: vec4 = vec4( 0.1, 0.1, 1.0, 0.2); // Rim + + fn DistributionGGX(N: vec3, H: vec3, roughness: f32) -> f32 { + // Add epsilon to avoid singularities at very low roughness. + let a = max(roughness * roughness, 0.0025); + let a2 = a * a; + let NdotH = max(dot(N, H), 0.0); + let NdotH2 = NdotH * NdotH; + let denom = (NdotH2 * (a2 - 1.0) + 1.0); + return a2 / (PI * denom * denom + 1e-7); + } + + fn GeometrySchlickGGX(NdotV: f32, roughness: f32) -> f32 { + // Heitz's k for direct lighting approximation + let r = roughness + 1.0; + let k = (r * r) / 8.0; + let denom = NdotV * (1.0 - k) + k; + return NdotV / denom; + } + + fn GeometrySmith(N: vec3, V: vec3, L: vec3, roughness: f32) -> f32 { + let NdotV = max(dot(N, V), 0.0); + let NdotL = max(dot(N, L), 0.0); + let ggx2 = GeometrySchlickGGX(NdotV, roughness); + let ggx1 = GeometrySchlickGGX(NdotL, roughness); + return ggx1 * ggx2; + } + + fn fresnelSchlick(cosTheta: f32, F0: vec3) -> vec3 { + return F0 + (vec3(1.0, 1.0, 1.0) - F0) * pow(1.0 - cosTheta, 5.0); + } + + // Directional light accumulator (white light scaled by intensity). + fn accumulateDirLight( + N: vec3, V: vec3, F0: vec3, + albedo: vec3, roughness: f32, metallic: f32, + lightDir: vec3, lightColor: vec4 + ) -> vec3 { + // lightDir is direction from light to surface; incoming L is opposite + let L = normalize(-lightDir.xyz); + let H = normalize(V + L); + let radiance = lightColor.w * lightColor.xyz; + + let NDF = DistributionGGX(N, H, roughness); + let G = GeometrySmith(N, V, L, roughness); + let F = fresnelSchlick(max(dot(H, V), 0.0), F0); + + let kS = F; + var kD = vec3(1.0, 1.0, 1.0) - kS; + kD = kD * (1.0 - metallic); + + let numerator = NDF * G * F; + let denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.0001; + let specular = numerator / vec3(denominator, denominator, denominator); + + let NdotL = max(dot(N, L), 0.0); + return (kD * (albedo / PI) + specular) * radiance * NdotL; + } + + @fragment fn main( + @location(0) uv: vec2, + @location(1) WorldPos: vec3, + @location(2) NormalIn: vec3 + ) -> @location(0) vec4 { + // Surface parameters + var albedo = textureSample(texBaseColor, smpBaseColor, uv).rgb * UMaterial.baseColorFactor; + let mr = textureSample(texMetallicRoughness, smpMetallicRoughness, uv).gb; + var roughness = clamp(mr.x * UMaterial.roughnessFactor, 0.0, 1.0); + // Robustness: keep roughness in a sane range to preserve highlight and stability. + roughness = clamp(roughness, 0.045, 0.99); + let metallic = clamp(mr.y * UMaterial.metallicFactor, 0.0, 1.0); + // TODO: Support normal mapping (tangent space) and AO texture. + let N = normalize(NormalIn); + let V = normalize(UCamera.position.xyz - WorldPos); + + var F0 = vec3(0.04, 0.04, 0.04); + F0 = mix(F0, albedo, vec3(metallic, metallic, metallic)); + + var Lo = vec3(0.0, 0.0, 0.0); + Lo = Lo + accumulateDirLight(N, V, F0, albedo, roughness, metallic, LIGHT_DIR0, LIGHT_COL0); + Lo = Lo + accumulateDirLight(N, V, F0, albedo, roughness, metallic, LIGHT_DIR1, LIGHT_COL1); + Lo = Lo + accumulateDirLight(N, V, F0, albedo, roughness, metallic, LIGHT_DIR2, LIGHT_COL2); + + var color = Lo; + // color = vec3(uv, 0.0); // DBG uv + // color = vec3(N*0.5+0.5); // DBG Normal + // color = vec3(WorldPos); // DBG WorldPos + // color = vec3(roughness); // DBG roughness + return vec4(color, 1.0); + } + "#; + + // Describe bind group layouts for pipeline creation + let bind_groups: Vec> = vec![ + // 0: camera + vec![wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + // 1: material textures (PBR) + vec![ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 4, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 5, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 6, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 7, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + // 2: material params (PBR) + vec![wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + // 3: per-draw dynamic UBO (MVP) + vec![wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: Some( + std::num::NonZeroU64::new(std::mem::size_of::() as u64) + .unwrap(), + ), + }, + count: None, + }], + ]; + + let desc = PipelineV2Desc { + label: Some("scene_pipeline_v2"), + vs: ShaderDesc { + source: vertex_wgsl, + entry_func: "main", + }, + ps: ShaderDesc { + source: fragment_wgsl, + entry_func: "main", + }, + bind_groups, + targets: &[Some(wgpu::ColorTargetState { + format: self.color_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState::default(), + }; + + let pipeline = renderer.create_pipeline_v2(desc)?; + + // Camera buffer and bind group using pipeline's camera layout (group 0) + let (camera_buffer, camera_bind_group) = { + let device = renderer.get_device(); + let camera_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("camera_buffer"), + size: std::mem::size_of::() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("camera_bind_group"), + layout: &pipeline.bind_group_layouts[MATERIAL_BIND_GROUP_GLOBALS], + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: camera_buffer.as_entire_binding(), + }], + }); + (camera_buffer, camera_bind_group) + }; + + // Pre-create per-draw dynamic UBO ring and its bind group with large preallocation + // Creation happens in init; growth/recreation only when capacity is insufficient during draw + self.per_draw_capacity = MAX_EXPECTED_PER_DRAW_INSTANCES as u64; + let size = self.per_draw_stride * self.per_draw_capacity; + let per_draw_buffer = renderer.create_buffer(BufferDesc { + label: Some("per_draw_dynamic_ubo_ring"), + byte_size: size, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + })?; + let per_draw_bind_group = { + let layout_ptr: *const wgpu::BindGroupLayout = + &pipeline.bind_group_layouts[MATERIAL_BIND_GROUP_PERDRAW as usize] as *const _; + let device = renderer.get_device(); + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("per_draw_bind_group"), + layout: unsafe { &*layout_ptr }, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { + buffer: &per_draw_buffer, + offset: 0, + size: Some( + std::num::NonZeroU64::new(std::mem::size_of::() as u64) + .unwrap(), + ), + }), + }], + }) + }; + + self.state = Some(PassSceneState { + camera_uniform: CameraUniform::new(), + camera_buffer, + camera_bind_group, + pipeline, + per_draw_buffer, + per_draw_bind_group, + }); + + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut CommandEncoder, + renderer: &mut dyn EnginePillRenderer, + _frame: &wgpu::SurfaceTexture, + _view: &wgpu::TextureView, + world: &WorldQuery, + ) -> Result<()> { + let active_camera_entity_handle = world.active_camera; + let camera_component_storage = world.camera_components; + let transform_component_storage = world.transform_components; + + // TODO: Update all isDirty=true materials, for each: + // - Color3f -> Update material uniform + // - Texture2D -> Update material bind groups + + // Read active camera + transform + let camera_storage = camera_component_storage + .data + .get(active_camera_entity_handle.data().index as usize) + .unwrap(); + let active_camera_component = camera_storage.as_ref().unwrap(); + let camera_transform_storage = transform_component_storage + .data + .get(active_camera_entity_handle.data().index as usize) + .unwrap(); + let active_camera_transform_component = camera_transform_storage.as_ref().unwrap(); + + // Update camera uniform and write to GPU buffer (no allocations) + let vp_mat_arr: [[f32; 4]; 4] = { + let state = get_state(self); + state + .camera_uniform + .update_data(active_camera_component, active_camera_transform_component); + renderer.get_queue().write_buffer( + &state.camera_buffer, + 0, + bytemuck::bytes_of(&state.camera_uniform), + ); + state.camera_uniform.view_projection_matrix + }; + let clear_color = active_camera_component.clear_color; + + // View-projection matrix + let vp_mat: Mat4 = Mat4::from_cols_array_2d(&vp_mat_arr); + + // Build visible set + self.visible_pre_draw_buffer.clear(); + for render_queue_item in world.render_queue.iter() { + let key = internal::decompose_render_queue_key(render_queue_item.key); + let mesh_handle = + RendererMeshHandle::from_parts(key.mesh_index as u32, key.mesh_version as u32); + let material_handle = RendererMaterialHandle::from_parts( + key.material_index as u32, + key.material_version as u32, + ); + + // Transform + let entity_index = render_queue_item.entity_index as usize; + let transform = transform_component_storage + .data + .get(entity_index) + .unwrap() + .as_ref() + .unwrap(); + let model_arr = internal::get_model_matrix(transform); + let model: Mat4 = Mat4::from_cols_array_2d(&model_arr); + + // NOTE: Temporarily skip AABB-based frustum culling to avoid accessing renderer internals. + + let view_proj: Mat4 = vp_mat; + let mvp: [[f32; 4]; 4] = (view_proj * model).to_cols_array_2d(); + self.visible_pre_draw_buffer.push(VisiblePreDraw { + material_handle, + mesh_handle, + entity_index: render_queue_item.entity_index, + mvp, + model: model.to_cols_array_2d(), + }); + } + + // Sort and group (Pipeline -> Material -> Mesh) + self.visible_pre_draw_buffer.sort_by_key(|v| { + ((v.material_handle.generation() as u64) << 32) | (v.material_handle.index() as u64) + }); + self.groups_buffer.clear(); + // Resolve pipeline pointer for this frame (single pipeline for now) + let pipeline_ptr: *const wgpu::RenderPipeline = { + let state = get_state(self); + &state.pipeline.pipeline as *const _ + }; + for v in &self.visible_pre_draw_buffer { + let need_new_group = self + .groups_buffer + .last() + .map(|g| g.material_handle != v.material_handle) + .unwrap_or(true); + if need_new_group { + self.groups_buffer.push(GroupCmd { + pipeline: pipeline_ptr, + material_handle: v.material_handle, + batches: Vec::new(), + }); + } + let g = self.groups_buffer.last_mut().unwrap(); + if let Some(batch) = g + .batches + .iter_mut() + .find(|b| b.mesh_handle == v.mesh_handle) + { + batch.instances.push(PerDrawStd140 { + mvp: v.mvp, + model: v.model, + }); + } else { + g.batches.push(MeshBatch { + mesh_handle: v.mesh_handle, + instances: vec![PerDrawStd140 { + mvp: v.mvp, + model: v.model, + }], + base_offset_u32: 0, + }); + } + } + + // Per-draw ring buffer setup + let needed: u64 = self + .groups_buffer + .iter() + .map(|g| { + g.batches + .iter() + .map(|b| b.instances.len() as u64) + .sum::() + }) + .sum(); + if self.per_draw_capacity < needed { + #[cfg(debug_assertions)] + { + log::error!( + "Per-draw capacity exceeded: needed={} capacity={}", + needed, + self.per_draw_capacity + ); + } + // Release: proceed; only first (capacity) entries will be used by draws + } + + self.staging_buffer.clear(); + let mut next_offset_u32: u32 = 0; + for g in self.groups_buffer.iter_mut() { + for b in g.batches.iter_mut() { + b.base_offset_u32 = next_offset_u32; + for pd in &b.instances { + self.staging_buffer + .extend_from_slice(bytemuck::bytes_of(pd)); + let pad = + (self.per_draw_stride as usize) - std::mem::size_of::(); + self.staging_buffer.extend(std::iter::repeat(0u8).take(pad)); + next_offset_u32 = next_offset_u32.wrapping_add(self.per_draw_stride as u32); + } + } + } + { + let state_ref: &PassSceneState = unsafe { self.state.as_ref().unwrap_unchecked() }; + renderer + .get_queue() + .write_buffer(&state_ref.per_draw_buffer, 0, &self.staging_buffer); + } + + // Encode offscreen pass + let color_view = renderer + .get_texture(self.offscreen_color_texture) + .create_view(&wgpu::TextureViewDescriptor::default()); + let depth_view = renderer + .get_texture(self.depth_texture) + .create_view(&wgpu::TextureViewDescriptor::default()); + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("m6_offscreen_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &color_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: clear_color.x as f64, + g: clear_color.y as f64, + b: clear_color.z as f64, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &depth_view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + // Immutable view of pass state for binding (no aliasing with groups_buffer immutable borrow) + let state_ref: &PassSceneState = unsafe { self.state.as_ref().unwrap_unchecked() }; + for group in &self.groups_buffer { + rpass.set_pipeline(unsafe { &*group.pipeline }); + rpass.set_bind_group( + MATERIAL_BIND_GROUP_GLOBALS as u32, + &state_ref.camera_bind_group, + &[], + ); + rpass.set_bind_group( + MATERIAL_BIND_GROUP_TEXTURES as u32, + renderer.get_material_texture_bind_group(group.material_handle), + &[], + ); + rpass.set_bind_group( + MATERIAL_BIND_GROUP_PARAMS as u32, + renderer.get_material_params_bind_group(group.material_handle), + &[], + ); + for batch in &group.batches { + let (vbuf, ibuf, index_count) = + renderer.get_mesh_buffers_and_count(batch.mesh_handle); + rpass.set_vertex_buffer(0, vbuf.slice(..)); + rpass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32); + for i in 0..batch.instances.len() { + let offset = batch + .base_offset_u32 + .wrapping_add((i as u32) * (self.per_draw_stride as u32)); + rpass.set_bind_group( + MATERIAL_BIND_GROUP_PERDRAW as u32, + &state_ref.per_draw_bind_group, + &[offset], + ); + rpass.draw_indexed(0..index_count, 0, 0..1); + } + } + } + + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/renderer.rs b/engine/pill_engine/src/graphics/renderer.rs index 75a04e66..71cd57b7 100644 --- a/engine/pill_engine/src/graphics/renderer.rs +++ b/engine/pill_engine/src/graphics/renderer.rs @@ -71,13 +71,39 @@ pub struct MaterialDesc<'a> { pub emissive_tex: Option, } +pub struct RendererTargetDesc { + pub name: String, + pub format: wgpu::TextureFormat, + pub width: u32, + pub height: u32, +} + // --- Renderer trait definition --- +// RendererTexture is renderer-internal; engine code uses handles only. + pub struct PipelineV2 { pub pipeline: wgpu::RenderPipeline, pub bind_group_layouts: Vec, } +// --- Shared pass API (engine-visible) ------------------------------------------------- +pub trait Pass { + fn get_label(&self) -> &str; + fn init(&mut self, renderer: &mut dyn PillRenderer) -> Result<()>; + fn draw( + &mut self, + encoder: &mut wgpu::CommandEncoder, + renderer: &mut dyn PillRenderer, + frame: &wgpu::SurfaceTexture, + view: &wgpu::TextureView, + world: &WorldQuery, + ) -> Result<()>; +} + +// Alias engine render query for passes +pub type WorldQuery<'a> = RenderQuery<'a>; + pub trait PillRenderer { fn new(window: Arc, config: config::Config) -> Self where @@ -95,6 +121,8 @@ pub trait PillRenderer { renderer_material_handle: RendererMaterialHandle, desc: MaterialDesc, ) -> Result; + fn create_render_target(&mut self, desc: RendererTargetDesc) -> Result; + fn create_depth_texture(&mut self, label: &str) -> Result; fn create_mesh(&mut self, name: &str, mesh_data: &MeshData) -> Result; fn create_texture( &mut self, @@ -119,6 +147,19 @@ pub trait PillRenderer { egui_ui: Box, timer: &mut Timer, ) -> Result<()>; + + // --- Engine pass management and helpers --- + fn set_passes(&mut self, passes: Vec>) -> Result<()>; + fn get_surface_format(&self) -> wgpu::TextureFormat; + fn get_device(&self) -> &wgpu::Device; + fn get_queue(&self) -> &wgpu::Queue; + fn get_texture(&self, h: RendererTextureHandle) -> &wgpu::Texture; + fn get_mesh_buffers_and_count( + &self, + h: RendererMeshHandle, + ) -> (&wgpu::Buffer, &wgpu::Buffer, u32); + fn get_material_texture_bind_group(&self, h: RendererMaterialHandle) -> &wgpu::BindGroup; + fn get_material_params_bind_group(&self, h: RendererMaterialHandle) -> &wgpu::BindGroup; } pub type Renderer = Box; diff --git a/engine/pill_engine/src/lib.rs b/engine/pill_engine/src/lib.rs index 27ffc601..9e02f2dd 100644 --- a/engine/pill_engine/src/lib.rs +++ b/engine/pill_engine/src/lib.rs @@ -90,13 +90,13 @@ pub mod internal { }, engine::{Engine, PillGame}, graphics::{ - decompose_render_queue_key, BufferDesc, MaterialDesc, PillRenderer, PipelineV2, - PipelineV2Desc, RenderQueueItem, RenderQueueKey, RenderQueueKeyFields, + decompose_render_queue_key, BufferDesc, MaterialDesc, Pass, PillRenderer, PipelineV2, + PipelineV2Desc, RenderQueueItem, RenderQueueKey, RenderQueueKeyFields, Renderer, RendererBufferHandle, RendererBufferTag, RendererCameraHandle, RendererCameraTag, RendererMaterialHandle, RendererMaterialTag, RendererMeshHandle, RendererMeshTag, RendererPipelineHandle, RendererPipelineTag, RendererPipelineV2Handle, - RendererPipelineV2Tag, RendererTextureHandle, RendererTextureTag, ShaderDesc, - RENDER_QUEUE_KEY_ORDER, + RendererPipelineV2Tag, RendererTargetDesc, RendererTextureHandle, RendererTextureTag, + ShaderDesc, WorldQuery, RENDER_QUEUE_KEY_ORDER, }, resources::{ Mesh, MeshData, MeshHandle, MeshVertex, PBRMaterial, PBRMaterialHandle, diff --git a/engine/pill_renderer/src/lib.rs b/engine/pill_renderer/src/lib.rs index 5b37f506..5baff22a 100644 --- a/engine/pill_renderer/src/lib.rs +++ b/engine/pill_renderer/src/lib.rs @@ -2,11 +2,6 @@ pub mod egui; pub mod instance; -pub mod pass_compose; -pub mod pass_overlay_depth; -pub mod pass_overlay_logo; -pub mod pass_overlay_uv; -pub mod pass_scene; pub mod renderer; pub mod resource_manager; pub mod resource_snapshot; @@ -17,5 +12,3 @@ pub mod resources; pub use renderer::*; pub use instance::Instance; -pub use pass_compose::PassCompose; -pub use pass_overlay_uv::PassOverlayUV; diff --git a/engine/pill_renderer/src/pass_scene.rs b/engine/pill_renderer/src/pass_scene.rs index ae8d12a1..dc482e36 100644 --- a/engine/pill_renderer/src/pass_scene.rs +++ b/engine/pill_renderer/src/pass_scene.rs @@ -1,7 +1,7 @@ -use crate::renderer::{Pass, Renderer, WorldQuery}; use crate::resource_manager::ResourceManager; use crate::resources::CameraUniform; use crate::resources::RendererTexture; +use pill_engine::internal::{Pass, Renderer, WorldQuery}; use std::sync::Arc; use anyhow::{Error, Result}; diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_renderer/src/renderer.rs index 7ef6baa6..fa549ba4 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_renderer/src/renderer.rs @@ -3,11 +3,6 @@ // https://github.com/emilk/egui/discussions/3067 use crate::{ - pass_compose::PassCompose, - pass_overlay_depth::PassOverlayDepth, - pass_overlay_logo::PassOverlayLogo, - pass_overlay_uv::PassOverlayUV, - pass_scene::PassScene, resource_manager::ResourceManager, resources::{ RendererCamera, RendererMaterial, RendererMaterialParamsStd140, RendererMaterialTextures, @@ -19,10 +14,11 @@ use std::sync::Arc; use pill_engine::internal::{ get_renderer_resource_handle_from_camera_component, BufferDesc, CameraComponent, - ComponentStorage, EntityHandle, MaterialDesc, MeshData, PillRenderer, PipelineV2, + ComponentStorage, EntityHandle, MaterialDesc, MeshData, Pass, PillRenderer, PipelineV2, PipelineV2Desc, RenderQueueItem, RendererBufferHandle, RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, RendererPipelineHandle, RendererPipelineV2Handle, - RendererTextureHandle, ShaderDesc, TextureType, TransformComponent, RENDER_QUEUE_KEY_ORDER, + RendererTargetDesc, RendererTextureHandle, ShaderDesc, TextureType, TransformComponent, + WorldQuery, RENDER_QUEUE_KEY_ORDER, }; use pill_core::{Handle, PillSlotMapKey, PillStyle, RendererError, Timer}; @@ -45,9 +41,6 @@ pub const MAX_INSTANCE_BATCH_SIZE: usize = 10000; // Maximum number of instances pub const INITIAL_INSTANCE_VECTOR_CAPACITY: usize = 10000; // M2 inline draw: no MeshDrawer/instance batching -// Default resource handle - Master pipeline -pub const MASTER_PIPELINE_HANDLE: RendererPipelineHandle = Handle::INVALID; - fn compile_glsl_to_wgsl(source: &str, stage: naga::ShaderStage) -> Result { let mut frontend = glsl::Frontend::default(); let options = glsl::Options::from(stage); @@ -109,19 +102,6 @@ fn create_render_target( } } -pub trait Pass { - fn get_label(&self) -> &str; - fn init(&mut self, renderer: &mut Renderer) -> Result<()>; - fn draw( - &mut self, - encoder: &mut wgpu::CommandEncoder, - renderer: &mut Renderer, - frame: &wgpu::SurfaceTexture, - view: &wgpu::TextureView, - world: &WorldQuery, - ) -> Result<()>; -} - pub struct DeviceContext { pub(crate) config: config::Config, // Window @@ -135,15 +115,6 @@ pub struct DeviceContext { pub(crate) surface_configuration: wgpu::SurfaceConfiguration, } -// Local world query for passes (avoids depending on pill_engine::graphics visibility) -// TODO: Rename to WorldView, SceneView, ... -pub struct WorldQuery<'a> { - pub active_camera: EntityHandle, - pub render_queue: &'a Vec, - pub camera_components: &'a ComponentStorage, - pub transform_components: &'a ComponentStorage, -} - pub struct State { passes: Vec>, egui_renderer: crate::egui::EguiRenderer, // TODO: Separate system adding Pass @@ -151,13 +122,10 @@ pub struct State { pub(crate) resource_manager: ResourceManager, pub(crate) color_format: wgpu::TextureFormat, pub(crate) depth_format: wgpu::TextureFormat, - pub(crate) depth_texture: Arc, - pub(crate) offscreen_color_texture: Arc, + // pub(crate) depth_texture: Arc, + // pub(crate) offscreen_color_texture: Arc, // Prebuilt PSO handle // [SIMILAR] Prebuilt once; no per-draw pipeline churn per TALK - pub(crate) master_pipeline_handle: RendererPipelineHandle, - // resources - pub(crate) tex_logo: RendererTextureHandle, } pub struct Renderer { @@ -295,12 +263,12 @@ impl PillRenderer for Renderer { let mut resource_manager = ResourceManager::new(); // Create depth and color texture - let depth_texture = RendererTexture::new_depth_texture( - &ctx.device, - &ctx.surface_configuration, - "depth_texture", - ) - .unwrap(); + // let depth_texture = RendererTexture::new_depth_texture( + // &ctx.device, + // &ctx.surface_configuration, + // "depth_texture", + // ) + // .unwrap(); // Use Rgba16Float for HDR color buffers; it's the common, well-supported, // performant choice. Reserve Rgba32Float for niche cases needing extreme @@ -311,13 +279,13 @@ impl PillRenderer for Renderer { let depth_format = wgpu::TextureFormat::Depth32Float; // Milestone 6: Create offscreen color target (RENDER_ATTACHMENT | TEXTURE_BINDING) - let offscreen_color_texture = Arc::new(create_render_target( - &ctx.device, - color_format, - ctx.surface_configuration.width, - ctx.surface_configuration.height, - "offscreen_color", - )); + // let offscreen_color_texture = Arc::new(create_render_target( + // &ctx.device, + // color_format, + // ctx.surface_configuration.width, + // ctx.surface_configuration.height, + // "offscreen_color", + // )); let egui_renderer = EguiRenderer::new( &ctx.device, @@ -327,105 +295,7 @@ impl PillRenderer for Renderer { ctx.window_ref.clone(), ); - // Build static shader modules and pipeline once ([SIMILAR] prebuilt PSO per TALK) - let vertex_wgsl = r#" - struct Camera { position: vec4, view_projection_matrix: mat4x4, }; - @group(0) @binding(0) var GCamera: Camera; - - struct PerDraw { mvp: mat4x4, }; - @group(3) @binding(0) var UPerDraw: PerDraw; - - struct VSIn { @location(0) pos: vec3, @location(1) uv: vec2, }; - struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; - - @vertex fn main(input: VSIn) -> VSOut { - var out: VSOut; - out.pos = UPerDraw.mvp * vec4(input.pos, 1.0); - out.uv = input.uv; - return out; - } - "#; - let fragment_wgsl = r#" - // Material set(1): - @group(1) @binding(0) var tex_diffuse: texture_2d; - @group(1) @binding(1) var smp_diffuse: sampler; - @group(1) @binding(2) var tex_normal: texture_2d; - @group(1) @binding(3) var smp_normal: sampler; - - // Material parameters set(2) - pack tint.rgb + specularity in a single vec4 - struct MaterialParams { tint_spec: vec4, } - @group(2) @binding(0) var UMaterial: MaterialParams; - - // Per-draw (set 3): MVP only - struct PerDraw { mvp: mat4x4, }; - @group(3) @binding(0) var UPerDraw: PerDraw; - - @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { - let albedo = textureSample(tex_diffuse, smp_diffuse, uv); - let tinted = vec4(UMaterial.tint_spec.rgb, 1.0); - let spec_boost = 0.5 + 0.5 * UMaterial.tint_spec.a; - let color = albedo * tinted * spec_boost; - return vec4(color.rgb, 1.0); - } - "#; - let vertex_shader = ctx - .device - .create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("m2_vertex_shader"), - source: wgpu::ShaderSource::Wgsl(vertex_wgsl.into()), - }); - let fragment_shader = ctx - .device - .create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("m2_fragment_shader"), - source: wgpu::ShaderSource::Wgsl(fragment_wgsl.into()), - }); - let default_pipeline = RendererPipeline::new( - &ctx.device, - vertex_shader, - fragment_shader, - color_format, - Some(depth_format), - &[RendererMesh::data_layout_descriptor()], - ) - .unwrap(); - let master_handle = resource_manager.pipelines.insert(default_pipeline); - // Create state - let _per_draw_bind_group_layout = - ctx.device - .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("per_draw_bind_group_layout_unused"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: true, - min_binding_size: Some(std::num::NonZeroU64::new(80).unwrap()), - }, - count: None, - }], - }); - - // Ideally Client submitted pass via API - // Create logo texture via renderer API and pass handle into overlay factory - let logo_image = image::open( - "/Users/mk/dev/demo/Pill-Engine/engine/pill_renderer/res/pill_logo_horizontal_white.png", - ) - .expect("failed to load overlay logo image"); - - let tex_logo = { - let tex = RendererTexture::new_texture( - &ctx.device, - &ctx.queue, - Some("overlay_logo"), - &logo_image, - TextureType::Gamma, - ) - .expect("failed to create overlay logo texture"); - resource_manager.textures.insert(tex) - }; State { passes: vec![], @@ -436,70 +306,19 @@ impl PillRenderer for Renderer { // Renderer variables color_format, depth_format, - depth_texture: Arc::new(depth_texture), - offscreen_color_texture, + // depth_texture: Arc::new(depth_texture), + // offscreen_color_texture, // Scene pass owns per-draw ring and working buffers now - // Prebuilt PSO handle - master_pipeline_handle: master_handle, - // Overlays - tex_logo, } }; Self { state, ctx } } fn init(&mut self) -> Result<()> { - self.state.passes.clear(); - // Ensure default textures are created before any pass init that may need them self.state .resource_manager .ensure_default_textures(&self.ctx.device, &self.ctx.queue); - - // Scene pass first: renders into offscreen targets - self.state.passes.push(Box::new(PassScene::new( - "scene", - self.state.offscreen_color_texture.clone(), - self.state.depth_texture.clone(), - ))); - - self.state.passes.push(Box::new(PassCompose::new( - "compose", - self.state.offscreen_color_texture.clone(), - self.ctx.surface_configuration.format, - ))); - - self.state.passes.push(Box::new(PassOverlayUV::new( - "overlay_uv", - [0.75, 0.75, 0.95, 0.95], - self.ctx.surface_configuration.format, - ))); - - let h: f32 = 0.04; // Logo height - self.state.passes.push(Box::new(PassOverlayLogo::new( - "overlay_logo", - [0.98 - 3. * h, 0.02, 0.98, 0.02 + h], // bottom right - [1.0, 1.0, 1.0, 1.0], - self.state.tex_logo, - self.ctx.surface_configuration.format, - ))); - - self.state.passes.push(Box::new(PassOverlayDepth::new( - "overlay_depth", - [0.75, 0.50, 0.95, 0.70], - [1.0, 1.0, 1.0, 1.0], - self.state.depth_texture.clone(), - self.ctx.surface_configuration.format, - ))); - - // Initialize passes - call init on each pass - // Temporarily move passes out to avoid borrowing conflicts - let mut passes = std::mem::take(&mut self.state.passes); - for pass in passes.iter_mut() { - let _ = pass.init(self); - } - self.state.passes = passes; - Ok(()) } @@ -1023,6 +842,18 @@ impl PillRenderer for Renderer { Ok(renderer_material_handle) } + fn create_render_target(&mut self, desc: RendererTargetDesc) -> Result { + let tex = create_render_target( + &self.ctx.device, + desc.format, + desc.width, + desc.height, + desc.name.as_str(), + ); + let handle = self.state.resource_manager.textures.insert(tex); + Ok(handle) + } + fn create_mesh(&mut self, name: &str, mesh_data: &MeshData) -> Result { let mesh = RendererMesh::new(&self.ctx.device, name, mesh_data)?; let handle = self.state.resource_manager.meshes.insert(mesh); @@ -1048,6 +879,15 @@ impl PillRenderer for Renderer { Ok(handle) } + fn create_depth_texture(&mut self, label: &str) -> Result { + let tex = RendererTexture::new_depth_texture( + &self.ctx.device, + &self.ctx.surface_configuration, + label, + )?; + Ok(self.state.resource_manager.textures.insert(tex)) + } + fn create_camera(&mut self) -> Result { let camera_bind_group_layout = self.ctx @@ -1111,26 +951,30 @@ impl PillRenderer for Renderer { self.ctx .surface .configure(&self.ctx.device, &self.ctx.surface_configuration); - self.state.depth_texture = Arc::new( - RendererTexture::new_depth_texture( - &self.ctx.device, - &self.ctx.surface_configuration, - "depth_texture", - ) - .unwrap(), - ); + // self.state.depth_texture = Arc::new( + // RendererTexture::new_depth_texture( + // &self.ctx.device, + // &self.ctx.surface_configuration, + // "depth_texture", + // ) + // .unwrap(), + // ); // ================================ // Recreate offscreen color target and reinitialize passes - self.state.offscreen_color_texture = Arc::new(create_render_target( - &self.ctx.device, - self.state.color_format, - self.ctx.surface_configuration.width, - self.ctx.surface_configuration.height, - "offscreen_color", - )); - // Reinitialize all passes (including compose pass) - let _ = self.init(); + // self.state.offscreen_color_texture = Arc::new(create_render_target( + // &self.ctx.device, + // self.state.color_format, + // self.ctx.surface_configuration.width, + // self.ctx.surface_configuration.height, + // "offscreen_color", + // )); + // Reinitialize existing passes in place (engine may have set custom passes) + let mut passes = std::mem::take(&mut self.state.passes); + for pass in passes.iter_mut() { + let _ = pass.init(self); + } + self.state.passes = passes; // Old offscreen texture/view/sampler are dropped when replaced; wgpu defers actual GPU resource // destruction until safe. See optional early reclamation via device.poll: // https://docs.rs/wgpu/latest/wgpu/struct.Device.html#method.poll @@ -1236,6 +1080,65 @@ impl PillRenderer for Renderer { self.state.egui_renderer.handle_input(event); Ok(()) } + + // --- Engine pass management and helpers --- + fn set_passes(&mut self, mut passes: Vec>) -> Result<()> { + for pass in passes.iter_mut() { + let _ = pass.init(self); + } + self.state.passes = passes; + Ok(()) + } + + fn get_surface_format(&self) -> wgpu::TextureFormat { + self.ctx.surface_configuration.format + } + + fn get_device(&self) -> &wgpu::Device { + &self.ctx.device + } + + fn get_texture(&self, h: RendererTextureHandle) -> &wgpu::Texture { + let tex = self + .state + .resource_manager + .textures + .get(h) + .expect("texture"); + &tex.texture + } + + fn get_mesh_buffers_and_count( + &self, + h: RendererMeshHandle, + ) -> (&wgpu::Buffer, &wgpu::Buffer, u32) { + let mesh = self.state.resource_manager.meshes.get(h).expect("mesh"); + (&mesh.vertex_buffer, &mesh.index_buffer, mesh.index_count) + } + + fn get_material_texture_bind_group(&self, h: RendererMaterialHandle) -> &wgpu::BindGroup { + let mat = self + .state + .resource_manager + .materials + .get(h) + .expect("material"); + &mat.texture_bind_group + } + + fn get_material_params_bind_group(&self, h: RendererMaterialHandle) -> &wgpu::BindGroup { + let mat = self + .state + .resource_manager + .materials + .get(h) + .expect("material"); + &mat.param_bind_group + } + + fn get_queue(&self) -> &wgpu::Queue { + &self.ctx.queue + } } impl Renderer { @@ -1251,7 +1154,24 @@ impl Renderer { &self.ctx.surface } - pub fn get_surface_view(&self) -> &wgpu::TextureView { - &self.state.offscreen_color_texture.texture_view + // Lightweight resolve helpers for engine passes + pub fn get_texture_view(&self, h: RendererTextureHandle) -> &wgpu::TextureView { + let tex = self + .state + .resource_manager + .textures + .get(h) + .expect("texture"); + &tex.texture_view + } + + pub fn get_texture_sampler(&self, h: RendererTextureHandle) -> &wgpu::Sampler { + let tex = self + .state + .resource_manager + .textures + .get(h) + .expect("texture"); + &tex.sampler } } From 40bb2f3930ab38e802054109568253e5cc99f1b1 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Sat, 15 Nov 2025 05:00:26 +0100 Subject: [PATCH 59/69] consolidated renderer in engine with wgpu rhi --- engine/Cargo.toml | 1 - engine/pill_engine/Cargo.toml | 3 + .../src/ecs/systems/rendering_system.rs | 2 +- engine/pill_engine/src/graphics/pass_scene.rs | 7 +- engine/pill_engine/src/graphics/renderer.rs | 6 +- engine/pill_engine/src/lib.rs | 3 + .../src/renderer}/DESIGN.md | 0 .../src/renderer}/DESIGN_DEPTH_PREPASS.md | 0 .../src/renderer}/PLAN_v0.md | 0 .../src/renderer}/PLAN_v1.md | 0 .../src/renderer}/TALK_0.md | 0 .../src => pill_engine/src/renderer}/egui.rs | 25 +- .../src/renderer}/instance.rs | 0 .../src => pill_engine/src/renderer}/lib.rs | 0 engine/pill_engine/src/renderer/mod.rs | 10 + .../renderer}/renderer_resource_storage.rs | 6 +- .../src/renderer}/resource_manager.rs | 15 +- .../src/renderer}/resources/mod.rs | 0 .../renderer}/resources/renderer_camera.rs | 2 +- .../renderer}/resources/renderer_material.rs | 2 +- .../src/renderer}/resources/renderer_mesh.rs | 2 +- .../renderer}/resources/renderer_pipeline.rs | 0 .../renderer}/resources/renderer_texture.rs | 2 +- .../src/renderer/wgpu.rs} | 55 +- .../src/resources/gpu_resources.rs | 121 +++ engine/pill_engine/src/resources/mod.rs | 2 + .../src/resources/resource_manager.rs | 13 +- engine/pill_renderer/Cargo.toml | 39 - .../res/pill_logo_horizontal_white.png | Bin 9434 -> 0 bytes engine/pill_renderer/src/pass_compose.rs | 199 ----- .../pill_renderer/src/pass_overlay_depth.rs | 455 ---------- engine/pill_renderer/src/pass_overlay_logo.rs | 275 ------ engine/pill_renderer/src/pass_overlay_uv.rs | 182 ---- engine/pill_renderer/src/pass_scene.rs | 839 ------------------ engine/pill_renderer/src/resource_snapshot.rs | 31 - engine/pill_standalone/Cargo.toml | 6 +- engine/pill_standalone/src/lib.rs | 3 + engine/pill_standalone/src/main.rs | 8 +- 38 files changed, 213 insertions(+), 2101 deletions(-) rename engine/{pill_renderer => pill_engine/src/renderer}/DESIGN.md (100%) rename engine/{pill_renderer => pill_engine/src/renderer}/DESIGN_DEPTH_PREPASS.md (100%) rename engine/{pill_renderer => pill_engine/src/renderer}/PLAN_v0.md (100%) rename engine/{pill_renderer => pill_engine/src/renderer}/PLAN_v1.md (100%) rename engine/{pill_renderer => pill_engine/src/renderer}/TALK_0.md (100%) rename engine/{pill_renderer/src => pill_engine/src/renderer}/egui.rs (86%) rename engine/{pill_renderer/src => pill_engine/src/renderer}/instance.rs (100%) rename engine/{pill_renderer/src => pill_engine/src/renderer}/lib.rs (100%) create mode 100644 engine/pill_engine/src/renderer/mod.rs rename engine/{pill_renderer/src => pill_engine/src/renderer}/renderer_resource_storage.rs (96%) rename engine/{pill_renderer/src => pill_engine/src/renderer}/resource_manager.rs (95%) rename engine/{pill_renderer/src => pill_engine/src/renderer}/resources/mod.rs (100%) rename engine/{pill_renderer/src => pill_engine/src/renderer}/resources/renderer_camera.rs (98%) rename engine/{pill_renderer/src => pill_engine/src/renderer}/resources/renderer_material.rs (95%) rename engine/{pill_renderer/src => pill_engine/src/renderer}/resources/renderer_mesh.rs (97%) rename engine/{pill_renderer/src => pill_engine/src/renderer}/resources/renderer_pipeline.rs (100%) rename engine/{pill_renderer/src => pill_engine/src/renderer}/resources/renderer_texture.rs (99%) rename engine/{pill_renderer/src/renderer.rs => pill_engine/src/renderer/wgpu.rs} (96%) create mode 100644 engine/pill_engine/src/resources/gpu_resources.rs delete mode 100644 engine/pill_renderer/Cargo.toml delete mode 100644 engine/pill_renderer/res/pill_logo_horizontal_white.png delete mode 100644 engine/pill_renderer/src/pass_compose.rs delete mode 100644 engine/pill_renderer/src/pass_overlay_depth.rs delete mode 100644 engine/pill_renderer/src/pass_overlay_logo.rs delete mode 100644 engine/pill_renderer/src/pass_overlay_uv.rs delete mode 100644 engine/pill_renderer/src/pass_scene.rs delete mode 100644 engine/pill_renderer/src/resource_snapshot.rs create mode 100644 engine/pill_standalone/src/lib.rs diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 5a432d79..894ecd1e 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -3,7 +3,6 @@ resolver = "2" members = [ "pill_core", "pill_engine", - "pill_renderer", "pill_standalone", "/Users/mk/dev/demo/pill_demo", ### Game project crate (This will be changed by Pill Launcher on build to allow proper compilation of game project) ] diff --git a/engine/pill_engine/Cargo.toml b/engine/pill_engine/Cargo.toml index 8f62b31a..a0560113 100644 --- a/engine/pill_engine/Cargo.toml +++ b/engine/pill_engine/Cargo.toml @@ -27,6 +27,8 @@ rodio = { version = "0.20", features = ["wav", "mp3"] } # Rendering wgpu = { version = "0.20.1", features = [ "spirv" ] } +egui-wgpu = "0.28.1" +egui-winit = "0.28.1" # Other readonly = "0.2" @@ -45,6 +47,7 @@ bincode = "1.0" gilrs = "0.11.0" once_cell = "1.21.3" glam = { version = "0.27" } +pollster = "0.3" [build-dependencies] anyhow = "1.0" diff --git a/engine/pill_engine/src/ecs/systems/rendering_system.rs b/engine/pill_engine/src/ecs/systems/rendering_system.rs index a32a4053..3baf8d4d 100644 --- a/engine/pill_engine/src/ecs/systems/rendering_system.rs +++ b/engine/pill_engine/src/ecs/systems/rendering_system.rs @@ -399,7 +399,7 @@ fn init_default_resources(engine: &mut Engine) -> Result<(), Error> { // Pill logo (overlay) texture let pill_logo = Box::new(*include_bytes!( - "../../../../pill_renderer/res/pill_logo_horizontal_white.png" + "../../../../../media/logo/pill_logo_horizontal_white.png" )); let mut tex_logo = Texture::new( "pill_logo_horizontal_white", diff --git a/engine/pill_engine/src/graphics/pass_scene.rs b/engine/pill_engine/src/graphics/pass_scene.rs index d70f96e2..3063ab7d 100644 --- a/engine/pill_engine/src/graphics/pass_scene.rs +++ b/engine/pill_engine/src/graphics/pass_scene.rs @@ -1,10 +1,11 @@ +use crate::ecs::get_model_matrix; +use crate::graphics::decompose_render_queue_key; use crate::graphics::renderer::{ Pass, PillRenderer as EnginePillRenderer, PipelineV2, PipelineV2Desc, ShaderDesc, WorldQuery, }; use crate::graphics::{ BufferDesc, RendererMaterialHandle, RendererMeshHandle, RendererTextureHandle, }; -use crate::internal; use anyhow::Result; use glam::{EulerRot, Mat4, Quat, Vec3, Vec4}; use pill_core::PillSlotMapKey; @@ -597,7 +598,7 @@ impl Pass for PassScene { // Build visible set self.visible_pre_draw_buffer.clear(); for render_queue_item in world.render_queue.iter() { - let key = internal::decompose_render_queue_key(render_queue_item.key); + let key = decompose_render_queue_key(render_queue_item.key); let mesh_handle = RendererMeshHandle::from_parts(key.mesh_index as u32, key.mesh_version as u32); let material_handle = RendererMaterialHandle::from_parts( @@ -613,7 +614,7 @@ impl Pass for PassScene { .unwrap() .as_ref() .unwrap(); - let model_arr = internal::get_model_matrix(transform); + let model_arr = get_model_matrix(transform); let model: Mat4 = Mat4::from_cols_array_2d(&model_arr); // NOTE: Temporarily skip AABB-based frustum culling to avoid accessing renderer internals. diff --git a/engine/pill_engine/src/graphics/renderer.rs b/engine/pill_engine/src/graphics/renderer.rs index 71cd57b7..79afdb0f 100644 --- a/engine/pill_engine/src/graphics/renderer.rs +++ b/engine/pill_engine/src/graphics/renderer.rs @@ -154,10 +154,8 @@ pub trait PillRenderer { fn get_device(&self) -> &wgpu::Device; fn get_queue(&self) -> &wgpu::Queue; fn get_texture(&self, h: RendererTextureHandle) -> &wgpu::Texture; - fn get_mesh_buffers_and_count( - &self, - h: RendererMeshHandle, - ) -> (&wgpu::Buffer, &wgpu::Buffer, u32); + fn get_mesh_buffers_and_count(&self, h: RendererMeshHandle) + -> (&wgpu::Buffer, &wgpu::Buffer, u32); fn get_material_texture_bind_group(&self, h: RendererMaterialHandle) -> &wgpu::BindGroup; fn get_material_params_bind_group(&self, h: RendererMaterialHandle) -> &wgpu::BindGroup; } diff --git a/engine/pill_engine/src/lib.rs b/engine/pill_engine/src/lib.rs index 9e02f2dd..c40d313d 100644 --- a/engine/pill_engine/src/lib.rs +++ b/engine/pill_engine/src/lib.rs @@ -4,6 +4,7 @@ mod config; mod ecs; mod engine; mod graphics; +mod renderer; mod resources; // --- Macros --- @@ -103,4 +104,6 @@ pub mod internal { ResourceLoadType, ResourceManager, Texture, TextureHandle, TextureType, }, }; + // Expose the concrete wgpu renderer type for standalone host + pub use crate::renderer::Renderer as WgpuRenderer; } diff --git a/engine/pill_renderer/DESIGN.md b/engine/pill_engine/src/renderer/DESIGN.md similarity index 100% rename from engine/pill_renderer/DESIGN.md rename to engine/pill_engine/src/renderer/DESIGN.md diff --git a/engine/pill_renderer/DESIGN_DEPTH_PREPASS.md b/engine/pill_engine/src/renderer/DESIGN_DEPTH_PREPASS.md similarity index 100% rename from engine/pill_renderer/DESIGN_DEPTH_PREPASS.md rename to engine/pill_engine/src/renderer/DESIGN_DEPTH_PREPASS.md diff --git a/engine/pill_renderer/PLAN_v0.md b/engine/pill_engine/src/renderer/PLAN_v0.md similarity index 100% rename from engine/pill_renderer/PLAN_v0.md rename to engine/pill_engine/src/renderer/PLAN_v0.md diff --git a/engine/pill_renderer/PLAN_v1.md b/engine/pill_engine/src/renderer/PLAN_v1.md similarity index 100% rename from engine/pill_renderer/PLAN_v1.md rename to engine/pill_engine/src/renderer/PLAN_v1.md diff --git a/engine/pill_renderer/TALK_0.md b/engine/pill_engine/src/renderer/TALK_0.md similarity index 100% rename from engine/pill_renderer/TALK_0.md rename to engine/pill_engine/src/renderer/TALK_0.md diff --git a/engine/pill_renderer/src/egui.rs b/engine/pill_engine/src/renderer/egui.rs similarity index 86% rename from engine/pill_renderer/src/egui.rs rename to engine/pill_engine/src/renderer/egui.rs index 58728a21..432645a0 100644 --- a/engine/pill_renderer/src/egui.rs +++ b/engine/pill_engine/src/renderer/egui.rs @@ -2,15 +2,15 @@ use std::sync::Arc; use egui::epaint::Shadow; use egui::{Context, Visuals}; -use egui_wgpu::ScreenDescriptor; use egui_wgpu::Renderer; +use egui_wgpu::ScreenDescriptor; +use anyhow::{Error, Result}; use egui_winit::State; use pill_core::Timer; use wgpu::{CommandEncoder, Device, Queue, TextureFormat, TextureView}; use winit::event::WindowEvent; use winit::window::Window; -use anyhow::{Error, Result}; pub struct EguiRenderer { pub context: Context, @@ -32,7 +32,7 @@ impl EguiRenderer { let egui_context = egui::Context::default(); let id = egui_context.viewport_id(); const BORDER_RADIUS: f32 = 2.0; - + let visuals = egui::Visuals { window_rounding: egui::Rounding::same(BORDER_RADIUS), window_shadow: egui::Shadow::NONE, @@ -54,7 +54,7 @@ impl EguiRenderer { state: egui_state, renderer: egui_renderer, window_scale_factor, - window + window, } } @@ -82,18 +82,23 @@ impl EguiRenderer { timer.record("Handle platform output"); - self.state.handle_platform_output(&window, full_output.platform_output); + self.state + .handle_platform_output(&window, full_output.platform_output); timer.record("Tesselate and update textures"); - let tris = self.context.tessellate(full_output.shapes, full_output.pixels_per_point); + let tris = self + .context + .tessellate(full_output.shapes, full_output.pixels_per_point); for (id, image_delta) in &full_output.textures_delta.set { - self.renderer.update_texture(&device, &queue, *id, &image_delta); + self.renderer + .update_texture(&device, &queue, *id, &image_delta); } timer.record("Update buffers and record render pass"); - self.renderer.update_buffers(&device, &queue, encoder, &tris, &screen_descriptor); + self.renderer + .update_buffers(&device, &queue, encoder, &tris, &screen_descriptor); let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { color_attachments: &[Some(wgpu::RenderPassColorAttachment { @@ -112,7 +117,8 @@ impl EguiRenderer { timer.record("Render"); - self.renderer.render(&mut render_pass, &tris, &screen_descriptor); + self.renderer + .render(&mut render_pass, &tris, &screen_descriptor); drop(render_pass); for x in &full_output.textures_delta.free { @@ -122,4 +128,3 @@ impl EguiRenderer { Ok(()) } } - diff --git a/engine/pill_renderer/src/instance.rs b/engine/pill_engine/src/renderer/instance.rs similarity index 100% rename from engine/pill_renderer/src/instance.rs rename to engine/pill_engine/src/renderer/instance.rs diff --git a/engine/pill_renderer/src/lib.rs b/engine/pill_engine/src/renderer/lib.rs similarity index 100% rename from engine/pill_renderer/src/lib.rs rename to engine/pill_engine/src/renderer/lib.rs diff --git a/engine/pill_engine/src/renderer/mod.rs b/engine/pill_engine/src/renderer/mod.rs new file mode 100644 index 00000000..a2bff96f --- /dev/null +++ b/engine/pill_engine/src/renderer/mod.rs @@ -0,0 +1,10 @@ +#![cfg_attr(debug_assertions, allow(dead_code, unused_imports))] + +pub mod egui; +pub mod wgpu; +pub mod resource_manager; +pub mod resources; + +pub use wgpu::Renderer; + + diff --git a/engine/pill_renderer/src/renderer_resource_storage.rs b/engine/pill_engine/src/renderer/renderer_resource_storage.rs similarity index 96% rename from engine/pill_renderer/src/renderer_resource_storage.rs rename to engine/pill_engine/src/renderer/renderer_resource_storage.rs index e4e6de1a..2927c26a 100644 --- a/engine/pill_renderer/src/renderer_resource_storage.rs +++ b/engine/pill_engine/src/renderer/renderer_resource_storage.rs @@ -1,10 +1,10 @@ -use crate::resources::{ +use super::resources::{ RendererCamera, RendererMaterial, RendererMesh, RendererPipeline, RendererTexture, }; use pill_core::PillSlotMap; -use pill_engine::internal::{ +use crate::graphics::{ RendererBufferHandle, RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, RendererPipelineHandle, RendererPipelineV2Handle, RendererTextureHandle, }; @@ -28,7 +28,7 @@ pub struct RendererResourceStorage { } impl RendererResourceStorage { - pub fn new(config: &config::Config) -> Self { + pub fn new(config: &crate::config::Config) -> Self { let max_pipeline_count = config .get_int("MAX_PIPELINES") .unwrap_or(MAX_PIPELINES as i64) as usize; diff --git a/engine/pill_renderer/src/resource_manager.rs b/engine/pill_engine/src/renderer/resource_manager.rs similarity index 95% rename from engine/pill_renderer/src/resource_manager.rs rename to engine/pill_engine/src/renderer/resource_manager.rs index 0df7a5e2..3a37509a 100644 --- a/engine/pill_renderer/src/resource_manager.rs +++ b/engine/pill_engine/src/renderer/resource_manager.rs @@ -1,15 +1,16 @@ use std::collections::HashMap; +use crate::graphics::{ + RendererBufferHandle, RendererCameraHandle, RendererMeshHandle, RendererPipelineHandle, + RendererTextureHandle, +}; +use crate::resources::TextureType; use pill_core::{ Handle, RendererBufferTag, RendererCameraTag, RendererMaterialTag, RendererMeshTag, RendererPipelineTag, RendererTextureTag, ResourcePool, }; -use pill_engine::internal::{ - RendererBufferHandle, RendererCameraHandle, RendererMeshHandle, RendererPipelineHandle, - RendererTextureHandle, -}; -use crate::resources::{ +use crate::renderer::resources::{ RendererCamera, RendererMaterial, RendererMesh, RendererPipeline, RendererTexture, }; use image::{DynamicImage, Rgba, RgbaImage}; @@ -89,7 +90,7 @@ impl ResourceManager { queue, Some("default_color"), &dyn_img, - pill_engine::internal::TextureType::Gamma, + TextureType::Gamma, ) .expect("create default color tex"); let h = self.textures.insert(tex); @@ -105,7 +106,7 @@ impl ResourceManager { queue, Some("default_normal"), &dyn_img, - pill_engine::internal::TextureType::Linear, + TextureType::Linear, ) .expect("create default normal tex"); let h = self.textures.insert(tex); diff --git a/engine/pill_renderer/src/resources/mod.rs b/engine/pill_engine/src/renderer/resources/mod.rs similarity index 100% rename from engine/pill_renderer/src/resources/mod.rs rename to engine/pill_engine/src/renderer/resources/mod.rs diff --git a/engine/pill_renderer/src/resources/renderer_camera.rs b/engine/pill_engine/src/renderer/resources/renderer_camera.rs similarity index 98% rename from engine/pill_renderer/src/resources/renderer_camera.rs rename to engine/pill_engine/src/renderer/resources/renderer_camera.rs index e828fd90..a82f4c82 100644 --- a/engine/pill_renderer/src/resources/renderer_camera.rs +++ b/engine/pill_engine/src/renderer/resources/renderer_camera.rs @@ -1,5 +1,5 @@ use cgmath::{EuclideanSpace, SquareMatrix, Zero}; -use pill_engine::internal::{CameraComponent, TransformComponent}; +use crate::ecs::{CameraComponent, TransformComponent}; use anyhow::Result; use std::f32::consts::FRAC_PI_2; diff --git a/engine/pill_renderer/src/resources/renderer_material.rs b/engine/pill_engine/src/renderer/resources/renderer_material.rs similarity index 95% rename from engine/pill_renderer/src/resources/renderer_material.rs rename to engine/pill_engine/src/renderer/resources/renderer_material.rs index 6b869283..4cfffae4 100644 --- a/engine/pill_renderer/src/resources/renderer_material.rs +++ b/engine/pill_engine/src/renderer/resources/renderer_material.rs @@ -1,4 +1,4 @@ -use crate::resources::RendererTexture; +use crate::renderer::resources::RendererTexture; // COLD: construction-only helper; no allocations, just references bundled for factory use. pub struct RendererMaterialTextures<'a> { diff --git a/engine/pill_renderer/src/resources/renderer_mesh.rs b/engine/pill_engine/src/renderer/resources/renderer_mesh.rs similarity index 97% rename from engine/pill_renderer/src/resources/renderer_mesh.rs rename to engine/pill_engine/src/renderer/resources/renderer_mesh.rs index 3d527e82..af82c929 100644 --- a/engine/pill_renderer/src/resources/renderer_mesh.rs +++ b/engine/pill_engine/src/renderer/resources/renderer_mesh.rs @@ -1,4 +1,4 @@ -use pill_engine::internal::{MeshData, MeshVertex}; +use crate::resources::{MeshData, MeshVertex}; use anyhow::*; use wgpu::util::DeviceExt; diff --git a/engine/pill_renderer/src/resources/renderer_pipeline.rs b/engine/pill_engine/src/renderer/resources/renderer_pipeline.rs similarity index 100% rename from engine/pill_renderer/src/resources/renderer_pipeline.rs rename to engine/pill_engine/src/renderer/resources/renderer_pipeline.rs diff --git a/engine/pill_renderer/src/resources/renderer_texture.rs b/engine/pill_engine/src/renderer/resources/renderer_texture.rs similarity index 99% rename from engine/pill_renderer/src/resources/renderer_texture.rs rename to engine/pill_engine/src/renderer/resources/renderer_texture.rs index ca681d05..3b9640ed 100644 --- a/engine/pill_renderer/src/resources/renderer_texture.rs +++ b/engine/pill_engine/src/renderer/resources/renderer_texture.rs @@ -1,6 +1,6 @@ +use crate::resources::TextureType; use anyhow::Result; use image::{DynamicImage, GenericImageView}; -use pill_engine::internal::TextureType; use wgpu::{Device, Queue, SurfaceConfiguration, TextureFormat}; pill_core::define_new_pill_slotmap_key! { diff --git a/engine/pill_renderer/src/renderer.rs b/engine/pill_engine/src/renderer/wgpu.rs similarity index 96% rename from engine/pill_renderer/src/renderer.rs rename to engine/pill_engine/src/renderer/wgpu.rs index fa549ba4..9696a71c 100644 --- a/engine/pill_renderer/src/renderer.rs +++ b/engine/pill_engine/src/renderer/wgpu.rs @@ -2,24 +2,22 @@ // https://github.com/kaphula/winit-egui-wgpu-template/blob/master/src/main.rs // https://github.com/emilk/egui/discussions/3067 -use crate::{ - resource_manager::ResourceManager, - resources::{ - RendererCamera, RendererMaterial, RendererMaterialParamsStd140, RendererMaterialTextures, - RendererMesh, RendererPipeline, RendererTexture, Vertex, - }, +use crate::renderer::resource_manager::ResourceManager as RendererResourceManager; +use crate::renderer::resources::{ + RendererCamera, RendererMaterial, RendererMaterialParamsStd140, RendererMaterialTextures, + RendererMesh, RendererPipeline, RendererTexture, Vertex, }; use std::sync::Arc; -use pill_engine::internal::{ - get_renderer_resource_handle_from_camera_component, BufferDesc, CameraComponent, - ComponentStorage, EntityHandle, MaterialDesc, MeshData, Pass, PillRenderer, PipelineV2, - PipelineV2Desc, RenderQueueItem, RendererBufferHandle, RendererCameraHandle, - RendererMaterialHandle, RendererMeshHandle, RendererPipelineHandle, RendererPipelineV2Handle, - RendererTargetDesc, RendererTextureHandle, ShaderDesc, TextureType, TransformComponent, - WorldQuery, RENDER_QUEUE_KEY_ORDER, +use crate::ecs::{CameraComponent, ComponentStorage, EntityHandle, TransformComponent}; +use crate::graphics::{ + BufferDesc, MaterialDesc, Pass, PillRenderer, PipelineV2, PipelineV2Desc, RenderQueueItem, + RendererBufferHandle, RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, + RendererPipelineHandle, RendererPipelineV2Handle, RendererTargetDesc, RendererTextureHandle, + ShaderDesc, WorldQuery, }; +use crate::resources::{MeshData, TextureType}; use pill_core::{Handle, PillSlotMapKey, PillStyle, RendererError, Timer}; @@ -27,13 +25,11 @@ use std::num::NonZeroU32; use cgmath::{Deg, InnerSpace}; use glam::{Mat4, Vec3, Vec4}; -use naga::back::wgsl; -use naga::front::glsl; use anyhow::{Error, Result}; use log::{error, info}; -use crate::egui::EguiRenderer; +use crate::renderer::egui::EguiRenderer; use image::GenericImageView; use wgpu::util::DeviceExt; @@ -41,25 +37,6 @@ pub const MAX_INSTANCE_BATCH_SIZE: usize = 10000; // Maximum number of instances pub const INITIAL_INSTANCE_VECTOR_CAPACITY: usize = 10000; // M2 inline draw: no MeshDrawer/instance batching -fn compile_glsl_to_wgsl(source: &str, stage: naga::ShaderStage) -> Result { - let mut frontend = glsl::Frontend::default(); - let options = glsl::Options::from(stage); - let module = frontend.parse(&options, source).unwrap(); - - let mut validator = naga::valid::Validator::new( - naga::valid::ValidationFlags::all(), - naga::valid::Capabilities::empty(), - ); - - let info = validator.validate(&module)?; - - let mut output = String::new(); - let mut writer = wgsl::Writer::new(&mut output, wgsl::WriterFlags::empty()); - writer.write(&module, &info)?; - - Ok(output) -} - // KISS helper: create a color render target texture+view+sampler fn create_render_target( device: &wgpu::Device, @@ -117,9 +94,9 @@ pub struct DeviceContext { pub struct State { passes: Vec>, - egui_renderer: crate::egui::EguiRenderer, // TODO: Separate system adding Pass + egui_renderer: crate::renderer::egui::EguiRenderer, // TODO: Separate system adding Pass // Resources and GPU objects moved from ctor into here explicitly - pub(crate) resource_manager: ResourceManager, + pub(crate) resource_manager: RendererResourceManager, pub(crate) color_format: wgpu::TextureFormat, pub(crate) depth_format: wgpu::TextureFormat, // pub(crate) depth_texture: Arc, @@ -260,7 +237,7 @@ impl PillRenderer for Renderer { let state: State = { // Configure collections - let mut resource_manager = ResourceManager::new(); + let mut resource_manager = RendererResourceManager::new(); // Create depth and color texture // let depth_texture = RendererTexture::new_depth_texture( @@ -428,7 +405,7 @@ impl PillRenderer for Renderer { vertex: wgpu::VertexState { module: &vs, entry_point: desc.vs.entry_func, - buffers: &[crate::resources::RendererMesh::data_layout_descriptor()], + buffers: &[RendererMesh::data_layout_descriptor()], compilation_options: Default::default(), }, fragment: Some(wgpu::FragmentState { diff --git a/engine/pill_engine/src/resources/gpu_resources.rs b/engine/pill_engine/src/resources/gpu_resources.rs new file mode 100644 index 00000000..bc44f316 --- /dev/null +++ b/engine/pill_engine/src/resources/gpu_resources.rs @@ -0,0 +1,121 @@ +use std::collections::HashMap; + +use crate::graphics::{ + RendererBufferHandle, RendererCameraHandle, RendererMaterialHandle, RendererMeshHandle, + RendererPipelineHandle, RendererTextureHandle, +}; +use crate::renderer::resources::{ + RendererCamera, RendererMaterial, RendererMesh, RendererPipeline, RendererTexture, +}; +use crate::resources::TextureType; + +use pill_core::{ + Handle, RendererBufferTag, RendererCameraTag, RendererMaterialTag, RendererMeshTag, + RendererPipelineTag, RendererTextureTag, ResourcePool, +}; + +use image::{DynamicImage, Rgba, RgbaImage}; + +#[derive(Hash, PartialEq, Eq, Clone, Copy)] +pub struct PipelineKey(pub u64); + +pub struct PipelineCache { + key_to_handle: HashMap, +} + +impl PipelineCache { + pub fn new() -> Self { + Self { + key_to_handle: HashMap::new(), + } + } + + pub fn lookup(&self, key: &PipelineKey) -> Option { + self.key_to_handle.get(key).copied() + } + + pub fn insert(&mut self, key: PipelineKey, h: RendererPipelineHandle) { + self.key_to_handle.insert(key, h); + } +} + +pub struct GpuResources { + pub buffers: ResourcePool, + pub pipelines: ResourcePool, + pub textures: ResourcePool, + pub materials: ResourcePool, + pub meshes: ResourcePool, + pub cameras: ResourcePool, + pub pipeline_cache: PipelineCache, + // cached defaults + pub default_color_tex: Option, + pub default_normal_tex: Option, +} + +impl GpuResources { + pub fn new() -> Self { + Self { + buffers: ResourcePool::new(), + pipelines: ResourcePool::new(), + textures: ResourcePool::new(), + materials: ResourcePool::new(), + meshes: ResourcePool::new(), + cameras: ResourcePool::new(), + pipeline_cache: PipelineCache::new(), + default_color_tex: None, + default_normal_tex: None, + } + } + + #[inline] + pub fn buffer(&self, h: RendererBufferHandle) -> &wgpu::Buffer { + self.buffers.get(h).expect("Invalid buffer handle") + } + + #[inline] + pub fn pipeline(&self, h: RendererPipelineHandle) -> &RendererPipeline { + self.pipelines.get(h).expect("Invalid pipeline handle") + } + + pub fn ensure_default_textures( + &mut self, + device: &wgpu::Device, + queue: &wgpu::Queue, + ) -> (RendererTextureHandle, RendererTextureHandle) { + if self.default_color_tex.is_none() { + let mut img = RgbaImage::new(1, 1); + img.put_pixel(0, 0, Rgba([255, 255, 255, 255])); + let dyn_img = DynamicImage::ImageRgba8(img); + let tex = RendererTexture::new_texture( + device, + queue, + Some("default_color"), + &dyn_img, + TextureType::Gamma, + ) + .expect("create default color tex"); + let h = self.textures.insert(tex); + self.default_color_tex = Some(h); + } + if self.default_normal_tex.is_none() { + let mut img = RgbaImage::new(1, 1); + // normal map flat (128,128,255) + img.put_pixel(0, 0, Rgba([128, 128, 255, 255])); + let dyn_img = DynamicImage::ImageRgba8(img); + let tex = RendererTexture::new_texture( + device, + queue, + Some("default_normal"), + &dyn_img, + TextureType::Linear, + ) + .expect("create default normal tex"); + let h = self.textures.insert(tex); + self.default_normal_tex = Some(h); + } + ( + self.default_color_tex.unwrap(), + self.default_normal_tex.unwrap(), + ) + } +} diff --git a/engine/pill_engine/src/resources/mod.rs b/engine/pill_engine/src/resources/mod.rs index e83c266a..53588160 100644 --- a/engine/pill_engine/src/resources/mod.rs +++ b/engine/pill_engine/src/resources/mod.rs @@ -1,5 +1,6 @@ #![cfg_attr(debug_assertions, allow(dead_code, unused_imports, unused_variables))] +mod gpu_resources; mod material; mod mesh; mod model; @@ -13,6 +14,7 @@ mod texture; pub use resource_manager::ResourceManager; +pub use gpu_resources::GpuResources; pub use resource::{Resource, ResourceLoadType}; pub use resource_storage::ResourceStorage; diff --git a/engine/pill_engine/src/resources/resource_manager.rs b/engine/pill_engine/src/resources/resource_manager.rs index 474e84e1..c1b9242f 100644 --- a/engine/pill_engine/src/resources/resource_manager.rs +++ b/engine/pill_engine/src/resources/resource_manager.rs @@ -1,7 +1,8 @@ use crate::{ graphics::RendererTextureHandle, resources::{ - PBRMaterial, PBRMaterialHandle, Resource, ResourceStorage, Texture, TextureHandle, TextureType, + PBRMaterial, PBRMaterialHandle, Resource, ResourceStorage, Texture, TextureHandle, + TextureType, }, }; @@ -13,15 +14,25 @@ use std::{collections::HashMap, convert::TryInto, env, num::NonZeroU32, path::Pa pub struct ResourceManager { resources: PillTypeMap, + pub gpu: crate::resources::GpuResources, } impl ResourceManager { pub fn new() -> Self { Self { resources: PillTypeMap::new(), + gpu: crate::resources::GpuResources::new(), } } + pub fn gpu(&self) -> &crate::resources::GpuResources { + &self.gpu + } + + pub fn gpu_mut(&mut self) -> &mut crate::resources::GpuResources { + &mut self.gpu + } + // --- Slots --- pub(crate) fn get_resource_slot<'a, T>( diff --git a/engine/pill_renderer/Cargo.toml b/engine/pill_renderer/Cargo.toml deleted file mode 100644 index fb1f7a70..00000000 --- a/engine/pill_renderer/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -name = "pill_renderer" -version = "0.1.0" -edition = "2021" - -[dependencies] -pill_core = {path = "../pill_core"} -pill_engine = {path = "../pill_engine", features = ["internal"]} - -# Data containers -slotmap = "1.0.6" -slab = "0.4.5" - -# Loaders -image = "0.23" -tobj = "3.0" - -# Errors -anyhow = "1.0" - -# Logging -env_logger = "0.9.0" -log = "0.4" - -# Other -winit = "0.29" -cgmath = "0.18" -glam = "0.27" -wgpu = { version = "0.20.1", features = [ "spirv" ] } -futures = "0.3" -pollster = "0.2" -bytemuck = { version = "1.4", features = [ "derive" ] } -config = "0.11" -naga = { version = "0.19", features = ["glsl-in", "wgsl-out"] } - -# egui -egui = "0.28.1" -egui-wgpu = { version = "0.28.1", features = ["winit"] } -egui-winit = "0.28.1" \ No newline at end of file diff --git a/engine/pill_renderer/res/pill_logo_horizontal_white.png b/engine/pill_renderer/res/pill_logo_horizontal_white.png deleted file mode 100644 index d4e5bc353398f8fc570b1d93cb403cc45ad579e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9434 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 diff --git a/engine/pill_renderer/src/pass_compose.rs b/engine/pill_renderer/src/pass_compose.rs deleted file mode 100644 index 4ed467e0..00000000 --- a/engine/pill_renderer/src/pass_compose.rs +++ /dev/null @@ -1,199 +0,0 @@ -use crate::renderer::{Pass, Renderer, WorldQuery}; -use crate::resource_manager::ResourceManager; -use crate::resources::RendererTexture; -use anyhow::Result; -use pill_engine::internal::{PillRenderer, PipelineV2, PipelineV2Desc, ShaderDesc}; -use std::sync::Arc; -use wgpu::CommandEncoder; -use wgpu::Queue; - -pub struct PassCompose { - label: String, - offscreen_texture: Arc, - target_format: wgpu::TextureFormat, - pipeline: Option, - bind_group: Option, -} - -impl PassCompose { - pub fn new( - label: &str, - offscreen_texture: Arc, - target_format: wgpu::TextureFormat, - ) -> Self { - Self { - label: label.to_string(), - offscreen_texture, - target_format, - pipeline: None, - bind_group: None, - } - } -} - -impl Pass for PassCompose { - fn get_label(&self) -> &str { - &self.label - } - - fn init(&mut self, renderer: &mut Renderer) -> Result<()> { - println!("Initializing pass: {}", self.label); - let device = &renderer.ctx.device; - - let vs = r#" - struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; - @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { - var pos = array, 3>( - vec2(-1.0, -3.0), - vec2( 3.0, 1.0), - vec2(-1.0, 1.0) - ); - var tuv = array, 3>( - vec2(0.0, 2.0), - vec2(2.0, 0.0), - vec2(0.0, 0.0) - ); - var o: VSOut; - o.pos = vec4(pos[vi], 0.0, 1.0); - o.uv = tuv[vi]; - return o; - } - "#; - - let fs = r#" - @group(0) @binding(0) var t_src: texture_2d; - @group(0) @binding(1) var s_src: sampler; - @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { - let hdr = textureSample(t_src, s_src, uv).rgb; - // Reinhard tone mapping; output remains in linear space. The sRGB swapchain will encode. - let ldr = hdr / (1.0 + hdr); - return vec4(ldr, 1.0); - } - "#; - - let pipeline = { - // Build bind group layout - let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("compose_bgl"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], - }); - - let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("compose_pl"), - bind_group_layouts: &[&bgl], - push_constant_ranges: &[], - }); - - let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("compose_vs"), - source: wgpu::ShaderSource::Wgsl(vs.into()), - }); - let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("compose_fs"), - source: wgpu::ShaderSource::Wgsl(fs.into()), - }); - - let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("compose_pipeline"), - layout: Some(&pl), - vertex: wgpu::VertexState { - module: &vs_mod, - entry_point: "main", - buffers: &[], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &fs_mod, - entry_point: "main", - targets: &[Some(wgpu::ColorTargetState { - format: self.target_format, - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState::default(), - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview: None, - }); - - (pipeline, bgl) - }; - - let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("compose_bind_group"), - layout: &pipeline.1, // Group 0: Texture and sampler bindings - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView( - &self.offscreen_texture.texture_view, - ), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&self.offscreen_texture.sampler), - }, - ], - }); - - // Store pipeline handle - self.pipeline = Some(PipelineV2 { - pipeline: pipeline.0, - bind_group_layouts: vec![pipeline.1], - }); - self.bind_group = Some(bind_group); - - Ok(()) - } - - fn draw( - &mut self, - encoder: &mut CommandEncoder, - _renderer: &mut Renderer, - _frame: &wgpu::SurfaceTexture, - view: &wgpu::TextureView, - _world: &WorldQuery, - ) -> Result<()> { - // Create render pass for this pass using the provided frame - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some(&self.label), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - // Draw fullscreen triangle for composition - pass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); - pass.set_bind_group(0, &self.bind_group.as_ref().unwrap(), &[]); - pass.draw(0..3, 0..1); - - Ok(()) - } -} diff --git a/engine/pill_renderer/src/pass_overlay_depth.rs b/engine/pill_renderer/src/pass_overlay_depth.rs deleted file mode 100644 index 2843c9b5..00000000 --- a/engine/pill_renderer/src/pass_overlay_depth.rs +++ /dev/null @@ -1,455 +0,0 @@ -use crate::renderer::{Pass, Renderer, WorldQuery}; -use crate::resource_manager::ResourceManager; -use crate::resources::RendererTexture; -use anyhow::Result; -use pill_engine::internal::{BufferDesc, PillRenderer, PipelineV2, PipelineV2Desc, ShaderDesc}; -use std::sync::Arc; -use wgpu::CommandEncoder; -use wgpu::Queue; - -pub struct PassOverlayDepth { - label: String, - depth_texture: Arc, - pipeline: Option, - bind_group_rect: Option, - bind_group_material: Option, - tint_buffer: Option, - rect: [f32; 4], - tint: [f32; 4], - target_format: wgpu::TextureFormat, -} - -impl PassOverlayDepth { - pub fn new( - label: &str, - rect: [f32; 4], - tint: [f32; 4], - depth_texture: Arc, - target_format: wgpu::TextureFormat, - ) -> Self { - Self { - label: label.to_string(), - depth_texture, - pipeline: None, - bind_group_rect: None, - bind_group_material: None, - tint_buffer: None, - rect: rect, - tint: tint, - target_format, - } - } -} - -impl Pass for PassOverlayDepth { - fn get_label(&self) -> &str { - &self.label - } - - fn init(&mut self, renderer: &mut Renderer) -> Result<()> { - println!("Initializing pass: {}", self.label); - let device = &renderer.ctx.device; - - // Create buffer for overlay rect UBO - let buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("overlay_rect_ubo"), - size: 256, - usage: wgpu::BufferUsages::UNIFORM, - mapped_at_creation: true, - }); - { - let mut view = buffer.slice(..).get_mapped_range_mut(); - let src = bytemuck::bytes_of(&self.rect); - view[..src.len()].copy_from_slice(src); - } - buffer.unmap(); - - let vs = r#" - @group(1) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] - - struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; - @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { - var unit = array, 6>( - vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), - vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) - ); - let p = unit[vi]; // [-1,1] NDC space - let s = p*0.5+0.5; // [0,1] screen space - let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] - let ndc = r*2.0-1.0; // [-1,1] NDC space - var out: VSOut; - out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); - out.uv = vec2(s.x, 1.0 - s.y); - return out; - } - "#; - - let fs = r#" - @group(0) @binding(0) var tex: texture_depth_2d; - @group(0) @binding(1) var UTint: vec4; - @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { - let dims = textureDimensions(tex, 0u); - let coord = vec2(uv * vec2(dims)); - let d = textureLoad(tex, coord, 0); - let vis = fract(1000.0*d); - return vec4(vis, vis, vis, 1.0) * UTint; - } - "#; - - // Build bind group layouts - let bgl_material = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("overlay_depth_material_bgl"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Depth, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), - }, - count: None, - }, - ], - }); - let bgl_rect = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("overlay_depth_rect_bgl"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), - }, - count: None, - }], - }); - let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("overlay_depth_pl"), - bind_group_layouts: &[&bgl_material, &bgl_rect], - push_constant_ranges: &[], - }); - let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("overlay_depth_vs"), - source: wgpu::ShaderSource::Wgsl(vs.into()), - }); - let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("overlay_depth_fs"), - source: wgpu::ShaderSource::Wgsl(fs.into()), - }); - let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("overlay_depth_pipeline"), - layout: Some(&pl), - vertex: wgpu::VertexState { - module: &vs_mod, - entry_point: "main", - buffers: &[], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &fs_mod, - entry_point: "main", - targets: &[Some(wgpu::ColorTargetState { - format: self.target_format, - blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState::default(), - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview: None, - }); - - let bind_group_rect = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_rect_bind_group"), - layout: &bgl_rect, // Group 1: Rect UBO - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: buffer.as_entire_binding(), - }], - }); - - // Create tint buffer - let tint_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("overlay_depth_tint"), - size: 256, - usage: wgpu::BufferUsages::UNIFORM, - mapped_at_creation: true, - }); - { - let mut view = tint_buffer.slice(..).get_mapped_range_mut(); - let src = bytemuck::bytes_of(&self.tint); - view[..src.len()].copy_from_slice(src); - } - tint_buffer.unmap(); - - // Create material bind group - let bind_group_material = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_depth_material_bg"), - layout: &bgl_material, // Group 0: Material bindings - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&self.depth_texture.texture_view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &tint_buffer, - offset: 0, - size: Some(std::num::NonZeroU64::new(16).unwrap()), - }), - }, - ], - }); - - // Store pipeline handle - self.pipeline = Some(PipelineV2 { - pipeline, - bind_group_layouts: vec![bgl_material, bgl_rect], - }); - self.bind_group_rect = Some(bind_group_rect); - self.bind_group_material = Some(bind_group_material); - self.tint_buffer = Some(tint_buffer); - - Ok(()) - } - - fn draw( - &mut self, - encoder: &mut CommandEncoder, - _renderer: &mut Renderer, - _frame: &wgpu::SurfaceTexture, - view: &wgpu::TextureView, - _world: &WorldQuery, - ) -> Result<()> { - // Create render pass for this pass using the provided frame - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some(&self.label), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - // Draw depth overlay (bind material at group 0 and rect at group 1) - pass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); - pass.set_bind_group(0, &self.bind_group_material.as_ref().unwrap(), &[]); - pass.set_bind_group(1, &self.bind_group_rect.as_ref().unwrap(), &[]); - pass.draw(0..6, 0..1); - - Ok(()) - } -} - -/* -fn create_overlay_depth( - device: &wgpu::Device, - queue: &wgpu::Queue, - surface_format: wgpu::TextureFormat, - depth_rt: &RendererTexture, - rect: [f32; 4], -) -> TextureOverlayResources { - let overlay_vs_wgsl = r#" -@group(1) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] - -struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; -@vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { - var unit = array, 6>( - vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), - vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) - ); - let p = unit[vi]; // [-1,1] NDC space - let s = p*0.5+0.5; // [0,1] screen space - let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] - let ndc = r*2.0-1.0; // [-1,1] NDC space - var out: VSOut; - out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); - out.uv = vec2(s.x, 1.0 - s.y); - return out; - } - "#; - - let overlay_fs_wgsl = r#" -@group(0) @binding(0) var tex: texture_depth_2d; -@group(0) @binding(1) var UTint: vec4; -@fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { - let dims = textureDimensions(tex, 0u); - let coord = vec2(uv * vec2(dims)); - let d = textureLoad(tex, coord, 0); - let vis = fract(100.0*d); - return vec4(vis, vis, vis, 1.0) * UTint; -} -"#; - - let overlay_vs = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("overlay_vs"), - source: wgpu::ShaderSource::Wgsl(overlay_vs_wgsl.into()), - }); - let overlay_fs = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("overlay_fs"), - source: wgpu::ShaderSource::Wgsl(overlay_fs_wgsl.into()), - }); - - // Group 0: material (texture + tint) - let overlay_material_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("overlay_logo_material_bgl"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Depth, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), - }, - count: None, - }, - ], - }); - // Group 1: rect UBO - let overlay_rect_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("overlay_rect_bgl"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), - }, - count: None, - }], - }); - let overlay_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("overlay_pl"), - bind_group_layouts: &[ - &overlay_material_bind_group_layout, - &overlay_rect_bind_group_layout, - ], - push_constant_ranges: &[], - }); - - // No vertex buffer layout needed; vertices are generated procedurally in the vertex shader - let overlay_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("overlay_pipeline"), - layout: Some(&overlay_pipeline_layout), - vertex: wgpu::VertexState { - module: &overlay_vs, - entry_point: "main", - buffers: &[], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &overlay_fs, - entry_point: "main", - targets: &[Some(wgpu::ColorTargetState { - format: surface_format, - blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState { - cull_mode: None, - ..wgpu::PrimitiveState::default() - }, - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview: None, - }); - - // Apple Metal constant buffer alignment (256 bytes) - let overlay_rect_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("overlay_rect_ubo"), - size: 256, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - queue.write_buffer(&overlay_rect_buffer, 0, bytemuck::bytes_of(&rect)); - - let overlay_rect_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_rect_bg"), - layout: &overlay_rect_bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &overlay_rect_buffer, - offset: 0, - size: Some(std::num::NonZeroU64::new(16).unwrap()), - }), - }], - }); - - let tint_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("overlay_tint"), - size: 256, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - let tint: [f32; 4] = [1.0, 1.0, 1.0, 1.0]; - queue.write_buffer(&tint_buffer, 0, bytemuck::bytes_of(&tint)); - - let overlay_material_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_material_bg"), - layout: &overlay_material_bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&depth_rt.texture_view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &tint_buffer, - offset: 0, - size: Some(std::num::NonZeroU64::new(16).unwrap()), - }), - }, - ], - }); - - TextureOverlayResources { - pipeline: overlay_pipeline, - rect_bind_group_layout: overlay_rect_bind_group_layout, - rect_bind_group: overlay_rect_bind_group, - rect_buffer: overlay_rect_buffer, - material_bind_group_layout: overlay_material_bind_group_layout, - material_bind_group: overlay_material_bind_group, - material_tint_buffer: tint_buffer, - } -} - -*/ diff --git a/engine/pill_renderer/src/pass_overlay_logo.rs b/engine/pill_renderer/src/pass_overlay_logo.rs deleted file mode 100644 index ac82cc5d..00000000 --- a/engine/pill_renderer/src/pass_overlay_logo.rs +++ /dev/null @@ -1,275 +0,0 @@ -use crate::renderer::{Pass, Renderer, WorldQuery}; -use crate::resource_manager::ResourceManager; -use anyhow::Result; -use pill_engine::internal::{ - BufferDesc, PillRenderer, PipelineV2, PipelineV2Desc, RendererTextureHandle, ShaderDesc, -}; -use wgpu::CommandEncoder; -use wgpu::Queue; - -pub struct PassOverlayLogo { - label: String, - tex_logo: RendererTextureHandle, - buffer: Option, - pipeline: Option, - bind_group_rect: Option, - bind_group_material: Option, - rect: [f32; 4], - tint: [f32; 4], - target_format: wgpu::TextureFormat, -} - -impl PassOverlayLogo { - pub fn new( - label: &str, - rect: [f32; 4], - tint: [f32; 4], - tex_logo: RendererTextureHandle, - target_format: wgpu::TextureFormat, - ) -> Self { - Self { - label: label.to_string(), - tex_logo, - buffer: None, - pipeline: None, - bind_group_rect: None, - bind_group_material: None, - rect: rect, - tint: tint, - target_format, - } - } -} - -impl Pass for PassOverlayLogo { - fn get_label(&self) -> &str { - &self.label - } - - fn init(&mut self, renderer: &mut Renderer) -> Result<()> { - println!("Initializing pass: {}", self.label); - let device = &renderer.ctx.device; - - // Create buffer for overlay rect UBO - let buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("overlay_logo_ubo"), - size: 256, - usage: wgpu::BufferUsages::UNIFORM, - mapped_at_creation: true, - }); - { - let mut view = buffer.slice(..).get_mapped_range_mut(); - let src = bytemuck::bytes_of(&self.rect); - view[..src.len()].copy_from_slice(src); - } - buffer.unmap(); - - let vs = r#" - @group(1) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] - - struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; - @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { - var unit = array, 6>( - vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), - vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) - ); - let p = unit[vi]; // [-1,1] NDC space - let s = p*0.5+0.5; // [0,1] screen space - let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] - let ndc = r*2.0-1.0; // [-1,1] NDC space - var out: VSOut; - out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); - out.uv = vec2(s.x, 1.0 - s.y); - return out; - } - "#; - - let fs = r#" - @group(0) @binding(0) var tex: texture_2d; - @group(0) @binding(1) var smp: sampler; - @group(0) @binding(2) var UTint: vec4; - @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { - let c = textureSample(tex, smp, uv); - return c * UTint; - } - "#; - - // Build bind group layouts - let bgl_material = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("overlay_logo_material_bgl"), - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 2, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), - }, - count: None, - }, - ], - }); - let bgl_rect = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("overlay_logo_rect_bgl"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), - }, - count: None, - }], - }); - let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("overlay_logo_pl"), - bind_group_layouts: &[&bgl_material, &bgl_rect], - push_constant_ranges: &[], - }); - let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("overlay_logo_vs"), - source: wgpu::ShaderSource::Wgsl(vs.into()), - }); - let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("overlay_logo_fs"), - source: wgpu::ShaderSource::Wgsl(fs.into()), - }); - let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("overlay_logo_pipeline"), - layout: Some(&pl), - vertex: wgpu::VertexState { - module: &vs_mod, - entry_point: "main", - buffers: &[], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &fs_mod, - entry_point: "main", - targets: &[Some(wgpu::ColorTargetState { - format: self.target_format, - blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState::default(), - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview: None, - }); - - let bind_group_rect = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_rect_bind_group"), - layout: &bgl_rect, // Group 1: Rect UBO - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: buffer.as_entire_binding(), - }], - }); - - let tex = renderer - .state - .resource_manager - .textures - .get(self.tex_logo) - .expect("logo texture handle invalid"); - let logo_texture_view = &tex.texture_view; - let logo_sampler = &tex.sampler; - let tint_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("overlay_logo_tint"), - size: 256, - usage: wgpu::BufferUsages::UNIFORM, - mapped_at_creation: true, - }); - { - let mut view = tint_buffer.slice(..).get_mapped_range_mut(); - let src = bytemuck::bytes_of(&self.tint); - view[..src.len()].copy_from_slice(src); - } - tint_buffer.unmap(); - - let bind_group_material = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_logo_material_bg"), - layout: &bgl_material, // Group 0: Material bindings - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(logo_texture_view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(logo_sampler), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &tint_buffer, - offset: 0, - size: Some(std::num::NonZeroU64::new(16).unwrap()), - }), - }, - ], - }); - - // Store pipeline handle - self.pipeline = Some(PipelineV2 { - pipeline, - bind_group_layouts: vec![bgl_material, bgl_rect], - }); - self.bind_group_rect = Some(bind_group_rect); - self.bind_group_material = Some(bind_group_material); - - Ok(()) - } - - fn draw( - &mut self, - encoder: &mut CommandEncoder, - _renderer: &mut Renderer, - _frame: &wgpu::SurfaceTexture, - view: &wgpu::TextureView, - _world: &WorldQuery, - ) -> Result<()> { - // Create render pass for this pass using the provided frame - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some(&self.label), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - pass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); - pass.set_bind_group(0, &self.bind_group_material.as_ref().unwrap(), &[]); - pass.set_bind_group(1, &self.bind_group_rect.as_ref().unwrap(), &[]); - pass.draw(0..6, 0..1); - - Ok(()) - } -} diff --git a/engine/pill_renderer/src/pass_overlay_uv.rs b/engine/pill_renderer/src/pass_overlay_uv.rs deleted file mode 100644 index a93edf36..00000000 --- a/engine/pill_renderer/src/pass_overlay_uv.rs +++ /dev/null @@ -1,182 +0,0 @@ -use crate::renderer::{Pass, Renderer, WorldQuery}; -use crate::resource_manager::ResourceManager; -use anyhow::Result; -use pill_engine::internal::{BufferDesc, PillRenderer, PipelineV2, PipelineV2Desc, ShaderDesc}; -use wgpu::CommandEncoder; -use wgpu::Queue; - -pub struct PassOverlayUV { - label: String, - buffer: Option, - pipeline: Option, - bind_group: Option, - rect: [f32; 4], - target_format: wgpu::TextureFormat, -} - -impl PassOverlayUV { - pub fn new(label: &str, rect: [f32; 4], target_format: wgpu::TextureFormat) -> Self { - Self { - label: label.to_string(), - buffer: None, - pipeline: None, - bind_group: None, - rect: rect, - target_format, - } - } -} - -impl Pass for PassOverlayUV { - fn get_label(&self) -> &str { - &self.label - } - - fn init(&mut self, renderer: &mut Renderer) -> Result<()> { - println!("Initializing pass: {}", self.label); - let device = &renderer.ctx.device; - - // Create buffer for overlay rect UBO - let aligned_size = 256u64; - let buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("overlay_rect_ubo"), - size: aligned_size, - usage: wgpu::BufferUsages::UNIFORM, - mapped_at_creation: true, - }); - { - let mut view = buffer.slice(..).get_mapped_range_mut(); - let src = bytemuck::bytes_of(&self.rect); - view[..src.len()].copy_from_slice(src); - } - buffer.unmap(); - - let vs = r#" - @group(0) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] - - struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; - @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { - var unit = array, 6>( - vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), - vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) - ); - let p = unit[vi]; // [-1,1] NDC space - let s = p*0.5+0.5; // [0,1] screen space - let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] - let ndc = r*2.0-1.0; // [-1,1] NDC space - var out: VSOut; - out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); - out.uv = s; - return out; - } - "#; - - let fs = r#" - @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { - return vec4(uv, 0.0, 0.5); - } - "#; - - // Build bind group layout and pipeline - let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("overlay_uv_bgl"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), - }, - count: None, - }], - }); - let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("overlay_uv_pl"), - bind_group_layouts: &[&bgl], - push_constant_ranges: &[], - }); - let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("overlay_uv_vs"), - source: wgpu::ShaderSource::Wgsl(vs.into()), - }); - let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("overlay_uv_fs"), - source: wgpu::ShaderSource::Wgsl(fs.into()), - }); - let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("overlay_uv_pipeline"), - layout: Some(&pl), - vertex: wgpu::VertexState { - module: &vs_mod, - entry_point: "main", - buffers: &[], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &fs_mod, - entry_point: "main", - targets: &[Some(wgpu::ColorTargetState { - format: self.target_format, - blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), - write_mask: wgpu::ColorWrites::ALL, - })], - compilation_options: Default::default(), - }), - primitive: wgpu::PrimitiveState::default(), - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview: None, - }); - - let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("overlay_rect_bind_group"), - layout: &bgl, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: buffer.as_entire_binding(), - }], - }); - - // Store pipeline handle - self.pipeline = Some(PipelineV2 { - pipeline, - bind_group_layouts: vec![bgl], - }); - self.bind_group = Some(bind_group); - - Ok(()) - } - - fn draw( - &mut self, - encoder: &mut CommandEncoder, - _renderer: &mut Renderer, - _frame: &wgpu::SurfaceTexture, - view: &wgpu::TextureView, - _world: &WorldQuery, - ) -> Result<()> { - // Create render pass for this pass using the provided frame - let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some(&self.label), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - // Draw small overlay UV quad in normalized rect via UBO - pass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); - pass.set_bind_group(0, self.bind_group.as_ref().unwrap(), &[]); - pass.draw(0..6, 0..1); - - Ok(()) - } -} diff --git a/engine/pill_renderer/src/pass_scene.rs b/engine/pill_renderer/src/pass_scene.rs deleted file mode 100644 index dc482e36..00000000 --- a/engine/pill_renderer/src/pass_scene.rs +++ /dev/null @@ -1,839 +0,0 @@ -use crate::resource_manager::ResourceManager; -use crate::resources::CameraUniform; -use crate::resources::RendererTexture; -use pill_engine::internal::{Pass, Renderer, WorldQuery}; -use std::sync::Arc; - -use anyhow::{Error, Result}; -use glam::{Mat4, Vec3, Vec4}; -use pill_core::{PillSlotMapKey, RendererError}; -use pill_engine::internal::{ - get_renderer_resource_handle_from_camera_component, CameraComponent, ComponentStorage, - EntityHandle, MaterialDesc, RenderQueueItem, RendererMaterialHandle, RendererMeshHandle, - TransformComponent, -}; -use pill_engine::internal::{BufferDesc, PillRenderer, PipelineV2, PipelineV2Desc, ShaderDesc}; -use wgpu::util::DeviceExt; -use wgpu::CommandEncoder; - -#[repr(C)] -#[derive(Copy, Clone)] -pub(crate) struct PerDrawStd140 { - mvp: [[f32; 4]; 4], - model: [[f32; 4]; 4], -} -unsafe impl bytemuck::Zeroable for PerDrawStd140 {} -unsafe impl bytemuck::Pod for PerDrawStd140 {} - -#[repr(C)] -#[derive(Copy, Clone)] -struct MaterialParamsStd140 { - base_color_factor: [f32; 3], - _pad0: f32, - metallic: f32, - roughness: f32, - _pad1: [f32; 2], - emissive_factor: [f32; 3], - _pad2: f32, -} -unsafe impl bytemuck::Zeroable for MaterialParamsStd140 {} -unsafe impl bytemuck::Pod for MaterialParamsStd140 {} - -// Static configuration for per-draw buffering (derive capacities, avoid magic numbers) -const MAX_EXPECTED_PER_DRAW_INSTANCES: usize = 100_000; -const UNIFORM_OFFSET_ALIGNMENT: usize = 256; -const PER_DRAW_STRIDE_BYTES: usize = ((std::mem::size_of::() - + (UNIFORM_OFFSET_ALIGNMENT - 1)) - / UNIFORM_OFFSET_ALIGNMENT) - * UNIFORM_OFFSET_ALIGNMENT; - -pub const MATERIAL_BIND_GROUP_GLOBALS: usize = 0; -pub const MATERIAL_BIND_GROUP_TEXTURES: usize = 1; -pub const MATERIAL_BIND_GROUP_PARAMS: usize = 2; -pub const MATERIAL_BIND_GROUP_PERDRAW: usize = 3; - -// Preallocated buffer structs for hot path optimization -pub(crate) struct VisiblePreDraw { - pub(crate) material_handle: RendererMaterialHandle, - pub(crate) mesh_handle: RendererMeshHandle, - pub(crate) entity_index: u32, - pub(crate) mvp: [[f32; 4]; 4], - pub(crate) model: [[f32; 4]; 4], -} - -// M3: Hello Mesh + per-draw MVP (dynamic offsets) -pub(crate) struct MeshBatch { - pub(crate) mesh_handle: RendererMeshHandle, - pub(crate) instances: Vec, - pub(crate) base_offset_u32: u32, // offset into per-draw ring for first instance -} - -pub(crate) struct GroupCmd { - pub(crate) pipeline: *const wgpu::RenderPipeline, - pub(crate) material_handle: RendererMaterialHandle, - pub(crate) batches: Vec, -} - -pub struct PassScene { - label: String, - // Resources - offscreen_color_texture: Arc, - depth_texture: Arc, - // Per-frame dynamic UBO ring (Milestone 5) - per_draw_stride: u64, - per_draw_capacity: u64, - // Working buffers - visible_pre_draw_buffer: Vec, - groups_buffer: Vec, - staging_buffer: Vec, - // Pass-local state initialized in init(), read every draw - state: Option, -} - -struct PassSceneState { - camera_uniform: CameraUniform, - camera_buffer: wgpu::Buffer, - camera_bind_group: wgpu::BindGroup, - pipeline: PipelineV2, - // Per-draw resources - per_draw_buffer: wgpu::Buffer, - per_draw_bind_group: wgpu::BindGroup, -} - -impl PassScene { - pub fn new( - label: &str, - offscreen_color_texture: Arc, - depth_texture: Arc, - ) -> Self { - Self { - label: label.to_string(), - offscreen_color_texture: offscreen_color_texture, - depth_texture: depth_texture, - per_draw_stride: PER_DRAW_STRIDE_BYTES as u64, - per_draw_capacity: 0, - visible_pre_draw_buffer: Vec::with_capacity(MAX_EXPECTED_PER_DRAW_INSTANCES), - groups_buffer: Vec::with_capacity(2000), - staging_buffer: Vec::with_capacity( - MAX_EXPECTED_PER_DRAW_INSTANCES * PER_DRAW_STRIDE_BYTES, - ), - state: None, - } - } -} - -fn get_state(pass: &mut PassScene) -> &mut PassSceneState { - debug_assert!(pass.state.is_some()); - // SAFETY: initialized once in init(), read every draw - unsafe { pass.state.as_mut().unwrap_unchecked() } -} - -impl Pass for PassScene { - fn get_label(&self) -> &str { - &self.label - } - - fn init(&mut self, renderer: &mut Renderer) -> Result<()> { - // [SIMILAR] Prebuilt PSO used; avoid hot-path pipeline creation per TALK - // Shaders: must match bind group layout indices: 0(camera),1(material textures),2(material params),3(per-draw) - let vertex_wgsl = r#" - struct Camera { - position: vec4, - viewProjection: mat4x4, - }; - @group(0) @binding(0) var UCamera: Camera; - struct PerDraw { mvp: mat4x4, model: mat4x4, }; - @group(3) @binding(0) var UPerDraw: PerDraw; - struct VSIn { - @location(0) pos: vec3, - @location(1) uv: vec2, - @location(2) normal: vec3, - }; - struct VSOut { - @builtin(position) pos: vec4, - @location(0) uv: vec2, - @location(1) worldPos: vec3, - @location(2) worldNormal: vec3, - }; - @vertex fn main(input: VSIn) -> VSOut { - var out: VSOut; - let worldPos4 = UPerDraw.model * vec4(input.pos, 1.0); - // TODO: Use a proper normal matrix (inverse-transpose of model) if non-uniform scaling is used. - let n = normalize((UPerDraw.model * vec4(input.normal, 0.0)).xyz); - out.pos = UPerDraw.mvp * vec4(input.pos, 1.0); - out.uv = input.uv; - out.worldPos = worldPos4.xyz; - out.worldNormal = n; - return out; - } - "#; - let fragment_wgsl = r#" - // PBR material textures (set 1) - @group(1) @binding(0) var texBaseColor: texture_2d; - @group(1) @binding(1) var smpBaseColor: sampler; - @group(1) @binding(2) var texNormal: texture_2d; - @group(1) @binding(3) var smpNormal: sampler; - @group(1) @binding(4) var texMetallicRoughness: texture_2d; - @group(1) @binding(5) var smpMetallicRoughness: sampler; - @group(1) @binding(6) var texEmissive: texture_2d; - @group(1) @binding(7) var smpEmissive: sampler; - // PBR params UBO (set 2) - struct MaterialParams { - baseColorFactor: vec3, _pad0: f32, - metallicFactor: f32, roughnessFactor: f32, _pad1: vec2, - emissiveFactor: vec3, _pad2: f32, - } - @group(2) @binding(0) var UMaterial: MaterialParams; - // Per-draw - struct PerDraw { mvp: mat4x4, model: mat4x4, }; - @group(3) @binding(0) var UPerDraw: PerDraw; - struct Camera { - position: vec4, - viewProjection: mat4x4, - }; - @group(0) @binding(0) var UCamera: Camera; - - const PI: f32 = 3.14159265359; - - // Directional lights: vec4(direction.xyz, intensity) - // Direction points toward the surface (light-to-surface). - const LIGHT_DIR0: vec3 = vec3(-0.5, -1.0, -0.2); - const LIGHT_DIR1: vec3 = vec3( 0.5, -0.5, 0.2); - const LIGHT_DIR2: vec3 = vec3( 0.0, 1.0, 0.0); - const LIGHT_COL0: vec4 = vec4( 1.0, 0.5, 0.5, 10.0); // Key - const LIGHT_COL1: vec4 = vec4( 0.5, 0.5, 1.0, 3.0); // Fill - const LIGHT_COL2: vec4 = vec4( 0.1, 0.1, 1.0, 0.2); // Rim - - fn DistributionGGX(N: vec3, H: vec3, roughness: f32) -> f32 { - // Add epsilon to avoid singularities at very low roughness. - let a = max(roughness * roughness, 0.0025); - let a2 = a * a; - let NdotH = max(dot(N, H), 0.0); - let NdotH2 = NdotH * NdotH; - let denom = (NdotH2 * (a2 - 1.0) + 1.0); - return a2 / (PI * denom * denom + 1e-7); - } - - fn GeometrySchlickGGX(NdotV: f32, roughness: f32) -> f32 { - // Heitz's k for direct lighting approximation - let r = roughness + 1.0; - let k = (r * r) / 8.0; - let denom = NdotV * (1.0 - k) + k; - return NdotV / denom; - } - - fn GeometrySmith(N: vec3, V: vec3, L: vec3, roughness: f32) -> f32 { - let NdotV = max(dot(N, V), 0.0); - let NdotL = max(dot(N, L), 0.0); - let ggx2 = GeometrySchlickGGX(NdotV, roughness); - let ggx1 = GeometrySchlickGGX(NdotL, roughness); - return ggx1 * ggx2; - } - - fn fresnelSchlick(cosTheta: f32, F0: vec3) -> vec3 { - return F0 + (vec3(1.0, 1.0, 1.0) - F0) * pow(1.0 - cosTheta, 5.0); - } - - // Directional light accumulator (white light scaled by intensity). - fn accumulateDirLight( - N: vec3, V: vec3, F0: vec3, - albedo: vec3, roughness: f32, metallic: f32, - lightDir: vec3, lightColor: vec4 - ) -> vec3 { - // lightDir is direction from light to surface; incoming L is opposite - let L = normalize(-lightDir.xyz); - let H = normalize(V + L); - let radiance = lightColor.w * lightColor.xyz; - - let NDF = DistributionGGX(N, H, roughness); - let G = GeometrySmith(N, V, L, roughness); - let F = fresnelSchlick(max(dot(H, V), 0.0), F0); - - let kS = F; - var kD = vec3(1.0, 1.0, 1.0) - kS; - kD = kD * (1.0 - metallic); - - let numerator = NDF * G * F; - let denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.0001; - let specular = numerator / vec3(denominator, denominator, denominator); - - let NdotL = max(dot(N, L), 0.0); - return (kD * (albedo / PI) + specular) * radiance * NdotL; - } - - @fragment fn main( - @location(0) uv: vec2, - @location(1) WorldPos: vec3, - @location(2) NormalIn: vec3 - ) -> @location(0) vec4 { - // Surface parameters - var albedo = textureSample(texBaseColor, smpBaseColor, uv).rgb * UMaterial.baseColorFactor; - let mr = textureSample(texMetallicRoughness, smpMetallicRoughness, uv).gb; - var roughness = clamp(mr.x * UMaterial.roughnessFactor, 0.0, 1.0); - // Robustness: keep roughness in a sane range to preserve highlight and stability. - roughness = clamp(roughness, 0.045, 0.99); - let metallic = clamp(mr.y * UMaterial.metallicFactor, 0.0, 1.0); - // TODO: Support normal mapping (tangent space) and AO texture. - let N = normalize(NormalIn); - let V = normalize(UCamera.position.xyz - WorldPos); - - var F0 = vec3(0.04, 0.04, 0.04); - F0 = mix(F0, albedo, vec3(metallic, metallic, metallic)); - - var Lo = vec3(0.0, 0.0, 0.0); - Lo = Lo + accumulateDirLight(N, V, F0, albedo, roughness, metallic, LIGHT_DIR0, LIGHT_COL0); - Lo = Lo + accumulateDirLight(N, V, F0, albedo, roughness, metallic, LIGHT_DIR1, LIGHT_COL1); - Lo = Lo + accumulateDirLight(N, V, F0, albedo, roughness, metallic, LIGHT_DIR2, LIGHT_COL2); - - var color = Lo; - // color = vec3(uv, 0.0); // DBG uv - // color = vec3(N*0.5+0.5); // DBG Normal - // color = vec3(WorldPos); // DBG WorldPos - // color = vec3(roughness); // DBG roughness - return vec4(color, 1.0); - } - "#; - - // Describe bind group layouts for pipeline creation - let bind_groups: Vec> = vec![ - // 0: camera - vec![wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }], - // 1: material textures (PBR) - vec![ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 2, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 3, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 4, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 5, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 6, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - multisampled: false, - view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 7, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], - // 2: material params (PBR) - vec![wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }], - // 3: per-draw dynamic UBO (MVP) - vec![wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: true, - min_binding_size: Some( - std::num::NonZeroU64::new(std::mem::size_of::() as u64) - .unwrap(), - ), - }, - count: None, - }], - ]; - - let desc = PipelineV2Desc { - label: Some("scene_pipeline_v2"), - vs: ShaderDesc { - source: vertex_wgsl, - entry_func: "main", - }, - ps: ShaderDesc { - source: fragment_wgsl, - entry_func: "main", - }, - bind_groups, - targets: &[Some(wgpu::ColorTargetState { - format: self.offscreen_color_texture.texture.format(), - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], - depth_stencil: Some(wgpu::DepthStencilState { - format: self.depth_texture.texture.format(), - depth_write_enabled: true, - depth_compare: wgpu::CompareFunction::Less, - stencil: wgpu::StencilState::default(), - bias: wgpu::DepthBiasState::default(), - }), - multisample: wgpu::MultisampleState::default(), - }; - - let pipeline = renderer.create_pipeline_v2(desc)?; - - // Camera buffer and bind group using pipeline's camera layout (group 0) - let device = &renderer.ctx.device; - let camera_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("camera_buffer"), - size: std::mem::size_of::() as u64, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("camera_bind_group"), - layout: &pipeline.bind_group_layouts[MATERIAL_BIND_GROUP_GLOBALS], - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: camera_buffer.as_entire_binding(), - }], - }); - - // Pre-create per-draw dynamic UBO ring and its bind group with large preallocation - // Creation happens in init; growth/recreation only when capacity is insufficient during draw - self.per_draw_capacity = MAX_EXPECTED_PER_DRAW_INSTANCES as u64; - let size = self.per_draw_stride * self.per_draw_capacity; - let per_draw_buffer = renderer.create_buffer(BufferDesc { - label: Some("per_draw_dynamic_ubo_ring"), - byte_size: size, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - })?; - let layout_ptr: *const wgpu::BindGroupLayout = - &pipeline.bind_group_layouts[MATERIAL_BIND_GROUP_PERDRAW as usize] as *const _; - let per_draw_bind_group = - renderer - .ctx - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("per_draw_bind_group"), - layout: unsafe { &*layout_ptr }, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { - buffer: &per_draw_buffer, - offset: 0, - size: Some( - std::num::NonZeroU64::new( - std::mem::size_of::() as u64 - ) - .unwrap(), - ), - }), - }], - }); - - self.state = Some(PassSceneState { - camera_uniform: CameraUniform::new(), - camera_buffer, - camera_bind_group, - pipeline, - per_draw_buffer, - per_draw_bind_group, - }); - - Ok(()) - } - - fn draw( - &mut self, - encoder: &mut CommandEncoder, - renderer: &mut Renderer, - _frame: &wgpu::SurfaceTexture, - _view: &wgpu::TextureView, - world: &WorldQuery, - ) -> Result<()> { - let active_camera_entity_handle = world.active_camera; - let camera_component_storage = world.camera_components; - let transform_component_storage = world.transform_components; - - // TODO: Update all isDirty=true materials, for each: - // - Color3f -> Update material uniform - // - Texture2D -> Update material bind groups - - // Read active camera + transform - let camera_storage = camera_component_storage - .data - .get(active_camera_entity_handle.data().index as usize) - .unwrap(); - let active_camera_component = camera_storage.as_ref().unwrap(); - let camera_transform_storage = transform_component_storage - .data - .get(active_camera_entity_handle.data().index as usize) - .unwrap(); - let active_camera_transform_component = camera_transform_storage.as_ref().unwrap(); - - // Update camera uniform and write to GPU buffer (no allocations) - let vp_mat_arr: [[f32; 4]; 4] = { - let state = get_state(self); - state - .camera_uniform - .update_data(active_camera_component, active_camera_transform_component); - renderer.ctx.queue.write_buffer( - &state.camera_buffer, - 0, - bytemuck::bytes_of(&state.camera_uniform), - ); - state.camera_uniform.view_projection_matrix - }; - let clear_color = active_camera_component.clear_color; - - // View-projection matrix - let vp_mat: Mat4 = Mat4::from_cols_array_2d(&vp_mat_arr); - - // Extract frustum planes - let row3 = vp_mat.row(3); - let row0 = vp_mat.row(0); - let row1 = vp_mat.row(1); - let row2 = vp_mat.row(2); - let make_plane = |plane_vec: Vec4| -> (Vec3, f32) { - let normal = Vec3::new(plane_vec.x, plane_vec.y, plane_vec.z); - let len = normal.length(); - if len > 0.0 { - let normalized = normal / len; - (normalized, plane_vec.w / len) - } else { - (normal, plane_vec.w) - } - }; - let planes = [ - make_plane(row3 + row0), - make_plane(row3 - row0), - make_plane(row3 + row1), - make_plane(row3 - row1), - make_plane(row3 + row2), - make_plane(row3 - row2), - ]; - - // Build visible set - self.visible_pre_draw_buffer.clear(); - for render_queue_item in world.render_queue.iter() { - let key = pill_engine::internal::decompose_render_queue_key(render_queue_item.key); - let mesh_handle = - RendererMeshHandle::from_parts(key.mesh_index as u32, key.mesh_version as u32); - let material_handle = RendererMaterialHandle::from_parts( - key.material_index as u32, - key.material_version as u32, - ); - - // Transform - let entity_index = render_queue_item.entity_index as usize; - let transform = transform_component_storage - .data - .get(entity_index) - .unwrap() - .as_ref() - .unwrap(); - let model_arr = pill_engine::internal::get_model_matrix(transform); - let model: Mat4 = Mat4::from_cols_array_2d(&model_arr); - - // Mesh AABB -> world AABB - let mesh = renderer - .state - .resource_manager - .meshes - .get(mesh_handle) - .unwrap(); - let local_min = Vec3::new(mesh.aabb_min[0], mesh.aabb_min[1], mesh.aabb_min[2]); - let local_max = Vec3::new(mesh.aabb_max[0], mesh.aabb_max[1], mesh.aabb_max[2]); - let corners = [ - Vec3::new(local_min.x, local_min.y, local_min.z), - Vec3::new(local_max.x, local_min.y, local_min.z), - Vec3::new(local_min.x, local_max.y, local_min.z), - Vec3::new(local_max.x, local_max.y, local_min.z), - Vec3::new(local_min.x, local_min.y, local_max.z), - Vec3::new(local_max.x, local_min.y, local_max.z), - Vec3::new(local_min.x, local_max.y, local_max.z), - Vec3::new(local_max.x, local_max.y, local_max.z), - ]; - let mut world_min = Vec3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY); - let mut world_max = Vec3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY); - for c in &corners { - let p4 = model * Vec4::new(c.x, c.y, c.z, 1.0); - let p = Vec3::new(p4.x, p4.y, p4.z); - world_min = world_min.min(p); - world_max = world_max.max(p); - } - // Frustum culling - let mut outside = false; - for (normal, d) in &planes { - let p = Vec3::new( - if normal.x >= 0.0 { - world_max.x - } else { - world_min.x - }, - if normal.y >= 0.0 { - world_max.y - } else { - world_min.y - }, - if normal.z >= 0.0 { - world_max.z - } else { - world_min.z - }, - ); - let dist = normal.dot(p) + *d; - if dist < 0.0 { - outside = true; - break; - } - } - if outside { - continue; - } - - let view_proj: Mat4 = vp_mat; - let mvp: [[f32; 4]; 4] = (view_proj * model).to_cols_array_2d(); - self.visible_pre_draw_buffer.push(VisiblePreDraw { - material_handle, - mesh_handle, - entity_index: render_queue_item.entity_index, - mvp, - model: model.to_cols_array_2d(), - }); - } - - // Sort and group (Pipeline -> Material -> Mesh) - self.visible_pre_draw_buffer.sort_by_key(|v| { - ((v.material_handle.generation() as u64) << 32) | (v.material_handle.index() as u64) - }); - self.groups_buffer.clear(); - // Resolve pipeline pointer for this frame (single pipeline for now) - let pipeline_ptr: *const wgpu::RenderPipeline = { - let state = get_state(self); - &state.pipeline.pipeline as *const _ - }; - for v in &self.visible_pre_draw_buffer { - let need_new_group = self - .groups_buffer - .last() - .map(|g| g.material_handle != v.material_handle) - .unwrap_or(true); - if need_new_group { - self.groups_buffer.push(GroupCmd { - pipeline: pipeline_ptr, - material_handle: v.material_handle, - batches: Vec::new(), - }); - } - let g = self.groups_buffer.last_mut().unwrap(); - if let Some(batch) = g - .batches - .iter_mut() - .find(|b| b.mesh_handle == v.mesh_handle) - { - batch.instances.push(PerDrawStd140 { - mvp: v.mvp, - model: v.model, - }); - } else { - g.batches.push(MeshBatch { - mesh_handle: v.mesh_handle, - instances: vec![PerDrawStd140 { - mvp: v.mvp, - model: v.model, - }], - base_offset_u32: 0, - }); - } - } - - // Per-draw ring buffer setup - let needed: u64 = self - .groups_buffer - .iter() - .map(|g| { - g.batches - .iter() - .map(|b| b.instances.len() as u64) - .sum::() - }) - .sum(); - if self.per_draw_capacity < needed { - #[cfg(debug_assertions)] - { - log::error!( - "Per-draw capacity exceeded: needed={} capacity={}", - needed, - self.per_draw_capacity - ); - } - // Release: proceed; only first (capacity) entries will be used by draws - } - - self.staging_buffer.clear(); - let mut next_offset_u32: u32 = 0; - for g in self.groups_buffer.iter_mut() { - for b in g.batches.iter_mut() { - b.base_offset_u32 = next_offset_u32; - for pd in &b.instances { - self.staging_buffer - .extend_from_slice(bytemuck::bytes_of(pd)); - let pad = - (self.per_draw_stride as usize) - std::mem::size_of::(); - self.staging_buffer.extend(std::iter::repeat(0u8).take(pad)); - next_offset_u32 = next_offset_u32.wrapping_add(self.per_draw_stride as u32); - } - } - } - { - let state_ref: &PassSceneState = unsafe { self.state.as_ref().unwrap_unchecked() }; - renderer - .ctx - .queue - .write_buffer(&state_ref.per_draw_buffer, 0, &self.staging_buffer); - } - - // Encode offscreen pass - let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("m6_offscreen_pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &self.offscreen_color_texture.texture_view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: clear_color.x as f64, - g: clear_color.y as f64, - b: clear_color.z as f64, - a: 1.0, - }), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { - view: &self.depth_texture.texture_view, - depth_ops: Some(wgpu::Operations { - load: wgpu::LoadOp::Clear(1.0), - store: wgpu::StoreOp::Store, - }), - stencil_ops: None, - }), - timestamp_writes: None, - occlusion_query_set: None, - }); - // Immutable view of pass state for binding (no aliasing with groups_buffer immutable borrow) - let state_ref: &PassSceneState = unsafe { self.state.as_ref().unwrap_unchecked() }; - for group in &self.groups_buffer { - rpass.set_pipeline(unsafe { &*group.pipeline }); - rpass.set_bind_group( - MATERIAL_BIND_GROUP_GLOBALS as u32, - &state_ref.camera_bind_group, - &[], - ); - // Bind group-specific material (HOT-ish path; debug-validated, unchecked in release) - let material = unsafe { - let m = renderer - .state - .resource_manager - .materials - .get(group.material_handle); - debug_assert!( - m.is_some(), - "stale RendererMaterialHandle {:?} in render queue", - group.material_handle - ); - m.unwrap_unchecked() - }; - rpass.set_bind_group( - MATERIAL_BIND_GROUP_TEXTURES as u32, - &material.texture_bind_group, - &[], - ); - rpass.set_bind_group( - MATERIAL_BIND_GROUP_PARAMS as u32, - &material.param_bind_group, - &[], - ); - for batch in &group.batches { - // Hot path: assume valid handles (render queue built from alive resources). - // Debug-only validation; no branches in release. - let mesh = unsafe { - let m = renderer - .state - .resource_manager - .meshes - .get(batch.mesh_handle); - debug_assert!( - m.is_some(), - "stale RendererMeshHandle {:?} in render queue", - batch.mesh_handle - ); - m.unwrap_unchecked() - }; - rpass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); - rpass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32); - for i in 0..batch.instances.len() { - let offset = batch - .base_offset_u32 - .wrapping_add((i as u32) * (self.per_draw_stride as u32)); - rpass.set_bind_group( - MATERIAL_BIND_GROUP_PERDRAW as u32, - &state_ref.per_draw_bind_group, - &[offset], - ); - rpass.draw_indexed(0..mesh.index_count, 0, 0..1); - } - } - } - - Ok(()) - } -} diff --git a/engine/pill_renderer/src/resource_snapshot.rs b/engine/pill_renderer/src/resource_snapshot.rs deleted file mode 100644 index d067b1aa..00000000 --- a/engine/pill_renderer/src/resource_snapshot.rs +++ /dev/null @@ -1,31 +0,0 @@ -use pill_core::{ - RendererBufferTag, RendererCameraTag, RendererMaterialTag, RendererMeshTag, - RendererPipelineTag, RendererTextureTag, ResourcePool, -}; - -use crate::resource_manager::ResourceManager; -use crate::resources::{ - RendererCamera, RendererMaterial, RendererMesh, RendererPipeline, RendererTexture, -}; - -pub struct ResourceSnapshot { - pub buffers: ResourcePool, - pub pipelines: ResourcePool, - pub textures: ResourcePool, - pub materials: ResourcePool, - pub meshes: ResourcePool, - pub cameras: ResourcePool, -} - -impl From for ResourceSnapshot { - fn from(m: ResourceManager) -> Self { - Self { - buffers: m.buffers, - pipelines: m.pipelines, - textures: m.textures, - materials: m.materials, - meshes: m.meshes, - cameras: m.cameras, - } - } -} diff --git a/engine/pill_standalone/Cargo.toml b/engine/pill_standalone/Cargo.toml index 6ae5e7bb..b838381f 100644 --- a/engine/pill_standalone/Cargo.toml +++ b/engine/pill_standalone/Cargo.toml @@ -4,10 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -pill_core = {path = "../pill_core"} -pill_engine = {path = "../pill_engine", features = ["internal"]} -pill_renderer = {path = "../pill_renderer"} - +pill_core = { path = "../pill_core" } +pill_engine = { path = "../pill_engine", features = ["internal"] } winit = "0.29" env_logger = "0.9.0" pretty_env_logger = "0.4.0" diff --git a/engine/pill_standalone/src/lib.rs b/engine/pill_standalone/src/lib.rs new file mode 100644 index 00000000..5711efc7 --- /dev/null +++ b/engine/pill_standalone/src/lib.rs @@ -0,0 +1,3 @@ +pub use pill_engine as engine; + + diff --git a/engine/pill_standalone/src/main.rs b/engine/pill_standalone/src/main.rs index ef5988db..5a09e146 100644 --- a/engine/pill_standalone/src/main.rs +++ b/engine/pill_standalone/src/main.rs @@ -2,8 +2,8 @@ mod file_watcher; use crate::file_watcher::FileWatcher; use config::Config; use pill_core::{ PillStyle, EngineError }; -use pill_engine::internal::*; -use pill_renderer; +use pill_engine::internal::{Engine, PillGame}; +use pill_engine::internal::PillRenderer; use anyhow::{ Context, Ok, Result }; use winit::{ event::{ Event, WindowEvent, DeviceEvent }, @@ -234,7 +234,7 @@ fn check_and_reload_game( // Load new game dynamic library let (game_library, game) = load_game_dynamic_library(&project_paths.game_dynamic_library_path); - let renderer: Box = Box::new(::new(Arc::clone(&window), config.clone())); + let renderer: Box = Box::new(::new(Arc::clone(&window), config.clone())); let mut new_engine = Engine::new(game, project_paths.game_resources_directory_path.clone(), renderer, config.clone()); new_engine.initialize(*window_size).unwrap(); *engine = Some(new_engine); @@ -447,7 +447,7 @@ fn main() { game_dynamic_library = Some(game_library); // Initialize renderer and engine - let renderer: Box = Box::new(::new(Arc::clone(&window_data.window), config.clone())); + let renderer: Box = Box::new(::new(Arc::clone(&window_data.window), config.clone())); let mut engine: Option = Some(Engine::new(game, project_paths.game_resources_directory_path.clone(), renderer, config.clone())); engine.as_mut().unwrap().initialize(window_data.size).context("Failed to initialize engine").unwrap(); From 4a5e8b8c83104c4da50fdcf123910c66329e7979 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Sun, 16 Nov 2025 01:41:30 +0100 Subject: [PATCH 60/69] fix path --- .../res/textures/pill_logo_horizontal_white.png | Bin 0 -> 9434 bytes .../src/ecs/systems/rendering_system.rs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 engine/pill_engine/res/textures/pill_logo_horizontal_white.png diff --git a/engine/pill_engine/res/textures/pill_logo_horizontal_white.png b/engine/pill_engine/res/textures/pill_logo_horizontal_white.png new file mode 100644 index 0000000000000000000000000000000000000000..d4e5bc353398f8fc570b1d93cb403cc45ad579e2 GIT binary patch literal 9434 zcmd5?c|26@+dngwu~V`WrAUY#g|f_~lwB!=7)2>dwva3{)?_aal_ey3D!T@S5VB=U zjO_c8ofyl^-0v~-{CX2Kd64MSZk)=V@|D3-PO24hP?`y6ISWLJ&Le-hqIUQw8A;a+eLRX~Pcz z0Q585C@Cp{&-4$S7!?&27#K*W#K*_OjiRC=`0VTJODDqT&d$#K{Cv3aUp{4JWpKN$ zt_~(PH#b*RRq^ofynp|mot>RNN>o&ofq@}0F%iB+p-_^NlFG`;^ha4)S*fY1aNF70 z`NWA6o}Qj?J1i`0cz9SqKmcw_OH0G&Teoh(=jrL`%avui-Q9iT#tjmQ1XGrlmf$n}V{UE^H^^l2e+d8kDk35RD~v!OgoK3n`1lxOj>EV? z_76;quEMDCC<2VTZ6p$a#SB0Kzd)fljMMjkcS*npMA9@7V1X}_@Nwx10P^tl9( zHC4&;fX4m~UZPt;Qr^mZvxx)cfjH;#65y zL5b-XJqf3)t#NTc6RhnLDmyuwbqTVf)egn2 z=B)NEzqhyZsL$QoK+*`4R^ z!jeC$g*8%-g|~M65av4a<80Bha?Ag+%5IqR>@KN*onE1)Bo9W1)^VYM>m_4bL&swV z?T-t^Uz`6WQq{62xMh2LH*-T^xzt>Lqt&lJR;A3mVY@9V4F$eFtznrJ)9=k&yMC;d z8g;WXdD$plpDv?id+U%QX?)nR%Gzo8@0}CD4nbMfi~(ND_TzzxO%`$A=OmT$7>YpT z?dJvioBFSSb}gLPaNUxBXc?wzW~`1DHb@P|6` zh@FxB1?X`vpBT+H$L*O4(!6*#Rau2{nglkaXNV+4(g*wDDD0)_&g7^PUkmT;<3YKG zGUzx_N#hP_r=Fd(6bM`X#ikntwLBR;ia3#H_nYP*z3t!qTRRf&CElMV@K|;B;2crP z@pMFHchC(>&olvPd%B@(ITK?qPbYa-Re4JzRh3ccs}FMh*X)qMd+Xz?;|kcL@dTOr zH0pq=gLB_GAN`t3y=>}Jzlo;=`IW=!-+5uYX!;z=2g|SdsaY?Rz4zlcgfL|q(4y+T zQ(U7z&*pxng#s55q^KMEeVIK1O{n9)UBx4UHxt$z7f(Hxub_3{Ac6sZL%)i#?+l7A ztin?Z#E(3yCah(>IgT`YTHGk;rh!rw{g0{3a$eWRC~F^iR(NXi2z3TY3u~rGi4;%*V`H0oE8g{{}sXFPL-al@l_dhTC?Pc*xLEK$(yNNc4kH=O0M4be@is8A3dM`KIfG`*~vgCG?OwAmKq?E~-7nqO*IzEl}!Z8MCKT z+dKI^fV1N`K!T^`^y5&|W*aD8sn<$+i}0TQDE0v0{b8(cxMhN|aXb3MHpu-j9y`yQ zy*FP=Kkh?QeO}&vfjVIGI=N2N@v%XK*n!=N^(Av#zT2e z|HJm(;d|7MiLWQroA(9%;>1s~N?@EO7qVzBuu9h1L<_x^W;pUtLA4GMk2UJsLVmpX zrTaL|$4Q4x(t%6~YZ=2GRY*s3tN|eiEns?+4z5v_7npPuIbTdc5}zm++Tsv3T=p*S zo(2P5jj_N{0~91=^szLWM!okm91Pv7*|Nhsqo6ao9(p{?kQ$6(=)yMod9A$yqrEz$ zbLJekEmX*0VRiP+B+etzSxjOcf!Qy|L?<49GlkRQ=UrrK@rNeImlbLuTU2rBDIEv0 z@h78Gtnu5BghNpdLJ{f<%9Up_`GbCG{83187r_I{EjS==38w_(WU9|FYU`lSeh35A zQ(M38aZsLnqe3nFlxM-Z#1_T_@t#D{?UPD*KrwF0r?`M`xuT&^gc?f?t$XvEMVv>f zJwML{sNTPeg8?tr)Ia&`_f~qb18!vfMNh~+Xw?i!)57rh-0sLH35liCL5fI;;a?C9 zI4NExs`P+vFCvJsMk`gej^6d~yEx#&%*g?n7+sQOhA=K`m&b7q(w7H`K^cq~&2O=` zbQ{8jL5dJ$FTs}wS5`u^R>RhaR2O@#EfW7gQ;dbC&5_I&@TQ~w45VfAHj|G5%1}OY zY#l9h(oLxI?S!}>4S3yiX~1xh<-*(+3!dhnzb>Bwm(HAWl`TA<69&;`;q^cQ=Y$Qj z$IHJ9Xnadgai{NhZuZv&j*I594W?T z3C;|7K8U?bf$7$sYg23w?2h;ft#p(VnE0T{m(DaZo@mdJ)7zd=HqaAh0%p*KWz;ts z=geayP@AfO65RE`Tu1O|a7NH{J$z*e=fEaG-KZ%<;+=JSBN5Q$6wI|a@Wf9+XD|S- zb#}7>bJ*rps{9J>9*e^>!Yv5;DaFM}#@$;+M&OiQyp9d18c{m)9kjVT%)TsZpMdF= z;@~7NM?=F61tni&hr|=&5gKd+EoHcLy}pd5y1G4eX3QwJORpLQJ9mr@IEFdH^@g!) z0hpv!Hwb1t53x!gCt7+nE>%w!)SO@2mlHxaS1_3}Vwy7UdXjnbSsr4xv>(>%$73 z&&#r>EL&Z|iC1fVtNO0R2eN#U=x0`m%@n+{@B?R8m{IdkJ~3#Y#S@!r&mjk#F)U~(fcb;@$>5=XwN;EOoO{OT?@k9I*U--$X8+;NJNgH~;KfB_ufGTFlrvfVqWb;mt_r$;IazCkKbY*bz>6qg z3O&IqO~4u^8GcvU^hk@+_!kQ6cl7K<@a1`MxXE zozP(}`m}SdU#Y&$xvFC2FgOG3t?(Kj=dUAj7(&I^!)baNoaz8!TO<9WuBEZ%R1jtO z*jsF%4Vzggi=<7mn6QV`vfL9??|CR4Y|QrsP}vgs?)Bu$Ga)TR7~CleU|&Xh2N3p_ z10sl<0--l)`PBo)(P@lYroyb#uz_2sz=&sJ8`skJ3zzcJQ^@}A-(aXMd4|-`c5iv2 z{VnBD{$Ln%A2{_Dk^O?C4&3W0kZ(X(SeaN-(4^5rqj&>faoNd1YMcu-i9O7d?3#sb zeC1(&%vn}~mTr!|@g<{OEeL0SZQj7L6=(dmuT8DYs%6~5*2M<2SZAgl&f6hmtHL$x znVRMb}lV{VwcvoGRGIjDHCCxgY|3w0@{dER(B4!o^yxGb<10 z372Lo!jItTgiZo^LNd#Ys~zCVsfFuQQV^_ep?nyuF7i5A!GTAj37m!TxCm)mo_5y1 ziRtA}zXQ0x{i%*-uhZVfHpWhIHiFT zeg}}k1}G=HZxN9VdqW$}K%8*-krIWyVolv-=b6QcsU?9MnE%R(>sGlymbsy=qXt$daI1$R((&rUg<=hzcctXSLUm&2D>rhI5l~Y-*e~qczY0ODg ziH9*i8-@-@{iJ#IB}Nb$hb405QOmGvLaTwl6+)P5*GKQy92Vk6j@laiG{Ilr^IJ!` z?8gk94}mT+TuuGAI@PH;YCt7;hT1TH2FFsUG7*qEJg}WR}7Lkbz zW33pNeV&pFxg!|Q$g_y!+%spia5spCPo88jS;6*vyxX3L)xWc0tcSgpo*m=~!rLKIt?HNed;;GqUzllUV zL3=$%2Uzv49`$5sS*Z>blO1);xUX$VaMBNgU8P$Bc6}+X78SW=%u(X7mc2z)WLIQ7 zpSGo!lm1hcQDa33oQx-ROcX1B80#$L{lGKNprE?tn z`4eu4%5{KbFFB9~aoWJ?)7&bOskl^8^K8J%jCcupeRfPzp>N+4>ml*Yy(3()vwK#S;iKyo8}cc{_i@p|r1X_#QU?2AKa@G(ZT1B`xRN_{uyv==RvLnc>IMOuJ#P}E-vnOpqYDZ z&krxi2LIaVw5z+li%p4LOq(o-sC)5O-sg|m91>1Wtv&-j?G|5Bu1xdQw+~;dn1SY~ z#B4k4lb4{x_k+O)DHjz4@#$Ek0nBHElvkZmy=DQxtWbZnq<=U7sB5o)3;Y=`5k4nXS z5SA0%WO72KyqR@j`XV)9^GbAtSfy_9xiVYmq+j{H$FaML@xg#F+h@)|PFy*2CCBt} z>EBGSac)Uaq&Hz#Tm&Fb{JzDACRVA8Qd#G>`OU3;gHx3PnN#NkTU1?`7wdKT9 zqD-rRz2QDfab9HEnuL;*<(B^(2=PJrrw-J2uU3!8E^6>Og9vL#5K_nVZVEo)sEO8) z^g-`7r3aV)6D?5j`ECANZ!2n$r?agd)x5uG*A5vA?q%lE*ISz}TuVut3wq}Ax}xTK z=dC%zk2g#^K#kyCG;q1*wU98irU=Z@(=WC1;ti1&19r14cq2qhKf~nScG28WLHO+g zZl9Q1Z$d=gQc*Ln%(V2=C^&@obYc0-YTIZNRq<>Id^?sQ+vgDf8E8MqH8RknEfohO zsl`J+4nzyG3=|6*8Gw(MX>eoo+f|$}^)Wv*HUKr;ZGfPE7WX|6;IU@7o0h>l1!R7M zub1_qH)`Ng5Fd1t7|hnax9qf%Mb93nIkS2Z^5m|xV%OB{Yg9`cwSyO)?*DXd#f|@b zy(`u}7V>UK{5o`D0l7bGZyjA~Hmh7?QPC2xnKSfgXrKk{=QX;>_UhH=4R59nQw7S0 zuP6nIb*uCk;Snh8*qQG2VrK~}V*tsP^i4`LPnONY0VZvzsk>j~yc@lyg zou;q@_{ZLfbDM)sTIx6~V+k^<_T)LG&;uGDMiM-i?b2>M64$Ra8$G9`XSy?4{pdbU z^mwB8+_Q-jly4s?+_KQuI3!Q@zZ$yOO2Vv8WV|H!c2p6*oCY@o?R!QfKbmjMuyL_d z@zmF&%V6lAon0JJp@Y4B5S#hXP4n8ZE+PEOZLg!%pav=7!3M8B;c0HUeaouz6~iCg z_Xcc{c=40agjMswZ zQS%D0pZ~*&=K(oq!6gJQl$He5(;_O5Z{W!TmQltU=j=(bUni|aN#0CEvyLkN7b3a4 zAz@EHqumS5Nz(hc3nH&-H-hZeN5uF=VD02n>10FzwTze?4Ji2I-fpJ1<>}(1T@$y) zF~3^p(JZxC}N!$Mq5|r z^&u2p#93y^`Nlqcm6U3w58AF^dC=DF#fVO`xZHv%`EgKP%PW%m%C`|<^*N85@3{8v zxm%^>FUPk>4neH=rVtFHBh6)ee&RJZCZ6}>P!Z2JP`fzxu=n%0R+DuqRtetql8zju zZSe?N1x&f894ik_&xTT~k855BB5dRWDoI=q`t#;LF&Bi1x#{QDDo_B#<-kBAq_T}2 z+ZtiM097tmG9J^R`=RrEE$T^zQmqPM6CD9H#k_q>k66v_k;RKKjkmX|ThlHsWW~Jh z2NKAP34Lgsju=~&K+_kYtvhGG_mZjNZ(Lq`lU*4kpj%uJJ$okNoQ;1()s~ZxaUXYZ zI!b_n1z*kpZ-oy^IQcPZ@YHDTdxQSo$4bE->%3i)>ApzjmVlDFf9|D$0m)gpjH*jz z_P7`MW|SC=ll5@^sO8Ad*URfDks!U=iy7~FOu7aro~DHq>?1Wds;)TrDJ?r^k1v9>SHh%C(EGl&6|)W>6fYcD?yM>UWtN}b39wC=su`X5 zwtISq(%}!uT^M1MxIx1;ZhI-FTi?ut>=e4A-w<34%33$8)#5vE0xYJKK_Y!Nj*Yez zJb2y}MQDWVSBn}ZiUN8}S>6JX4)6-3ZQ!iQ>8ntLI}83g&E*>5He7dNRE}$OEP&|^ z^a%LVekFjMz6eEpVY?hon~VLJM#wKl4_my*ZaF3Z(=^Z%E{DRL382SKP(H)= zzDprWE=+gTfeN6OZB+l>M(c$$Nc0Lp!L+S?Z>;1%=B63G_2uTlxYr75)DAbuK`>E1%j-UCrIPH%5ErGi7|h z^0{mb1?!oQ!e-}Xz{?I{$S#r|di*o{M*H4<_E8lU}7amaLrT=1Lo zlfr@25g_AX*Vg^)R5I{svWF%o#1K;FXntADObN<8Q88^k>`H0x2`U3VZy(Grf;>}K zN-H$YeSE32`cWWOf#X*-*yKYD7*dIL)VOC2P?N@0y7@c{bwOzpwIK;X(+6`|)%y+= zu8-oX4wAfqAzyg@bXr$kYWO+;8GhRP#lNj01vYjyx4jsoD z0|z;%PaH7a+sicaqMyY(EkBO1>J&@5Jqqx8Up}4B@x4=;MZOUzTd?786qn&{)AFo5 z(m~jkXu~F9P2j06%T%5hJN)sXcEkK{Ajeb_btDJq&=oxJ2XE^XqR9~LT5k-+sU?dI zuU7|BJB*-7)jOb`E|ksZuv?d=MEv03;>PZ!Kx@*rA2Jq2iGlP(s6tv5F>&o{={~Lh z+@HXu1BlBK2mj= znb%`#Cs694Vfm)5N6oHkg|!cr5?pb6tHe=ku^9tZju7c@64SQ+w2O_zsc9Ye6C*Ns z@1{1RJtnw7Ucr0Q(2M|xgWY#WfS!S4$y|p=Db^|Mi0_A6_Nv0h0lS5QhRHS0!#{Fy$n9LzjTR{E-VkC`fEav(jQE zs*Wg0Y!%SPcAmSrd*7~CoXcR@Cbz(JiGsT^@+XO-`dxkiqc93{-{AXTx(*G!hVcXy zj=~e~xlOHKnZNxU9v+#!8X`q7pC48Z%EFlvHP~DjY%{H93#ujP{U=LEJhqGX(=X}6 zQR~W1_(IP5JpS~qFytVMY@{M-X@dW`G1lL_!wkS5gaor(ksP6LSd5Tc))Kq#ua?HpmCL1#DGO;lL^`W^RYcl%P$MpPU#A|9Qh zp13utaE+#fGkI%kdw`VGRnt|V&;fR_J*V|Ue?U!!m7;0^k~eFOlWBRjZ+mrhhKQ~X z*j^i}d#o_>3P)W=R}skrhcfI&s&&4#1_d=2Exnpj`H{ZeKwF8m-8J}qo%RAtNjTvw zKMkAcZ*O87XfI~7PQJ6F?f?Iuc2nYkG&)Si=DJm~nvLv3bKCAJLiP80Mk6*Z$Qe`~ zBh{<_EmW+WRSWUtoOY6OZ>pMovKqo_k)gMC?j^*J={*1Mh=C*aN?9nH z{3|seA+#g${J&}<6`LcU(1(~oQbI~s=$+VVj$|s`jwc9lGUHYAAOnv(ujwNB@VeoK zKj~^A6?NM$(6y3<65W`j2jjOnGC!}G7Y$?fr!ECp% zs-5F4RxUaD&ib~~lmhll=DqySw~$TW6-?i+l*uyV%hwWlcOtTAwCkr$*9I4p)1dgg z#A^Sk&}8_B>#Sz?GAC~%%Tc1RMqEAii-#7x(@W{%@Pw&I+jGTcRG5Jy{GvVjPH=^F zNl9f(!RfQmR;zit2rogXjn%be+Eg~8!*8iNb5{Q?OgEZ6g0gqHs8lnME7X5;`5r7X%bhwxLsng7`F*#0 zF+~%HCI)UF=F@_8yTPt5q;`8sCaza-UhKxy2Wbyo0oXadwZrd)`k5wcR+p~kdf6}e zeE566hQVGOZ@zr~a9zj`8r>0!^7>@|7AK{a>j7J-Y?Yg5X=c4X8C~S45f#YyqQalE z@2~4AzZBb-4eZ@+5uUr(@RDvmCirs-F8xf4C%iONxQgU(SbG~rbiQO&|Hb-Mkw53G zdNVwq#uO_T&pg!yD%41!_R^;G7I`dc+#mrCSXI}ArHvB~q)bQIx!f7IVwuVMsd>zq zVNuwDce*PQCr6112I+;W$#)e~6r=;jWGuGN#z$3Uixex@`3~`}%SfQ(w|SG&zXc|> z85gG>6{z+XYf4N=UF5PznOlhS@>f$Ot)0?$g&wcHSGzbkW&G%R!?)vDcB#^5mT>4z zZ2S2xXg9eW`c%^VN(CQ&HA{nGAL;dEM~kfg6#?rn{Bo6px76fd1SjK$yUKE`?53I# zDb3AmeZ4IM0X%4O9+~pzf%BUEE!7!=`%|9m?7Rw6tyZmBx1Qx^`x`dRgo_-nc)ra6 lzlHxl|AU172b4-OdNa`e%g9|h;k|#|=xQ5X$k(zB{vWIbV>kc+ literal 0 HcmV?d00001 diff --git a/engine/pill_engine/src/ecs/systems/rendering_system.rs b/engine/pill_engine/src/ecs/systems/rendering_system.rs index 3baf8d4d..9fbf0e78 100644 --- a/engine/pill_engine/src/ecs/systems/rendering_system.rs +++ b/engine/pill_engine/src/ecs/systems/rendering_system.rs @@ -399,7 +399,7 @@ fn init_default_resources(engine: &mut Engine) -> Result<(), Error> { // Pill logo (overlay) texture let pill_logo = Box::new(*include_bytes!( - "../../../../../media/logo/pill_logo_horizontal_white.png" + "../../../res/textures/pill_logo_horizontal_white.png" )); let mut tex_logo = Texture::new( "pill_logo_horizontal_white", From be4796878611a35e6ccdfebfc0a19188eea66063 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Sun, 16 Nov 2025 01:41:49 +0100 Subject: [PATCH 61/69] cleanup resource manager --- .../src/ecs/systems/rendering_system.rs | 90 ++--- engine/pill_engine/src/engine.rs | 3 +- engine/pill_engine/src/graphics/renderer.rs | 12 +- engine/pill_engine/src/renderer/mod.rs | 5 +- .../src/renderer/resource_manager.rs | 120 ------ engine/pill_engine/src/renderer/wgpu.rs | 246 +++++------- engine/pill_standalone/src/main.rs | 373 +++++++++++------- 7 files changed, 380 insertions(+), 469 deletions(-) delete mode 100644 engine/pill_engine/src/renderer/resource_manager.rs diff --git a/engine/pill_engine/src/ecs/systems/rendering_system.rs b/engine/pill_engine/src/ecs/systems/rendering_system.rs index 9fbf0e78..0c732123 100644 --- a/engine/pill_engine/src/ecs/systems/rendering_system.rs +++ b/engine/pill_engine/src/ecs/systems/rendering_system.rs @@ -60,6 +60,14 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { .resource_manager .get_resource_by_name::("pill_logo_horizontal_white")?; let fmt = engine.renderer.get_surface_format(); + // Convert engine Texture -> renderer texture handle + let tex_logo_rt = tex_logo + .renderer_resource_handle + .expect("renderer handle for logo texture"); + + // Logo overlay + let h: f32 = 0.04; + let rect_logo = [0.98 - 3.0 * h, 0.02, 0.98, 0.02 + h]; let offscreen_color_texture = engine.renderer.create_render_target(RendererTargetDesc { @@ -72,55 +80,39 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { // Create depth and color texture let depth_texture = engine.renderer.create_depth_texture("depth_texture")?; - // Build passes - let mut passes: Vec> = Vec::new(); - - // Scene (renders into offscreen targets) - passes.push(Box::new(crate::graphics::pass_scene::PassScene::new( - "scene", - offscreen_color_texture, - depth_texture, - fmt, - ))); - - // Compose (tone map) from offscreen to swapchain (last) - passes.push(Box::new(crate::graphics::PassCompose::new( - "compose", - offscreen_color_texture, - fmt, - ))); - - // UV overlay - passes.push(Box::new(crate::graphics::PassOverlayUV::new( - "overlay_uv", - [0.75, 0.75, 0.95, 0.95], - fmt, - ))); - - // Depth overlay - passes.push(Box::new(crate::graphics::PassOverlayDepth::new( - "overlay_depth", - [0.75, 0.50, 0.95, 0.70], - [1.0, 1.0, 1.0, 1.0], - fmt, - depth_texture, - ))); - - // Logo overlay - let h: f32 = 0.04; - let rect_logo = [0.98 - 3.0 * h, 0.02, 0.98, 0.02 + h]; - // Convert engine Texture -> renderer texture handle - let tex_logo_rt = tex_logo - .renderer_resource_handle - .expect("renderer handle for logo texture"); - passes.push(Box::new(PassLogo::new( - "overlay_logo", - rect_logo, - [1.0, 1.0, 1.0, 1.0], - tex_logo_rt, - fmt, - ))); - + // Build passes as an array with elements, then convert to Vec + let passes: Vec> = vec![ + // Box::new(crate::graphics::pass_scene::PassScene::new( + // "scene", + // offscreen_color_texture, + // depth_texture, + // fmt, + // )), + // Box::new(crate::graphics::PassCompose::new( + // "compose", + // offscreen_color_texture, + // fmt, + // )), + // Box::new(crate::graphics::PassOverlayUV::new( + // "overlay_uv", + // [0.75, 0.75, 0.95, 0.95], + // fmt, + // )), + // Box::new(crate::graphics::PassOverlayDepth::new( + // "overlay_depth", + // [0.75, 0.50, 0.95, 0.70], + // [1.0, 1.0, 1.0, 1.0], + // fmt, + // depth_texture, + // )), + // Box::new(PassLogo::new( + // "overlay_logo", + // rect_logo, + // [1.0, 1.0, 1.0, 1.0], + // tex_logo_rt, + // fmt, + // )), + ]; engine.renderer.set_passes(passes)?; } diff --git a/engine/pill_engine/src/engine.rs b/engine/pill_engine/src/engine.rs index 5122eb12..9b957b50 100644 --- a/engine/pill_engine/src/engine.rs +++ b/engine/pill_engine/src/engine.rs @@ -68,6 +68,7 @@ impl Engine { game: Box, game_resources_directory_path: std::path::PathBuf, renderer: Box, + resource_manager: ResourceManager, config: config::Config, ) -> Self { let max_entity_count = config @@ -80,7 +81,7 @@ impl Engine { renderer, scene_manager: SceneManager::new(max_entity_count), system_manager: SystemManager::new(), - resource_manager: ResourceManager::new(), + resource_manager, global_components: PillTypeMap::new(), input_queue: VecDeque::new(), render_queue: Vec::::with_capacity(max_entity_count), diff --git a/engine/pill_engine/src/graphics/renderer.rs b/engine/pill_engine/src/graphics/renderer.rs index 79afdb0f..ecfc850f 100644 --- a/engine/pill_engine/src/graphics/renderer.rs +++ b/engine/pill_engine/src/graphics/renderer.rs @@ -105,7 +105,11 @@ pub trait Pass { pub type WorldQuery<'a> = RenderQuery<'a>; pub trait PillRenderer { - fn new(window: Arc, config: config::Config) -> Self + fn new( + window: Arc, + config: config::Config, + gpu_resources: &mut crate::resources::GpuResources, + ) -> Self where Self: Sized; @@ -154,8 +158,10 @@ pub trait PillRenderer { fn get_device(&self) -> &wgpu::Device; fn get_queue(&self) -> &wgpu::Queue; fn get_texture(&self, h: RendererTextureHandle) -> &wgpu::Texture; - fn get_mesh_buffers_and_count(&self, h: RendererMeshHandle) - -> (&wgpu::Buffer, &wgpu::Buffer, u32); + fn get_mesh_buffers_and_count( + &self, + h: RendererMeshHandle, + ) -> (&wgpu::Buffer, &wgpu::Buffer, u32); fn get_material_texture_bind_group(&self, h: RendererMaterialHandle) -> &wgpu::BindGroup; fn get_material_params_bind_group(&self, h: RendererMaterialHandle) -> &wgpu::BindGroup; } diff --git a/engine/pill_engine/src/renderer/mod.rs b/engine/pill_engine/src/renderer/mod.rs index a2bff96f..2671d713 100644 --- a/engine/pill_engine/src/renderer/mod.rs +++ b/engine/pill_engine/src/renderer/mod.rs @@ -1,10 +1,7 @@ #![cfg_attr(debug_assertions, allow(dead_code, unused_imports))] pub mod egui; -pub mod wgpu; -pub mod resource_manager; pub mod resources; +pub mod wgpu; pub use wgpu::Renderer; - - diff --git a/engine/pill_engine/src/renderer/resource_manager.rs b/engine/pill_engine/src/renderer/resource_manager.rs deleted file mode 100644 index 3a37509a..00000000 --- a/engine/pill_engine/src/renderer/resource_manager.rs +++ /dev/null @@ -1,120 +0,0 @@ -use std::collections::HashMap; - -use crate::graphics::{ - RendererBufferHandle, RendererCameraHandle, RendererMeshHandle, RendererPipelineHandle, - RendererTextureHandle, -}; -use crate::resources::TextureType; -use pill_core::{ - Handle, RendererBufferTag, RendererCameraTag, RendererMaterialTag, RendererMeshTag, - RendererPipelineTag, RendererTextureTag, ResourcePool, -}; - -use crate::renderer::resources::{ - RendererCamera, RendererMaterial, RendererMesh, RendererPipeline, RendererTexture, -}; -use image::{DynamicImage, Rgba, RgbaImage}; - -#[derive(Hash, PartialEq, Eq, Clone, Copy)] -pub struct PipelineKey(pub u64); - -pub struct PipelineCache { - key_to_handle: HashMap, -} - -impl PipelineCache { - pub fn new() -> Self { - Self { - key_to_handle: HashMap::new(), - } - } - - pub fn lookup(&self, key: &PipelineKey) -> Option { - self.key_to_handle.get(key).copied() - } - - pub fn insert(&mut self, key: PipelineKey, h: RendererPipelineHandle) { - self.key_to_handle.insert(key, h); - } -} - -pub struct ResourceManager { - pub buffers: ResourcePool, - pub pipelines: ResourcePool, - pub textures: ResourcePool, - pub materials: ResourcePool, - pub meshes: ResourcePool, - pub cameras: ResourcePool, - pub pipeline_cache: PipelineCache, - // cached defaults - pub default_color_tex: Option, - pub default_normal_tex: Option, -} - -impl ResourceManager { - pub fn new() -> Self { - Self { - buffers: ResourcePool::new(), - pipelines: ResourcePool::new(), - textures: ResourcePool::new(), - materials: ResourcePool::new(), - meshes: ResourcePool::new(), - cameras: ResourcePool::new(), - pipeline_cache: PipelineCache::new(), - default_color_tex: None, - default_normal_tex: None, - } - } - - #[inline] - pub fn buffer(&self, h: RendererBufferHandle) -> &wgpu::Buffer { - self.buffers.get(h).expect("Invalid buffer handle") - } - - #[inline] - pub fn pipeline(&self, h: RendererPipelineHandle) -> &RendererPipeline { - self.pipelines.get(h).expect("Invalid pipeline handle") - } - - pub fn ensure_default_textures( - &mut self, - device: &wgpu::Device, - queue: &wgpu::Queue, - ) -> (RendererTextureHandle, RendererTextureHandle) { - if self.default_color_tex.is_none() { - let mut img = RgbaImage::new(1, 1); - img.put_pixel(0, 0, Rgba([255, 255, 255, 255])); - let dyn_img = DynamicImage::ImageRgba8(img); - let tex = RendererTexture::new_texture( - device, - queue, - Some("default_color"), - &dyn_img, - TextureType::Gamma, - ) - .expect("create default color tex"); - let h = self.textures.insert(tex); - self.default_color_tex = Some(h); - } - if self.default_normal_tex.is_none() { - let mut img = RgbaImage::new(1, 1); - // normal map flat (128,128,255) - img.put_pixel(0, 0, Rgba([128, 128, 255, 255])); - let dyn_img = DynamicImage::ImageRgba8(img); - let tex = RendererTexture::new_texture( - device, - queue, - Some("default_normal"), - &dyn_img, - TextureType::Linear, - ) - .expect("create default normal tex"); - let h = self.textures.insert(tex); - self.default_normal_tex = Some(h); - } - ( - self.default_color_tex.unwrap(), - self.default_normal_tex.unwrap(), - ) - } -} diff --git a/engine/pill_engine/src/renderer/wgpu.rs b/engine/pill_engine/src/renderer/wgpu.rs index 9696a71c..0cd0671f 100644 --- a/engine/pill_engine/src/renderer/wgpu.rs +++ b/engine/pill_engine/src/renderer/wgpu.rs @@ -2,7 +2,6 @@ // https://github.com/kaphula/winit-egui-wgpu-template/blob/master/src/main.rs // https://github.com/emilk/egui/discussions/3067 -use crate::renderer::resource_manager::ResourceManager as RendererResourceManager; use crate::renderer::resources::{ RendererCamera, RendererMaterial, RendererMaterialParamsStd140, RendererMaterialTextures, RendererMesh, RendererPipeline, RendererTexture, Vertex, @@ -95,23 +94,20 @@ pub struct DeviceContext { pub struct State { passes: Vec>, egui_renderer: crate::renderer::egui::EguiRenderer, // TODO: Separate system adding Pass - // Resources and GPU objects moved from ctor into here explicitly - pub(crate) resource_manager: RendererResourceManager, - pub(crate) color_format: wgpu::TextureFormat, - pub(crate) depth_format: wgpu::TextureFormat, - // pub(crate) depth_texture: Arc, - // pub(crate) offscreen_color_texture: Arc, - // Prebuilt PSO handle - // [SIMILAR] Prebuilt once; no per-draw pipeline churn per TALK } pub struct Renderer { pub state: State, pub ctx: DeviceContext, + gpu_resources: *mut crate::resources::GpuResources, } impl PillRenderer for Renderer { - fn new(window: Arc, config: config::Config) -> Self { + fn new( + window: Arc, + config: config::Config, + gpu_resources: &mut crate::resources::GpuResources, + ) -> Self { info!("Initializing {}", "Renderer".mobj_style()); let ctx: DeviceContext = pollster::block_on(async move { let window_size = window.inner_size(); @@ -237,32 +233,15 @@ impl PillRenderer for Renderer { let state: State = { // Configure collections - let mut resource_manager = RendererResourceManager::new(); - - // Create depth and color texture - // let depth_texture = RendererTexture::new_depth_texture( - // &ctx.device, - // &ctx.surface_configuration, - // "depth_texture", - // ) - // .unwrap(); + // let mut resource_manager = RendererResourceManager::new(); // Use Rgba16Float for HDR color buffers; it's the common, well-supported, // performant choice. Reserve Rgba32Float for niche cases needing extreme // precision and accept the 2× bandwidth/memory hit. If alpha isn't needed, // Rg11b10Float/R11G11B10_FLOAT is a fast alternative. // Tone-map to the sRGB swapchain in the composite pass. - let color_format = wgpu::TextureFormat::Rgba16Float; - let depth_format = wgpu::TextureFormat::Depth32Float; - - // Milestone 6: Create offscreen color target (RENDER_ATTACHMENT | TEXTURE_BINDING) - // let offscreen_color_texture = Arc::new(create_render_target( - // &ctx.device, - // color_format, - // ctx.surface_configuration.width, - // ctx.surface_configuration.height, - // "offscreen_color", - // )); + // let color_format = wgpu::TextureFormat::Rgba16Float; + // let depth_format = wgpu::TextureFormat::Depth32Float; let egui_renderer = EguiRenderer::new( &ctx.device, @@ -272,30 +251,23 @@ impl PillRenderer for Renderer { ctx.window_ref.clone(), ); - // Create state - State { passes: vec![], - // Other egui_renderer, - // Resources - resource_manager, - // Renderer variables - color_format, - depth_format, - // depth_texture: Arc::new(depth_texture), - // offscreen_color_texture, - // Scene pass owns per-draw ring and working buffers now } }; - Self { state, ctx } + Self { + state, + ctx, + gpu_resources: gpu_resources as *mut crate::resources::GpuResources, + } } fn init(&mut self) -> Result<()> { // Ensure default textures are created before any pass init that may need them - self.state - .resource_manager - .ensure_default_textures(&self.ctx.device, &self.ctx.queue); + // self.state + // .resource_manager + // .ensure_default_textures(&self.ctx.device, &self.ctx.queue); Ok(()) } @@ -307,10 +279,6 @@ impl PillRenderer for Renderer { usage: desc.usage, mapped_at_creation: false, }); - // let handle = unsafe { - // let storage = &mut *self.state.renderer_resource_storage.as_ptr(); - // storage.buffers.insert(buffer) - // }; Ok(buffer) } @@ -431,22 +399,22 @@ impl PillRenderer for Renderer { fn create_material(&mut self, desc: MaterialDesc) -> Result { // Ensure default textures exist - let (def_color_h, def_normal_h) = self - .state - .resource_manager - .ensure_default_textures(&self.ctx.device, &self.ctx.queue); - let def_color = self - .state - .resource_manager - .textures - .get(def_color_h) - .expect("default color"); - let def_normal = self - .state - .resource_manager - .textures - .get(def_normal_h) - .expect("default normal"); + // let (def_color_h, def_normal_h) = self + // .state + // .resource_manager + // .ensure_default_textures(&self.ctx.device, &self.ctx.queue); + // let def_color = self + // .state + // .resource_manager + // .textures + // .get(def_color_h) + // .expect("default color"); + // let def_normal = self + // .state + // .resource_manager + // .textures + // .get(def_normal_h) + // .expect("default normal"); // Create bind group layouts matching PassScene let textures_bgl = @@ -570,21 +538,26 @@ impl PillRenderer for Renderer { label: Some(&format!("{}_material_params_bg", desc.label)), }); // Resolve textures (use defaults if None) + let (def_color_h, def_normal_h) = unsafe { + (*self.gpu_resources).ensure_default_textures(&self.ctx.device, &self.ctx.queue) + }; + let def_color = self.resources().textures.get(def_color_h).unwrap(); + let def_normal = self.resources().textures.get(def_normal_h).unwrap(); let albedo_tex = desc .albedo_tex - .and_then(|h| self.state.resource_manager.textures.get(h)) + .and_then(|h| self.resources().textures.get(h)) .unwrap_or(def_color); let normal_tex = desc .normal_tex - .and_then(|h| self.state.resource_manager.textures.get(h)) + .and_then(|h| self.resources().textures.get(h)) .unwrap_or(def_normal); let mr_tex = desc .metallic_roughness_tex - .and_then(|h| self.state.resource_manager.textures.get(h)) + .and_then(|h| self.resources().textures.get(h)) .unwrap_or(def_color); let emissive_tex = desc .emissive_tex - .and_then(|h| self.state.resource_manager.textures.get(h)) + .and_then(|h| self.resources().textures.get(h)) .unwrap_or(def_color); // Build texture bind group @@ -635,7 +608,7 @@ impl PillRenderer for Renderer { param_buffer, param_bind_group, }; - let handle = self.state.resource_manager.materials.insert(mat); + let handle = self.resources_mut().materials.insert(mat); Ok(handle) } @@ -656,37 +629,26 @@ impl PillRenderer for Renderer { }; // Resolve textures without holding a mutable borrow to materials - let (def_color_h, def_normal_h) = self - .state - .resource_manager - .ensure_default_textures(&self.ctx.device, &self.ctx.queue); - let def_color = self - .state - .resource_manager - .textures - .get(def_color_h) - .unwrap(); - let def_normal = self - .state - .resource_manager - .textures - .get(def_normal_h) - .unwrap(); + let (def_color_h, def_normal_h) = unsafe { + (*self.gpu_resources).ensure_default_textures(&self.ctx.device, &self.ctx.queue) + }; + let def_color = self.resources().textures.get(def_color_h).unwrap(); + let def_normal = self.resources().textures.get(def_normal_h).unwrap(); let albedo_tex = desc .albedo_tex - .and_then(|h| self.state.resource_manager.textures.get(h)) + .and_then(|h| self.resources().textures.get(h)) .unwrap_or(def_color); let normal_tex = desc .normal_tex - .and_then(|h| self.state.resource_manager.textures.get(h)) + .and_then(|h| self.resources().textures.get(h)) .unwrap_or(def_normal); let mr_tex = desc .metallic_roughness_tex - .and_then(|h| self.state.resource_manager.textures.get(h)) + .and_then(|h| self.resources().textures.get(h)) .unwrap_or(def_color); let emissive_tex = desc .emissive_tex - .and_then(|h| self.state.resource_manager.textures.get(h)) + .and_then(|h| self.resources().textures.get(h)) .unwrap_or(def_color); // Create a compatible layout and new texture bind group @@ -805,15 +767,15 @@ impl PillRenderer for Renderer { }); // Mutate material once + let queue_ptr: *const wgpu::Queue = &self.ctx.queue; if let Some(mat) = self - .state - .resource_manager + .resources_mut() .materials .get_mut(renderer_material_handle) { - self.ctx - .queue - .write_buffer(&mat.param_buffer, 0, bytemuck::bytes_of(¶ms)); + unsafe { + (*queue_ptr).write_buffer(&mat.param_buffer, 0, bytemuck::bytes_of(¶ms)); + } mat.texture_bind_group = new_texture_bg; } Ok(renderer_material_handle) @@ -827,13 +789,13 @@ impl PillRenderer for Renderer { desc.height, desc.name.as_str(), ); - let handle = self.state.resource_manager.textures.insert(tex); + let handle = self.resources_mut().textures.insert(tex); Ok(handle) } fn create_mesh(&mut self, name: &str, mesh_data: &MeshData) -> Result { let mesh = RendererMesh::new(&self.ctx.device, name, mesh_data)?; - let handle = self.state.resource_manager.meshes.insert(mesh); + let handle = self.resources_mut().meshes.insert(mesh); Ok(handle) } @@ -851,7 +813,7 @@ impl PillRenderer for Renderer { image_data, texture_type, )?; - let handle = self.state.resource_manager.textures.insert(texture); + let handle = self.resources_mut().textures.insert(texture); Ok(handle) } @@ -862,7 +824,7 @@ impl PillRenderer for Renderer { &self.ctx.surface_configuration, label, )?; - Ok(self.state.resource_manager.textures.insert(tex)) + Ok(self.resources_mut().textures.insert(tex)) } fn create_camera(&mut self) -> Result { @@ -883,14 +845,13 @@ impl PillRenderer for Renderer { }], }); let camera = RendererCamera::new(&self.ctx.device, &camera_bind_group_layout)?; - let handle = self.state.resource_manager.cameras.insert(camera); + let handle = self.resources_mut().cameras.insert(camera); Ok(handle) } fn destroy_mesh(&mut self, renderer_mesh_handle: RendererMeshHandle) -> Result<()> { - self.state - .resource_manager + self.resources_mut() .meshes .remove(renderer_mesh_handle) .unwrap(); @@ -899,8 +860,7 @@ impl PillRenderer for Renderer { } fn destroy_texture(&mut self, renderer_texture_handle: RendererTextureHandle) -> Result<()> { - self.state - .resource_manager + self.resources_mut() .textures .remove(renderer_texture_handle) .unwrap(); @@ -909,8 +869,7 @@ impl PillRenderer for Renderer { } fn destroy_camera(&mut self, renderer_camera_handle: RendererCameraHandle) -> Result<()> { - self.state - .resource_manager + self.resources_mut() .cameras .remove(renderer_camera_handle) .unwrap(); @@ -920,7 +879,6 @@ impl PillRenderer for Renderer { fn resize(&mut self, new_window_size: winit::dpi::PhysicalSize) { info!("Resizing {} resources", "Renderer".mobj_style()); - // self.state.resize(new_window_size) if new_window_size.width > 0 && new_window_size.height > 0 { self.ctx.window_size = new_window_size; self.ctx.surface_configuration.width = new_window_size.width; @@ -928,24 +886,7 @@ impl PillRenderer for Renderer { self.ctx .surface .configure(&self.ctx.device, &self.ctx.surface_configuration); - // self.state.depth_texture = Arc::new( - // RendererTexture::new_depth_texture( - // &self.ctx.device, - // &self.ctx.surface_configuration, - // "depth_texture", - // ) - // .unwrap(), - // ); - - // ================================ - // Recreate offscreen color target and reinitialize passes - // self.state.offscreen_color_texture = Arc::new(create_render_target( - // &self.ctx.device, - // self.state.color_format, - // self.ctx.surface_configuration.width, - // self.ctx.surface_configuration.height, - // "offscreen_color", - // )); + // Reinitialize existing passes in place (engine may have set custom passes) let mut passes = std::mem::take(&mut self.state.passes); for pass in passes.iter_mut() { @@ -982,7 +923,6 @@ impl PillRenderer for Renderer { timer.record("Frame: setup view"); // Scene draw is now a Pass; remaining work is handled in user passes encoder below - { // User passes with separate encoder timer.begin_context("User passes"); @@ -1076,12 +1016,7 @@ impl PillRenderer for Renderer { } fn get_texture(&self, h: RendererTextureHandle) -> &wgpu::Texture { - let tex = self - .state - .resource_manager - .textures - .get(h) - .expect("texture"); + let tex = self.resources().textures.get(h).expect("texture"); &tex.texture } @@ -1089,27 +1024,17 @@ impl PillRenderer for Renderer { &self, h: RendererMeshHandle, ) -> (&wgpu::Buffer, &wgpu::Buffer, u32) { - let mesh = self.state.resource_manager.meshes.get(h).expect("mesh"); + let mesh = self.resources().meshes.get(h).expect("mesh"); (&mesh.vertex_buffer, &mesh.index_buffer, mesh.index_count) } fn get_material_texture_bind_group(&self, h: RendererMaterialHandle) -> &wgpu::BindGroup { - let mat = self - .state - .resource_manager - .materials - .get(h) - .expect("material"); + let mat = self.resources().materials.get(h).expect("material"); &mat.texture_bind_group } fn get_material_params_bind_group(&self, h: RendererMaterialHandle) -> &wgpu::BindGroup { - let mat = self - .state - .resource_manager - .materials - .get(h) - .expect("material"); + let mat = self.resources().materials.get(h).expect("material"); &mat.param_bind_group } @@ -1119,6 +1044,23 @@ impl PillRenderer for Renderer { } impl Renderer { + #[inline] + fn resources(&self) -> &crate::resources::GpuResources { + assert!( + !self.gpu_resources.is_null(), + "Renderer GPU resources not attached. Use Renderer::new(window, config, gpu_resources)." + ); + unsafe { &*self.gpu_resources } + } + + #[inline] + fn resources_mut(&mut self) -> &mut crate::resources::GpuResources { + assert!( + !self.gpu_resources.is_null(), + "Renderer GPU resources not attached. Use Renderer::new(window, config, gpu_resources)." + ); + unsafe { &mut *self.gpu_resources } + } pub fn get_surface_format(&self) -> wgpu::TextureFormat { self.ctx.surface_configuration.format } @@ -1133,22 +1075,12 @@ impl Renderer { // Lightweight resolve helpers for engine passes pub fn get_texture_view(&self, h: RendererTextureHandle) -> &wgpu::TextureView { - let tex = self - .state - .resource_manager - .textures - .get(h) - .expect("texture"); + let tex = self.resources().textures.get(h).expect("texture"); &tex.texture_view } pub fn get_texture_sampler(&self, h: RendererTextureHandle) -> &wgpu::Sampler { - let tex = self - .state - .resource_manager - .textures - .get(h) - .expect("texture"); + let tex = self.resources().textures.get(h).expect("texture"); &tex.sampler } } diff --git a/engine/pill_standalone/src/main.rs b/engine/pill_standalone/src/main.rs index 5a09e146..81b32451 100644 --- a/engine/pill_standalone/src/main.rs +++ b/engine/pill_standalone/src/main.rs @@ -1,22 +1,26 @@ mod file_watcher; use crate::file_watcher::FileWatcher; +use anyhow::{Context, Ok, Result}; use config::Config; -use pill_core::{ PillStyle, EngineError }; -use pill_engine::internal::{Engine, PillGame}; +use libloading::{Library, Symbol}; +use log::{info, warn}; +use pill_core::{EngineError, PillStyle}; use pill_engine::internal::PillRenderer; -use anyhow::{ Context, Ok, Result }; -use winit::{ - event::{ Event, WindowEvent, DeviceEvent }, - window::Icon, +use pill_engine::internal::{Engine, PillGame}; +use std::ffi::c_void; +use std::{ + fs::{remove_file, rename}, + io::Write, + path::{Path, PathBuf}, + sync::Arc, + time::{Duration, Instant}, }; #[cfg(target_os = "windows")] use winit::platform::windows::IconExtWindows; -use std::{ - fs::{remove_file, rename}, io::{ Write }, path::{Path, PathBuf}, sync::Arc, time::{Duration, Instant} +use winit::{ + event::{DeviceEvent, Event, WindowEvent}, + window::Icon, }; -use log::{ info, warn }; -use libloading::{Library, Symbol}; -use std::ffi::c_void; const RELOAD_COOLDOWN: Duration = Duration::from_millis(1000); struct WindowData { @@ -74,7 +78,9 @@ fn set_log_level(config: &Config) { #[cfg(debug_assertions)] env_logger::Builder::new() .format(|buf, record| { - writeln!(buf, "[{}] {} {}:{}: {}", + writeln!( + buf, + "[{}] {} {}:{}: {}", record.level(), chrono::Local::now().format("%Y-%m-%dT%H:%M:%S"), record.file().unwrap_or("unknown"), @@ -86,7 +92,7 @@ fn set_log_level(config: &Config) { .filter_module("pill_standalone", log_level) .filter_module("pill_engine", log_level) .filter_module("pill_renderer", log_level) - .filter_module("pill_game", log_level) + .filter_module("pill_game", log_level) .init(); } @@ -108,7 +114,10 @@ pub fn load_window_icon(path: &Path) -> Option { } fn create_window(config: &Config, game_resources_directory_path: PathBuf) -> WindowData { - let window_title = config.get_str("WINDOW_TITLE").context(EngineError::InvalidGameConfig()).unwrap(); + let window_title = config + .get_str("WINDOW_TITLE") + .context(EngineError::InvalidGameConfig()) + .unwrap(); let window_width = config.get_int("WINDOW_WIDTH").unwrap_or(1280) as u32; let window_height = config.get_int("WINDOW_HEIGHT").unwrap_or(720) as u32; let window_fullscreen = config.get_bool("WINDOW_FULLSCREEN").unwrap_or(false); @@ -124,14 +133,17 @@ fn create_window(config: &Config, game_resources_directory_path: PathBuf) -> Win // Initialize other window parameters let window_size = winit::dpi::PhysicalSize::::new(window_width, window_height); let window_min_size = winit::dpi::PhysicalSize::::new(100, 100); - let window = Arc::new(winit::window::WindowBuilder::new() - .with_title(window_title) - .with_inner_size(window_size) - .with_min_inner_size(window_min_size) - .with_window_icon(window_icon.clone()) - .with_visible(false) // Temporarily hide window (this is a hack to make taskbar icon loaded correctly) - .build(&window_event_loop) - .context("Failed to initialize window").unwrap()); + let window = Arc::new( + winit::window::WindowBuilder::new() + .with_title(window_title) + .with_inner_size(window_size) + .with_min_inner_size(window_min_size) + .with_window_icon(window_icon.clone()) + .with_visible(false) // Temporarily hide window (this is a hack to make taskbar icon loaded correctly) + .build(&window_event_loop) + .context("Failed to initialize window") + .unwrap(), + ); // Possibly set window to fullscreen let window_fullscreen_mode = match window_fullscreen { @@ -139,17 +151,29 @@ fn create_window(config: &Config, game_resources_directory_path: PathBuf) -> Win let monitor_handle = window.current_monitor(); Some(winit::window::Fullscreen::Borderless(monitor_handle)) } - false => None + false => None, }; window.set_fullscreen(window_fullscreen_mode); - WindowData { window, event_loop: window_event_loop, size: window_size } + WindowData { + window, + event_loop: window_event_loop, + size: window_size, + } } fn load_game_dynamic_library(library_path: &PathBuf) -> (Library, Box) { type CreateGameFn = unsafe extern "C" fn() -> *mut c_void; - let game_dynamic_library = unsafe { Library::new(library_path).context(format!("Failed to load game dynamic library at {}", library_path.display())).unwrap() }; - let get_game_function: Symbol = unsafe { game_dynamic_library.get(b"get_game").unwrap() }; + let game_dynamic_library = unsafe { + Library::new(library_path) + .context(format!( + "Failed to load game dynamic library at {}", + library_path.display() + )) + .unwrap() + }; + let get_game_function: Symbol = + unsafe { game_dynamic_library.get(b"get_game").unwrap() }; let game = unsafe { *Box::from_raw(get_game_function() as *mut Box) }; (game_dynamic_library, game) } @@ -158,20 +182,27 @@ fn build_standalone_and_game_crates(project_paths: &ProjectPaths) -> Result<()> // Locate Pill Launcher manifest directly let launcher_manifest = project_paths .game_project_directory_path - .parent().unwrap() // …/examples - .parent().unwrap() // …/ + .parent() + .unwrap() // …/examples + .parent() + .unwrap() // …/ .join("engine") .join("pill_launcher") .join("Cargo.toml"); let output = std::process::Command::new("cargo") .args(&[ - "run", "--quiet", - "--manifest-path", launcher_manifest.to_str().unwrap(), + "run", + "--quiet", + "--manifest-path", + launcher_manifest.to_str().unwrap(), "--", - "-a", "build", - "-p", project_paths.game_project_directory_path.to_str().unwrap(), - "-c", "hot-reload", + "-a", + "build", + "-p", + project_paths.game_project_directory_path.to_str().unwrap(), + "-c", + "hot-reload", ]) // run inside the engine workspace so relative paths in Cargo.toml work .current_dir(launcher_manifest.parent().unwrap().parent().unwrap()) // …//engine @@ -206,19 +237,28 @@ fn check_and_reload_game( } // Check for game project source files changes - if let Some(paths) = file_watchers.game_project_source_files_watcher.get_changes() { + if let Some(paths) = file_watchers + .game_project_source_files_watcher + .get_changes() + { info!("Game project source file change detected: {:?}", paths); build_standalone_and_game_crates(project_paths)?; } // Check for game project resource files changes - if let Some(paths) = file_watchers.game_project_resources_files_watcher.get_changes() { + if let Some(paths) = file_watchers + .game_project_resources_files_watcher + .get_changes() + { info!("Game project resource file change detected: {:?}", paths); build_standalone_and_game_crates(project_paths)?; } // Check for game dynamic library changes - if let Some(_) = file_watchers.game_dynamic_library_files_watcher.get_changes() { + if let Some(_) = file_watchers + .game_dynamic_library_files_watcher + .get_changes() + { info!("Reloading game project..."); // Shutdown and drop engine @@ -230,18 +270,37 @@ fn check_and_reload_game( // Remove old game dynamic library file and rename new one remove_file(&project_paths.game_dynamic_library_path).unwrap(); - rename(&project_paths.game_dynamic_library_hot_reloaded_path, &project_paths.game_dynamic_library_path).unwrap(); + rename( + &project_paths.game_dynamic_library_hot_reloaded_path, + &project_paths.game_dynamic_library_path, + ) + .unwrap(); // Load new game dynamic library - let (game_library, game) = load_game_dynamic_library(&project_paths.game_dynamic_library_path); - let renderer: Box = Box::new(::new(Arc::clone(&window), config.clone())); - let mut new_engine = Engine::new(game, project_paths.game_resources_directory_path.clone(), renderer, config.clone()); + let (game_library, game) = + load_game_dynamic_library(&project_paths.game_dynamic_library_path); + let mut resource_manager = pill_engine::internal::ResourceManager::new(); + let renderer: Box = + Box::new(::new( + Arc::clone(&window), + config.clone(), + resource_manager.gpu_mut(), + )); + let mut new_engine = Engine::new( + game, + project_paths.game_resources_directory_path.clone(), + renderer, + resource_manager, + config.clone(), + ); new_engine.initialize(*window_size).unwrap(); *engine = Some(new_engine); *game_dynamic_library = Some(game_library); // Run again to clear changes (otherwise it will trigger reload again since file was renamed) - file_watchers.game_dynamic_library_files_watcher.get_changes(); + file_watchers + .game_dynamic_library_files_watcher + .get_changes(); *last_reload_time = now; } @@ -250,9 +309,12 @@ fn check_and_reload_game( } fn create_file_watchers(project_paths: &ProjectPaths) -> FileWatchers { - let game_dynamic_library_files_watcher = FileWatcher::new(project_paths.build_data_directory_path.clone()); - let game_project_source_files_watcher = FileWatcher::new(project_paths.game_source_directory_path.clone()).set_recursive(true); - let game_project_resources_files_watcher = FileWatcher::new(project_paths.game_resources_directory_path.clone()).set_recursive(true); + let game_dynamic_library_files_watcher = + FileWatcher::new(project_paths.build_data_directory_path.clone()); + let game_project_source_files_watcher = + FileWatcher::new(project_paths.game_source_directory_path.clone()).set_recursive(true); + let game_project_resources_files_watcher = + FileWatcher::new(project_paths.game_resources_directory_path.clone()).set_recursive(true); FileWatchers { game_dynamic_library_files_watcher, @@ -269,7 +331,6 @@ fn main_loop( config: Config, development_mode: bool, ) -> Result<()> { - // Create a file watcher to monitor game project file changes as well as game output file changes let mut file_watchers: Option = if development_mode { Some(create_file_watchers(&project_paths)) @@ -281,104 +342,99 @@ fn main_loop( let mut last_reload_time = Instant::now(); // Main program loop - let _ = window_data.event_loop.run(move |event, event_loop_window_target| { // Run function takes closure - match event { - Event::AboutToWait => { - window_data.window.request_redraw(); - } + let _ = window_data + .event_loop + .run(move |event, event_loop_window_target| { + // Run function takes closure + match event { + Event::AboutToWait => { + window_data.window.request_redraw(); + } - // Handle device events - Event::DeviceEvent { - ref event, - .. - } => { - match event { - DeviceEvent::MouseMotion { - delta, - } => { + // Handle device events + Event::DeviceEvent { ref event, .. } => match event { + DeviceEvent::MouseMotion { delta } => { if let Some(ref mut engine) = engine { engine.pass_mouse_delta_input(delta); } - }, + } _ => {} - } - } - - // Handle window events - Event::WindowEvent { - ref event, - window_id, - } - if window_id == window_data.window.id() => { - if let Some(ref mut engine) = engine { - engine.pass_input_to_egui(event); - } - - match event { - WindowEvent::RedrawRequested => { - let now = std::time::Instant::now(); - let delta_time = now - last_render_time; - last_render_time = now; - - if let Some(ref mut e) = engine { - e.update(delta_time); - } + }, + + // Handle window events + Event::WindowEvent { + ref event, + window_id, + } if window_id == window_data.window.id() => { + if let Some(ref mut engine) = engine { + engine.pass_input_to_egui(event); + } - if development_mode { - check_and_reload_game( - engine, - game_dynamic_library, - &project_paths, - &mut last_reload_time, - &window_data.size, - &window_data.window, - &config, - file_watchers.as_mut().unwrap(), - ).unwrap(); + match event { + WindowEvent::RedrawRequested => { + let now = std::time::Instant::now(); + let delta_time = now - last_render_time; + last_render_time = now; + + if let Some(ref mut e) = engine { + e.update(delta_time); + } + + if development_mode { + check_and_reload_game( + engine, + game_dynamic_library, + &project_paths, + &mut last_reload_time, + &window_data.size, + &window_data.window, + &config, + file_watchers.as_mut().unwrap(), + ) + .unwrap(); + } } - } - WindowEvent::KeyboardInput { event, .. } => { - if let Some(ref mut e) = engine { - e.pass_keyboard_key_input(&event); + WindowEvent::KeyboardInput { event, .. } => { + if let Some(ref mut e) = engine { + e.pass_keyboard_key_input(&event); + } } - } - WindowEvent::MouseInput { button, state, .. } => { - if let Some(ref mut e) = engine { - e.pass_mouse_key_input(&button, &state); + WindowEvent::MouseInput { button, state, .. } => { + if let Some(ref mut e) = engine { + e.pass_mouse_key_input(&button, &state); + } } - } - WindowEvent::MouseWheel { delta, .. } => { - if let Some(ref mut e) = engine { - e.pass_mouse_wheel_input(&delta); + WindowEvent::MouseWheel { delta, .. } => { + if let Some(ref mut e) = engine { + e.pass_mouse_wheel_input(&delta); + } } - } - WindowEvent::CursorMoved { position, .. } => { - if let Some(ref mut e) = engine { - e.pass_mouse_position_input(&position); + WindowEvent::CursorMoved { position, .. } => { + if let Some(ref mut e) = engine { + e.pass_mouse_position_input(&position); + } } - } - WindowEvent::CloseRequested => { - if let Some(mut e) = engine.take() { - e.shutdown(); + WindowEvent::CloseRequested => { + if let Some(mut e) = engine.take() { + e.shutdown(); + } + event_loop_window_target.exit(); } - event_loop_window_target.exit(); - } - WindowEvent::Resized(physical_size) => { - if let Some(ref mut e) = engine { - e.resize(*physical_size); + WindowEvent::Resized(physical_size) => { + if let Some(ref mut e) = engine { + e.resize(*physical_size); + } } + _ => {} } - _ => {} } + _ => {} } - _ => {} - } - }); + }); Ok(()) } - fn main() { // In the development build, standalone will look for the resource files in the "res" directory of the game project directory // In the release build, "res" directory is copied to /build/release/data/res (TODO: pack all resources use by game into a single data file) @@ -406,18 +462,38 @@ fn main() { let development_mode = true; - let current_directory_path = std::env::current_exe().unwrap().parent().unwrap().to_path_buf(); // Path where the executable is located - let game_project_directory_path = current_directory_path.parent().unwrap().parent().unwrap().to_path_buf(); + let current_directory_path = std::env::current_exe() + .unwrap() + .parent() + .unwrap() + .to_path_buf(); // Path where the executable is located + let game_project_directory_path = current_directory_path + .parent() + .unwrap() + .parent() + .unwrap() + .to_path_buf(); let build_data_directory_path = current_directory_path.join("data"); let game_resources_directory_path = if development_mode { - current_directory_path.parent().unwrap().parent().unwrap().join("res") // /res + current_directory_path + .parent() + .unwrap() + .parent() + .unwrap() + .join("res") // /res } else { build_data_directory_path.join("res") // /data/res }; - let game_source_directory_path = current_directory_path.parent().unwrap().parent().unwrap().join("src"); // /src + let game_source_directory_path = current_directory_path + .parent() + .unwrap() + .parent() + .unwrap() + .join("src"); // /src let config_path = game_resources_directory_path.join("config.ini"); let game_dynamic_library_path = build_data_directory_path.join(dylib("pill_game")); - let game_dynamic_library_hot_reloaded_path = build_data_directory_path.join(dylib("pill_game_hot_reloaded")); + let game_dynamic_library_hot_reloaded_path = + build_data_directory_path.join(dylib("pill_game_hot_reloaded")); let project_paths = ProjectPaths { build_data_directory_path: build_data_directory_path, @@ -431,7 +507,11 @@ fn main() { // Load config let mut config = Config::default(); - config.merge(config::File::with_name(project_paths.config_path.to_str().unwrap())).unwrap(); + config + .merge(config::File::with_name( + project_paths.config_path.to_str().unwrap(), + )) + .unwrap(); // Set log level set_log_level(&config); @@ -447,13 +527,34 @@ fn main() { game_dynamic_library = Some(game_library); // Initialize renderer and engine - let renderer: Box = Box::new(::new(Arc::clone(&window_data.window), config.clone())); - let mut engine: Option = Some(Engine::new(game, project_paths.game_resources_directory_path.clone(), renderer, config.clone())); - engine.as_mut().unwrap().initialize(window_data.size).context("Failed to initialize engine").unwrap(); + let mut resource_manager = pill_engine::internal::ResourceManager::new(); + let renderer: Box = + Box::new(::new( + Arc::clone(&window_data.window), + config.clone(), + resource_manager.gpu_mut(), + )); + let mut engine: Option = Some(Engine::new( + game, + project_paths.game_resources_directory_path.clone(), + renderer, + resource_manager, + config.clone(), + )); + engine + .as_mut() + .unwrap() + .initialize(window_data.size) + .context("Failed to initialize engine") + .unwrap(); // Run loop - window_data.event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll); - window_data.event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait); + window_data + .event_loop + .set_control_flow(winit::event_loop::ControlFlow::Poll); + window_data + .event_loop + .set_control_flow(winit::event_loop::ControlFlow::Wait); // Show window (now the taskbar icon will be set correctly) window_data.window.set_visible(true); @@ -466,5 +567,7 @@ fn main() { window_data, config, development_mode, - ).context("Main loop failed").unwrap(); + ) + .context("Main loop failed") + .unwrap(); } From 51a6a77d2af302197d3a0a76b4bf3a67e7624760 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Sun, 16 Nov 2025 03:15:22 +0100 Subject: [PATCH 62/69] cleanup resource manager --- .../src/ecs/systems/rendering_system.rs | 60 +++--- .../pill_engine/src/graphics/pass_compose.rs | 23 ++- engine/pill_engine/src/graphics/pass_logo.rs | 36 ++-- .../src/graphics/pass_overlay_depth.rs | 18 +- .../src/graphics/pass_overlay_uv.rs | 8 +- engine/pill_engine/src/graphics/pass_scene.rs | 60 ++++-- engine/pill_engine/src/graphics/renderer.rs | 16 +- engine/pill_engine/src/renderer/wgpu.rs | 171 ++++++++---------- engine/pill_standalone/src/lib.rs | 2 - engine/pill_standalone/src/main.rs | 4 +- 10 files changed, 210 insertions(+), 188 deletions(-) diff --git a/engine/pill_engine/src/ecs/systems/rendering_system.rs b/engine/pill_engine/src/ecs/systems/rendering_system.rs index 0c732123..4cdba02e 100644 --- a/engine/pill_engine/src/ecs/systems/rendering_system.rs +++ b/engine/pill_engine/src/ecs/systems/rendering_system.rs @@ -82,36 +82,36 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { // Build passes as an array with elements, then convert to Vec let passes: Vec> = vec![ - // Box::new(crate::graphics::pass_scene::PassScene::new( - // "scene", - // offscreen_color_texture, - // depth_texture, - // fmt, - // )), - // Box::new(crate::graphics::PassCompose::new( - // "compose", - // offscreen_color_texture, - // fmt, - // )), - // Box::new(crate::graphics::PassOverlayUV::new( - // "overlay_uv", - // [0.75, 0.75, 0.95, 0.95], - // fmt, - // )), - // Box::new(crate::graphics::PassOverlayDepth::new( - // "overlay_depth", - // [0.75, 0.50, 0.95, 0.70], - // [1.0, 1.0, 1.0, 1.0], - // fmt, - // depth_texture, - // )), - // Box::new(PassLogo::new( - // "overlay_logo", - // rect_logo, - // [1.0, 1.0, 1.0, 1.0], - // tex_logo_rt, - // fmt, - // )), + Box::new(crate::graphics::pass_scene::PassScene::new( + "scene", + offscreen_color_texture, + depth_texture, + fmt, + )), + Box::new(crate::graphics::PassCompose::new( + "compose", + offscreen_color_texture, + fmt, + )), + Box::new(crate::graphics::PassOverlayUV::new( + "overlay_uv", + [0.75, 0.75, 0.95, 0.95], + fmt, + )), + Box::new(crate::graphics::PassOverlayDepth::new( + "overlay_depth", + [0.75, 0.50, 0.95, 0.70], + [1.0, 1.0, 1.0, 1.0], + fmt, + depth_texture, + )), + Box::new(PassLogo::new( + "overlay_logo", + rect_logo, + [1.0, 1.0, 1.0, 1.0], + tex_logo_rt, + fmt, + )), ]; engine.renderer.set_passes(passes)?; } diff --git a/engine/pill_engine/src/graphics/pass_compose.rs b/engine/pill_engine/src/graphics/pass_compose.rs index ca13fdf0..69c976d0 100644 --- a/engine/pill_engine/src/graphics/pass_compose.rs +++ b/engine/pill_engine/src/graphics/pass_compose.rs @@ -34,9 +34,12 @@ impl Pass for PassCompose { &self.label } - fn init(&mut self, renderer: &mut dyn EnginePillRenderer) -> Result<()> { + fn init( + &mut self, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { let device = renderer.get_device(); - let vs = r#" struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { @@ -131,14 +134,15 @@ impl Pass for PassCompose { multiview: None, }); - // Bind offscreen color texture from renderer (offscreen target) - // let (view, sampler) = renderer.get_offscreen_color_view_and_sampler(); - let view = renderer - .get_texture(self.offscreen_color_texture) + // Bind offscreen color texture using ResourceManager GPU pools + let view = resources + .gpu() + .textures + .get(self.offscreen_color_texture) + .expect("offscreen color") + .texture .create_view(&wgpu::TextureViewDescriptor::default()); - let sampler = renderer - .get_device() - .create_sampler(&wgpu::SamplerDescriptor::default()); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("compose_bind_group"), layout: &bgl, // Group 0: Texture and sampler bindings @@ -168,6 +172,7 @@ impl Pass for PassCompose { &mut self, encoder: &mut CommandEncoder, _renderer: &mut dyn EnginePillRenderer, + _resources: &mut crate::resources::ResourceManager, _frame: &wgpu::SurfaceTexture, view: &wgpu::TextureView, _world: &WorldQuery, diff --git a/engine/pill_engine/src/graphics/pass_logo.rs b/engine/pill_engine/src/graphics/pass_logo.rs index fefffb77..cfc46446 100644 --- a/engine/pill_engine/src/graphics/pass_logo.rs +++ b/engine/pill_engine/src/graphics/pass_logo.rs @@ -48,9 +48,12 @@ impl Pass for PassLogo { &self.label } - fn init(&mut self, renderer: &mut dyn EnginePillRenderer) -> Result<()> { + fn init( + &mut self, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { let device = renderer.get_device(); - // Create mapped buffer for overlay rect UBO let buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("overlay_logo_ubo"), @@ -187,20 +190,22 @@ impl Pass for PassLogo { }], }); - let tex_logo_view = renderer - .get_texture(self.tex_logo) + let tex_logo_view = resources + .gpu() + .textures + .get(self.tex_logo) + .expect("texture") + .texture .create_view(&wgpu::TextureViewDescriptor::default()); - let tex_logo_sampler = renderer - .get_device() - .create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::Repeat, - address_mode_v: wgpu::AddressMode::Repeat, - address_mode_w: wgpu::AddressMode::Repeat, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Nearest, - mipmap_filter: wgpu::FilterMode::Nearest, - ..Default::default() - }); + let tex_logo_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::Repeat, + address_mode_v: wgpu::AddressMode::Repeat, + address_mode_w: wgpu::AddressMode::Repeat, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); let tint_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("overlay_logo_tint"), @@ -258,6 +263,7 @@ impl Pass for PassLogo { &mut self, encoder: &mut CommandEncoder, _renderer: &mut dyn EnginePillRenderer, + _resources: &mut crate::resources::ResourceManager, _frame: &wgpu::SurfaceTexture, view: &wgpu::TextureView, _world: &WorldQuery, diff --git a/engine/pill_engine/src/graphics/pass_overlay_depth.rs b/engine/pill_engine/src/graphics/pass_overlay_depth.rs index 66e828cd..3a54fb77 100644 --- a/engine/pill_engine/src/graphics/pass_overlay_depth.rs +++ b/engine/pill_engine/src/graphics/pass_overlay_depth.rs @@ -44,9 +44,12 @@ impl Pass for PassOverlayDepth { &self.label } - fn init(&mut self, renderer: &mut dyn EnginePillRenderer) -> Result<()> { + fn init( + &mut self, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { let device = renderer.get_device(); - // Create buffer for overlay rect UBO let buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("overlay_depth_rect_ubo"), @@ -193,9 +196,13 @@ impl Pass for PassOverlayDepth { } tint_buffer.unmap(); - // Create material bind group using renderer-provided depth texture view - let depth_view = renderer - .get_texture(self.depth_texture) + // Create material bind group using ResourceManager-provided depth texture view + let depth_view = resources + .gpu() + .textures + .get(self.depth_texture) + .expect("depth texture") + .texture .create_view(&wgpu::TextureViewDescriptor::default()); let bind_group_material = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("overlay_depth_material_bg"), @@ -232,6 +239,7 @@ impl Pass for PassOverlayDepth { &mut self, encoder: &mut CommandEncoder, _renderer: &mut dyn EnginePillRenderer, + _resources: &mut crate::resources::ResourceManager, _frame: &wgpu::SurfaceTexture, view: &wgpu::TextureView, _world: &WorldQuery, diff --git a/engine/pill_engine/src/graphics/pass_overlay_uv.rs b/engine/pill_engine/src/graphics/pass_overlay_uv.rs index 136c8868..87176b1b 100644 --- a/engine/pill_engine/src/graphics/pass_overlay_uv.rs +++ b/engine/pill_engine/src/graphics/pass_overlay_uv.rs @@ -32,9 +32,12 @@ impl Pass for PassOverlayUV { &self.label } - fn init(&mut self, renderer: &mut dyn EnginePillRenderer) -> Result<()> { + fn init( + &mut self, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { let device = renderer.get_device(); - // Create buffer for overlay rect UBO let buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("overlay_uv_rect_ubo"), @@ -151,6 +154,7 @@ impl Pass for PassOverlayUV { &mut self, encoder: &mut CommandEncoder, _renderer: &mut dyn EnginePillRenderer, + _resources: &mut crate::resources::ResourceManager, _frame: &wgpu::SurfaceTexture, view: &wgpu::TextureView, _world: &WorldQuery, diff --git a/engine/pill_engine/src/graphics/pass_scene.rs b/engine/pill_engine/src/graphics/pass_scene.rs index 3063ab7d..96848d9e 100644 --- a/engine/pill_engine/src/graphics/pass_scene.rs +++ b/engine/pill_engine/src/graphics/pass_scene.rs @@ -191,7 +191,11 @@ impl Pass for PassScene { &self.label } - fn init(&mut self, renderer: &mut dyn EnginePillRenderer) -> Result<()> { + fn init( + &mut self, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { // [SIMILAR] Prebuilt PSO used; avoid hot-path pipeline creation per TALK // Shaders: must match bind group layout indices: 0(camera),1(material textures),2(material params),3(per-draw) let vertex_wgsl = r#" @@ -485,6 +489,7 @@ impl Pass for PassScene { multisample: wgpu::MultisampleState::default(), }; + // Use renderer API to build pipeline let pipeline = renderer.create_pipeline_v2(desc)?; // Camera buffer and bind group using pipeline's camera layout (group 0) @@ -553,6 +558,7 @@ impl Pass for PassScene { &mut self, encoder: &mut CommandEncoder, renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, _frame: &wgpu::SurfaceTexture, _view: &wgpu::TextureView, world: &WorldQuery, @@ -721,11 +727,19 @@ impl Pass for PassScene { } // Encode offscreen pass - let color_view = renderer - .get_texture(self.offscreen_color_texture) + let color_view = resources + .gpu() + .textures + .get(self.offscreen_color_texture) + .expect("offscreen color") + .texture .create_view(&wgpu::TextureViewDescriptor::default()); - let depth_view = renderer - .get_texture(self.depth_texture) + let depth_view = resources + .gpu() + .textures + .get(self.depth_texture) + .expect("depth") + .texture .create_view(&wgpu::TextureViewDescriptor::default()); let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("m6_offscreen_pass"), @@ -762,21 +776,27 @@ impl Pass for PassScene { &state_ref.camera_bind_group, &[], ); - rpass.set_bind_group( - MATERIAL_BIND_GROUP_TEXTURES as u32, - renderer.get_material_texture_bind_group(group.material_handle), - &[], - ); - rpass.set_bind_group( - MATERIAL_BIND_GROUP_PARAMS as u32, - renderer.get_material_params_bind_group(group.material_handle), - &[], - ); + { + let mat = resources + .gpu() + .materials + .get(group.material_handle) + .expect("material"); + rpass.set_bind_group( + MATERIAL_BIND_GROUP_TEXTURES as u32, + &mat.texture_bind_group, + &[], + ); + rpass.set_bind_group( + MATERIAL_BIND_GROUP_PARAMS as u32, + &mat.param_bind_group, + &[], + ); + } for batch in &group.batches { - let (vbuf, ibuf, index_count) = - renderer.get_mesh_buffers_and_count(batch.mesh_handle); - rpass.set_vertex_buffer(0, vbuf.slice(..)); - rpass.set_index_buffer(ibuf.slice(..), wgpu::IndexFormat::Uint32); + let mesh = resources.gpu().meshes.get(batch.mesh_handle).expect("mesh"); + rpass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); + rpass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32); for i in 0..batch.instances.len() { let offset = batch .base_offset_u32 @@ -786,7 +806,7 @@ impl Pass for PassScene { &state_ref.per_draw_bind_group, &[offset], ); - rpass.draw_indexed(0..index_count, 0, 0..1); + rpass.draw_indexed(0..mesh.index_count, 0, 0..1); } } } diff --git a/engine/pill_engine/src/graphics/renderer.rs b/engine/pill_engine/src/graphics/renderer.rs index ecfc850f..752d3fb7 100644 --- a/engine/pill_engine/src/graphics/renderer.rs +++ b/engine/pill_engine/src/graphics/renderer.rs @@ -90,11 +90,16 @@ pub struct PipelineV2 { // --- Shared pass API (engine-visible) ------------------------------------------------- pub trait Pass { fn get_label(&self) -> &str; - fn init(&mut self, renderer: &mut dyn PillRenderer) -> Result<()>; + fn init( + &mut self, + renderer: &mut dyn PillRenderer, + resources: &mut crate::resources::ResourceManager, + ) -> Result<()>; fn draw( &mut self, encoder: &mut wgpu::CommandEncoder, renderer: &mut dyn PillRenderer, + resources: &mut crate::resources::ResourceManager, frame: &wgpu::SurfaceTexture, view: &wgpu::TextureView, world: &WorldQuery, @@ -106,9 +111,9 @@ pub type WorldQuery<'a> = RenderQuery<'a>; pub trait PillRenderer { fn new( + resources: &mut crate::resources::ResourceManager, window: Arc, config: config::Config, - gpu_resources: &mut crate::resources::GpuResources, ) -> Self where Self: Sized; @@ -157,13 +162,6 @@ pub trait PillRenderer { fn get_surface_format(&self) -> wgpu::TextureFormat; fn get_device(&self) -> &wgpu::Device; fn get_queue(&self) -> &wgpu::Queue; - fn get_texture(&self, h: RendererTextureHandle) -> &wgpu::Texture; - fn get_mesh_buffers_and_count( - &self, - h: RendererMeshHandle, - ) -> (&wgpu::Buffer, &wgpu::Buffer, u32); - fn get_material_texture_bind_group(&self, h: RendererMaterialHandle) -> &wgpu::BindGroup; - fn get_material_params_bind_group(&self, h: RendererMaterialHandle) -> &wgpu::BindGroup; } pub type Renderer = Box; diff --git a/engine/pill_engine/src/renderer/wgpu.rs b/engine/pill_engine/src/renderer/wgpu.rs index 0cd0671f..c9ba32be 100644 --- a/engine/pill_engine/src/renderer/wgpu.rs +++ b/engine/pill_engine/src/renderer/wgpu.rs @@ -99,14 +99,14 @@ pub struct State { pub struct Renderer { pub state: State, pub ctx: DeviceContext, - gpu_resources: *mut crate::resources::GpuResources, + resource_manager: *mut crate::resources::ResourceManager, } impl PillRenderer for Renderer { fn new( + resources: &mut crate::resources::ResourceManager, window: Arc, config: config::Config, - gpu_resources: &mut crate::resources::GpuResources, ) -> Self { info!("Initializing {}", "Renderer".mobj_style()); let ctx: DeviceContext = pollster::block_on(async move { @@ -238,7 +238,7 @@ impl PillRenderer for Renderer { // Use Rgba16Float for HDR color buffers; it's the common, well-supported, // performant choice. Reserve Rgba32Float for niche cases needing extreme // precision and accept the 2× bandwidth/memory hit. If alpha isn't needed, - // Rg11b10Float/R11G11B10_FLOAT is a fast alternative. + // R11b10Float/R11G11B10_FLOAT is a fast alternative. // Tone-map to the sRGB swapchain in the composite pass. // let color_format = wgpu::TextureFormat::Rgba16Float; // let depth_format = wgpu::TextureFormat::Depth32Float; @@ -259,15 +259,19 @@ impl PillRenderer for Renderer { Self { state, ctx, - gpu_resources: gpu_resources as *mut crate::resources::GpuResources, + resource_manager: resources as *mut _, } } fn init(&mut self) -> Result<()> { // Ensure default textures are created before any pass init that may need them - // self.state - // .resource_manager - // .ensure_default_textures(&self.ctx.device, &self.ctx.queue); + let (device_ptr, queue_ptr): (*const wgpu::Device, *const wgpu::Queue) = + (&self.ctx.device, &self.ctx.queue); + unsafe { + self.rm_mut() + .gpu_mut() + .ensure_default_textures(&*device_ptr, &*queue_ptr); + } Ok(()) } @@ -398,24 +402,6 @@ impl PillRenderer for Renderer { } fn create_material(&mut self, desc: MaterialDesc) -> Result { - // Ensure default textures exist - // let (def_color_h, def_normal_h) = self - // .state - // .resource_manager - // .ensure_default_textures(&self.ctx.device, &self.ctx.queue); - // let def_color = self - // .state - // .resource_manager - // .textures - // .get(def_color_h) - // .expect("default color"); - // let def_normal = self - // .state - // .resource_manager - // .textures - // .get(def_normal_h) - // .expect("default normal"); - // Create bind group layouts matching PassScene let textures_bgl = self.ctx @@ -538,26 +524,30 @@ impl PillRenderer for Renderer { label: Some(&format!("{}_material_params_bg", desc.label)), }); // Resolve textures (use defaults if None) + let (device_ptr, queue_ptr): (*const wgpu::Device, *const wgpu::Queue) = + (&self.ctx.device, &self.ctx.queue); let (def_color_h, def_normal_h) = unsafe { - (*self.gpu_resources).ensure_default_textures(&self.ctx.device, &self.ctx.queue) + self.rm_mut() + .gpu_mut() + .ensure_default_textures(&*device_ptr, &*queue_ptr) }; - let def_color = self.resources().textures.get(def_color_h).unwrap(); - let def_normal = self.resources().textures.get(def_normal_h).unwrap(); + let def_color = self.rm().gpu().textures.get(def_color_h).unwrap(); + let def_normal = self.rm().gpu().textures.get(def_normal_h).unwrap(); let albedo_tex = desc .albedo_tex - .and_then(|h| self.resources().textures.get(h)) + .and_then(|h| self.rm().gpu().textures.get(h)) .unwrap_or(def_color); let normal_tex = desc .normal_tex - .and_then(|h| self.resources().textures.get(h)) + .and_then(|h| self.rm().gpu().textures.get(h)) .unwrap_or(def_normal); let mr_tex = desc .metallic_roughness_tex - .and_then(|h| self.resources().textures.get(h)) + .and_then(|h| self.rm().gpu().textures.get(h)) .unwrap_or(def_color); let emissive_tex = desc .emissive_tex - .and_then(|h| self.resources().textures.get(h)) + .and_then(|h| self.rm().gpu().textures.get(h)) .unwrap_or(def_color); // Build texture bind group @@ -608,7 +598,7 @@ impl PillRenderer for Renderer { param_buffer, param_bind_group, }; - let handle = self.resources_mut().materials.insert(mat); + let handle = self.rm_mut().gpu_mut().materials.insert(mat); Ok(handle) } @@ -629,26 +619,30 @@ impl PillRenderer for Renderer { }; // Resolve textures without holding a mutable borrow to materials + let (device_ptr, queue_ptr): (*const wgpu::Device, *const wgpu::Queue) = + (&self.ctx.device, &self.ctx.queue); let (def_color_h, def_normal_h) = unsafe { - (*self.gpu_resources).ensure_default_textures(&self.ctx.device, &self.ctx.queue) + self.rm_mut() + .gpu_mut() + .ensure_default_textures(&*device_ptr, &*queue_ptr) }; - let def_color = self.resources().textures.get(def_color_h).unwrap(); - let def_normal = self.resources().textures.get(def_normal_h).unwrap(); + let def_color = self.rm().gpu().textures.get(def_color_h).unwrap(); + let def_normal = self.rm().gpu().textures.get(def_normal_h).unwrap(); let albedo_tex = desc .albedo_tex - .and_then(|h| self.resources().textures.get(h)) + .and_then(|h| self.rm().gpu().textures.get(h)) .unwrap_or(def_color); let normal_tex = desc .normal_tex - .and_then(|h| self.resources().textures.get(h)) + .and_then(|h| self.rm().gpu().textures.get(h)) .unwrap_or(def_normal); let mr_tex = desc .metallic_roughness_tex - .and_then(|h| self.resources().textures.get(h)) + .and_then(|h| self.rm().gpu().textures.get(h)) .unwrap_or(def_color); let emissive_tex = desc .emissive_tex - .and_then(|h| self.resources().textures.get(h)) + .and_then(|h| self.rm().gpu().textures.get(h)) .unwrap_or(def_color); // Create a compatible layout and new texture bind group @@ -769,7 +763,8 @@ impl PillRenderer for Renderer { // Mutate material once let queue_ptr: *const wgpu::Queue = &self.ctx.queue; if let Some(mat) = self - .resources_mut() + .rm_mut() + .gpu_mut() .materials .get_mut(renderer_material_handle) { @@ -789,13 +784,13 @@ impl PillRenderer for Renderer { desc.height, desc.name.as_str(), ); - let handle = self.resources_mut().textures.insert(tex); + let handle = self.rm_mut().gpu_mut().textures.insert(tex); Ok(handle) } fn create_mesh(&mut self, name: &str, mesh_data: &MeshData) -> Result { let mesh = RendererMesh::new(&self.ctx.device, name, mesh_data)?; - let handle = self.resources_mut().meshes.insert(mesh); + let handle = self.rm_mut().gpu_mut().meshes.insert(mesh); Ok(handle) } @@ -813,7 +808,7 @@ impl PillRenderer for Renderer { image_data, texture_type, )?; - let handle = self.resources_mut().textures.insert(texture); + let handle = self.rm_mut().gpu_mut().textures.insert(texture); Ok(handle) } @@ -824,7 +819,7 @@ impl PillRenderer for Renderer { &self.ctx.surface_configuration, label, )?; - Ok(self.resources_mut().textures.insert(tex)) + Ok(self.rm_mut().gpu_mut().textures.insert(tex)) } fn create_camera(&mut self) -> Result { @@ -845,13 +840,14 @@ impl PillRenderer for Renderer { }], }); let camera = RendererCamera::new(&self.ctx.device, &camera_bind_group_layout)?; - let handle = self.resources_mut().cameras.insert(camera); + let handle = self.rm_mut().gpu_mut().cameras.insert(camera); Ok(handle) } fn destroy_mesh(&mut self, renderer_mesh_handle: RendererMeshHandle) -> Result<()> { - self.resources_mut() + self.rm_mut() + .gpu_mut() .meshes .remove(renderer_mesh_handle) .unwrap(); @@ -860,7 +856,8 @@ impl PillRenderer for Renderer { } fn destroy_texture(&mut self, renderer_texture_handle: RendererTextureHandle) -> Result<()> { - self.resources_mut() + self.rm_mut() + .gpu_mut() .textures .remove(renderer_texture_handle) .unwrap(); @@ -869,7 +866,8 @@ impl PillRenderer for Renderer { } fn destroy_camera(&mut self, renderer_camera_handle: RendererCameraHandle) -> Result<()> { - self.resources_mut() + self.rm_mut() + .gpu_mut() .cameras .remove(renderer_camera_handle) .unwrap(); @@ -889,8 +887,11 @@ impl PillRenderer for Renderer { // Reinitialize existing passes in place (engine may have set custom passes) let mut passes = std::mem::take(&mut self.state.passes); + let self_ptr: *mut Renderer = self as *mut _; + let rm_ptr: *mut crate::resources::ResourceManager = self.resource_manager; for pass in passes.iter_mut() { - let _ = pass.init(self); + // Avoid double mutable borrow of self by using raw pointer for resources + let _ = unsafe { pass.init(&mut *self_ptr, &mut *rm_ptr) }; } self.state.passes = passes; // Old offscreen texture/view/sampler are dropped when replaced; wgpu defers actual GPU resource @@ -943,10 +944,22 @@ impl PillRenderer for Renderer { transform_components: transform_component_storage, }; let mut passes = std::mem::take(&mut self.state.passes); + let self_ptr: *mut Renderer = self as *mut _; + let rm_ptr: *mut crate::resources::ResourceManager = self.resource_manager; for pass in passes.iter_mut() { let label_owned = pass.get_label().to_string(); timer.begin_context(&label_owned); - let _ = pass.draw(&mut encoder, self, &frame, &view, &world_q); + // Avoid double mutable borrow of self by using raw pointer for resources + let _ = unsafe { + pass.draw( + &mut encoder, + &mut *self_ptr, + &mut *rm_ptr, + &frame, + &view, + &world_q, + ) + }; timer.record(&label_owned); } self.state.passes = passes; @@ -1000,8 +1013,11 @@ impl PillRenderer for Renderer { // --- Engine pass management and helpers --- fn set_passes(&mut self, mut passes: Vec>) -> Result<()> { + let self_ptr: *mut Renderer = self as *mut _; + let rm_ptr: *mut crate::resources::ResourceManager = self.resource_manager; for pass in passes.iter_mut() { - let _ = pass.init(self); + // Avoid double mutable borrow of self by using raw pointer for resources + let _ = unsafe { pass.init(&mut *self_ptr, &mut *rm_ptr) }; } self.state.passes = passes; Ok(()) @@ -1015,29 +1031,6 @@ impl PillRenderer for Renderer { &self.ctx.device } - fn get_texture(&self, h: RendererTextureHandle) -> &wgpu::Texture { - let tex = self.resources().textures.get(h).expect("texture"); - &tex.texture - } - - fn get_mesh_buffers_and_count( - &self, - h: RendererMeshHandle, - ) -> (&wgpu::Buffer, &wgpu::Buffer, u32) { - let mesh = self.resources().meshes.get(h).expect("mesh"); - (&mesh.vertex_buffer, &mesh.index_buffer, mesh.index_count) - } - - fn get_material_texture_bind_group(&self, h: RendererMaterialHandle) -> &wgpu::BindGroup { - let mat = self.resources().materials.get(h).expect("material"); - &mat.texture_bind_group - } - - fn get_material_params_bind_group(&self, h: RendererMaterialHandle) -> &wgpu::BindGroup { - let mat = self.resources().materials.get(h).expect("material"); - &mat.param_bind_group - } - fn get_queue(&self) -> &wgpu::Queue { &self.ctx.queue } @@ -1045,22 +1038,23 @@ impl PillRenderer for Renderer { impl Renderer { #[inline] - fn resources(&self) -> &crate::resources::GpuResources { + fn rm(&self) -> &crate::resources::ResourceManager { assert!( - !self.gpu_resources.is_null(), - "Renderer GPU resources not attached. Use Renderer::new(window, config, gpu_resources)." + !self.resource_manager.is_null(), + "Renderer ResourceManager not attached" ); - unsafe { &*self.gpu_resources } + unsafe { &*self.resource_manager } } #[inline] - fn resources_mut(&mut self) -> &mut crate::resources::GpuResources { + fn rm_mut(&mut self) -> &mut crate::resources::ResourceManager { assert!( - !self.gpu_resources.is_null(), - "Renderer GPU resources not attached. Use Renderer::new(window, config, gpu_resources)." + !self.resource_manager.is_null(), + "Renderer ResourceManager not attached" ); - unsafe { &mut *self.gpu_resources } + unsafe { &mut *self.resource_manager } } + pub fn get_surface_format(&self) -> wgpu::TextureFormat { self.ctx.surface_configuration.format } @@ -1072,15 +1066,4 @@ impl Renderer { pub fn get_surface(&self) -> &wgpu::Surface<'_> { &self.ctx.surface } - - // Lightweight resolve helpers for engine passes - pub fn get_texture_view(&self, h: RendererTextureHandle) -> &wgpu::TextureView { - let tex = self.resources().textures.get(h).expect("texture"); - &tex.texture_view - } - - pub fn get_texture_sampler(&self, h: RendererTextureHandle) -> &wgpu::Sampler { - let tex = self.resources().textures.get(h).expect("texture"); - &tex.sampler - } } diff --git a/engine/pill_standalone/src/lib.rs b/engine/pill_standalone/src/lib.rs index 5711efc7..4601fdce 100644 --- a/engine/pill_standalone/src/lib.rs +++ b/engine/pill_standalone/src/lib.rs @@ -1,3 +1 @@ pub use pill_engine as engine; - - diff --git a/engine/pill_standalone/src/main.rs b/engine/pill_standalone/src/main.rs index 81b32451..b82b9941 100644 --- a/engine/pill_standalone/src/main.rs +++ b/engine/pill_standalone/src/main.rs @@ -282,9 +282,9 @@ fn check_and_reload_game( let mut resource_manager = pill_engine::internal::ResourceManager::new(); let renderer: Box = Box::new(::new( + &mut resource_manager, Arc::clone(&window), config.clone(), - resource_manager.gpu_mut(), )); let mut new_engine = Engine::new( game, @@ -530,9 +530,9 @@ fn main() { let mut resource_manager = pill_engine::internal::ResourceManager::new(); let renderer: Box = Box::new(::new( + &mut resource_manager, Arc::clone(&window_data.window), config.clone(), - resource_manager.gpu_mut(), )); let mut engine: Option = Some(Engine::new( game, From 2dd2e9100fe68869032b29ba7a70e406c83e9aa3 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Sun, 16 Nov 2025 03:50:04 +0100 Subject: [PATCH 63/69] feat: PassEgui --- .../ecs/components/egui_manager_component.rs | 4 +- .../ecs/components/render_state_component.rs | 3 +- engine/pill_engine/src/ecs/egui_client.rs | 35 +++++ engine/pill_engine/src/ecs/mod.rs | 4 + .../src/ecs/systems/rendering_system.rs | 28 +++- engine/pill_engine/src/engine.rs | 11 +- engine/pill_engine/src/graphics/mod.rs | 2 + engine/pill_engine/src/graphics/pass_egui.rs | 93 +++++++++++++ engine/pill_engine/src/graphics/renderer.rs | 11 +- engine/pill_engine/src/renderer/egui.rs | 11 -- engine/pill_engine/src/renderer/wgpu.rs | 130 +++++------------- engine/pill_standalone/src/main.rs | 4 +- 12 files changed, 205 insertions(+), 131 deletions(-) create mode 100644 engine/pill_engine/src/ecs/egui_client.rs create mode 100644 engine/pill_engine/src/graphics/pass_egui.rs diff --git a/engine/pill_engine/src/ecs/components/egui_manager_component.rs b/engine/pill_engine/src/ecs/components/egui_manager_component.rs index 1bb52b66..af648100 100644 --- a/engine/pill_engine/src/ecs/components/egui_manager_component.rs +++ b/engine/pill_engine/src/ecs/components/egui_manager_component.rs @@ -25,7 +25,7 @@ impl EguiManagerComponent { } } - pub fn get_ui(engine: &mut Engine) -> Box { + pub fn get_ui(engine: &mut Engine) -> Box { let entity_count = engine .scene_manager .get_active_scene() @@ -143,7 +143,7 @@ impl EguiManagerComponent { } }); }); - }); + }) as Box; ui } diff --git a/engine/pill_engine/src/ecs/components/render_state_component.rs b/engine/pill_engine/src/ecs/components/render_state_component.rs index 826007c7..76c35910 100644 --- a/engine/pill_engine/src/ecs/components/render_state_component.rs +++ b/engine/pill_engine/src/ecs/components/render_state_component.rs @@ -4,11 +4,12 @@ use crate::PillTypeMapKey; // Keeps renderer/bootstrap state; minimal for now pub struct RenderStateComponent { pub boot_done: bool, + pub egui_client: Option>, } impl RenderStateComponent { pub fn new() -> Self { - Self { boot_done: false } + Self { boot_done: false, egui_client: None } } } diff --git a/engine/pill_engine/src/ecs/egui_client.rs b/engine/pill_engine/src/ecs/egui_client.rs new file mode 100644 index 00000000..f4101918 --- /dev/null +++ b/engine/pill_engine/src/ecs/egui_client.rs @@ -0,0 +1,35 @@ +use std::sync::{Arc, Mutex}; + +pub struct EguiClient { + events: Mutex>, + ui: Mutex>>, +} + +impl EguiClient { + pub fn new() -> Arc { + Arc::new(Self { + events: Mutex::new(Vec::new()), + ui: Mutex::new(None), + }) + } + + pub fn handle_input(&self, event: &winit::event::WindowEvent) { + let mut q = self.events.lock().unwrap(); + q.push(event.clone()); + } + + pub fn take_events(&self) -> Vec { + let mut q = self.events.lock().unwrap(); + std::mem::take(&mut *q) + } + + pub fn set_ui(&self, ui: Box) { + let mut u = self.ui.lock().unwrap(); + *u = Some(ui); + } + + pub fn take_ui(&self) -> Option> { + let mut u = self.ui.lock().unwrap(); + u.take() + } +} diff --git a/engine/pill_engine/src/ecs/mod.rs b/engine/pill_engine/src/ecs/mod.rs index 9fefc130..4979cb0b 100644 --- a/engine/pill_engine/src/ecs/mod.rs +++ b/engine/pill_engine/src/ecs/mod.rs @@ -5,6 +5,7 @@ mod entity; mod scene; mod scene_manager; mod systems; +mod egui_client; // --- Use --- @@ -48,6 +49,9 @@ pub use components::render_state_component::RenderStateComponent; pub use systems::{SystemFunction, SystemManager, UpdatePhase}; +// - Egui client (UI input/closure mailbox) +pub use egui_client::EguiClient; + pub use systems::rendering_system::rendering_system; pub use systems::deferred_update_system::deferred_update_system; diff --git a/engine/pill_engine/src/ecs/systems/rendering_system.rs b/engine/pill_engine/src/ecs/systems/rendering_system.rs index 4cdba02e..029908e5 100644 --- a/engine/pill_engine/src/ecs/systems/rendering_system.rs +++ b/engine/pill_engine/src/ecs/systems/rendering_system.rs @@ -65,6 +65,15 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { .renderer_resource_handle .expect("renderer handle for logo texture"); + // Ensure EguiClient exists in global render state + let egui_client = { + let rs = engine.get_global_component_mut::()?; + if rs.egui_client.is_none() { + rs.egui_client = Some(crate::ecs::EguiClient::new()); + } + rs.egui_client.as_ref().unwrap().clone() + }; + // Logo overlay let h: f32 = 0.04; let rect_logo = [0.98 - 3.0 * h, 0.02, 0.98, 0.02 + h]; @@ -112,6 +121,11 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { tex_logo_rt, fmt, )), + Box::new(crate::graphics::PassEgui::new( + "egui", + engine.window.clone(), + egui_client, + )), ]; engine.renderer.set_passes(passes)?; } @@ -285,7 +299,12 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { timer.record("Get component storages"); - let egui_ui = EguiManagerComponent::get_ui(engine); // egui_manager_component.get_ui(engine); + let egui_ui: Box = EguiManagerComponent::get_ui(engine); // egui_manager_component.get_ui(engine) + if let Ok(rs) = engine.get_global_component_mut::() { + if let Some(ref client) = rs.egui_client { + client.set_ui(egui_ui); + } + } // Storages fetched inside renderer via immutable Engine @@ -307,12 +326,7 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { } }; let factory = crate::graphics::WorldViewFactory { world: world_view }; - match crate::graphics::render_with_factory( - engine.renderer.as_mut(), - &factory, - egui_ui, - &mut timer, - ) { + match crate::graphics::render_with_factory(engine.renderer.as_mut(), &factory, &mut timer) { Ok(_) => { timer.end_context()?; // End "Render" context engine.system_manager.update_system_timer( diff --git a/engine/pill_engine/src/engine.rs b/engine/pill_engine/src/engine.rs index 9b957b50..a3e729d0 100644 --- a/engine/pill_engine/src/engine.rs +++ b/engine/pill_engine/src/engine.rs @@ -38,6 +38,7 @@ pub struct Engine { pub(crate) input_queue: VecDeque, pub(crate) render_queue: Vec, pub(crate) window_size: winit::dpi::PhysicalSize, + pub(crate) window: std::sync::Arc, pub(crate) game_resources_directory_path: std::path::PathBuf, pub(crate) frame_delta_time: f32, // In milliseconds } @@ -70,6 +71,7 @@ impl Engine { renderer: Box, resource_manager: ResourceManager, config: config::Config, + window: std::sync::Arc, ) -> Self { let max_entity_count = config .get_int("MAX_ENTITIES") @@ -86,6 +88,7 @@ impl Engine { input_queue: VecDeque::new(), render_queue: Vec::::with_capacity(max_entity_count), window_size: winit::dpi::PhysicalSize::::default(), + window, game_resources_directory_path, frame_delta_time: 0.0.into(), } @@ -361,8 +364,12 @@ impl Engine { debug!("Got new mouse position input"); } - pub fn pass_input_to_egui(&mut self, event: &winit::event::WindowEvent) { - self.renderer.pass_input_to_egui(event).unwrap(); + pub fn pass_input_to_ui(&mut self, event: &winit::event::WindowEvent) { + if let Ok(rs) = self.get_global_component_mut::() { + if let Some(ref client) = rs.egui_client { + client.handle_input(event); + } + } } pub fn get_input_queue(&self) -> &VecDeque { diff --git a/engine/pill_engine/src/graphics/mod.rs b/engine/pill_engine/src/graphics/mod.rs index 4b7fe666..4daadc5c 100644 --- a/engine/pill_engine/src/graphics/mod.rs +++ b/engine/pill_engine/src/graphics/mod.rs @@ -35,3 +35,5 @@ pub mod pass_compose; pub use pass_compose::PassCompose; pub mod pass_scene; pub use pass_scene::PassScene; +pub mod pass_egui; +pub use pass_egui::PassEgui; diff --git a/engine/pill_engine/src/graphics/pass_egui.rs b/engine/pill_engine/src/graphics/pass_egui.rs new file mode 100644 index 00000000..f09554e1 --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_egui.rs @@ -0,0 +1,93 @@ +use std::sync::Arc; + +use anyhow::Result; +use wgpu::CommandEncoder; + +use crate::graphics::renderer::{Pass, PillRenderer as EnginePillRenderer, WorldQuery}; + +pub struct PassEgui { + label: String, + window: Arc, + client: Arc, + state: Option, +} + +struct PassEguiState { + egui: crate::renderer::egui::EguiRenderer, +} + +impl PassEgui { + pub fn new( + label: &str, + window: Arc, + client: Arc, + ) -> Self { + Self { + label: label.to_string(), + window, + client, + state: None, + } + } +} + +impl Pass for PassEgui { + fn get_label(&self) -> &str { + &self.label + } + + fn init( + &mut self, + renderer: &mut dyn EnginePillRenderer, + _resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { + let device = renderer.get_device(); + let format = renderer.get_surface_format(); + let egui = crate::renderer::egui::EguiRenderer::new( + device, + format, + None, + 1, + self.window.clone(), + ); + self.state = Some(PassEguiState { egui }); + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut CommandEncoder, + renderer: &mut dyn EnginePillRenderer, + _resources: &mut crate::resources::ResourceManager, + _frame: &wgpu::SurfaceTexture, + view: &wgpu::TextureView, + _world: &WorldQuery, + ) -> Result<()> { + let state = self.state.as_mut().expect("PassEgui not initialized"); + // Feed queued input events + for ev in self.client.take_events().into_iter() { + state.egui.handle_input(&ev); + } + // Build screen descriptor and UI callback + let size = self.window.inner_size(); + let screen_descriptor = egui_wgpu::ScreenDescriptor { + size_in_pixels: [size.width, size.height], + pixels_per_point: state.egui.window_scale_factor, + }; + let run_ui = self + .client + .take_ui() + .unwrap_or_else(|| Box::new(|_ctx: &egui::Context| {})); + state.egui.draw( + renderer.get_device(), + renderer.get_queue(), + encoder, + view, + screen_descriptor, + run_ui, + )?; + Ok(()) + } +} + + diff --git a/engine/pill_engine/src/graphics/renderer.rs b/engine/pill_engine/src/graphics/renderer.rs index 752d3fb7..1590781c 100644 --- a/engine/pill_engine/src/graphics/renderer.rs +++ b/engine/pill_engine/src/graphics/renderer.rs @@ -145,15 +145,12 @@ pub trait PillRenderer { fn destroy_texture(&mut self, renderer_texture_handle: RendererTextureHandle) -> Result<()>; fn destroy_camera(&mut self, renderer_camera_handle: RendererCameraHandle) -> Result<()>; - fn pass_input_to_egui(&mut self, event: &winit::event::WindowEvent) -> Result<()>; - fn render( &mut self, active_camera_entity_handle: EntityHandle, render_queue: &Vec, camera_component_storage: &ComponentStorage, transform_component_storage: &ComponentStorage, - egui_ui: Box, timer: &mut Timer, ) -> Result<()>; @@ -171,12 +168,7 @@ pub type Renderer = Box; /// Renders using a zero-cost factory that provides borrowed references. /// Keeps the PillRenderer trait object-safe while allowing call sites to be generic and inlined. #[inline(always)] -pub fn render_with_factory( - renderer: &mut R, - factory: &F, - egui_ui: Box, - timer: &mut Timer, -) -> Result<()> +pub fn render_with_factory(renderer: &mut R, factory: &F, timer: &mut Timer) -> Result<()> where R: PillRenderer + ?Sized, F: RenderQueueFactory, @@ -187,7 +179,6 @@ where q.render_queue, q.camera_components, q.transform_components, - egui_ui, timer, ) } diff --git a/engine/pill_engine/src/renderer/egui.rs b/engine/pill_engine/src/renderer/egui.rs index 432645a0..0cd77486 100644 --- a/engine/pill_engine/src/renderer/egui.rs +++ b/engine/pill_engine/src/renderer/egui.rs @@ -70,23 +70,16 @@ impl EguiRenderer { window_surface_view: &TextureView, screen_descriptor: ScreenDescriptor, run_ui: impl FnOnce(&Context), - timer: &mut Timer, ) -> Result<()> { - timer.record("Prepare window and input"); - let window = &self.window; let raw_input = self.state.take_egui_input(&window); let full_output = self.context.run(raw_input, |_| { run_ui(&self.context); }); - timer.record("Handle platform output"); - self.state .handle_platform_output(&window, full_output.platform_output); - timer.record("Tesselate and update textures"); - let tris = self .context .tessellate(full_output.shapes, full_output.pixels_per_point); @@ -95,8 +88,6 @@ impl EguiRenderer { .update_texture(&device, &queue, *id, &image_delta); } - timer.record("Update buffers and record render pass"); - self.renderer .update_buffers(&device, &queue, encoder, &tris, &screen_descriptor); @@ -115,8 +106,6 @@ impl EguiRenderer { occlusion_query_set: None, }); - timer.record("Render"); - self.renderer .render(&mut render_pass, &tris, &screen_descriptor); diff --git a/engine/pill_engine/src/renderer/wgpu.rs b/engine/pill_engine/src/renderer/wgpu.rs index c9ba32be..b62f65ab 100644 --- a/engine/pill_engine/src/renderer/wgpu.rs +++ b/engine/pill_engine/src/renderer/wgpu.rs @@ -28,55 +28,11 @@ use glam::{Mat4, Vec3, Vec4}; use anyhow::{Error, Result}; use log::{error, info}; -use crate::renderer::egui::EguiRenderer; use image::GenericImageView; use wgpu::util::DeviceExt; pub const MAX_INSTANCE_BATCH_SIZE: usize = 10000; // Maximum number of instances that can be drawn in a single draw call pub const INITIAL_INSTANCE_VECTOR_CAPACITY: usize = 10000; -// M2 inline draw: no MeshDrawer/instance batching - -// KISS helper: create a color render target texture+view+sampler -fn create_render_target( - device: &wgpu::Device, - format: wgpu::TextureFormat, - width: u32, - height: u32, - label: &str, -) -> RendererTexture { - let size = wgpu::Extent3d { - width, - height, - depth_or_array_layers: 1, - }; - let texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some(label), - size, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }); - let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - let sampler = device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Linear, - mipmap_filter: wgpu::FilterMode::Nearest, - lod_min_clamp: 0.0, - lod_max_clamp: 100.0, - ..Default::default() - }); - RendererTexture { - texture, - texture_view, - sampler, - } -} pub struct DeviceContext { pub(crate) config: config::Config, @@ -93,7 +49,6 @@ pub struct DeviceContext { pub struct State { passes: Vec>, - egui_renderer: crate::renderer::egui::EguiRenderer, // TODO: Separate system adding Pass } pub struct Renderer { @@ -243,18 +198,7 @@ impl PillRenderer for Renderer { // let color_format = wgpu::TextureFormat::Rgba16Float; // let depth_format = wgpu::TextureFormat::Depth32Float; - let egui_renderer = EguiRenderer::new( - &ctx.device, - ctx.surface_configuration.format, - None, - 1, - ctx.window_ref.clone(), - ); - - State { - passes: vec![], - egui_renderer, - } + State { passes: vec![] } }; Self { state, @@ -777,14 +721,38 @@ impl PillRenderer for Renderer { } fn create_render_target(&mut self, desc: RendererTargetDesc) -> Result { - let tex = create_render_target( - &self.ctx.device, - desc.format, - desc.width, - desc.height, - desc.name.as_str(), - ); - let handle = self.rm_mut().gpu_mut().textures.insert(tex); + let size = wgpu::Extent3d { + width: desc.width, + height: desc.height, + depth_or_array_layers: 1, + }; + let texture = self.ctx.device.create_texture(&wgpu::TextureDescriptor { + label: Some(desc.name.as_str()), + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: desc.format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + let sampler = self.ctx.device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Nearest, + lod_min_clamp: 0.0, + lod_max_clamp: 100.0, + ..Default::default() + }); + let handle = self.rm_mut().gpu_mut().textures.insert(RendererTexture { + texture, + texture_view, + sampler, + }); Ok(handle) } @@ -907,7 +875,6 @@ impl PillRenderer for Renderer { render_queue: &Vec, camera_component_storage: &ComponentStorage, transform_component_storage: &ComponentStorage, - egui_ui: Box, timer: &mut Timer, ) -> Result<()> { timer.record("Frame: acquire"); @@ -969,32 +936,6 @@ impl PillRenderer for Renderer { timer.end_context()?; // End "User passes" } - // Egui overlay (load over the rendered frame) - timer.begin_context("UI pass"); - // ALLOCATION: CommandEncoder creation (CPU memory) - ~1KB command buffer - let mut encoder = self - .ctx - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("egui_encoder"), - }); - let screen_descriptor = egui_wgpu::ScreenDescriptor { - size_in_pixels: [self.ctx.window_size.width, self.ctx.window_size.height], - pixels_per_point: self.state.egui_renderer.window_scale_factor, - }; - self.state.egui_renderer.draw( - &self.ctx.device, - &self.ctx.queue, - &mut encoder, - &view, - screen_descriptor, - |ctx| egui_ui(ctx), - timer, - )?; - self.ctx.queue.submit([encoder.finish()]); - timer.record("UI draw & submit"); - timer.end_context()?; // End "UI pass" - // Present frame frame.present(); timer.record("Present"); @@ -1006,11 +947,6 @@ impl PillRenderer for Renderer { Ok(()) } - fn pass_input_to_egui(&mut self, event: &winit::event::WindowEvent) -> Result<()> { - self.state.egui_renderer.handle_input(event); - Ok(()) - } - // --- Engine pass management and helpers --- fn set_passes(&mut self, mut passes: Vec>) -> Result<()> { let self_ptr: *mut Renderer = self as *mut _; diff --git a/engine/pill_standalone/src/main.rs b/engine/pill_standalone/src/main.rs index b82b9941..ca7d2c03 100644 --- a/engine/pill_standalone/src/main.rs +++ b/engine/pill_standalone/src/main.rs @@ -292,6 +292,7 @@ fn check_and_reload_game( renderer, resource_manager, config.clone(), + Arc::clone(&window), ); new_engine.initialize(*window_size).unwrap(); *engine = Some(new_engine); @@ -367,7 +368,7 @@ fn main_loop( window_id, } if window_id == window_data.window.id() => { if let Some(ref mut engine) = engine { - engine.pass_input_to_egui(event); + engine.pass_input_to_ui(event); } match event { @@ -540,6 +541,7 @@ fn main() { renderer, resource_manager, config.clone(), + Arc::clone(&window_data.window), )); engine .as_mut() From 8c54158b814dc0596da42c82d36682968df27029 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Sun, 16 Nov 2025 16:53:59 +0100 Subject: [PATCH 64/69] cleanup warnings --- engine/pill_engine/src/engine.rs | 2 +- engine/pill_engine/src/graphics/pass_overlay_uv.rs | 2 +- engine/pill_engine/src/graphics/pass_scene.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/engine/pill_engine/src/engine.rs b/engine/pill_engine/src/engine.rs index a3e729d0..d04fb723 100644 --- a/engine/pill_engine/src/engine.rs +++ b/engine/pill_engine/src/engine.rs @@ -175,7 +175,7 @@ impl Engine { RENDERING_SYSTEM.update_phase, )?; - self.renderer.init(); + let _ = self.renderer.init(); self.init_renderer(); // Start game diff --git a/engine/pill_engine/src/graphics/pass_overlay_uv.rs b/engine/pill_engine/src/graphics/pass_overlay_uv.rs index 87176b1b..aa27faa8 100644 --- a/engine/pill_engine/src/graphics/pass_overlay_uv.rs +++ b/engine/pill_engine/src/graphics/pass_overlay_uv.rs @@ -35,7 +35,7 @@ impl Pass for PassOverlayUV { fn init( &mut self, renderer: &mut dyn EnginePillRenderer, - resources: &mut crate::resources::ResourceManager, + _resources: &mut crate::resources::ResourceManager, ) -> Result<()> { let device = renderer.get_device(); // Create buffer for overlay rect UBO diff --git a/engine/pill_engine/src/graphics/pass_scene.rs b/engine/pill_engine/src/graphics/pass_scene.rs index 96848d9e..2494deb2 100644 --- a/engine/pill_engine/src/graphics/pass_scene.rs +++ b/engine/pill_engine/src/graphics/pass_scene.rs @@ -194,7 +194,7 @@ impl Pass for PassScene { fn init( &mut self, renderer: &mut dyn EnginePillRenderer, - resources: &mut crate::resources::ResourceManager, + _resources: &mut crate::resources::ResourceManager, ) -> Result<()> { // [SIMILAR] Prebuilt PSO used; avoid hot-path pipeline creation per TALK // Shaders: must match bind group layout indices: 0(camera),1(material textures),2(material params),3(per-draw) From 594c6a370dcf200a78278cba317d14a82cf88b11 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Mon, 17 Nov 2025 04:54:52 +0100 Subject: [PATCH 65/69] feat: ibl, TODO: cleanup --- engine/pill_engine/Cargo.toml | 2 +- .../src/ecs/systems/rendering_system.rs | 142 ++++ engine/pill_engine/src/graphics/mod.rs | 6 + .../src/graphics/pass_ibl_diffuse_equirect.rs | 279 ++++++++ .../graphics/pass_ibl_specular_equirect.rs | 644 ++++++++++++++++++ engine/pill_engine/src/graphics/pass_scene.rs | 286 +++++++- .../src/graphics/pass_skybox_equirect.rs | 395 +++++++++++ engine/pill_engine/src/graphics/renderer.rs | 2 + .../renderer/resources/renderer_texture.rs | 8 +- engine/pill_engine/src/renderer/wgpu.rs | 7 +- engine/pill_engine/src/resources/model.rs | 4 +- engine/pill_engine/src/resources/texture.rs | 5 +- 12 files changed, 1737 insertions(+), 43 deletions(-) create mode 100644 engine/pill_engine/src/graphics/pass_ibl_diffuse_equirect.rs create mode 100644 engine/pill_engine/src/graphics/pass_ibl_specular_equirect.rs create mode 100644 engine/pill_engine/src/graphics/pass_skybox_equirect.rs diff --git a/engine/pill_engine/Cargo.toml b/engine/pill_engine/Cargo.toml index a0560113..a5cf1ee2 100644 --- a/engine/pill_engine/Cargo.toml +++ b/engine/pill_engine/Cargo.toml @@ -18,7 +18,7 @@ env_logger = "0.9.0" log = "0.4" # Loaders -image = "0.23" +image = { version = "0.25.6", features = ["hdr"] } tobj = "3.0" gltf = { version = "1.4", features = ["import", "names"] } diff --git a/engine/pill_engine/src/ecs/systems/rendering_system.rs b/engine/pill_engine/src/ecs/systems/rendering_system.rs index 029908e5..2b5af1c9 100644 --- a/engine/pill_engine/src/ecs/systems/rendering_system.rs +++ b/engine/pill_engine/src/ecs/systems/rendering_system.rs @@ -90,12 +90,154 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { let depth_texture = engine.renderer.create_depth_texture("depth_texture")?; // Build passes as an array with elements, then convert to Vec + // Load equirectangular HDR environment map (as 2D) + let env_tex_handle = { + // Add if not already present + match engine.get_resource_handle::("ibl_env_equirect") { + Ok(h) => h, + Err(_) => { + let tex = Texture::new( + "ibl_env_equirect", + TextureType::Linear, + ResourceLoadType::Path("ibl/HDR_111_Parking_Lot_2_Env.hdr".into()), + ); + engine.add_resource::(tex)? + } + } + }; + let env_tex_rt = engine + .get_resource::(&env_tex_handle)? + .renderer_resource_handle + .expect("renderer handle for env texture"); + + // Create IBL output resources up front + let ibl_irradiance_rt = engine.renderer.create_render_target(RendererTargetDesc { + name: "ibl_irradiance_2d".to_string(), + format: wgpu::TextureFormat::Rgba16Float, + width: 64, + height: 32, + })?; + let ibl_brdf_rt = engine.renderer.create_render_target(RendererTargetDesc { + name: "ibl_brdf_lut".to_string(), + format: wgpu::TextureFormat::Rgba16Float, + width: 512, + height: 512, + })?; + + // Build and run diffuse IBL convolution once (init-time) + let mut pass_ibl_diff = crate::graphics::PassIblDiffuseEquirect::new( + "ibl_diffuse_equirect", + env_tex_rt, + ibl_irradiance_rt, + wgpu::TextureFormat::Rgba16Float, + ); + // Manually init to precompute before scene pass init + { + // SAFETY: renderer exposes init path; we can call with engine resources + let self_ptr: *mut dyn crate::graphics::PillRenderer = &mut *engine.renderer; + let rm_ptr: *mut crate::resources::ResourceManager = &mut engine.resource_manager; + unsafe { + pass_ibl_diff.init(&mut *self_ptr, &mut *rm_ptr)?; + } + } + // Provide BRDF target to specular pass + + // Create prefilter target (mip chain) in RenderSystem and pass to pass + let prefilter_handle = { + let device = engine.renderer.get_device(); + let tex = device.create_texture(&wgpu::TextureDescriptor { + label: Some("ibl_prefilter_2d"), + size: wgpu::Extent3d { + width: 256, + height: 128, + depth_or_array_layers: 1, + }, + mip_level_count: 5, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba16Float, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + let view = tex.create_view(&wgpu::TextureViewDescriptor::default()); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::Repeat, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Linear, + lod_min_clamp: 0.0, + lod_max_clamp: 5.0, + ..Default::default() + }); + let handle = engine.resource_manager.gpu_mut().textures.insert( + crate::renderer::resources::RendererTexture { + texture: tex, + texture_view: view, + sampler, + format: wgpu::TextureFormat::Rgba16Float, + }, + ); + // Sanity: log handle id and format + { + use pill_core::PillSlotMapKey; + let fmt = engine + .resource_manager + .gpu() + .textures + .get(handle) + .map(|t| t.format) + .unwrap_or(wgpu::TextureFormat::Rgba8UnormSrgb); + log::info!( + "rendering_system: created prefilter handle=({},{}) format={:?}", + handle.index(), + handle.generation(), + fmt + ); + } + handle + }; + + // Specular IBL prefilter + BRDF LUT + let mut pass_ibl_spec = crate::graphics::PassIblSpecularEquirect::new( + "ibl_specular_equirect", + env_tex_rt, + prefilter_handle, + ibl_brdf_rt, + 5, + ); + { + let self_ptr: *mut dyn crate::graphics::PillRenderer = &mut *engine.renderer; + let rm_ptr: *mut crate::resources::ResourceManager = &mut engine.resource_manager; + unsafe { + pass_ibl_spec.init(&mut *self_ptr, &mut *rm_ptr)?; + } + } + let ibl_prefilter_rt = pass_ibl_spec.prefilter_texture_handle(); + let passes: Vec> = vec![ + // Keep the pass in list so it's a no-op afterwards (done flag) + Box::new(pass_ibl_diff), + Box::new(pass_ibl_spec), + // Skybox (draws into offscreen color); must come before scene + // TODO: move after all geometry was drawn and draw at end of depth + Box::new(crate::graphics::PassSkyboxEquirect::new( + "skybox_equirect", + offscreen_color_texture, + fmt, + env_tex_rt, + )), Box::new(crate::graphics::pass_scene::PassScene::new( "scene", offscreen_color_texture, depth_texture, fmt, + true, // load offscreen color (skybox already cleared/drew background) + Some(ibl_irradiance_rt), + Some(ibl_prefilter_rt), + Some(ibl_brdf_rt), )), Box::new(crate::graphics::PassCompose::new( "compose", diff --git a/engine/pill_engine/src/graphics/mod.rs b/engine/pill_engine/src/graphics/mod.rs index 4daadc5c..8b744ced 100644 --- a/engine/pill_engine/src/graphics/mod.rs +++ b/engine/pill_engine/src/graphics/mod.rs @@ -37,3 +37,9 @@ pub mod pass_scene; pub use pass_scene::PassScene; pub mod pass_egui; pub use pass_egui::PassEgui; +pub mod pass_skybox_equirect; +pub use pass_skybox_equirect::PassSkyboxEquirect; +pub mod pass_ibl_diffuse_equirect; +pub use pass_ibl_diffuse_equirect::PassIblDiffuseEquirect; +pub mod pass_ibl_specular_equirect; +pub use pass_ibl_specular_equirect::PassIblSpecularEquirect; diff --git a/engine/pill_engine/src/graphics/pass_ibl_diffuse_equirect.rs b/engine/pill_engine/src/graphics/pass_ibl_diffuse_equirect.rs new file mode 100644 index 00000000..e4cb9db9 --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_ibl_diffuse_equirect.rs @@ -0,0 +1,279 @@ +use crate::graphics::renderer::{Pass, PillRenderer as EnginePillRenderer, WorldQuery}; +use crate::graphics::{PipelineV2, PipelineV2Desc, RendererTextureHandle, ShaderDesc}; +use anyhow::Result; +use wgpu::CommandEncoder; + +pub struct PassIblDiffuseEquirect { + label: String, + env_texture: RendererTextureHandle, + output_handle: RendererTextureHandle, + output_format: wgpu::TextureFormat, + // GPU + bind_group_env: Option, + pipeline: Option, + done: bool, +} + +impl PassIblDiffuseEquirect { + pub fn new( + label: &str, + env_texture: RendererTextureHandle, + output_handle: RendererTextureHandle, + output_format: wgpu::TextureFormat, + ) -> Self { + Self { + label: label.to_string(), + env_texture, + output_handle, + output_format, + bind_group_env: None, + pipeline: None, + done: false, + } + } +} + +impl Pass for PassIblDiffuseEquirect { + fn get_label(&self) -> &str { + &self.label + } + + fn init( + &mut self, + renderer: &mut dyn EnginePillRenderer, + _resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { + if self.pipeline.is_some() { + return Ok(()); + } + // BGLs are declared via PipelineV2Desc (no manual BGL creation needed here) + + // Build pipeline (fullscreen triangle; writes to color via standard render pass) + let vs = r#" + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var pos = array, 3>( + vec2(-1.0, -3.0), + vec2( 3.0, 1.0), + vec2(-1.0, 1.0) + ); + var tuv = array, 3>( + vec2(0.0, 2.0), + vec2(2.0, 0.0), + vec2(0.0, 0.0) + ); + var o: VSOut; + o.pos = vec4(pos[vi], 0.0, 1.0); + o.uv = tuv[vi]; + return o; + } + "#; + + let fs = r#" + const PI: f32 = 3.14159265359; + @group(0) @binding(0) var TEnv: texture_2d; + @group(0) @binding(1) var SEnv: sampler; + + fn dir_to_equirect_uv(dir: vec3) -> vec2 { + let d = normalize(dir); + let u = 0.5 + atan2(d.x, -d.z) / (2.0 * PI); + let v = 0.5 - asin(clamp(d.y, -1.0, 1.0)) / PI; + return vec2(fract(u), clamp(v, 0.0, 1.0)); + } + + fn uv_to_dir_equirect(uv: vec2) -> vec3 { + let phi = (uv.x * 2.0 * PI) - PI; + let theta = uv.y * PI; + let x = sin(phi) * sin(theta); + let y = cos(theta); + let z = -cos(phi) * sin(theta); + return normalize(vec3(x, y, z)); + } + + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + // Output direction (normal) from output pixel uv + let N = uv_to_dir_equirect(uv); + // Hemisphere integration around N (discrete) + let sample_delta: f32 = 0.025; + var irradiance = vec3(0.0, 0.0, 0.0); + var weight = 0.0; + var phi: f32 = 0.0; + loop { + if (phi >= 2.0 * PI) { break; } + var theta: f32 = 0.0; + loop { + if (theta >= 0.5 * PI) { break; } + let x = cos(phi) * sin(theta); + let y = sin(theta); + let z = sin(phi) * sin(theta); + // tangent-to-world around N + let up = vec3(0.0, 1.0, 0.0); + let right = normalize(cross(up, N)); + let upn = cross(N, right); + let sample_vec = normalize(right * x + upn * y + N * cos(theta)); + let st = dir_to_equirect_uv(sample_vec); + let radiance = textureSample(TEnv, SEnv, st).rgb; + irradiance = irradiance + radiance * cos(theta) * sin(theta); + weight = weight + cos(theta) * sin(theta); + theta = theta + sample_delta; + } + phi = phi + sample_delta; + } + irradiance = irradiance / max(weight, 1e-4); + return vec4(irradiance, 1.0); + } + "#; + + // Pipeline fragment target format must match the output render target format used in draw(). + // See wgpu ColorTargetState docs: + // https://docs.rs/wgpu/0.20.1/wgpu/struct.ColorTargetState.html + log::info!( + "ibl_diff:init output_format={:?} surface_format={:?}", + self.output_format, + renderer.get_surface_format() + ); + let pipeline = renderer.create_pipeline_v2(PipelineV2Desc { + label: Some("ibl_diffuse_equirect"), + vs: ShaderDesc { + source: vs, + entry_func: "main", + }, + ps: ShaderDesc { + source: fs, + entry_func: "main", + }, + vertex_buffers: &[], // fullscreen triangle via vertex_index; no vertex buffers + bind_groups: vec![ + // env + vec![ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + ], + targets: &[Some(wgpu::ColorTargetState { + format: self.output_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + })?; + + self.pipeline = Some(pipeline); + // Bind groups built lazily in draw once resources are guaranteed ready + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut CommandEncoder, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + _frame: &wgpu::SurfaceTexture, + _view: &wgpu::TextureView, + _world: &WorldQuery, + ) -> Result<()> { + if self.done { + return Ok(()); + } + // Log formats to verify pipeline target equals the render pass attachment format. + let surface_fmt = renderer.get_surface_format(); + log::info!( + "ibl_diff:draw start surface_format={:?} irradiance_target_format={:?}", + surface_fmt, + self.output_format + ); + if surface_fmt == self.output_format { + log::warn!( + "ibl_diff: target format equals surface; ensure this pass renders to irradiance RT, not the swapchain" + ); + } + // Lazy build bind groups + if self.bind_group_env.is_none() { + if let Some(pipeline) = &self.pipeline { + let device = renderer.get_device(); + // env + if let Some(env) = resources.gpu().textures.get(self.env_texture) { + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::Repeat, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + let bg = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("ibl_diff_env_bg"), + layout: &pipeline.bind_group_layouts[0], + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&env.texture_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + ], + }); + self.bind_group_env = Some(bg); + } + } + } + // If still not ready (e.g., env missing), skip this frame + if self.bind_group_env.is_none() { + return Ok(()); + } + // Render once into output + let out_view = resources + .gpu() + .textures + .get(self.output_handle) + .expect("out") + .texture + .create_view(&wgpu::TextureViewDescriptor { + label: Some("ibl_irradiance_rt_view"), + ..Default::default() + }); + log::info!( + "ibl_diff:begin pass target=irradiance_2d format={:?}", + self.output_format + ); + let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.label), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &out_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + rp.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); + rp.set_bind_group(0, self.bind_group_env.as_ref().unwrap(), &[]); + rp.draw(0..3, 0..1); + drop(rp); + log::info!("ibl_diff:draw completed"); + self.done = true; + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/pass_ibl_specular_equirect.rs b/engine/pill_engine/src/graphics/pass_ibl_specular_equirect.rs new file mode 100644 index 00000000..26422a44 --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_ibl_specular_equirect.rs @@ -0,0 +1,644 @@ +use crate::graphics::renderer::{Pass, PillRenderer as EnginePillRenderer, WorldQuery}; +use crate::graphics::{PipelineV2, PipelineV2Desc, ShaderDesc}; +use anyhow::Result; +use pill_core::PillSlotMapKey; +use wgpu::CommandEncoder; + +pub struct PassIblSpecularEquirect { + label: String, + env_texture: crate::graphics::RendererTextureHandle, + // Outputs + prefilter_handle: crate::graphics::RendererTextureHandle, + brdf_lut_handle: crate::graphics::RendererTextureHandle, + // GPU + env_bind_group: Option, + params_buffer: Option, + params_bind_group: Option, + prefilter_pipeline: Option, + brdf_pipeline: Option, + // Tracking last-known formats to support adaptive re-init after resize/changes + last_prefilter_format: Option, + last_brdf_format: Option, + // Config + mip_count: u32, + done: bool, +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct PrefilterParams { + roughness: f32, + _pad: [f32; 3], +} + +impl PassIblSpecularEquirect { + pub fn new( + label: &str, + env_texture: crate::graphics::RendererTextureHandle, + prefilter_handle: crate::graphics::RendererTextureHandle, + brdf_lut_handle: crate::graphics::RendererTextureHandle, + mip_count: u32, + ) -> Self { + Self { + label: label.to_string(), + env_texture, + prefilter_handle, + brdf_lut_handle, + env_bind_group: None, + params_buffer: None, + params_bind_group: None, + prefilter_pipeline: None, + brdf_pipeline: None, + last_prefilter_format: None, + last_brdf_format: None, + mip_count, + done: false, + } + } + + pub fn prefilter_texture_handle(&self) -> crate::graphics::RendererTextureHandle { + self.prefilter_handle + } + pub fn brdf_lut_handle(&self) -> crate::graphics::RendererTextureHandle { + self.brdf_lut_handle + } +} + +impl Pass for PassIblSpecularEquirect { + fn get_label(&self) -> &str { + &self.label + } + + fn init( + &mut self, + renderer: &mut dyn EnginePillRenderer, + _resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { + // All outputs provided by caller; build/refresh pipelines based on current target formats. + + // Build env bind group layout via pipeline desc for prefilter (set 0 -> env tex+sampler, set 1 -> params) + let vs = r#" + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var pos = array, 3>( + vec2(-1.0, -3.0), + vec2( 3.0, 1.0), + vec2(-1.0, 1.0) + ); + var tuv = array, 3>( + vec2(0.0, 2.0), + vec2(2.0, 0.0), + vec2(0.0, 0.0) + ); + var o: VSOut; + o.pos = vec4(pos[vi], 0.0, 1.0); + o.uv = tuv[vi]; + return o; + } + "#; + let fs_prefilter = r#" + const PI: f32 = 3.14159265359; + @group(0) @binding(0) var TEnv: texture_2d; + @group(0) @binding(1) var SEnv: sampler; + struct Params { roughness: f32, _pad0: vec3, } + @group(1) @binding(0) var UParams: Params; + + fn dir_to_equirect_uv(dir: vec3) -> vec2 { + let d = normalize(dir); + let u = 0.5 + atan2(d.x, -d.z) / (2.0 * PI); + let v = 0.5 - asin(clamp(d.y, -1.0, 1.0)) / PI; + return vec2(fract(u), clamp(v, 0.0, 1.0)); + } + fn uv_to_dir_equirect(uv: vec2) -> vec3 { + let phi = (uv.x * 2.0 * PI) - PI; + let theta = uv.y * PI; + let x = sin(phi) * sin(theta); + let y = cos(theta); + let z = -cos(phi) * sin(theta); + return normalize(vec3(x, y, z)); + } + + // GGX VNDF importance sampling (approx) + fn hammersley(i: u32, n: u32) -> vec2 { + // radical inverse Van der Corput in base 2 (underscores removed for wider WGSL parser support) + var bits = i; + bits = (bits << 16u) | (bits >> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); + bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); + bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); + let rdi = f32(bits) * 2.3283064365386963e-10; + return vec2(f32(i)/f32(n), rdi); + } + fn importance_sample_ggx(xi: vec2, roughness: f32, N: vec3) -> vec3 { + let a = roughness * roughness; + let phi = 2.0 * PI * xi.x; + let cos_theta = sqrt((1.0 - xi.y) / (1.0 + (a*a - 1.0) * xi.y)); + let sin_theta = sqrt(1.0 - cos_theta * cos_theta); + // tangent space + let Ht = vec3(cos(phi) * sin_theta, sin(phi) * sin_theta, cos_theta); + // build basis + let up = vec3(0.0, 1.0, 0.0); + let T = normalize(cross(up, N)); + let B = cross(N, T); + // to world + return normalize(T * Ht.x + B * Ht.y + N * Ht.z); + } + + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let N = uv_to_dir_equirect(uv); + let V = N; + let R = N; + let rough = clamp(UParams.roughness, 0.001, 1.0); + var prefiltered = vec3(0.0, 0.0, 0.0); + var total = 0.0; + let SAMPLE_COUNT: u32 = 1024u; + for (var i: u32 = 0u; i < SAMPLE_COUNT; i = i + 1u) { + let xi = hammersley(i, SAMPLE_COUNT); + let H = importance_sample_ggx(xi, rough, N); + let L = normalize(reflect(-V, H)); + let NdotL = max(dot(N, L), 0.0); + if (NdotL > 0.0) { + let st = dir_to_equirect_uv(L); + let c = textureSample(TEnv, SEnv, st).rgb; + prefiltered = prefiltered + c * NdotL; + total = total + NdotL; + } + } + prefiltered = prefiltered / max(total, 1e-4); + return vec4(prefiltered, 1.0); + } + "#; + + // Current target formats (may change after resize or handle reuse). + // IMPORTANT: In wgpu, RenderPipeline.fragment.targets[n].format MUST equal the + // RenderPass color_attachments[n] texture view format or validation will fail at set_pipeline(). + // See ColorTargetState docs: + // https://docs.rs/wgpu/0.20.1/wgpu/struct.ColorTargetState.html + let prefilter_format = _resources + .gpu() + .textures + .get(self.prefilter_handle) + .expect("prefilter texture handle") + .format; + let env_h = self.env_texture; + let pre_h = self.prefilter_handle; + let brdf_h = self.brdf_lut_handle; + log::info!( + "ibl_spec:init env=({},{}) prefilter=({},{}) brdf=({},{})", + env_h.index(), + env_h.generation(), + pre_h.index(), + pre_h.generation(), + brdf_h.index(), + brdf_h.generation() + ); + log::info!( + "ibl_spec:init prefilter_format={:?} surface_format={:?}", + prefilter_format, + renderer.get_surface_format() + ); + // Rebuild prefilter pipeline if first time or target format changed. + if self + .last_prefilter_format + .map(|f| f != prefilter_format) + .unwrap_or(false) + || self.prefilter_pipeline.is_none() + { + let pipeline = renderer.create_pipeline_v2(PipelineV2Desc { + label: Some("ibl_specular_prefilter_equirect"), + vs: ShaderDesc { + source: vs, + entry_func: "main", + }, + ps: ShaderDesc { + source: fs_prefilter, + entry_func: "main", + }, + vertex_buffers: &[], + bind_groups: vec![ + vec![ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + vec![wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + ], + targets: &[Some(wgpu::ColorTargetState { + format: prefilter_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + })?; + self.prefilter_pipeline = Some(pipeline); + self.last_prefilter_format = Some(prefilter_format); + log::info!( + "ibl_spec:init (re)built prefilter pipeline for {:?}", + prefilter_format + ); + } + + // Params buffer/bind group + let params_buf = renderer + .get_device() + .create_buffer(&wgpu::BufferDescriptor { + label: Some("ibl_spec_params"), + // WGSL/std140: struct { f32 + vec3 } occupies 32 bytes (vec3 rounds to 16 and struct size rounds up). + // Allocate 32 bytes to satisfy shader expectations even if our Rust struct packs to 16. + size: 32, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let params_bg = renderer + .get_device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("ibl_spec_params_bg"), + layout: &self.prefilter_pipeline.as_ref().unwrap().bind_group_layouts[1], + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: params_buf.as_entire_binding(), + }], + }); + self.params_buffer = Some(params_buf); + self.params_bind_group = Some(params_bg); + + // Env bind group (lazy, created in draw when env is guaranteed accessible) + self.env_bind_group = None; + + // BRDF LUT pipeline (separate simple pipeline directly with device API) + // Fragment target format must match the BRDF LUT render target texture format. + let pl = renderer + .get_device() + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("ibl_brdf_pl"), + bind_group_layouts: &[], + push_constant_ranges: &[], + }); + let vs_mod = renderer + .get_device() + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("ibl_brdf_vs"), + source: wgpu::ShaderSource::Wgsl(vs.into()), + }); + let fs_brdf = r#" + const PI: f32 = 3.14159265359; + fn radical_inverse_vdc(bits_in: u32) -> f32 { + var bits = bits_in; + bits = (bits << 16u) | (bits >> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); + bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); + bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); + return f32(bits) * 2.3283064365386963e-10; + } + fn hammersley(i: u32, n: u32) -> vec2 { + return vec2(f32(i) / f32(n), radical_inverse_vdc(i)); + } + fn geometry_schlick_ggx(n_dot_v: f32, roughness: f32) -> f32 { + let a = roughness * roughness; + let k = (a * a) / 2.0; + let denom = n_dot_v * (1.0 - k) + k; + return n_dot_v / denom; + } + fn geometry_smith(n: vec3, v: vec3, l: vec3, roughness: f32) -> f32 { + let n_dot_v = max(dot(n, v), 0.0); + let n_dot_l = max(dot(n, l), 0.0); + let ggx2 = geometry_schlick_ggx(n_dot_v, roughness); + let ggx1 = geometry_schlick_ggx(n_dot_l, roughness); + return ggx1 * ggx2; + } + fn importance_sample_ggx(xi: vec2, roughness: f32, n: vec3) -> vec3 { + let a = roughness * roughness; + let phi = 2.0 * PI * xi.x; + let cos_theta = sqrt((1.0 - xi.y) / (1.0 + (a*a - 1.0) * xi.y)); + let sin_theta = sqrt(1.0 - cos_theta*cos_theta); + let h_t = vec3(cos(phi) * sin_theta, sin(phi) * sin_theta, cos_theta); + let up = vec3(0.0, 1.0, 0.0); + let t = normalize(cross(up, n)); + let b = cross(n, t); + return normalize(t*h_t.x + b*h_t.y + n*h_t.z); + } + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let n = vec3(0.0, 0.0, 1.0); + let v = vec3(sqrt(1.0 - uv.x*uv.x), 0.0, uv.x); + var a = 0.0; + var b = 0.0; + let roughness = uv.y; + let samples: u32 = 512u; + for (var i: u32 = 0u; i < samples; i = i + 1u) { + let xi = hammersley(i, samples); + let h = importance_sample_ggx(xi, roughness, n); + let l = normalize(2.0 * dot(v, h) * h - v); + let n_dot_l = max(l.z, 0.0); + let n_dot_h = max(h.z, 0.0); + let v_dot_h = max(dot(v, h), 0.0); + if (n_dot_l > 0.0) { + let g = geometry_smith(n, v, l, roughness); + let g_vis = (g * v_dot_h) / max(n_dot_h * max(n.z, 0.0), 1e-4); + let fc = pow(1.0 - v_dot_h, 5.0); + a = a + (1.0 - fc) * g_vis; + b = b + fc * g_vis; + } + } + a = a / f32(samples); + b = b / f32(samples); + return vec4(a, b, 0.0, 1.0); + } + "#; + let fs_mod = renderer + .get_device() + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("ibl_brdf_fs"), + source: wgpu::ShaderSource::Wgsl(fs_brdf.into()), + }); + // Match BRDF pipeline color target to BRDF LUT texture format + let brdf_format = _resources + .gpu() + .textures + .get(self.brdf_lut_handle) + .map(|t| t.format) + .unwrap_or(wgpu::TextureFormat::Rgba16Float); + log::info!( + "ibl_spec:init brdf_lut_format={:?} surface_format={:?}", + brdf_format, + renderer.get_surface_format() + ); + if self + .last_brdf_format + .map(|f| f != brdf_format) + .unwrap_or(false) + || self.brdf_pipeline.is_none() + { + let rp = + renderer + .get_device() + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("ibl_brdf_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fs_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: brdf_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + self.brdf_pipeline = Some(rp); + self.last_brdf_format = Some(brdf_format); + log::info!( + "ibl_spec:init (re)built brdf pipeline for {:?}", + brdf_format + ); + } + + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut CommandEncoder, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + _frame: &wgpu::SurfaceTexture, + _view: &wgpu::TextureView, + _world: &WorldQuery, + ) -> Result<()> { + if self.done { + return Ok(()); + } + // Log formats used by this pass to quickly diagnose target/pass mismatches. + log::info!( + "ibl_spec:draw handles env=({},{}) pre=({},{}) brdf=({},{})", + self.env_texture.index(), + self.env_texture.generation(), + self.prefilter_handle.index(), + self.prefilter_handle.generation(), + self.brdf_lut_handle.index(), + self.brdf_lut_handle.generation() + ); + let surface_fmt = renderer.get_surface_format(); + let prefilter_fmt = resources + .gpu() + .textures + .get(self.prefilter_handle) + .expect("prefilter") + .format; + log::info!( + "ibl_spec:draw start surface_format={:?} prefilter_target_format={:?}", + surface_fmt, + prefilter_fmt + ); + // Optional guardrail: warn if someone accidentally attempts to render IBL into the swapchain. + if surface_fmt == prefilter_fmt { + log::warn!( + "ibl_spec: surface format equals prefilter format; ensure this pass renders to the HDR prefilter target, not the swapchain" + ); + } + // Defensive guard: if pipeline was built for a different format, skip to avoid panic. + if let Some(built_for) = self.last_prefilter_format { + if built_for != prefilter_fmt { + log::error!( + "ibl_spec: prefilter format changed (built_for={:?}, current={:?}); skipping pass to avoid wgpu validation error", + built_for, + prefilter_fmt + ); + return Ok(()); + } + } + // Lazy env bind group + if self.env_bind_group.is_none() { + if let Some(env) = resources.gpu().textures.get(self.env_texture) { + let device = renderer.get_device(); + let bg = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("ibl_spec_env_bg"), + layout: &self.prefilter_pipeline.as_ref().unwrap().bind_group_layouts[0], + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&env.texture_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&env.sampler), + }, + ], + }); + self.env_bind_group = Some(bg); + } else { + return Ok(()); + } + } + + // Prefilter: render each mip level with roughness + { + let queue = renderer.get_queue(); + let params_buf = self.params_buffer.as_ref().unwrap(); + let prefilter_tex = &resources + .gpu() + .textures + .get(self.prefilter_handle) + .expect("prefilter") + .texture; + // Validate target format again before encoding multiple passes + if let (Some(built_for), Some(current)) = + (self.last_prefilter_format, Some(prefilter_fmt)) + { + if built_for != current { + log::error!( + "ibl_spec:refusing to render prefilter; pipeline format={:?} current target={:?}", + built_for, + current + ); + self.done = true; + return Ok(()); + } + } + // If the current prefilter texture doesn't have a mipchain (common for swapchain-like targets), + // avoid rendering higher mips to prevent TextureView creation errors. + let effective_mip_count = if self.last_prefilter_format + == Some(wgpu::TextureFormat::Rgba16Float) + { + self.mip_count + } else { + log::warn!( + "ibl_spec: prefilter target format {:?} likely has no mipchain; limiting to mip 0", + prefilter_fmt + ); + 1 + }; + for mip in 0..effective_mip_count { + let roughness = (mip as f32) / ((self.mip_count - 1) as f32).max(1.0); + let params = PrefilterParams { + roughness, + _pad: [0.0, 0.0, 0.0], + }; + log::info!( + "ibl_spec:prefilter pass mip={} roughness={:.4} target_format={:?}", + mip, + roughness, + prefilter_fmt + ); + queue.write_buffer(params_buf, 0, bytemuck::bytes_of(¶ms)); + let view = prefilter_tex.create_view(&wgpu::TextureViewDescriptor { + label: Some(&format!("prefilter_mip{}", mip)), + format: None, + dimension: Some(wgpu::TextureViewDimension::D2), + aspect: wgpu::TextureAspect::All, + base_mip_level: mip, + mip_level_count: Some(1), + base_array_layer: 0, + array_layer_count: Some(1), + }); + // Begin a render pass whose color attachment view inherits the HDR texture format. + // This MUST match the pipeline fragment target format; otherwise wgpu validation will error at set_pipeline. + // See RenderPassDescriptor docs: + // https://docs.rs/wgpu/0.20.1/wgpu/struct.RenderPassDescriptor.html + let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&format!("{}_mip{}", self.label, mip)), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + rp.set_pipeline(&self.prefilter_pipeline.as_ref().unwrap().pipeline); + rp.set_bind_group(0, self.env_bind_group.as_ref().unwrap(), &[]); + rp.set_bind_group(1, self.params_bind_group.as_ref().unwrap(), &[]); + rp.draw(0..3, 0..1); + drop(rp); + } + } + + // BRDF LUT + { + let brdf_fmt = resources + .gpu() + .textures + .get(self.brdf_lut_handle) + .expect("brdf") + .format; + log::info!("ibl_spec:brdf_lut pass target_format={:?}", brdf_fmt); + if let Some(built_for) = self.last_brdf_format { + if built_for != brdf_fmt { + log::error!( + "ibl_spec: brdf lut format changed (built_for={:?}, current={:?}); skipping BRDF pass", + built_for, + brdf_fmt + ); + self.done = true; + return Ok(()); + } + } + let brdf_view = resources + .gpu() + .textures + .get(self.brdf_lut_handle) + .expect("brdf") + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("ibl_brdf_lut_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &brdf_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + rp.set_pipeline(self.brdf_pipeline.as_ref().unwrap()); + rp.draw(0..3, 0..1); + drop(rp); + } + + self.done = true; + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/pass_scene.rs b/engine/pill_engine/src/graphics/pass_scene.rs index 2494deb2..d7890432 100644 --- a/engine/pill_engine/src/graphics/pass_scene.rs +++ b/engine/pill_engine/src/graphics/pass_scene.rs @@ -135,6 +135,10 @@ pub struct PassScene { offscreen_color_texture: RendererTextureHandle, depth_texture: RendererTextureHandle, color_format: wgpu::TextureFormat, + load_existing_color: bool, + ibl_irradiance_tex: Option, + ibl_prefilter_tex: Option, + ibl_brdf_lut_tex: Option, // Per-frame dynamic UBO ring (Milestone 5) per_draw_stride: u64, per_draw_capacity: u64, @@ -149,8 +153,9 @@ pub struct PassScene { struct PassSceneState { camera_uniform: CameraUniform, camera_buffer: wgpu::Buffer, - camera_bind_group: wgpu::BindGroup, + globals_bind_group: wgpu::BindGroup, pipeline: PipelineV2, + ibl_sampler: wgpu::Sampler, // Per-draw resources per_draw_buffer: wgpu::Buffer, per_draw_bind_group: wgpu::BindGroup, @@ -162,12 +167,20 @@ impl PassScene { offscreen_color_texture: RendererTextureHandle, depth_texture: RendererTextureHandle, color_format: wgpu::TextureFormat, + load_existing_color: bool, + ibl_irradiance_tex: Option, + ibl_prefilter_tex: Option, + ibl_brdf_lut_tex: Option, ) -> Self { Self { label: label.to_string(), offscreen_color_texture, depth_texture, color_format, + load_existing_color, + ibl_irradiance_tex, + ibl_prefilter_tex, + ibl_brdf_lut_tex, per_draw_stride: PER_DRAW_STRIDE_BYTES as u64, per_draw_capacity: 0, visible_pre_draw_buffer: Vec::with_capacity(MAX_EXPECTED_PER_DRAW_INSTANCES), @@ -194,7 +207,7 @@ impl Pass for PassScene { fn init( &mut self, renderer: &mut dyn EnginePillRenderer, - _resources: &mut crate::resources::ResourceManager, + resources: &mut crate::resources::ResourceManager, ) -> Result<()> { // [SIMILAR] Prebuilt PSO used; avoid hot-path pipeline creation per TALK // Shaders: must match bind group layout indices: 0(camera),1(material textures),2(material params),3(per-draw) @@ -204,6 +217,9 @@ impl Pass for PassScene { viewProjection: mat4x4, }; @group(0) @binding(0) var UCamera: Camera; + // IBL irradiance moved into globals (set 0) + @group(0) @binding(1) var texIrradiance: texture_2d; + @group(0) @binding(2) var smpIrradiance: sampler; struct PerDraw { mvp: mat4x4, model: mat4x4, }; @group(3) @binding(0) var UPerDraw: PerDraw; struct VSIn { @@ -254,6 +270,13 @@ impl Pass for PassScene { viewProjection: mat4x4, }; @group(0) @binding(0) var UCamera: Camera; + // IBL resources in globals (set 0) + @group(0) @binding(1) var texIrradiance: texture_2d; + @group(0) @binding(2) var smpIrradiance: sampler; + @group(0) @binding(3) var texPrefilter: texture_2d; + @group(0) @binding(4) var smpPrefilter: sampler; + @group(0) @binding(5) var texBrdfLut: texture_2d; + @group(0) @binding(6) var smpBrdfLut: sampler; const PI: f32 = 3.14159265359; @@ -295,6 +318,16 @@ impl Pass for PassScene { fn fresnelSchlick(cosTheta: f32, F0: vec3) -> vec3 { return F0 + (vec3(1.0, 1.0, 1.0) - F0) * pow(1.0 - cosTheta, 5.0); } + fn fresnelSchlickRoughness(cosTheta: f32, F0: vec3, roughness: f32) -> vec3 { + return F0 + (max(vec3(1.0, 1.0, 1.0) * (1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0); + } + + fn dir_to_equirect_uv(dir: vec3) -> vec2 { + let d = normalize(dir); + let u = 0.5 + atan2(d.x, -d.z) / (2.0 * PI); + let v = 0.5 - asin(clamp(d.y, -1.0, 1.0)) / PI; + return vec2(fract(u), clamp(v, 0.0, 1.0)); + } // Directional light accumulator (white light scaled by intensity). fn accumulateDirLight( @@ -348,6 +381,22 @@ impl Pass for PassScene { Lo = Lo + accumulateDirLight(N, V, F0, albedo, roughness, metallic, LIGHT_DIR2, LIGHT_COL2); var color = Lo; + // Diffuse IBL ambient (equirect irradiance) + let kS = fresnelSchlick(max(dot(N, V), 0.0), F0); + let kD = (vec3(1.0, 1.0, 1.0) - kS) * (1.0 - metallic); + let uvIrr = dir_to_equirect_uv(N); + let irradiance = textureSample(texIrradiance, smpIrradiance, uvIrr).rgb; + let ambient_diffuse = kD * irradiance * albedo; + // Specular IBL + let R = reflect(-V, N); + let MAX_REFLECTION_LOD: f32 = 4.0; + let prefilteredColor = textureSampleLevel( + texPrefilter, smpPrefilter, dir_to_equirect_uv(R), roughness * MAX_REFLECTION_LOD + ).rgb; + let envBRDF = textureSample(texBrdfLut, smpBrdfLut, vec2(max(dot(N, V), 0.0), roughness)).rg; + let F = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); + let specular = prefilteredColor * (F * envBRDF.x + envBRDF.y); + color = color + ambient_diffuse + specular; // color = vec3(uv, 0.0); // DBG uv // color = vec3(N*0.5+0.5); // DBG Normal // color = vec3(WorldPos); // DBG WorldPos @@ -358,17 +407,69 @@ impl Pass for PassScene { // Describe bind group layouts for pipeline creation let bind_groups: Vec> = vec![ - // 0: camera - vec![wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, + // 0: globals (camera + IBL) + vec![ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, }, - count: None, - }], + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + // Prefiltered spec texture + sampler + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 4, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + // BRDF LUT texture + sampler + wgpu::BindGroupLayoutEntry { + binding: 5, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 6, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], // 1: material textures (PBR) vec![ wgpu::BindGroupLayoutEntry { @@ -473,6 +574,7 @@ impl Pass for PassScene { source: fragment_wgsl, entry_func: "main", }, + vertex_buffers: &[::data_layout_descriptor()], bind_groups, targets: &[Some(wgpu::ColorTargetState { format: self.color_format, @@ -492,24 +594,15 @@ impl Pass for PassScene { // Use renderer API to build pipeline let pipeline = renderer.create_pipeline_v2(desc)?; - // Camera buffer and bind group using pipeline's camera layout (group 0) - let (camera_buffer, camera_bind_group) = { + // Camera buffer (group 0 binding 0) + let camera_buffer = { let device = renderer.get_device(); - let camera_buffer = device.create_buffer(&wgpu::BufferDescriptor { + device.create_buffer(&wgpu::BufferDescriptor { label: Some("camera_buffer"), size: std::mem::size_of::() as u64, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, - }); - let camera_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("camera_bind_group"), - layout: &pipeline.bind_group_layouts[MATERIAL_BIND_GROUP_GLOBALS], - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: camera_buffer.as_entire_binding(), - }], - }); - (camera_buffer, camera_bind_group) + }) }; // Pre-create per-draw dynamic UBO ring and its bind group with large preallocation @@ -542,11 +635,117 @@ impl Pass for PassScene { }) }; + // IBL resources and globals bind group (group 0: camera + IBL) + let (globals_bind_group, ibl_sampler) = { + let device = renderer.get_device(); + // choose texture view: if missing, use default color + let tex_view_ref: &wgpu::TextureView = if let Some(h) = self.ibl_irradiance_tex { + &resources + .gpu() + .textures + .get(h) + .expect("ibl irradiance") + .texture_view + } else { + let (def_color_h, _) = { + let device_ptr: *const wgpu::Device = renderer.get_device(); + let queue_ptr: *const wgpu::Queue = renderer.get_queue(); + unsafe { + resources + .gpu_mut() + .ensure_default_textures(&*device_ptr, &*queue_ptr) + } + }; + &resources + .gpu() + .textures + .get(def_color_h) + .expect("default color") + .texture_view + }; + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::Repeat, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + // Prefilter + BRDF LUT views (optional) + let prefilter_view_opt: Option<&wgpu::TextureView> = self + .ibl_prefilter_tex + .and_then(|h| resources.gpu().textures.get(h).map(|t| &t.texture_view)); + let brdf_view_opt: Option<&wgpu::TextureView> = self + .ibl_brdf_lut_tex + .and_then(|h| resources.gpu().textures.get(h).map(|t| &t.texture_view)); + + let bg = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("globals_bind_group"), + layout: &pipeline.bind_group_layouts[MATERIAL_BIND_GROUP_GLOBALS as usize], + entries: &[ + // binding 0: camera buffer + wgpu::BindGroupEntry { + binding: 0, + resource: camera_buffer.as_entire_binding(), + }, + // binding 1: IBL texture + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(tex_view_ref), + }, + // binding 2: IBL sampler + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + // binding 3: Prefilter texture (or fallback to irradiance if missing) + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::TextureView( + prefilter_view_opt.unwrap_or(tex_view_ref), + ), + }, + // binding 4: Prefilter sampler (use mipmap-capable sampler) + wgpu::BindGroupEntry { + binding: 4, + resource: wgpu::BindingResource::Sampler(&device.create_sampler( + &wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::Repeat, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Linear, + lod_min_clamp: 0.0, + lod_max_clamp: 16.0, + ..Default::default() + }, + )), + }, + // binding 5: BRDF LUT texture (fallback to irradiance if missing) + wgpu::BindGroupEntry { + binding: 5, + resource: wgpu::BindingResource::TextureView( + brdf_view_opt.unwrap_or(tex_view_ref), + ), + }, + // binding 6: BRDF LUT sampler + wgpu::BindGroupEntry { + binding: 6, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + ], + }); + (bg, sampler) + }; + self.state = Some(PassSceneState { camera_uniform: CameraUniform::new(), camera_buffer, - camera_bind_group, + globals_bind_group, pipeline, + ibl_sampler, per_draw_buffer, per_draw_bind_group, }); @@ -733,27 +932,40 @@ impl Pass for PassScene { .get(self.offscreen_color_texture) .expect("offscreen color") .texture - .create_view(&wgpu::TextureViewDescriptor::default()); + .create_view(&wgpu::TextureViewDescriptor { + label: Some("scene_offscreen_color_view"), + ..Default::default() + }); let depth_view = resources .gpu() .textures .get(self.depth_texture) .expect("depth") .texture - .create_view(&wgpu::TextureViewDescriptor::default()); + .create_view(&wgpu::TextureViewDescriptor { + label: Some("scene_depth_view"), + ..Default::default() + }); let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("m6_offscreen_pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &color_view, resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: clear_color.x as f64, - g: clear_color.y as f64, - b: clear_color.z as f64, - a: 1.0, - }), - store: wgpu::StoreOp::Store, + ops: if self.load_existing_color { + wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + } + } else { + wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: clear_color.x as f64, + g: clear_color.y as f64, + b: clear_color.z as f64, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + } }, })], depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { @@ -773,7 +985,7 @@ impl Pass for PassScene { rpass.set_pipeline(unsafe { &*group.pipeline }); rpass.set_bind_group( MATERIAL_BIND_GROUP_GLOBALS as u32, - &state_ref.camera_bind_group, + &state_ref.globals_bind_group, &[], ); { diff --git a/engine/pill_engine/src/graphics/pass_skybox_equirect.rs b/engine/pill_engine/src/graphics/pass_skybox_equirect.rs new file mode 100644 index 00000000..3d21f81c --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_skybox_equirect.rs @@ -0,0 +1,395 @@ +use crate::graphics::renderer::{ + Pass, PillRenderer as EnginePillRenderer, PipelineV2, PipelineV2Desc, ShaderDesc, WorldQuery, +}; +use crate::graphics::RendererTextureHandle; +use anyhow::Result; +use glam::{Mat4, Quat, Vec3}; +use pill_core::PillSlotMapKey; +use wgpu::CommandEncoder; + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct CameraUniform { + position: [f32; 4], + view_projection_matrix: [[f32; 4]; 4], + inverse_projection_matrix: [[f32; 4]; 4], + inverse_view_matrix: [[f32; 4]; 4], +} + +impl CameraUniform { + fn new() -> Self { + Self { + position: [0.0; 4], + view_projection_matrix: Mat4::IDENTITY.to_cols_array_2d(), + inverse_projection_matrix: Mat4::IDENTITY.to_cols_array_2d(), + inverse_view_matrix: Mat4::IDENTITY.to_cols_array_2d(), + } + } +} + +pub struct PassSkyboxEquirect { + label: String, + offscreen_color_texture: RendererTextureHandle, + color_format: wgpu::TextureFormat, + env_texture: RendererTextureHandle, + + // GPU + camera_buffer: Option, + camera_bind_group: Option, + env_bind_group: Option, + pipeline: Option, + sampler_clamp: Option, +} + +impl PassSkyboxEquirect { + pub fn new( + label: &str, + offscreen_color_texture: RendererTextureHandle, + color_format: wgpu::TextureFormat, + env_texture: RendererTextureHandle, + ) -> Self { + Self { + label: label.to_string(), + offscreen_color_texture, + color_format, + env_texture, + camera_buffer: None, + camera_bind_group: None, + env_bind_group: None, + pipeline: None, + sampler_clamp: None, + } + } +} + +impl Pass for PassSkyboxEquirect { + fn get_label(&self) -> &str { + &self.label + } + + fn init( + &mut self, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { + // Log the formats used by the skybox pass. The pipeline target format must match the + // offscreen color texture's format. This offscreen render target typically matches the + // swapchain/surface format for later composition. + log::info!( + "skybox:init offscreen_color_format={:?} surface_format={:?}", + self.color_format, + renderer.get_surface_format() + ); + // Camera buffer + bind group + let camera_bgl = + renderer + .get_device() + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("skybox_camera_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + let camera_buffer = renderer + .get_device() + .create_buffer(&wgpu::BufferDescriptor { + label: Some("skybox_camera_ubo"), + size: std::mem::size_of::() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let camera_bind_group = + renderer + .get_device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("skybox_camera_bg"), + layout: &camera_bgl, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: camera_buffer.as_entire_binding(), + }], + }); + + // Env texture + sampler (clamp) + let env_bgl = + renderer + .get_device() + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("skybox_env_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + let sampler_clamp = renderer + .get_device() + .create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::Repeat, // wrap horizontally + address_mode_v: wgpu::AddressMode::ClampToEdge, // clamp vertically at poles + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + let env_tex = resources + .gpu() + .textures + .get(self.env_texture) + .expect("env texture"); + let env_bind_group = renderer + .get_device() + .create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("skybox_env_bg"), + layout: &env_bgl, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&env_tex.texture_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&sampler_clamp), + }, + ], + }); + + // Pipeline (fullscreen triangle) + let vs_src = r#" + struct VSOut { + @builtin(position) pos: vec4, + @location(0) uv: vec2, + }; + @vertex + fn main(@builtin(vertex_index) vid: u32) -> VSOut { + var out: VSOut; + var p: vec2; + switch (vid) { + case 0u: { p = vec2(-1.0, -1.0); } + case 1u: { p = vec2(-1.0, 3.0); } + default: { p = vec2( 3.0, -1.0); } + } + let uv = p * 0.5 + vec2(0.5, 0.5); + out.pos = vec4(p, 0.0, 1.0); + out.uv = vec2(uv.x, 1.0 - uv.y); + return out; + } + "#; + let ps_src = r#" + const PI: f32 = 3.141592653589793; + struct Camera { + position: vec4, + viewProjection: mat4x4, + invProjection: mat4x4, + invView: mat4x4, + }; + @group(0) @binding(0) var UCamera: Camera; + @group(1) @binding(0) var TEnv: texture_2d; + @group(1) @binding(1) var SEnv: sampler; + + fn dir_to_equirect_uv(dir: vec3) -> vec2 { + let d = normalize(dir); + // Match common equirectangular convention: forward (-Z) maps to u=0.5 + let u = 0.5 + atan2(d.x, -d.z) / (2.0 * PI); + let v = 0.5 - asin(clamp(d.y, -1.0, 1.0)) / PI; + return vec2(fract(u), clamp(v, 0.0, 1.0)); + } + + fn setCamera(camDir: vec3) -> mat3x3 { + let cw = normalize(camDir); + let cp = vec3(0.0, 1.0, 0.0); + let cu = normalize(cross(cw, cp)); + let cv = cross(cu, cw); + return mat3x3(cu, cv, cw); + } + + @fragment + fn main(@location(0) uv: vec2) -> @location(0) vec4 { + // Raylib-style ray reconstruction using camera basis and aspect-scaled screen coords + let camDir = normalize((UCamera.invView * vec4(0.0, 0.0, -1.0, 0.0)).xyz); + let ca = setCamera(camDir); + var nCoord = vec2(-1.0 + 2.0 * uv.x, 1.0 - 2.0 * uv.y); + let aspect = UCamera.invProjection[0][0] / UCamera.invProjection[1][1]; + nCoord.x *= aspect; + let fl = length(camDir); + let rd = ca * normalize(vec3(nCoord, fl)); + let st = dir_to_equirect_uv(rd); + let col = textureSample(TEnv, SEnv, st); + return vec4(col.rgb, 1.0); + } + "#; + // Build pipeline directly (no vertex buffers) + let device = renderer.get_device(); + let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("skybox_equirect_pl"), + bind_group_layouts: &[&camera_bgl, &env_bgl], + push_constant_ranges: &[], + }); + let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("skybox_equirect_v2_vs"), + source: wgpu::ShaderSource::Wgsl(vs_src.into()), + }); + let ps_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("skybox_equirect_v2_ps"), + source: wgpu::ShaderSource::Wgsl(ps_src.into()), + }); + let rp = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("skybox_equirect_v2_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], // no vertex inputs + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &ps_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: self.color_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + self.camera_buffer = Some(camera_buffer); + self.camera_bind_group = Some(camera_bind_group); + self.env_bind_group = Some(env_bind_group); + self.pipeline = Some(PipelineV2 { + pipeline: rp, + bind_group_layouts: vec![camera_bgl, env_bgl], + }); + self.sampler_clamp = Some(sampler_clamp); + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut CommandEncoder, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + _frame: &wgpu::SurfaceTexture, + _view: &wgpu::TextureView, + world: &WorldQuery, + ) -> Result<()> { + // Update camera UBO + let active_camera_entity_handle = world.active_camera; + let camera_storage = world + .camera_components + .data + .get(active_camera_entity_handle.data().index as usize) + .unwrap(); + let camera = camera_storage.as_ref().unwrap(); + let transform_storage = world + .transform_components + .data + .get(active_camera_entity_handle.data().index as usize) + .unwrap(); + let transform = transform_storage.as_ref().unwrap(); + // Compute view-projection from camera + transform + let eye = Vec3::new( + transform.position.x, + transform.position.y, + transform.position.z, + ); + let rot_rad = Vec3::new( + transform.rotation.y.to_radians(), + transform.rotation.x.to_radians(), + transform.rotation.z.to_radians(), + ); + let q = Quat::from_euler(glam::EulerRot::YXZ, rot_rad.x, rot_rad.y, rot_rad.z); + // Engine forward is -Z; use -Z so that camera forward maps to center of the equirect + let dir = q * -Vec3::Z; + let view = Mat4::look_to_rh(eye, dir, Vec3::Y); + const OPENGL_TO_WGPU_MATRIX: Mat4 = Mat4::from_cols_array(&[ + 1.0, 0.0, 0.0, 0.0, // + 0.0, 1.0, 0.0, 0.0, // + 0.0, 0.0, 0.5, 0.5, // + 0.0, 0.0, 0.0, 1.0, // + ]); + let fov_y = camera.fov.to_radians(); + let aspect = camera.aspect.get_value(); + let z_near = camera.range.start; + let z_far = camera.range.end; + let proj = OPENGL_TO_WGPU_MATRIX * Mat4::perspective_rh(fov_y, aspect, z_near, z_far); + let view_proj = proj * view; + let inv_proj = proj.inverse(); + let inv_view = view.inverse(); + let mut cam = CameraUniform::new(); + cam.position = [eye.x, eye.y, eye.z, 1.0]; + cam.view_projection_matrix = view_proj.to_cols_array_2d(); + cam.inverse_projection_matrix = inv_proj.to_cols_array_2d(); + cam.inverse_view_matrix = inv_view.to_cols_array_2d(); + renderer.get_queue().write_buffer( + self.camera_buffer.as_ref().unwrap(), + 0, + bytemuck::bytes_of(&cam), + ); + + // Begin render pass targeting offscreen color + let color_view = resources + .gpu() + .textures + .get(self.offscreen_color_texture) + .expect("offscreen color") + .texture + .create_view(&wgpu::TextureViewDescriptor { + label: Some("skybox_offscreen_color_view"), + ..Default::default() + }); + log::info!( + "skybox:draw begin offscreen_view target_format={:?}", + self.color_format + ); + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.label), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &color_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + // Bind and draw fullscreen triangle + rpass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); + rpass.set_bind_group(0, self.camera_bind_group.as_ref().unwrap(), &[]); + rpass.set_bind_group(1, self.env_bind_group.as_ref().unwrap(), &[]); + rpass.draw(0..3, 0..1); + // Explicitly end the pass so pass boundaries are clear in logs and ordering. + drop(rpass); + log::info!("skybox:draw completed"); + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/renderer.rs b/engine/pill_engine/src/graphics/renderer.rs index 1590781c..9e888bcf 100644 --- a/engine/pill_engine/src/graphics/renderer.rs +++ b/engine/pill_engine/src/graphics/renderer.rs @@ -51,6 +51,8 @@ pub struct PipelineV2Desc<'a> { pub label: Option<&'a str>, pub vs: ShaderDesc<'a>, pub ps: ShaderDesc<'a>, + // Vertex buffer layouts; empty slice means no vertex buffers (e.g., fullscreen triangle via vertex_index) + pub vertex_buffers: &'a [wgpu::VertexBufferLayout<'a>], pub bind_groups: Vec>, pub targets: &'a [Option], pub depth_stencil: Option, diff --git a/engine/pill_engine/src/renderer/resources/renderer_texture.rs b/engine/pill_engine/src/renderer/resources/renderer_texture.rs index 3b9640ed..e40c1304 100644 --- a/engine/pill_engine/src/renderer/resources/renderer_texture.rs +++ b/engine/pill_engine/src/renderer/resources/renderer_texture.rs @@ -11,6 +11,7 @@ pub struct RendererTexture { pub texture: wgpu::Texture, pub texture_view: wgpu::TextureView, pub sampler: wgpu::Sampler, + pub format: wgpu::TextureFormat, } impl RendererTexture { @@ -43,7 +44,9 @@ impl RendererTexture { sample_count: 1, dimension: wgpu::TextureDimension::D2, format, - usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + usage: wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_DST + | wgpu::TextureUsages::RENDER_ATTACHMENT, view_formats: &[], }); @@ -113,6 +116,7 @@ impl RendererTexture { texture, texture_view, sampler, + format, }) } @@ -154,6 +158,7 @@ impl RendererTexture { texture, texture_view, sampler, + format: Self::DEPTH_FORMAT, }) } @@ -195,6 +200,7 @@ impl RendererTexture { texture, texture_view, sampler, + format, }) } } diff --git a/engine/pill_engine/src/renderer/wgpu.rs b/engine/pill_engine/src/renderer/wgpu.rs index b62f65ab..726752c0 100644 --- a/engine/pill_engine/src/renderer/wgpu.rs +++ b/engine/pill_engine/src/renderer/wgpu.rs @@ -321,7 +321,11 @@ impl PillRenderer for Renderer { vertex: wgpu::VertexState { module: &vs, entry_point: desc.vs.entry_func, - buffers: &[RendererMesh::data_layout_descriptor()], + buffers: if desc.vertex_buffers.is_empty() { + &[] + } else { + desc.vertex_buffers + }, compilation_options: Default::default(), }, fragment: Some(wgpu::FragmentState { @@ -752,6 +756,7 @@ impl PillRenderer for Renderer { texture, texture_view, sampler, + format: desc.format, }); Ok(handle) } diff --git a/engine/pill_engine/src/resources/model.rs b/engine/pill_engine/src/resources/model.rs index 5d5c484f..f940c146 100644 --- a/engine/pill_engine/src/resources/model.rs +++ b/engine/pill_engine/src/resources/model.rs @@ -8,7 +8,7 @@ use crate::{ use anyhow::{anyhow, Context, Result}; use cgmath::InnerSpace; use gltf::import as gltf_import; -use image::{DynamicImage, ImageBuffer, ImageOutputFormat, Luma, Rgb, Rgba}; +use image::{DynamicImage, ImageBuffer, ImageFormat, Luma, Rgb, Rgba}; use log::info; use pill_core::{get_type_name, PillSlotMapKey, PillTypeMapKey, Vector3f}; use std::collections::HashMap; @@ -524,7 +524,7 @@ fn encode_gltf_image_to_png_bytes(data: &gltf::image::Data) -> Result> let mut buf = Vec::new(); { let mut writer = Cursor::new(&mut buf); - dyn_img.write_to(&mut writer, ImageOutputFormat::Png)?; + dyn_img.write_to(&mut writer, ImageFormat::Png)?; } Ok(buf.into_boxed_slice()) } diff --git a/engine/pill_engine/src/resources/texture.rs b/engine/pill_engine/src/resources/texture.rs index 36aa393d..33eddb1e 100644 --- a/engine/pill_engine/src/resources/texture.rs +++ b/engine/pill_engine/src/resources/texture.rs @@ -72,7 +72,10 @@ impl Resource for Texture { ResourceLoadType::Path(path) => { // Check if path to asset is correct let resource_file_path = engine.game_resources_directory_path.join(path); - pill_core::validate_asset_path(&resource_file_path, &["png", "jpg", "gif", "tif"])?; + pill_core::validate_asset_path( + &resource_file_path, + &["png", "jpg", "gif", "tif", "hdr"], + )?; // Load data image::open(&resource_file_path)? From c1d76364726becd64dd255316c24f015438b6129 Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Mon, 17 Nov 2025 20:22:27 +0100 Subject: [PATCH 66/69] feat: linear depth --- .../src/ecs/systems/rendering_system.rs | 17 +- engine/pill_engine/src/graphics/mod.rs | 2 + .../src/graphics/pass_linearize_depth.rs | 256 ++++++++++++++++++ .../src/graphics/pass_overlay_depth.rs | 8 +- .../src/graphics/pass_skybox_equirect.rs | 5 - 5 files changed, 278 insertions(+), 10 deletions(-) create mode 100644 engine/pill_engine/src/graphics/pass_linearize_depth.rs diff --git a/engine/pill_engine/src/ecs/systems/rendering_system.rs b/engine/pill_engine/src/ecs/systems/rendering_system.rs index 2b5af1c9..4b197643 100644 --- a/engine/pill_engine/src/ecs/systems/rendering_system.rs +++ b/engine/pill_engine/src/ecs/systems/rendering_system.rs @@ -89,6 +89,14 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { // Create depth and color texture let depth_texture = engine.renderer.create_depth_texture("depth_texture")?; + // Linear-depth render target for post-processing + let linear_depth_rt = engine.renderer.create_render_target(RendererTargetDesc { + name: "linear_depth".to_string(), + format: wgpu::TextureFormat::R16Float, + width: engine.window_size.width, + height: engine.window_size.height, + })?; + // Build passes as an array with elements, then convert to Vec // Load equirectangular HDR environment map (as 2D) let env_tex_handle = { @@ -239,6 +247,13 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { Some(ibl_prefilter_rt), Some(ibl_brdf_rt), )), + // Linearize scene depth into a color RT for later passes + Box::new(crate::graphics::PassLinearizeDepth::new( + "linearize_depth", + depth_texture, + linear_depth_rt, + wgpu::TextureFormat::R16Float, + )), Box::new(crate::graphics::PassCompose::new( "compose", offscreen_color_texture, @@ -254,7 +269,7 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { [0.75, 0.50, 0.95, 0.70], [1.0, 1.0, 1.0, 1.0], fmt, - depth_texture, + linear_depth_rt, )), Box::new(PassLogo::new( "overlay_logo", diff --git a/engine/pill_engine/src/graphics/mod.rs b/engine/pill_engine/src/graphics/mod.rs index 8b744ced..fbf15784 100644 --- a/engine/pill_engine/src/graphics/mod.rs +++ b/engine/pill_engine/src/graphics/mod.rs @@ -31,6 +31,8 @@ pub mod pass_overlay_uv; pub use pass_overlay_uv::PassOverlayUV; pub mod pass_overlay_depth; pub use pass_overlay_depth::PassOverlayDepth; +pub mod pass_linearize_depth; +pub use pass_linearize_depth::PassLinearizeDepth; pub mod pass_compose; pub use pass_compose::PassCompose; pub mod pass_scene; diff --git a/engine/pill_engine/src/graphics/pass_linearize_depth.rs b/engine/pill_engine/src/graphics/pass_linearize_depth.rs new file mode 100644 index 00000000..2ee384ca --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_linearize_depth.rs @@ -0,0 +1,256 @@ +use anyhow::Result; +use wgpu::CommandEncoder; + +use crate::graphics::renderer::{ + Pass, PillRenderer as EnginePillRenderer, PipelineV2, RendererTextureHandle, WorldQuery, +}; +use pill_core::PillSlotMapKey; + +pub struct PassLinearizeDepth { + label: String, + pipeline: Option, + bind_group_material: Option, + near_far_buffer: Option, + target_format: wgpu::TextureFormat, + // Inputs/outputs + depth_texture: RendererTextureHandle, + linear_depth_target: RendererTextureHandle, +} + +impl PassLinearizeDepth { + pub fn new( + label: &str, + depth_texture: RendererTextureHandle, + linear_depth_target: RendererTextureHandle, + target_format: wgpu::TextureFormat, + ) -> Self { + Self { + label: label.to_string(), + pipeline: None, + bind_group_material: None, + near_far_buffer: None, + target_format, + depth_texture, + linear_depth_target, + } + } +} + +impl Pass for PassLinearizeDepth { + fn get_label(&self) -> &str { + &self.label + } + + fn init( + &mut self, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { + let device = renderer.get_device(); + + // Fullscreen triangle (no vertex buffers) + let vs = r#" + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2, }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var pos = array, 3>( + vec2(-1.0, -3.0), + vec2( 3.0, 1.0), + vec2(-1.0, 1.0) + ); + var tuv = array, 3>( + vec2(0.0, 2.0), + vec2(2.0, 0.0), + vec2(0.0, 0.0) + ); + var o: VSOut; + o.pos = vec4(pos[vi], 0.0, 1.0); + o.uv = tuv[vi]; + return o; + } + "#; + + let fs = r#" + struct NearFar { + near: f32, + far: f32, + _pad: vec2, + }; + @group(0) @binding(0) var texDepth: texture_depth_2d; + @group(0) @binding(1) var UNearFar: NearFar; + + fn linearizeDepth(depth: f32, near: f32, far: f32) -> f32 { + // WebGPU 0..1 depth convention + return (near * far) / (far - depth * (far - near)); + } + + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let dims = textureDimensions(texDepth, 0u); + let coord = vec2(uv * vec2(dims)); + let d = textureLoad(texDepth, coord, 0); + let lin = linearizeDepth(d, UNearFar.near, UNearFar.far); + // Pack: X = non-linear depth, Y = linear depth, Z = normalized linear depth + return vec4(d, lin, lin / UNearFar.far, 1.0); + } + "#; + + // Bind group layout: depth texture + near/far UBO + let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("linearize_depth_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Depth, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + // 16B to satisfy alignment (near, far, pad) + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }, + ], + }); + let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("linearize_depth_pl"), + bind_group_layouts: &[&bgl], + push_constant_ranges: &[], + }); + + let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("linearize_depth_vs"), + source: wgpu::ShaderSource::Wgsl(vs.into()), + }); + let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("linearize_depth_fs"), + source: wgpu::ShaderSource::Wgsl(fs.into()), + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("linearize_depth_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fs_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: self.target_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + // Material bind group: depth view + UBO (initialized with defaults; updated per-frame) + let depth_view = resources + .gpu() + .textures + .get(self.depth_texture) + .expect("scene depth") + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let near_far_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("linearize_depth_near_far"), + size: 16, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let bind_group_material = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("linearize_depth_bg"), + layout: &bgl, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&depth_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: near_far_buffer.as_entire_binding(), + }, + ], + }); + + self.pipeline = Some(PipelineV2 { + pipeline, + bind_group_layouts: vec![bgl], + }); + self.bind_group_material = Some(bind_group_material); + self.near_far_buffer = Some(near_far_buffer); + + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut CommandEncoder, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + _frame: &wgpu::SurfaceTexture, + _view: &wgpu::TextureView, + world: &WorldQuery, + ) -> Result<()> { + // Update near/far from active camera + let active_camera_entity_handle = world.active_camera; + let camera_storage = world + .camera_components + .data + .get(active_camera_entity_handle.data().index as usize) + .unwrap(); + let cam = camera_storage.as_ref().unwrap(); + let near_far = [cam.range.start, cam.range.end, 0.0_f32, 0.0_f32]; + renderer.get_queue().write_buffer( + self.near_far_buffer.as_ref().unwrap(), + 0, + bytemuck::bytes_of(&near_far), + ); + + // Target view: write to the linear-depth render target + let out_view = resources + .gpu() + .textures + .get(self.linear_depth_target) + .expect("linear depth target") + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.label), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &out_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + pass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); + pass.set_bind_group(0, &self.bind_group_material.as_ref().unwrap(), &[]); + pass.draw(0..3, 0..1); + + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/pass_overlay_depth.rs b/engine/pill_engine/src/graphics/pass_overlay_depth.rs index 3a54fb77..2b0d9df8 100644 --- a/engine/pill_engine/src/graphics/pass_overlay_depth.rs +++ b/engine/pill_engine/src/graphics/pass_overlay_depth.rs @@ -85,13 +85,13 @@ impl Pass for PassOverlayDepth { "#; let fs = r#" - @group(0) @binding(0) var tex: texture_depth_2d; + @group(0) @binding(0) var tex: texture_2d; @group(0) @binding(1) var UTint: vec4; @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { let dims = textureDimensions(tex, 0u); let coord = vec2(uv * vec2(dims)); - let d = textureLoad(tex, coord, 0); - let vis = fract(1000.0*d); + let d : f32 = textureLoad(tex, coord, 0).r; + let vis = fract(100.0*d); return vec4(vis, vis, vis, 1.0) * UTint; } "#; @@ -106,7 +106,7 @@ impl Pass for PassOverlayDepth { ty: wgpu::BindingType::Texture { multisampled: false, view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Depth, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, }, count: None, }, diff --git a/engine/pill_engine/src/graphics/pass_skybox_equirect.rs b/engine/pill_engine/src/graphics/pass_skybox_equirect.rs index 3d21f81c..718990e7 100644 --- a/engine/pill_engine/src/graphics/pass_skybox_equirect.rs +++ b/engine/pill_engine/src/graphics/pass_skybox_equirect.rs @@ -364,10 +364,6 @@ impl Pass for PassSkyboxEquirect { label: Some("skybox_offscreen_color_view"), ..Default::default() }); - log::info!( - "skybox:draw begin offscreen_view target_format={:?}", - self.color_format - ); let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some(&self.label), color_attachments: &[Some(wgpu::RenderPassColorAttachment { @@ -389,7 +385,6 @@ impl Pass for PassSkyboxEquirect { rpass.draw(0..3, 0..1); // Explicitly end the pass so pass boundaries are clear in logs and ordering. drop(rpass); - log::info!("skybox:draw completed"); Ok(()) } } From b2aa66f7f32108d271860f152b4056007aaa2f3a Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Sun, 14 Dec 2025 01:03:39 +0100 Subject: [PATCH 67/69] feat: depth of field pass, fixed depth projection --- .../ecs/components/egui_manager_component.rs | 18 + .../ecs/components/render_state_component.rs | 19 +- engine/pill_engine/src/ecs/mod.rs | 1 + .../src/ecs/systems/rendering_system.rs | 47 +- engine/pill_engine/src/graphics/mod.rs | 4 + .../src/graphics/pass_dof_bokeh.rs | 436 ++++++++++++++++++ .../src/graphics/pass_linearize_depth.rs | 118 +++-- .../src/graphics/pass_overlay_depth.rs | 25 +- engine/pill_engine/src/graphics/pass_scene.rs | 13 +- .../src/graphics/pass_skybox_equirect.rs | 9 +- engine/pill_engine/src/graphics/projection.rs | 48 ++ .../src/resources/resource_manager.rs | 8 + 12 files changed, 671 insertions(+), 75 deletions(-) create mode 100644 engine/pill_engine/src/graphics/pass_dof_bokeh.rs create mode 100644 engine/pill_engine/src/graphics/projection.rs diff --git a/engine/pill_engine/src/ecs/components/egui_manager_component.rs b/engine/pill_engine/src/ecs/components/egui_manager_component.rs index af648100..3073d061 100644 --- a/engine/pill_engine/src/ecs/components/egui_manager_component.rs +++ b/engine/pill_engine/src/ecs/components/egui_manager_component.rs @@ -8,6 +8,7 @@ use crate::{ engine::Engine, }; +use crate::ecs::components::render_state_component::RenderStateComponent; use egui::Ui; use indexmap::IndexMap; use pill_core::{PillTypeMapKey, Timer, TimerRecord}; @@ -69,6 +70,11 @@ impl EguiManagerComponent { let frame_delta_time = engine.frame_delta_time; let window_w = engine.window_size.width; let window_h = engine.window_size.height; + let post_process = engine + .get_global_component::() + .ok() + .map(|rs| rs.post_process.clone()) + .unwrap_or_else(|| engine.resource_manager.post_process.clone()); // Snapshot draw call counter from last frame let total_draw_calls: Option = engine @@ -97,6 +103,18 @@ impl EguiManagerComponent { ui.label(format!("FPS {:.1}", 1000.0 / frame_delta_time)); ui.label(format!("{}x{}", window_w, window_h)); }); + // Focus controls + { + let mut pp = post_process.lock().unwrap(); + ui.add( + egui::Slider::new(&mut pp.focus_point, 0.1..=200.0) + .text("focusPoint (m)"), + ); + ui.add( + egui::Slider::new(&mut pp.focus_scale, 0.0..=50.0) + .text("focusScale"), + ); + } ui.add(egui::Label::new(format!( "Frame Delta Time: {:.4} ms", frame_delta_time diff --git a/engine/pill_engine/src/ecs/components/render_state_component.rs b/engine/pill_engine/src/ecs/components/render_state_component.rs index 76c35910..2b0488ec 100644 --- a/engine/pill_engine/src/ecs/components/render_state_component.rs +++ b/engine/pill_engine/src/ecs/components/render_state_component.rs @@ -1,15 +1,32 @@ use crate::ecs::{GlobalComponent, GlobalComponentStorage}; use crate::PillTypeMapKey; +use std::sync::{Arc, Mutex}; + +#[derive(Debug, Clone, Copy)] +pub struct PostProcessParams { + pub time_s: f32, // seconds since start + pub focus_point: f32, // meters + pub focus_scale: f32, +} // Keeps renderer/bootstrap state; minimal for now pub struct RenderStateComponent { pub boot_done: bool, pub egui_client: Option>, + pub post_process: Arc>, } impl RenderStateComponent { pub fn new() -> Self { - Self { boot_done: false, egui_client: None } + Self { + boot_done: false, + egui_client: None, + post_process: Arc::new(Mutex::new(PostProcessParams { + time_s: 0.0, + focus_point: 5.0, + focus_scale: 3.0, + })), + } } } diff --git a/engine/pill_engine/src/ecs/mod.rs b/engine/pill_engine/src/ecs/mod.rs index 4979cb0b..37c847dc 100644 --- a/engine/pill_engine/src/ecs/mod.rs +++ b/engine/pill_engine/src/ecs/mod.rs @@ -44,6 +44,7 @@ pub use components::mesh_rendering_component::MeshRenderingComponent; pub use components::time_component::TimeComponent; pub use components::render_state_component::RenderStateComponent; +pub use components::render_state_component::PostProcessParams; // - Systems diff --git a/engine/pill_engine/src/ecs/systems/rendering_system.rs b/engine/pill_engine/src/ecs/systems/rendering_system.rs index 4b197643..84c98621 100644 --- a/engine/pill_engine/src/ecs/systems/rendering_system.rs +++ b/engine/pill_engine/src/ecs/systems/rendering_system.rs @@ -25,6 +25,7 @@ use crate::config::{ MAX_MESHES, MAX_MODELS, MAX_SOUNDS, MAX_TEXTURES, }; use crate::ecs::components::render_state_component::RenderStateComponent; +use crate::ecs::TimeComponent; use crate::graphics::{Pass, PassCompose, PassLogo, PassOverlayDepth, PassOverlayUV}; use crate::resources::{Resource, ResourceLoadType, Texture, TextureHandle, TextureType}; @@ -67,10 +68,13 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { // Ensure EguiClient exists in global render state let egui_client = { + let post_process = engine.resource_manager.post_process.clone(); let rs = engine.get_global_component_mut::()?; if rs.egui_client.is_none() { rs.egui_client = Some(crate::ecs::EguiClient::new()); } + // Share post-process params between Engine render state and ResourceManager for passes. + rs.post_process = post_process; rs.egui_client.as_ref().unwrap().clone() }; @@ -81,7 +85,7 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { let offscreen_color_texture = engine.renderer.create_render_target(RendererTargetDesc { name: "offscreen_color".to_string(), - format: fmt, + format: wgpu::TextureFormat::Rgba16Float, // HDR for PBR and bokeh width: engine.window_size.width, height: engine.window_size.height, })?; @@ -89,14 +93,22 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { // Create depth and color texture let depth_texture = engine.renderer.create_depth_texture("depth_texture")?; - // Linear-depth render target for post-processing - let linear_depth_rt = engine.renderer.create_render_target(RendererTargetDesc { - name: "linear_depth".to_string(), + // Depth copy render target for depth-based effects + let depth_copy_rt = engine.renderer.create_render_target(RendererTargetDesc { + name: "depth_copy".to_string(), format: wgpu::TextureFormat::R16Float, width: engine.window_size.width, height: engine.window_size.height, })?; + // DOF output render target (HDR to preserve bokeh highlights) + let dof_output_texture = engine.renderer.create_render_target(RendererTargetDesc { + name: "dof_output".to_string(), + format: wgpu::TextureFormat::Rgba16Float, + width: engine.window_size.width, + height: engine.window_size.height, + })?; + // Build passes as an array with elements, then convert to Vec // Load equirectangular HDR environment map (as 2D) let env_tex_handle = { @@ -234,14 +246,14 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { Box::new(crate::graphics::PassSkyboxEquirect::new( "skybox_equirect", offscreen_color_texture, - fmt, + wgpu::TextureFormat::Rgba16Float, // HDR format env_tex_rt, )), Box::new(crate::graphics::pass_scene::PassScene::new( "scene", offscreen_color_texture, depth_texture, - fmt, + wgpu::TextureFormat::Rgba16Float, // HDR format true, // load offscreen color (skybox already cleared/drew background) Some(ibl_irradiance_rt), Some(ibl_prefilter_rt), @@ -249,14 +261,22 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { )), // Linearize scene depth into a color RT for later passes Box::new(crate::graphics::PassLinearizeDepth::new( - "linearize_depth", + "copy_depth", depth_texture, - linear_depth_rt, + depth_copy_rt, wgpu::TextureFormat::R16Float, )), + Box::new(crate::graphics::PassDofBokeh::new( + "dof_bokeh", + offscreen_color_texture, + depth_copy_rt, + dof_output_texture, + wgpu::TextureFormat::Rgba16Float, + )), + // Compose reads from DoF output texture Box::new(crate::graphics::PassCompose::new( "compose", - offscreen_color_texture, + dof_output_texture, fmt, )), Box::new(crate::graphics::PassOverlayUV::new( @@ -269,7 +289,7 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { [0.75, 0.50, 0.95, 0.70], [1.0, 1.0, 1.0, 1.0], fmt, - linear_depth_rt, + depth_texture, )), Box::new(PassLogo::new( "overlay_logo", @@ -299,6 +319,13 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { timer.begin_context("rendering_system update"); timer.record("Get active camera"); + // Update shared post-process params (time) for render passes + if let Ok(tc) = engine.get_global_component::() { + if let Ok(mut pp) = engine.resource_manager.post_process.lock() { + pp.time_s = tc.time; + } + } + let active_scene_handle = engine.scene_manager.get_active_scene_handle()?; let mut active_camera_entity_handle_result: Option = None; diff --git a/engine/pill_engine/src/graphics/mod.rs b/engine/pill_engine/src/graphics/mod.rs index fbf15784..2cdec4d8 100644 --- a/engine/pill_engine/src/graphics/mod.rs +++ b/engine/pill_engine/src/graphics/mod.rs @@ -37,7 +37,9 @@ pub mod pass_compose; pub use pass_compose::PassCompose; pub mod pass_scene; pub use pass_scene::PassScene; +pub mod pass_dof_bokeh; pub mod pass_egui; +pub use pass_dof_bokeh::PassDofBokeh; pub use pass_egui::PassEgui; pub mod pass_skybox_equirect; pub use pass_skybox_equirect::PassSkyboxEquirect; @@ -45,3 +47,5 @@ pub mod pass_ibl_diffuse_equirect; pub use pass_ibl_diffuse_equirect::PassIblDiffuseEquirect; pub mod pass_ibl_specular_equirect; pub use pass_ibl_specular_equirect::PassIblSpecularEquirect; + +pub mod projection; diff --git a/engine/pill_engine/src/graphics/pass_dof_bokeh.rs b/engine/pill_engine/src/graphics/pass_dof_bokeh.rs new file mode 100644 index 00000000..292ec4d9 --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_dof_bokeh.rs @@ -0,0 +1,436 @@ +//! Depth of Field (DoF) post-processing pass (single-pass bokeh, disk kernel) + +use crate::graphics::Pass; +use crate::graphics::PillRenderer; +use crate::graphics::RendererTextureHandle; +use crate::renderer::Renderer; +use crate::resources::ResourceManager; +use anyhow::Result; +use bytemuck::{Pod, Zeroable}; +use wgpu::util::DeviceExt; +use wgpu::TextureFormat; + +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct DofParams { + focus_distance: f32, + aperture: f32, + focal_length: f32, + sensor_size: f32, + time_s: f32, + focus_point: f32, + focus_scale: f32, +} + +pub struct PassDofBokeh { + pub name: String, + pub input_color: RendererTextureHandle, + pub input_depth: RendererTextureHandle, + pub output: RendererTextureHandle, + pub format: TextureFormat, + // Global DoF params (hardcoded for now) + pub focus_distance: f32, + pub aperture: f32, + pub focal_length: f32, + pub sensor_size: f32, + // Pipeline and bind group + pipeline: Option, + bind_group: Option, + params_buffer: Option, +} + +impl PassDofBokeh { + pub fn new( + name: &str, + input_color: RendererTextureHandle, + input_depth: RendererTextureHandle, + output: RendererTextureHandle, + format: TextureFormat, + ) -> Self { + Self { + name: name.to_string(), + input_color, + input_depth, + output, + format, + focus_distance: 5.0, // meters + aperture: 2.8, // f-stop + focal_length: 50.0, // mm + sensor_size: 36.0, // mm (full-frame) + pipeline: None, + bind_group: None, + params_buffer: None, + } + } + + // Optionally: methods to update params, toggle debug, etc. +} + +impl Pass for PassDofBokeh { + fn get_label(&self) -> &str { + &self.name + } + + fn init( + &mut self, + renderer: &mut dyn PillRenderer, + resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { + let device = renderer.get_device(); + eprintln!("[DoF] Initializing PassDofBokeh: {}", self.name); + // Only this inlined WGSL shader is used for DoF (vertex + fragment) + let vs = r#" + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var pos = array, 3>( + vec2(-1.0, -3.0), + vec2( 3.0, 1.0), + vec2(-1.0, 1.0) + ); + var tuv = array, 3>( + vec2(0.0, 2.0), + vec2(2.0, 0.0), + vec2(0.0, 0.0) + ); + var o: VSOut; + o.pos = vec4(pos[vi], 0.0, 1.0); + o.uv = tuv[vi]; + return o; + } + "#; + + // WGSL fragment shader for step-by-step DoF debug and effect + // Set debug mode here: 0 = pass-through, 1 = depth/CoC debug, 2 = DoF effect + // To change mode, edit the value below and rebuild. + let fs = r#" + struct Params { + focus_distance: f32, + aperture: f32, + focal_length: f32, + sensor_size: f32, + time_s: f32, + focus_point: f32, // meters + focus_scale: f32, + }; + @group(0) @binding(0) var t_color: texture_2d; + // Linear view-space depth in meters (R16Float) from PassLinearizeDepth + @group(0) @binding(1) var t_depth: texture_2d; + @group(0) @binding(2) var params: Params; + @group(0) @binding(3) var s_color: sampler; + + // NOTE: Split view for proof: + // - uv.y > 0.5 : debug visualization + // - uv.y <= 0.5: DoF result + const DEBUG_VIEW: i32 = 2; // 0=None, 1=pass-through, 2=abs(CoC), 3=linearDepthNormalized + + const GOLDEN_ANGLE: f32 = 2.39996323; + const MAX_BLUR_SIZE: f32 = 20.0; + const RAD_SCALE: f32 = 0.5; // Smaller = nicer blur, larger = faster + const FAR: f32 = 100.0; + const NEAR: f32 = 0.1; + // Artistic CoC remap: <1 boosts small CoC (bigger bokeh for far objects) without increasing MAX_BLUR_SIZE. + const COC_CURVE: f32 = 0.5; // sqrt + + fn load_linear_depth(uv: vec2, dims: vec2) -> f32 { + let coord0 = vec2(uv * vec2(dims)); + let maxc = vec2(i32(dims.x) - 1, i32(dims.y) - 1); + let coord = clamp(coord0, vec2(0, 0), maxc); + return textureLoad(t_depth, coord, 0).r; + } + + // Reference: + // - `https://web.archive.org/web/20201215123940/https://blog.tuxedolabs.com/2018/05/04/bokeh-depth-of-field-in-single-pass.html` + // - `https://github.com/bkaradzic/bgfx/tree/master/examples/45-bokeh`. + fn coc(depthLinear: f32, focusPoint: f32, focusScale: f32) -> f32 { + return clamp((1.0 / focusPoint - 1.0 / depthLinear) * focusScale, -1.0, 1.0); + } + + @fragment fn main( + @builtin(position) pos: vec4, + @location(0) uv: vec2 + ) -> @location(0) vec4 { + let dims = textureDimensions(t_depth, 0u); + let color4 = textureSample(t_color, s_color, uv); + let color = color4.rgb; + let center_depth = max(load_linear_depth(uv, dims), 1e-4); + let focusPointFromUi = max(params.focus_point, 1e-4); + let focusPoint = focusPointFromUi; + let coc = coc(center_depth, max(focusPoint, 1e-4), params.focus_scale); + let coc_abs = pow(abs(coc), COC_CURVE); + + // Proof view in bottom half (split by real screen-space Y, not UVs). + let y01 = pos.y / f32(dims.y); + if (y01 < 0.25 && DEBUG_VIEW > 0) { + if (DEBUG_VIEW == 1) { + return color4; + } else if (DEBUG_VIEW == 2) { + return vec4(coc_abs, coc_abs, coc_abs, 1.0); + } else if (DEBUG_VIEW == 3) { + let linear_depth = clamp(center_depth / FAR, 0.0, 1.0); + return vec4(linear_depth, linear_depth, linear_depth, 1.0); + } + } + + // DoF result in top half + let center_size = coc_abs * MAX_BLUR_SIZE; + let pixel_size = vec2(1.0 / f32(dims.x), 1.0 / f32(dims.y)); + + var acc = color; + var tot = 1.0; + var radius = RAD_SCALE; + var ang = 0.0; + loop { + if (radius >= MAX_BLUR_SIZE) { break; } + + let dir = vec2(cos(ang), sin(ang)); + let tc = uv + dir * pixel_size * radius; + let sample_color = textureSample(t_color, s_color, tc).rgb; + let sample_depth = max(load_linear_depth(tc, dims), 1e-4); + + let sample_coc = coc(sample_depth, max(focusPoint, 1e-4), params.focus_scale); + var sample_size = pow(abs(sample_coc), COC_CURVE) * MAX_BLUR_SIZE; + + // Prevent background blur bleeding over in-focus foreground (Voxagon clamp trick) + if (sample_depth > center_depth) { + sample_size = clamp(sample_size, 0.0, center_size * 2.0); + } + + let m = smoothstep(radius - 0.5, radius + 0.5, sample_size); + acc = acc + mix(acc / tot, sample_color, m); + tot = tot + 1.0; + + ang = ang + GOLDEN_ANGLE; + radius = radius + RAD_SCALE / radius; + } + + acc = acc / tot; + return vec4(acc, 1.0); + } + "#; + + // Bind group layout: color, depth, params + let bgl = match device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("dof_bokeh_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: false }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }) { + bgl => bgl, + }; + eprintln!("[DoF] Created bind group layout "); + + let pl = match device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("dof_bokeh_pl"), + bind_group_layouts: &[&bgl], + push_constant_ranges: &[], + }) { + pl => pl, + }; + eprintln!("[DoF] Created pipeline layout "); + + let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("dof_bokeh_vs"), + source: wgpu::ShaderSource::Wgsl(vs.into()), + }); + let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("dof_bokeh_fs"), + source: wgpu::ShaderSource::Wgsl(fs.into()), + }); + + let pipeline = match device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("dof_bokeh_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fs_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: self.format, // Back to offscreen format + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }) { + p => p, + }; + eprintln!("[DoF] Created render pipeline "); + + // Prepare uniform buffer for params (updated per-frame) + let params = DofParams { + focus_distance: self.focus_distance, + aperture: self.aperture, + focal_length: self.focal_length, + sensor_size: self.sensor_size, + time_s: 0.0, + focus_point: 5.0, + focus_scale: 3.0, + }; + let params_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("dof_bokeh_params"), + contents: bytemuck::bytes_of(¶ms), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + // Get texture views + let color_view = match resources.gpu().textures.get(self.input_color) { + Some(tex) => { + eprintln!("[DoF] Found input_color texture "); + tex.texture + .create_view(&wgpu::TextureViewDescriptor::default()) + } + None => { + eprintln!("[DoF][ERROR] input_color texture not found!"); + return Err(anyhow::anyhow!("input_color texture not found ")); + } + }; + let depth_view = match resources.gpu().textures.get(self.input_depth) { + Some(tex) => { + eprintln!("[DoF] Found input_depth texture "); + tex.texture + .create_view(&wgpu::TextureViewDescriptor::default()) + } + None => { + eprintln!("[DoF][ERROR] input_depth texture not found!"); + return Err(anyhow::anyhow!("input_depth texture not found ")); + } + }; + + let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); + eprintln!("[DoF] Created sampler "); + let bind_group = match device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("dof_bokeh_bind_group"), + layout: &bgl, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&color_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&depth_view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: params_buf.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + ], + }) { + bg => bg, + }; + eprintln!("[DoF] Created bind group "); + + self.pipeline = Some(crate::graphics::PipelineV2 { + pipeline, + bind_group_layouts: vec![bgl], + }); + self.bind_group = Some(bind_group); + self.params_buffer = Some(params_buf); + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut wgpu::CommandEncoder, + renderer: &mut dyn PillRenderer, + resources: &mut crate::resources::ResourceManager, + _frame: &wgpu::SurfaceTexture, + _view: &wgpu::TextureView, + _world: &crate::graphics::WorldQuery, + ) -> Result<()> { + // Update params from UI/shared state + if let Ok(pp) = resources.post_process.lock() { + let params = DofParams { + focus_distance: self.focus_distance, + aperture: self.aperture, + focal_length: self.focal_length, + sensor_size: self.sensor_size, + time_s: pp.time_s, + focus_point: pp.focus_point, + focus_scale: pp.focus_scale, + }; + renderer.get_queue().write_buffer( + self.params_buffer.as_ref().unwrap(), + 0, + bytemuck::bytes_of(¶ms), + ); + } + + // Render to output texture (which gets read by Compose pass) + let out_view = match resources.gpu().textures.get(self.output) { + Some(tex) => tex + .texture + .create_view(&wgpu::TextureViewDescriptor::default()), + None => { + eprintln!("[DoF][ERROR] output texture not found!"); + return Err(anyhow::anyhow!("output texture not found ")); + } + }; + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.name), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &out_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + pass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); + pass.set_bind_group(0, self.bind_group.as_ref().unwrap(), &[]); + pass.draw(0..3, 0..1); + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/pass_linearize_depth.rs b/engine/pill_engine/src/graphics/pass_linearize_depth.rs index 2ee384ca..02c1a14e 100644 --- a/engine/pill_engine/src/graphics/pass_linearize_depth.rs +++ b/engine/pill_engine/src/graphics/pass_linearize_depth.rs @@ -1,37 +1,48 @@ use anyhow::Result; use wgpu::CommandEncoder; +use crate::graphics::projection::{depth_unpack_consts_from_proj, perspective_rh_no}; use crate::graphics::renderer::{ Pass, PillRenderer as EnginePillRenderer, PipelineV2, RendererTextureHandle, WorldQuery, }; +use bytemuck::{Pod, Zeroable}; use pill_core::PillSlotMapKey; +use wgpu::util::DeviceExt; pub struct PassLinearizeDepth { label: String, pipeline: Option, bind_group_material: Option, - near_far_buffer: Option, + params_buffer: Option, target_format: wgpu::TextureFormat, // Inputs/outputs depth_texture: RendererTextureHandle, - linear_depth_target: RendererTextureHandle, + depth_copy_target: RendererTextureHandle, +} + +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct DepthUnpackParams { + depth_mul: f32, + depth_add: f32, + _pad: [f32; 2], } impl PassLinearizeDepth { pub fn new( label: &str, depth_texture: RendererTextureHandle, - linear_depth_target: RendererTextureHandle, + depth_copy_target: RendererTextureHandle, target_format: wgpu::TextureFormat, ) -> Self { Self { label: label.to_string(), pipeline: None, bind_group_material: None, - near_far_buffer: None, + params_buffer: None, target_format, depth_texture, - linear_depth_target, + depth_copy_target, } } } @@ -70,32 +81,38 @@ impl Pass for PassLinearizeDepth { "#; let fs = r#" - struct NearFar { - near: f32, - far: f32, - _pad: vec2, - }; @group(0) @binding(0) var texDepth: texture_depth_2d; - @group(0) @binding(1) var UNearFar: NearFar; + @group(0) @binding(1) var u_depthUnpack: vec4; // (mul, add, _, _) - fn linearizeDepth(depth: f32, near: f32, far: f32) -> f32 { - // WebGPU 0..1 depth convention - return (near * far) / (far - depth * (far - near)); + fn linearize_depth(screenDepth: f32) -> f32 { + // See projection.rs reference comment. + // linear = depthMul / (depthAdd - screenDepth) + let depthMul = u_depthUnpack.x; + let depthAdd = u_depthUnpack.y; + return depthMul / (depthAdd - screenDepth); } @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + // IMPORTANT: textureLoad requires in-bounds coords; fullscreen-triangle UVs can go slightly out of range, + // so clamp to [0..dims-1] to avoid undefined results (often "two flat values"). let dims = textureDimensions(texDepth, 0u); - let coord = vec2(uv * vec2(dims)); - let d = textureLoad(texDepth, coord, 0); - let lin = linearizeDepth(d, UNearFar.near, UNearFar.far); - // Pack: X = non-linear depth, Y = linear depth, Z = normalized linear depth - return vec4(d, lin, lin / UNearFar.far, 1.0); + let w = i32(dims.x); + let h = i32(dims.y); + let px = vec2(f32(dims.x), f32(dims.y)); + let uv_clamped = clamp(uv, vec2(0.0), vec2(1.0) - vec2(1.0) / px); + let coord_f = uv_clamped * px; + let cx = clamp(i32(coord_f.x), 0, w - 1); + let cy = clamp(i32(coord_f.y), 0, h - 1); + let raw = textureLoad(texDepth, vec2(cx, cy), 0); + let lin = linearize_depth(raw); + // Linear view-space depth in meters in R (render target is R16Float) + return vec4(lin, 0.0, 0.0, 1.0); } "#; - // Bind group layout: depth texture + near/far UBO + // Bind group layout: depth texture + depth unpack params let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("linearize_depth_bgl"), + label: Some("copy_depth_bgl"), entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, @@ -113,8 +130,12 @@ impl Pass for PassLinearizeDepth { ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, - // 16B to satisfy alignment (near, far, pad) - min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + min_binding_size: Some( + std::num::NonZeroU64::new( + std::mem::size_of::() as u64 + ) + .unwrap(), + ), }, count: None, }, @@ -160,7 +181,18 @@ impl Pass for PassLinearizeDepth { multiview: None, }); - // Material bind group: depth view + UBO (initialized with defaults; updated per-frame) + // Params buffer (updated per-frame from active camera projection) + let params_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("depth_unpack_params"), + contents: bytemuck::bytes_of(&DepthUnpackParams { + depth_mul: 0.0, + depth_add: 1.0, + _pad: [0.0; 2], + }), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + // Material bind group: depth view + params buffer let depth_view = resources .gpu() .textures @@ -168,14 +200,8 @@ impl Pass for PassLinearizeDepth { .expect("scene depth") .texture .create_view(&wgpu::TextureViewDescriptor::default()); - let near_far_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("linearize_depth_near_far"), - size: 16, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); let bind_group_material = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("linearize_depth_bg"), + label: Some("copy_depth_bg"), layout: &bgl, entries: &[ wgpu::BindGroupEntry { @@ -184,7 +210,7 @@ impl Pass for PassLinearizeDepth { }, wgpu::BindGroupEntry { binding: 1, - resource: near_far_buffer.as_entire_binding(), + resource: params_buf.as_entire_binding(), }, ], }); @@ -194,7 +220,7 @@ impl Pass for PassLinearizeDepth { bind_group_layouts: vec![bgl], }); self.bind_group_material = Some(bind_group_material); - self.near_far_buffer = Some(near_far_buffer); + self.params_buffer = Some(params_buf); Ok(()) } @@ -208,27 +234,35 @@ impl Pass for PassLinearizeDepth { _view: &wgpu::TextureView, world: &WorldQuery, ) -> Result<()> { - // Update near/far from active camera - let active_camera_entity_handle = world.active_camera; + // Depth unpack consts derived from a [-1..1] depth projection. let camera_storage = world .camera_components .data - .get(active_camera_entity_handle.data().index as usize) + .get(world.active_camera.data().index as usize) .unwrap(); let cam = camera_storage.as_ref().unwrap(); - let near_far = [cam.range.start, cam.range.end, 0.0_f32, 0.0_f32]; + let fov_y = cam.fov.to_radians(); + let aspect = cam.aspect.get_value(); + let z_near = cam.range.start; + let z_far = cam.range.end; + let proj2 = perspective_rh_no(fov_y, aspect, z_near, z_far); + let (depth_mul, depth_add) = depth_unpack_consts_from_proj(proj2); renderer.get_queue().write_buffer( - self.near_far_buffer.as_ref().unwrap(), + self.params_buffer.as_ref().unwrap(), 0, - bytemuck::bytes_of(&near_far), + bytemuck::bytes_of(&DepthUnpackParams { + depth_mul, + depth_add, + _pad: [0.0; 2], + }), ); - // Target view: write to the linear-depth render target + // Target view: write to the depth copy render target let out_view = resources .gpu() .textures - .get(self.linear_depth_target) - .expect("linear depth target") + .get(self.depth_copy_target) + .expect("depth copy target") .texture .create_view(&wgpu::TextureViewDescriptor::default()); diff --git a/engine/pill_engine/src/graphics/pass_overlay_depth.rs b/engine/pill_engine/src/graphics/pass_overlay_depth.rs index 2b0d9df8..1b32ea07 100644 --- a/engine/pill_engine/src/graphics/pass_overlay_depth.rs +++ b/engine/pill_engine/src/graphics/pass_overlay_depth.rs @@ -66,7 +66,7 @@ impl Pass for PassOverlayDepth { let vs = r#" @group(1) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] - + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { var unit = array, 6>( @@ -85,13 +85,28 @@ impl Pass for PassOverlayDepth { "#; let fs = r#" - @group(0) @binding(0) var tex: texture_2d; + @group(0) @binding(0) var tex: texture_depth_2d; @group(0) @binding(1) var UTint: vec4; + + // Hardcoded toggle (compile-time): false=raw depth (0..1), true=linear view-space depth normalized by FAR. + const SHOW_VIEWSPACE_DEPTH: bool = true; + const NEAR: f32 = 0.1; + const FAR: f32 = 100.0; + + fn linearize_depth(rawDepth: f32) -> f32 { + // See projection.rs reference comment. + let depthMul = (FAR * NEAR) / (FAR - NEAR); + let depthAdd = FAR / (FAR - NEAR); + return depthMul / (depthAdd - rawDepth); + } + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { let dims = textureDimensions(tex, 0u); let coord = vec2(uv * vec2(dims)); - let d : f32 = textureLoad(tex, coord, 0).r; - let vis = fract(100.0*d); + let d : f32 = textureLoad(tex, coord, 0); + + let linear_depth = linearize_depth(d) / FAR; + let vis = select(d, linear_depth, SHOW_VIEWSPACE_DEPTH); return vec4(vis, vis, vis, 1.0) * UTint; } "#; @@ -106,7 +121,7 @@ impl Pass for PassOverlayDepth { ty: wgpu::BindingType::Texture { multisampled: false, view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, + sample_type: wgpu::TextureSampleType::Depth, }, count: None, }, diff --git a/engine/pill_engine/src/graphics/pass_scene.rs b/engine/pill_engine/src/graphics/pass_scene.rs index d7890432..74418781 100644 --- a/engine/pill_engine/src/graphics/pass_scene.rs +++ b/engine/pill_engine/src/graphics/pass_scene.rs @@ -1,5 +1,6 @@ use crate::ecs::get_model_matrix; use crate::graphics::decompose_render_queue_key; +use crate::graphics::projection::perspective_rh_zo; use crate::graphics::renderer::{ Pass, PillRenderer as EnginePillRenderer, PipelineV2, PipelineV2Desc, ShaderDesc, WorldQuery, }; @@ -27,14 +28,6 @@ impl CameraUniform { } } - // Matches renderer CameraUniform math (OPENGL_TO_WGPU * perspective * view) - const OPENGL_TO_WGPU_MATRIX: Mat4 = Mat4::from_cols_array(&[ - 1.0, 0.0, 0.0, 0.0, // - 0.0, 1.0, 0.0, 0.0, // - 0.0, 0.0, 0.5, 0.5, // - 0.0, 0.0, 0.0, 1.0, // - ]); - fn update_data( &mut self, camera_component: &crate::ecs::CameraComponent, @@ -61,12 +54,12 @@ impl CameraUniform { let dir = q * Vec3::Z; let view = Mat4::look_to_rh(eye, dir, Vec3::Y); - // Projection matrix (with OpenGL->WGPU depth transform) + // Projection matrix (see projection.rs reference comment) let fov_y = camera_component.fov.to_radians(); let aspect = camera_component.aspect.get_value(); let z_near = camera_component.range.start; let z_far = camera_component.range.end; - let proj = Self::OPENGL_TO_WGPU_MATRIX * Mat4::perspective_rh(fov_y, aspect, z_near, z_far); + let proj = perspective_rh_zo(fov_y, aspect, z_near, z_far); self.view_projection_matrix = (proj * view).to_cols_array_2d(); } diff --git a/engine/pill_engine/src/graphics/pass_skybox_equirect.rs b/engine/pill_engine/src/graphics/pass_skybox_equirect.rs index 718990e7..d65c0e2d 100644 --- a/engine/pill_engine/src/graphics/pass_skybox_equirect.rs +++ b/engine/pill_engine/src/graphics/pass_skybox_equirect.rs @@ -1,3 +1,4 @@ +use crate::graphics::projection::perspective_rh_zo; use crate::graphics::renderer::{ Pass, PillRenderer as EnginePillRenderer, PipelineV2, PipelineV2Desc, ShaderDesc, WorldQuery, }; @@ -328,17 +329,11 @@ impl Pass for PassSkyboxEquirect { // Engine forward is -Z; use -Z so that camera forward maps to center of the equirect let dir = q * -Vec3::Z; let view = Mat4::look_to_rh(eye, dir, Vec3::Y); - const OPENGL_TO_WGPU_MATRIX: Mat4 = Mat4::from_cols_array(&[ - 1.0, 0.0, 0.0, 0.0, // - 0.0, 1.0, 0.0, 0.0, // - 0.0, 0.0, 0.5, 0.5, // - 0.0, 0.0, 0.0, 1.0, // - ]); let fov_y = camera.fov.to_radians(); let aspect = camera.aspect.get_value(); let z_near = camera.range.start; let z_far = camera.range.end; - let proj = OPENGL_TO_WGPU_MATRIX * Mat4::perspective_rh(fov_y, aspect, z_near, z_far); + let proj = perspective_rh_zo(fov_y, aspect, z_near, z_far); let view_proj = proj * view; let inv_proj = proj.inverse(); let inv_view = view.inverse(); diff --git a/engine/pill_engine/src/graphics/projection.rs b/engine/pill_engine/src/graphics/projection.rs new file mode 100644 index 00000000..13071b62 --- /dev/null +++ b/engine/pill_engine/src/graphics/projection.rs @@ -0,0 +1,48 @@ +use glam::Mat4; + +// Reference: Dennis Gustafsson DoF blog + bgfx example 45-bokeh. + +/// Right-handed perspective projection with NDC z in [0..1] (wgpu/Metal/Vulkan/D3D). +pub fn perspective_rh_zo(fov_y_radians: f32, aspect: f32, z_near: f32, z_far: f32) -> Mat4 { + let tan_half_fovy = (0.5 * fov_y_radians).tan(); + let ys = 1.0 / tan_half_fovy; + let xs = ys / aspect; + + let zz = z_far / (z_near - z_far); + let wz = (z_far * z_near) / (z_near - z_far); + + Mat4::from_cols_array(&[ + xs, 0.0, 0.0, 0.0, // + 0.0, ys, 0.0, 0.0, // + 0.0, 0.0, zz, -1.0, // + 0.0, 0.0, wz, 0.0, // + ]) +} + +/// Right-handed perspective projection with NDC z in [-1..1] (OpenGL). +pub fn perspective_rh_no(fov_y_radians: f32, aspect: f32, z_near: f32, z_far: f32) -> Mat4 { + let tan_half_fovy = (0.5 * fov_y_radians).tan(); + let ys = 1.0 / tan_half_fovy; + let xs = ys / aspect; + + let zz = (z_far + z_near) / (z_near - z_far); + let wz = (2.0 * z_far * z_near) / (z_near - z_far); + + Mat4::from_cols_array(&[ + xs, 0.0, 0.0, 0.0, // + 0.0, ys, 0.0, 0.0, // + 0.0, 0.0, zz, -1.0, // + 0.0, 0.0, wz, 0.0, // + ]) +} + +/// Depth-unpack constants derived from the projection matrix (mul=-m[14], add=m[10], with sign correction). +pub fn depth_unpack_consts_from_proj(proj: Mat4) -> (f32, f32) { + let p = proj.to_cols_array(); // column-major float[16] + let depth_mul = -p[3 * 4 + 2]; + let mut depth_add = p[2 * 4 + 2]; + if depth_mul * depth_add < 0.0 { + depth_add = -depth_add; + } + (depth_mul, depth_add) +} diff --git a/engine/pill_engine/src/resources/resource_manager.rs b/engine/pill_engine/src/resources/resource_manager.rs index c1b9242f..2ca247ae 100644 --- a/engine/pill_engine/src/resources/resource_manager.rs +++ b/engine/pill_engine/src/resources/resource_manager.rs @@ -8,13 +8,16 @@ use crate::{ use pill_core::{get_type_name, EngineError, PillSlotMapKey, PillTypeMap, PillTypeMapKey}; +use crate::ecs::PostProcessParams; use anyhow::{Context, Error, Result}; use boolinator::Boolinator; +use std::sync::{Arc, Mutex}; use std::{collections::HashMap, convert::TryInto, env, num::NonZeroU32, path::PathBuf}; pub struct ResourceManager { resources: PillTypeMap, pub gpu: crate::resources::GpuResources, + pub post_process: Arc>, } impl ResourceManager { @@ -22,6 +25,11 @@ impl ResourceManager { Self { resources: PillTypeMap::new(), gpu: crate::resources::GpuResources::new(), + post_process: Arc::new(Mutex::new(PostProcessParams { + time_s: 0.0, + focus_point: 5.0, + focus_scale: 3.0, + })), } } From 45009737dc5f6380ccff751a95c9bf578ac4938a Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Sun, 14 Dec 2025 03:54:03 +0100 Subject: [PATCH 68/69] feat: velocity buffer + per object motion blur --- .../ecs/components/egui_manager_component.rs | 20 + engine/pill_engine/src/ecs/components/mod.rs | 2 +- .../ecs/components/render_state_component.rs | 16 +- .../src/ecs/components/transform_component.rs | 10 + engine/pill_engine/src/ecs/mod.rs | 4 + .../src/ecs/systems/rendering_system.rs | 33 +- engine/pill_engine/src/graphics/mod.rs | 4 + .../src/graphics/pass_dof_bokeh.rs | 2 +- .../src/graphics/pass_motion_blur.rs | 416 ++++++++++++++++++ .../src/graphics/pass_overlay_velocity.rs | 334 ++++++++++++++ engine/pill_engine/src/graphics/pass_scene.rs | 143 ++++-- .../src/resources/resource_manager.rs | 10 +- 12 files changed, 949 insertions(+), 45 deletions(-) create mode 100644 engine/pill_engine/src/graphics/pass_motion_blur.rs create mode 100644 engine/pill_engine/src/graphics/pass_overlay_velocity.rs diff --git a/engine/pill_engine/src/ecs/components/egui_manager_component.rs b/engine/pill_engine/src/ecs/components/egui_manager_component.rs index 3073d061..521d3a4d 100644 --- a/engine/pill_engine/src/ecs/components/egui_manager_component.rs +++ b/engine/pill_engine/src/ecs/components/egui_manager_component.rs @@ -114,6 +114,26 @@ impl EguiManagerComponent { egui::Slider::new(&mut pp.focus_scale, 0.0..=50.0) .text("focusScale"), ); + ui.checkbox(&mut pp.mb_enabled, "Motion blur enabled"); + ui.checkbox(&mut pp.mb_debug_velocity, "MB debug overlay"); + ui.add( + egui::Slider::new(&mut pp.mb_strength, 0.0..=4.0) + .text("MB velocity scale"), + ); + ui.add( + egui::Slider::new(&mut pp.mb_min_speed, 0.0..=0.05) + .text("MB min speed (texels)"), + ); + ui.add( + egui::Slider::new(&mut pp.mb_depth_softness, 0.0..=200.0) + .text("MB depth softness"), + ); + let mut max_samples_i = pp.mb_max_samples as i32; + ui.add( + egui::Slider::new(&mut max_samples_i, 1..=64) + .text("MB max samples"), + ); + pp.mb_max_samples = max_samples_i as u32; } ui.add(egui::Label::new(format!( "Frame Delta Time: {:.4} ms", diff --git a/engine/pill_engine/src/ecs/components/mod.rs b/engine/pill_engine/src/ecs/components/mod.rs index 67703a95..63bc9ee2 100644 --- a/engine/pill_engine/src/ecs/components/mod.rs +++ b/engine/pill_engine/src/ecs/components/mod.rs @@ -12,7 +12,7 @@ pub(crate) mod input_component; pub(crate) mod mesh_rendering_component; pub(crate) mod render_state_component; pub(crate) mod time_component; -pub(crate) mod transform_component; +pub mod transform_component; // --- Use --- diff --git a/engine/pill_engine/src/ecs/components/render_state_component.rs b/engine/pill_engine/src/ecs/components/render_state_component.rs index 2b0488ec..6540d009 100644 --- a/engine/pill_engine/src/ecs/components/render_state_component.rs +++ b/engine/pill_engine/src/ecs/components/render_state_component.rs @@ -7,6 +7,12 @@ pub struct PostProcessParams { pub time_s: f32, // seconds since start pub focus_point: f32, // meters pub focus_scale: f32, + pub mb_enabled: bool, + pub mb_debug_velocity: bool, + pub mb_strength: f32, + pub mb_max_samples: u32, + pub mb_min_speed: f32, + pub mb_depth_softness: f32, } // Keeps renderer/bootstrap state; minimal for now @@ -23,8 +29,14 @@ impl RenderStateComponent { egui_client: None, post_process: Arc::new(Mutex::new(PostProcessParams { time_s: 0.0, - focus_point: 5.0, - focus_scale: 3.0, + focus_point: 0.0, + focus_scale: 0.0, + mb_enabled: true, + mb_debug_velocity: false, + mb_strength: 1.0, + mb_max_samples: 16, + mb_min_speed: 0.001, + mb_depth_softness: 1.0, })), } } diff --git a/engine/pill_engine/src/ecs/components/transform_component.rs b/engine/pill_engine/src/ecs/components/transform_component.rs index b0c1940c..f4e1a4e6 100644 --- a/engine/pill_engine/src/ecs/components/transform_component.rs +++ b/engine/pill_engine/src/ecs/components/transform_component.rs @@ -70,6 +70,8 @@ pub struct TransformComponent { model_matrix: [[f32; 4]; 4], normal_matrix: [[f32; 3]; 3], + #[serde(skip)] + prev_model_matrix: [[f32; 4]; 4], // There may me multiple updates of the position/rotation/scale in the single frame. // Not to calculate matrices multiple times, we will update them only once per frame @@ -90,6 +92,7 @@ impl TransformComponent { scale: Vector3f::new(1.0, 1.0, 1.0), model_matrix: cgmath::Matrix4::identity().into(), normal_matrix: cgmath::Matrix3::identity().into(), + prev_model_matrix: cgmath::Matrix4::identity().into(), matrix_update_required: true, net_dirty: false, } @@ -199,6 +202,9 @@ impl TransformComponent { pub fn update_transform_matrices(transform_component: &mut TransformComponent) { // SIMD path via glam: quat + SRT + // Capture last frame's model before computing the new one. + transform_component.prev_model_matrix = transform_component.model_matrix; + let pos = Vec3::new( transform_component.position.x, transform_component.position.y, @@ -234,6 +240,10 @@ pub fn get_model_matrix(transform_component: &TransformComponent) -> [[f32; 4]; transform_component.model_matrix } +pub fn get_prev_model_matrix(transform_component: &TransformComponent) -> [[f32; 4]; 4] { + transform_component.prev_model_matrix +} + pub fn get_normal_matrix(transform_component: &TransformComponent) -> [[f32; 3]; 3] { transform_component.normal_matrix } diff --git a/engine/pill_engine/src/ecs/mod.rs b/engine/pill_engine/src/ecs/mod.rs index 37c847dc..26f9a0b3 100644 --- a/engine/pill_engine/src/ecs/mod.rs +++ b/engine/pill_engine/src/ecs/mod.rs @@ -63,6 +63,10 @@ pub use systems::time_system::time_system; pub use systems::audio_system::audio_system; +pub fn get_prev_model_matrix(transform_component: &TransformComponent) -> [[f32; 4]; 4] { + components::transform_component::get_prev_model_matrix(transform_component) +} + // - Other pub use entity::{Entity, EntityBuilder, EntityHandle}; diff --git a/engine/pill_engine/src/ecs/systems/rendering_system.rs b/engine/pill_engine/src/ecs/systems/rendering_system.rs index 84c98621..05f3e6e9 100644 --- a/engine/pill_engine/src/ecs/systems/rendering_system.rs +++ b/engine/pill_engine/src/ecs/systems/rendering_system.rs @@ -89,6 +89,12 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { width: engine.window_size.width, height: engine.window_size.height, })?; + let velocity_texture = engine.renderer.create_render_target(RendererTargetDesc { + name: "velocity_buffer".to_string(), + format: wgpu::TextureFormat::Rg16Float, + width: engine.window_size.width, + height: engine.window_size.height, + })?; // Create depth and color texture let depth_texture = engine.renderer.create_depth_texture("depth_texture")?; @@ -108,6 +114,12 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { width: engine.window_size.width, height: engine.window_size.height, })?; + let motion_blur_output = engine.renderer.create_render_target(RendererTargetDesc { + name: "motion_blur_output".to_string(), + format: wgpu::TextureFormat::Rgba16Float, + width: engine.window_size.width, + height: engine.window_size.height, + })?; // Build passes as an array with elements, then convert to Vec // Load equirectangular HDR environment map (as 2D) @@ -252,8 +264,10 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { Box::new(crate::graphics::pass_scene::PassScene::new( "scene", offscreen_color_texture, + velocity_texture, depth_texture, wgpu::TextureFormat::Rgba16Float, // HDR format + wgpu::TextureFormat::Rg16Float, // velocity format true, // load offscreen color (skybox already cleared/drew background) Some(ibl_irradiance_rt), Some(ibl_prefilter_rt), @@ -273,10 +287,18 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { dof_output_texture, wgpu::TextureFormat::Rgba16Float, )), - // Compose reads from DoF output texture + Box::new(crate::graphics::PassMotionBlur::new( + "motion_blur", + dof_output_texture, + velocity_texture, + depth_copy_rt, + motion_blur_output, + wgpu::TextureFormat::Rgba16Float, + )), + // Compose reads from motion blur output texture Box::new(crate::graphics::PassCompose::new( "compose", - dof_output_texture, + motion_blur_output, fmt, )), Box::new(crate::graphics::PassOverlayUV::new( @@ -284,6 +306,13 @@ pub fn rendering_system(engine: &mut Engine) -> Result<()> { [0.75, 0.75, 0.95, 0.95], fmt, )), + Box::new(crate::graphics::PassOverlayVelocity::new( + "overlay_velocity", + [0.75, 0.25, 0.95, 0.45], + [2.0, 0.0, 16.0, 2.0], // scale, mode, spacing_px, thickness_px + fmt, + velocity_texture, + )), Box::new(crate::graphics::PassOverlayDepth::new( "overlay_depth", [0.75, 0.50, 0.95, 0.70], diff --git a/engine/pill_engine/src/graphics/mod.rs b/engine/pill_engine/src/graphics/mod.rs index 2cdec4d8..23b60393 100644 --- a/engine/pill_engine/src/graphics/mod.rs +++ b/engine/pill_engine/src/graphics/mod.rs @@ -31,6 +31,8 @@ pub mod pass_overlay_uv; pub use pass_overlay_uv::PassOverlayUV; pub mod pass_overlay_depth; pub use pass_overlay_depth::PassOverlayDepth; +pub mod pass_overlay_velocity; +pub use pass_overlay_velocity::PassOverlayVelocity; pub mod pass_linearize_depth; pub use pass_linearize_depth::PassLinearizeDepth; pub mod pass_compose; @@ -41,6 +43,8 @@ pub mod pass_dof_bokeh; pub mod pass_egui; pub use pass_dof_bokeh::PassDofBokeh; pub use pass_egui::PassEgui; +pub mod pass_motion_blur; +pub use pass_motion_blur::PassMotionBlur; pub mod pass_skybox_equirect; pub use pass_skybox_equirect::PassSkyboxEquirect; pub mod pass_ibl_diffuse_equirect; diff --git a/engine/pill_engine/src/graphics/pass_dof_bokeh.rs b/engine/pill_engine/src/graphics/pass_dof_bokeh.rs index 292ec4d9..893ac5aa 100644 --- a/engine/pill_engine/src/graphics/pass_dof_bokeh.rs +++ b/engine/pill_engine/src/graphics/pass_dof_bokeh.rs @@ -121,7 +121,7 @@ impl Pass for PassDofBokeh { // NOTE: Split view for proof: // - uv.y > 0.5 : debug visualization // - uv.y <= 0.5: DoF result - const DEBUG_VIEW: i32 = 2; // 0=None, 1=pass-through, 2=abs(CoC), 3=linearDepthNormalized + const DEBUG_VIEW: i32 = 0; // 0=None, 1=pass-through, 2=abs(CoC), 3=linearDepthNormalized const GOLDEN_ANGLE: f32 = 2.39996323; const MAX_BLUR_SIZE: f32 = 20.0; diff --git a/engine/pill_engine/src/graphics/pass_motion_blur.rs b/engine/pill_engine/src/graphics/pass_motion_blur.rs new file mode 100644 index 00000000..2a7af30d --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_motion_blur.rs @@ -0,0 +1,416 @@ +use crate::graphics::Pass; +use crate::graphics::PillRenderer; +use crate::graphics::RendererTextureHandle; +use crate::resources::ResourceManager; +use anyhow::Result; +use bytemuck::{Pod, Zeroable}; +use wgpu::util::DeviceExt; +use wgpu::TextureFormat; + +#[repr(C)] +#[derive(Clone, Copy, Pod, Zeroable)] +struct MotionBlurParams { + velocity_scale: f32, + min_speed: f32, + depth_softness: f32, + enabled: u32, + max_samples: u32, + debug_velocity: u32, + _pad: [u32; 2], +} + +pub struct PassMotionBlur { + pub name: String, + pub input_color: RendererTextureHandle, + pub input_velocity: RendererTextureHandle, + pub input_depth: RendererTextureHandle, + pub output: RendererTextureHandle, + pub format: TextureFormat, + pipeline: Option, + bind_group: Option, + params_buffer: Option, +} + +impl PassMotionBlur { + pub fn new( + name: &str, + input_color: RendererTextureHandle, + input_velocity: RendererTextureHandle, + input_depth: RendererTextureHandle, + output: RendererTextureHandle, + format: TextureFormat, + ) -> Self { + Self { + name: name.to_string(), + input_color, + input_velocity, + input_depth, + output, + format, + pipeline: None, + bind_group: None, + params_buffer: None, + } + } +} + +impl Pass for PassMotionBlur { + fn get_label(&self) -> &str { + &self.name + } + + fn init( + &mut self, + renderer: &mut dyn PillRenderer, + resources: &mut ResourceManager, + ) -> Result<()> { + let device = renderer.get_device(); + let vs = r#" + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var pos = array, 3>( + vec2(-1.0, -3.0), + vec2( 3.0, 1.0), + vec2(-1.0, 1.0) + ); + var tuv = array, 3>( + vec2(0.0, 2.0), + vec2(2.0, 0.0), + vec2(0.0, 0.0) + ); + var o: VSOut; + o.pos = vec4(pos[vi], 0.0, 1.0); + o.uv = tuv[vi]; + return o; + } + "#; + + let fs = r#" + struct Params { + velocity_scale: f32, + min_speed: f32, + depth_softness: f32, + enabled: u32, + max_samples: u32, + debug_velocity: u32, + _pad: vec2, + }; + + @group(0) @binding(0) var t_color: texture_2d; + @group(0) @binding(1) var t_velocity: texture_2d; + @group(0) @binding(2) var t_depth: texture_2d; // linear depth + @group(0) @binding(3) var params: Params; + @group(0) @binding(4) var s_linear: sampler; + + const MAX_SAMPLES: i32 = 64; + + fn sample_depth(uv: vec2) -> f32 { + let dims = textureDimensions(t_depth, 0u); + let coord = clamp(vec2(uv * vec2(dims)), vec2(0,0), vec2(i32(dims.x)-1, i32(dims.y)-1)); + return textureLoad(t_depth, coord, 0).r; + } + + const ARROW_COLOR: vec3 = vec3(1.0, 1.0, 0.2); + + // xyz = arrow color, w = mask (coverage) + fn compute_arrow_overlay( + uv: vec2, + spacing_px: f32, + thickness_px: f32, + scale: f32, + dims: vec2 + ) -> vec4 { + let spacing = spacing_px / vec2(f32(dims.x), f32(dims.y)); + let cell = floor(uv / spacing); + let cell_center = (cell + vec2(0.5, 0.5)) * spacing; + let v_sample = textureSample(t_velocity, s_linear, cell_center).xy * scale; + let v_len = length(v_sample); + if (v_len <= 1e-4) { + return vec4(ARROW_COLOR, 0.0); + } + + let dir = v_sample / v_len; + let tangent = vec2(-dir.y, dir.x); + let rel = uv - cell_center; + let proj = dot(rel, dir); + let ortho = dot(rel, tangent); + let arrow_len = clamp(v_len * 0.5, spacing.x * 0.25, spacing.x * 1.5); + let half_thick = (thickness_px / vec2(f32(dims.x), f32(dims.y))).x; + let line_mask = smoothstep(half_thick, 0.0, abs(ortho)) * step(-spacing.x, proj) * step(proj, arrow_len); + let head_size = arrow_len * 0.2; + let head_mask = step(arrow_len - head_size, proj) * smoothstep(half_thick * 2.0, 0.0, abs(ortho + (proj - arrow_len) * 0.5)); + let mask = clamp(max(line_mask, head_mask), 0.0, 1.0); + return vec4(ARROW_COLOR, mask); + } + + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let dims = textureDimensions(t_color, 0u); + let texel = 1.0 / vec2(f32(dims.x), f32(dims.y)); + let spacing_px = 24.0; + let thickness_px = 1.25; + + var vel = textureSample(t_velocity, s_linear, uv).xy * params.velocity_scale; + let center_speed = length(vel / texel); + let enabled = params.enabled != 0u; + if (!enabled || center_speed < params.min_speed) { + let base = textureSample(t_color, s_linear, uv).rgb; + if (params.debug_velocity != 0u) { + let dbg = vec3(0.5, 0.5, 0.5) + vec3(vel, 0.0) * 0.5; + let overlay = compute_arrow_overlay(uv, spacing_px, thickness_px, params.velocity_scale, dims); + let out_rgb = mix(dbg, overlay.xyz, overlay.w); + return vec4(out_rgb, 1.0); + } + return vec4(base, 1.0); + } + + let center_depth = sample_depth(uv); + let maxSamples = min(i32(params.max_samples), MAX_SAMPLES); + let mut_samples = max(1, maxSamples); + let nSamples = clamp(i32(center_speed), 1, mut_samples); + + var acc = textureSample(t_color, s_linear, uv).rgb; + var w_acc = 1.0; + // guard for single sample + if (nSamples > 1) { + for (var i: i32 = 1; i < nSamples; i = i + 1) { + let t = (f32(i) / f32(nSamples - 1)) - 0.5; + let offset = vel * t; + let sample_uv = uv + offset; + let sample_color = textureSample(t_color, s_linear, sample_uv).rgb; + let sample_depth = sample_depth(sample_uv); + let dz = abs(sample_depth - center_depth); + let depth_w = 1.0 / (1.0 + dz * params.depth_softness); + acc = acc + sample_color * depth_w; + w_acc = w_acc + depth_w; + } + } + var color_out = acc / w_acc; + if (params.debug_velocity != 0u) { + let overlay = compute_arrow_overlay(uv, spacing_px, thickness_px, params.velocity_scale, dims); + let out_rgb = mix(color_out, overlay.xyz, overlay.w); + return vec4(out_rgb, 1.0); + } + return vec4(color_out, 1.0); + } + "#; + + let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("motion_blur_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: false }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 4, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + + let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("motion_blur_pl"), + bind_group_layouts: &[&bgl], + push_constant_ranges: &[], + }); + + let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("motion_blur_vs"), + source: wgpu::ShaderSource::Wgsl(vs.into()), + }); + let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("motion_blur_fs"), + source: wgpu::ShaderSource::Wgsl(fs.into()), + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("motion_blur_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fs_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: self.format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + let params = MotionBlurParams { + velocity_scale: 1.0, + min_speed: 0.001, + depth_softness: 50.0, + enabled: 1, + max_samples: 16, + debug_velocity: 0, + _pad: [0, 0], + }; + let params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("motion_blur_params"), + contents: bytemuck::bytes_of(¶ms), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + let color_view = resources + .gpu() + .textures + .get(self.input_color) + .expect("motion blur input color") + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let velocity_view = resources + .gpu() + .textures + .get(self.input_velocity) + .expect("motion blur velocity") + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let depth_view = resources + .gpu() + .textures + .get(self.input_depth) + .expect("motion blur depth") + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("motion_blur_bg"), + layout: &bgl, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&color_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&velocity_view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::TextureView(&depth_view), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: params_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 4, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + ], + }); + + self.pipeline = Some(crate::graphics::PipelineV2 { + pipeline, + bind_group_layouts: vec![bgl], + }); + self.bind_group = Some(bind_group); + self.params_buffer = Some(params_buffer); + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut wgpu::CommandEncoder, + renderer: &mut dyn PillRenderer, + resources: &mut crate::resources::ResourceManager, + _frame: &wgpu::SurfaceTexture, + _view: &wgpu::TextureView, + _world: &crate::graphics::WorldQuery, + ) -> Result<()> { + if let Ok(pp) = resources.post_process.lock() { + let params = MotionBlurParams { + velocity_scale: pp.mb_strength, + min_speed: pp.mb_min_speed, + depth_softness: pp.mb_depth_softness, + enabled: if pp.mb_enabled { 1 } else { 0 }, + max_samples: pp.mb_max_samples, + debug_velocity: if pp.mb_debug_velocity { 1 } else { 0 }, + _pad: [0, 0], + }; + renderer.get_queue().write_buffer( + self.params_buffer.as_ref().unwrap(), + 0, + bytemuck::bytes_of(¶ms), + ); + } + + let out_view = resources + .gpu() + .textures + .get(self.output) + .expect("motion blur output") + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.name), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &out_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + pass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); + pass.set_bind_group(0, self.bind_group.as_ref().unwrap(), &[]); + pass.draw(0..3, 0..1); + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/pass_overlay_velocity.rs b/engine/pill_engine/src/graphics/pass_overlay_velocity.rs new file mode 100644 index 00000000..4ff12bf4 --- /dev/null +++ b/engine/pill_engine/src/graphics/pass_overlay_velocity.rs @@ -0,0 +1,334 @@ +use anyhow::Result; +use wgpu::CommandEncoder; + +use crate::graphics::renderer::{ + Pass, PillRenderer as EnginePillRenderer, PipelineV2, RendererTextureHandle, WorldQuery, +}; + +pub struct PassOverlayVelocity { + label: String, + pipeline: Option, + bind_group_rect: Option, + bind_group_material: Option, + params_buffer: Option, + rect: [f32; 4], + params: [f32; 4], // x: scale, y: mode (0=signed rgb, 1=magnitude), z: spacing_px, w: thickness_px + target_format: wgpu::TextureFormat, + velocity_texture: RendererTextureHandle, +} + +impl PassOverlayVelocity { + pub fn new( + label: &str, + rect: [f32; 4], + params: [f32; 4], + target_format: wgpu::TextureFormat, + velocity_texture: RendererTextureHandle, + ) -> Self { + Self { + label: label.to_string(), + pipeline: None, + bind_group_rect: None, + bind_group_material: None, + params_buffer: None, + rect, + params, + target_format, + velocity_texture, + } + } +} + +impl Pass for PassOverlayVelocity { + fn get_label(&self) -> &str { + &self.label + } + + fn init( + &mut self, + renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + ) -> Result<()> { + let device = renderer.get_device(); + // Rect UBO + let rect_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_velocity_rect_ubo"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM, + mapped_at_creation: true, + }); + { + let mut view = rect_buffer.slice(..).get_mapped_range_mut(); + let src = bytemuck::bytes_of(&self.rect); + view[..src.len()].copy_from_slice(src); + } + rect_buffer.unmap(); + + // Params UBO + let params_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("overlay_velocity_params"), + size: 256, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: true, + }); + { + let mut view = params_buffer.slice(..).get_mapped_range_mut(); + let src = bytemuck::bytes_of(&self.params); + view[..src.len()].copy_from_slice(src); + } + params_buffer.unmap(); + + let vs = r#" + @group(1) @binding(0) var URect: vec4; // bottom-left, top-right in [0,1] + + struct VSOut { @builtin(position) pos: vec4, @location(0) uv: vec2 }; + @vertex fn main(@builtin(vertex_index) vi: u32) -> VSOut { + var unit = array, 6>( + vec2(-1.0, -1.0), vec2( 1.0, -1.0), vec2( 1.0, 1.0), + vec2( 1.0, 1.0), vec2(-1.0, 1.0), vec2(-1.0, -1.0) + ); + let p = unit[vi]; // [-1,1] NDC space + let s = p*0.5+0.5; // [0,1] screen space + let r = URect.xy + s * (URect.zw - URect.xy); // move by rect [0,1] + let ndc = r*2.0-1.0; // [-1,1] NDC space + var out: VSOut; + out.pos = vec4(ndc.x, ndc.y, 0.0, 1.0); + out.uv = vec2(s.x, 1.0 - s.y); + return out; + } + "#; + + let fs = r#" + @group(0) @binding(0) var tex: texture_2d; + @group(0) @binding(1) var smp: sampler; + @group(0) @binding(2) var UParams: vec4; // x: scale, y: mode, z: spacing_px, w: thickness_px + + const MODE_SIGNED: f32 = 0.0; + const ARROW_COLOR: vec3 = vec3(1.0, 1.0, 0.2); + + // xyz = arrow color, w = mask (coverage) + fn compute_arrow_overlay( + uv: vec2, + spacing_px: f32, + thickness_px: f32, + scale: f32, + dims: vec2 + ) -> vec4 { + let spacing = spacing_px / vec2(f32(dims.x), f32(dims.y)); + let cell = floor(uv / spacing); + let cell_center = (cell + vec2(0.5, 0.5)) * spacing; + let v_sample = textureSample(tex, smp, cell_center).xy * scale; + let v_len = length(v_sample); + if (v_len <= 1e-4) { + return vec4(ARROW_COLOR, 0.0); + } + + let dir = v_sample / v_len; + let tangent = vec2(-dir.y, dir.x); + let rel = uv - cell_center; + let proj = dot(rel, dir); + let ortho = dot(rel, tangent); + let arrow_len = clamp(v_len * 0.5, spacing.x * 0.25, spacing.x * 1.5); + let half_thick = (thickness_px / vec2(f32(dims.x), f32(dims.y))).x; + let line_mask = smoothstep(half_thick, 0.0, abs(ortho)) * step(-spacing.x, proj) * step(proj, arrow_len); + let head_size = arrow_len * 0.2; + let head_mask = step(arrow_len - head_size, proj) * smoothstep(half_thick * 2.0, 0.0, abs(ortho + (proj - arrow_len) * 0.5)); + let mask = clamp(max(line_mask, head_mask), 0.0, 1.0); + return vec4(ARROW_COLOR, mask); + } + + @fragment fn main(@location(0) uv: vec2) -> @location(0) vec4 { + let dims = textureDimensions(tex, 0u); + let scale = UParams.x; + let mode = UParams.y; + let spacing_px = max(UParams.z, 4.0); + let thickness_px = max(UParams.w, 1.0); + + // Sample velocity at the current pixel (for background visualization). + let coord = vec2(uv * vec2(dims)); + let vel_px = textureLoad(tex, coord, 0).xy; + + // Base visualization (signed or magnitude). + var base_rgb: vec3; + if (mode <= MODE_SIGNED + 0.5) { + base_rgb = vec3(0.5, 0.5, 0.5) + vec3(vel_px, 0.0) * scale; + } else { + let m = length(vel_px) * scale; + base_rgb = vec3(m, m, m); + } + + let overlay = compute_arrow_overlay(uv, spacing_px, thickness_px, scale, dims); + let arrow_rgb = mix(base_rgb, overlay.xyz, overlay.w); + + return vec4(arrow_rgb, 1.0); + } + "#; + + // Bind group layouts + let bgl_material = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_velocity_material_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }, + ], + }); + let bgl_rect = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("overlay_velocity_rect_bgl"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(std::num::NonZeroU64::new(16).unwrap()), + }, + count: None, + }], + }); + + let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("overlay_velocity_pl"), + bind_group_layouts: &[&bgl_material, &bgl_rect], + push_constant_ranges: &[], + }); + + let vs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_velocity_vs"), + source: wgpu::ShaderSource::Wgsl(vs.into()), + }); + let fs_mod = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("overlay_velocity_fs"), + source: wgpu::ShaderSource::Wgsl(fs.into()), + }); + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("overlay_velocity_pipeline"), + layout: Some(&pl), + vertex: wgpu::VertexState { + module: &vs_mod, + entry_point: "main", + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &fs_mod, + entry_point: "main", + targets: &[Some(wgpu::ColorTargetState { + format: self.target_format, + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + let bind_group_rect = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_velocity_rect_bg"), + layout: &bgl_rect, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: rect_buffer.as_entire_binding(), + }], + }); + + let velocity_view = resources + .gpu() + .textures + .get(self.velocity_texture) + .expect("velocity texture") + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); + let bind_group_material = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("overlay_velocity_material_bg"), + layout: &bgl_material, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&velocity_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: params_buffer.as_entire_binding(), + }, + ], + }); + + self.pipeline = Some(PipelineV2 { + pipeline, + bind_group_layouts: vec![bgl_material, bgl_rect], + }); + self.bind_group_rect = Some(bind_group_rect); + self.bind_group_material = Some(bind_group_material); + self.params_buffer = Some(params_buffer); + Ok(()) + } + + fn draw( + &mut self, + encoder: &mut CommandEncoder, + _renderer: &mut dyn EnginePillRenderer, + resources: &mut crate::resources::ResourceManager, + _frame: &wgpu::SurfaceTexture, + view: &wgpu::TextureView, + _world: &WorldQuery, + ) -> Result<()> { + if let Ok(pp) = resources.post_process.lock() { + if !pp.mb_debug_velocity { + return Ok(()); + } + } + + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some(&self.label), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + pass.set_pipeline(&self.pipeline.as_ref().unwrap().pipeline); + pass.set_bind_group(0, &self.bind_group_material.as_ref().unwrap(), &[]); + pass.set_bind_group(1, &self.bind_group_rect.as_ref().unwrap(), &[]); + pass.draw(0..6, 0..1); + Ok(()) + } +} diff --git a/engine/pill_engine/src/graphics/pass_scene.rs b/engine/pill_engine/src/graphics/pass_scene.rs index 74418781..049a4bd7 100644 --- a/engine/pill_engine/src/graphics/pass_scene.rs +++ b/engine/pill_engine/src/graphics/pass_scene.rs @@ -1,4 +1,4 @@ -use crate::ecs::get_model_matrix; +use crate::ecs::{get_model_matrix, get_prev_model_matrix}; use crate::graphics::decompose_render_queue_key; use crate::graphics::projection::perspective_rh_zo; use crate::graphics::renderer::{ @@ -69,6 +69,7 @@ impl CameraUniform { #[derive(Copy, Clone)] pub(crate) struct PerDrawStd140 { mvp: [[f32; 4]; 4], + prev_mvp: [[f32; 4]; 4], model: [[f32; 4]; 4], } unsafe impl bytemuck::Zeroable for PerDrawStd140 {} @@ -107,6 +108,7 @@ pub(crate) struct VisiblePreDraw { pub(crate) mesh_handle: RendererMeshHandle, pub(crate) entity_index: u32, pub(crate) mvp: [[f32; 4]; 4], + pub(crate) prev_mvp: [[f32; 4]; 4], pub(crate) model: [[f32; 4]; 4], } @@ -126,8 +128,10 @@ pub(crate) struct GroupCmd { pub struct PassScene { label: String, offscreen_color_texture: RendererTextureHandle, + velocity_texture: RendererTextureHandle, depth_texture: RendererTextureHandle, color_format: wgpu::TextureFormat, + velocity_format: wgpu::TextureFormat, load_existing_color: bool, ibl_irradiance_tex: Option, ibl_prefilter_tex: Option, @@ -146,6 +150,7 @@ pub struct PassScene { struct PassSceneState { camera_uniform: CameraUniform, camera_buffer: wgpu::Buffer, + prev_view_projection: Mat4, globals_bind_group: wgpu::BindGroup, pipeline: PipelineV2, ibl_sampler: wgpu::Sampler, @@ -158,8 +163,10 @@ impl PassScene { pub fn new( label: &str, offscreen_color_texture: RendererTextureHandle, + velocity_texture: RendererTextureHandle, depth_texture: RendererTextureHandle, color_format: wgpu::TextureFormat, + velocity_format: wgpu::TextureFormat, load_existing_color: bool, ibl_irradiance_tex: Option, ibl_prefilter_tex: Option, @@ -168,8 +175,10 @@ impl PassScene { Self { label: label.to_string(), offscreen_color_texture, + velocity_texture, depth_texture, color_format, + velocity_format, load_existing_color, ibl_irradiance_tex, ibl_prefilter_tex, @@ -213,7 +222,7 @@ impl Pass for PassScene { // IBL irradiance moved into globals (set 0) @group(0) @binding(1) var texIrradiance: texture_2d; @group(0) @binding(2) var smpIrradiance: sampler; - struct PerDraw { mvp: mat4x4, model: mat4x4, }; + struct PerDraw { mvp: mat4x4, prev_mvp: mat4x4, model: mat4x4, }; @group(3) @binding(0) var UPerDraw: PerDraw; struct VSIn { @location(0) pos: vec3, @@ -225,13 +234,19 @@ impl Pass for PassScene { @location(0) uv: vec2, @location(1) worldPos: vec3, @location(2) worldNormal: vec3, + @location(3) clipCurr: vec4, + @location(4) clipPrev: vec4, }; @vertex fn main(input: VSIn) -> VSOut { var out: VSOut; let worldPos4 = UPerDraw.model * vec4(input.pos, 1.0); // TODO: Use a proper normal matrix (inverse-transpose of model) if non-uniform scaling is used. let n = normalize((UPerDraw.model * vec4(input.normal, 0.0)).xyz); - out.pos = UPerDraw.mvp * vec4(input.pos, 1.0); + let clip_curr = UPerDraw.mvp * vec4(input.pos, 1.0); + let clip_prev = UPerDraw.prev_mvp * vec4(input.pos, 1.0); + out.pos = clip_curr; + out.clipCurr = clip_curr; + out.clipPrev = clip_prev; out.uv = input.uv; out.worldPos = worldPos4.xyz; out.worldNormal = n; @@ -256,7 +271,7 @@ impl Pass for PassScene { } @group(2) @binding(0) var UMaterial: MaterialParams; // Per-draw - struct PerDraw { mvp: mat4x4, model: mat4x4, }; + struct PerDraw { mvp: mat4x4, prev_mvp: mat4x4, model: mat4x4, }; @group(3) @binding(0) var UPerDraw: PerDraw; struct Camera { position: vec4, @@ -349,11 +364,18 @@ impl Pass for PassScene { return (kD * (albedo / PI) + specular) * radiance * NdotL; } + struct FSOut { + @location(0) color: vec4, + @location(1) velocity: vec2, + }; + @fragment fn main( @location(0) uv: vec2, @location(1) WorldPos: vec3, - @location(2) NormalIn: vec3 - ) -> @location(0) vec4 { + @location(2) NormalIn: vec3, + @location(3) clipCurr: vec4, + @location(4) clipPrev: vec4 + ) -> FSOut { // Surface parameters var albedo = textureSample(texBaseColor, smpBaseColor, uv).rgb * UMaterial.baseColorFactor; let mr = textureSample(texMetallicRoughness, smpMetallicRoughness, uv).gb; @@ -390,11 +412,21 @@ impl Pass for PassScene { let F = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); let specular = prefilteredColor * (F * envBRDF.x + envBRDF.y); color = color + ambient_diffuse + specular; - // color = vec3(uv, 0.0); // DBG uv - // color = vec3(N*0.5+0.5); // DBG Normal - // color = vec3(WorldPos); // DBG WorldPos - // color = vec3(roughness); // DBG roughness - return vec4(color, 1.0); + // Velocity (screen space) for motion blur + // Preserve sign of w (required for correct perspective divide); only clamp magnitude to avoid INF. + let w_curr = sign(clipCurr.w) * max(abs(clipCurr.w), 1e-4); + let w_prev = sign(clipPrev.w) * max(abs(clipPrev.w), 1e-4); + // Convert NDC (y-up) to texture UV (y-down) to match fullscreen post-process passes. + let ndc_curr = clipCurr.xy / w_curr; + let ndc_prev = clipPrev.xy / w_prev; + let a = vec2(ndc_curr.x * 0.5 + 0.5, -ndc_curr.y * 0.5 + 0.5); + let b = vec2(ndc_prev.x * 0.5 + 0.5, -ndc_prev.y * 0.5 + 0.5); + let vel = a - b; + + var out: FSOut; + out.color = vec4(color, 1.0); + out.velocity = vel; + return out; } "#; @@ -569,11 +601,18 @@ impl Pass for PassScene { }, vertex_buffers: &[::data_layout_descriptor()], bind_groups, - targets: &[Some(wgpu::ColorTargetState { - format: self.color_format, - blend: Some(wgpu::BlendState::REPLACE), - write_mask: wgpu::ColorWrites::ALL, - })], + targets: &[ + Some(wgpu::ColorTargetState { + format: self.color_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + }), + Some(wgpu::ColorTargetState { + format: self.velocity_format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + }), + ], depth_stencil: Some(wgpu::DepthStencilState { format: wgpu::TextureFormat::Depth32Float, depth_write_enabled: true, @@ -736,6 +775,7 @@ impl Pass for PassScene { self.state = Some(PassSceneState { camera_uniform: CameraUniform::new(), camera_buffer, + prev_view_projection: Mat4::IDENTITY, globals_bind_group, pipeline, ibl_sampler, @@ -776,8 +816,9 @@ impl Pass for PassScene { let active_camera_transform_component = camera_transform_storage.as_ref().unwrap(); // Update camera uniform and write to GPU buffer (no allocations) - let vp_mat_arr: [[f32; 4]; 4] = { + let (prev_view_proj, vp_mat_arr): (Mat4, [[f32; 4]; 4]) = { let state = get_state(self); + let prev = state.prev_view_projection; state .camera_uniform .update_data(active_camera_component, active_camera_transform_component); @@ -786,7 +827,9 @@ impl Pass for PassScene { 0, bytemuck::bytes_of(&state.camera_uniform), ); - state.camera_uniform.view_projection_matrix + let current_vp = Mat4::from_cols_array_2d(&state.camera_uniform.view_projection_matrix); + state.prev_view_projection = current_vp; + (prev, state.camera_uniform.view_projection_matrix) }; let clear_color = active_camera_component.clear_color; @@ -814,16 +857,20 @@ impl Pass for PassScene { .unwrap(); let model_arr = get_model_matrix(transform); let model: Mat4 = Mat4::from_cols_array_2d(&model_arr); + let prev_model_arr = get_prev_model_matrix(transform); + let prev_model: Mat4 = Mat4::from_cols_array_2d(&prev_model_arr); // NOTE: Temporarily skip AABB-based frustum culling to avoid accessing renderer internals. let view_proj: Mat4 = vp_mat; let mvp: [[f32; 4]; 4] = (view_proj * model).to_cols_array_2d(); + let prev_mvp: [[f32; 4]; 4] = (prev_view_proj * prev_model).to_cols_array_2d(); self.visible_pre_draw_buffer.push(VisiblePreDraw { material_handle, mesh_handle, entity_index: render_queue_item.entity_index, mvp, + prev_mvp, model: model.to_cols_array_2d(), }); } @@ -859,6 +906,7 @@ impl Pass for PassScene { { batch.instances.push(PerDrawStd140 { mvp: v.mvp, + prev_mvp: v.prev_mvp, model: v.model, }); } else { @@ -866,6 +914,7 @@ impl Pass for PassScene { mesh_handle: v.mesh_handle, instances: vec![PerDrawStd140 { mvp: v.mvp, + prev_mvp: v.prev_mvp, model: v.model, }], base_offset_u32: 0, @@ -929,6 +978,16 @@ impl Pass for PassScene { label: Some("scene_offscreen_color_view"), ..Default::default() }); + let velocity_view = resources + .gpu() + .textures + .get(self.velocity_texture) + .expect("velocity texture") + .texture + .create_view(&wgpu::TextureViewDescriptor { + label: Some("scene_velocity_view"), + ..Default::default() + }); let depth_view = resources .gpu() .textures @@ -941,26 +1000,36 @@ impl Pass for PassScene { }); let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("m6_offscreen_pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &color_view, - resolve_target: None, - ops: if self.load_existing_color { - wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - } - } else { - wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: clear_color.x as f64, - g: clear_color.y as f64, - b: clear_color.z as f64, - a: 1.0, - }), + color_attachments: &[ + Some(wgpu::RenderPassColorAttachment { + view: &color_view, + resolve_target: None, + ops: if self.load_existing_color { + wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + } + } else { + wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: clear_color.x as f64, + g: clear_color.y as f64, + b: clear_color.z as f64, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + } + }, + }), + Some(wgpu::RenderPassColorAttachment { + view: &velocity_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), store: wgpu::StoreOp::Store, - } - }, - })], + }, + }), + ], depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { view: &depth_view, depth_ops: Some(wgpu::Operations { diff --git a/engine/pill_engine/src/resources/resource_manager.rs b/engine/pill_engine/src/resources/resource_manager.rs index 2ca247ae..52aec336 100644 --- a/engine/pill_engine/src/resources/resource_manager.rs +++ b/engine/pill_engine/src/resources/resource_manager.rs @@ -27,8 +27,14 @@ impl ResourceManager { gpu: crate::resources::GpuResources::new(), post_process: Arc::new(Mutex::new(PostProcessParams { time_s: 0.0, - focus_point: 5.0, - focus_scale: 3.0, + focus_point: 0.0, + focus_scale: 0.0, + mb_enabled: true, + mb_debug_velocity: false, + mb_strength: 1.0, + mb_max_samples: 16, + mb_min_speed: 0.001, + mb_depth_softness: 1.0, })), } } From b324f33bf4ffaba37b2e9010ae583a941d9e8def Mon Sep 17 00:00:00 2001 From: Michal Klos Date: Sun, 14 Dec 2025 04:43:11 +0100 Subject: [PATCH 69/69] feat: lastest demo --- examples/pill_demo/res/config.ini | 7 +- .../res/ibl/HDR_111_Parking_Lot_2.ibl | 44 ++ .../res/ibl/HDR_111_Parking_Lot_2_Env.hdr | Bin 0 -> 298285 bytes .../res/ibl/HDR_111_Parking_Lot_2_Prev.jpg | Bin 0 -> 76947 bytes .../res/ibl/HDR_111_Parking_Lot_2_Thumb.jpg | Bin 0 -> 16134 bytes examples/pill_demo/res/models/README.md | 4 + .../pill_demo/res/models/SciFiHelmet/keep.txt | 0 examples/pill_demo/res/models/sponza/keep.txt | 0 examples/pill_demo/src/game.rs | 573 +++++++++++++++++- 9 files changed, 619 insertions(+), 9 deletions(-) create mode 100644 examples/pill_demo/res/ibl/HDR_111_Parking_Lot_2.ibl create mode 100644 examples/pill_demo/res/ibl/HDR_111_Parking_Lot_2_Env.hdr create mode 100644 examples/pill_demo/res/ibl/HDR_111_Parking_Lot_2_Prev.jpg create mode 100644 examples/pill_demo/res/ibl/HDR_111_Parking_Lot_2_Thumb.jpg create mode 100644 examples/pill_demo/res/models/README.md create mode 100644 examples/pill_demo/res/models/SciFiHelmet/keep.txt create mode 100644 examples/pill_demo/res/models/sponza/keep.txt diff --git a/examples/pill_demo/res/config.ini b/examples/pill_demo/res/config.ini index aad642f1..45b98baf 100644 --- a/examples/pill_demo/res/config.ini +++ b/examples/pill_demo/res/config.ini @@ -14,10 +14,11 @@ MAX_CONCURRENT_2D_SOUNDS=10 MAX_CONCURRENT_3D_SOUNDS=10 # ENGINE RESOURCES -MAX_TEXTURES=10 +MAX_TEXTURES=1000 MAX_MATERIALS=20000 -MAX_MESHES=10 -MAX_SOUNDS=10 +MAX_MESHES=1000 +MAX_MODELS=100 +MAX_SOUNDS=50 # OTHER LOG_LEVEL=Info diff --git a/examples/pill_demo/res/ibl/HDR_111_Parking_Lot_2.ibl b/examples/pill_demo/res/ibl/HDR_111_Parking_Lot_2.ibl new file mode 100644 index 00000000..5e48d5ba --- /dev/null +++ b/examples/pill_demo/res/ibl/HDR_111_Parking_Lot_2.ibl @@ -0,0 +1,44 @@ +[Header] +ICOfile = "HDR_111_Parking_Space_2_Thumb.jpg" +Name = "HDR 111 Parking Lot 2" +Author = "HDRI Hub" +Location = "Cologne, Germany" +GEOlat = 52.6556789875030520 +GEOlong = 8.4282530844211578 +Comment = "parking space, place, concrete, trees, clear" +PREVIEWfile = "HDR_111_Parking_Lot_2_Prev.jpg" +Link = "http://www.hdri-hub.com" +Date = "2012:05:07" +Time = "16:31:59" + +[Background] +BGfile = "HDR_111_Parking_Lot_2_Bg.jpg" +BGmap = 1 +BGu = 0.000000 +BGv = 0.000000 +BGheight = 4096 + +[Enviroment] +EVfile = "HDR_111_Parking_Lot_2_Env.hdr" +EVmap = 1 +EVu = 0.000000 +EVv = 0.000000 +EVheight = 256 +EVmulti = 1.000000 +EVgamma = 2.200000 + +[Reflection] +REFfile = "HDR_111_Parking_Lot_2_Ref.hdr" +REFmap = 1 +REFu = 0.000000 +REFv = 0.000000 +REFheight = 2560 +REFmulti = 1.000000 +REFgamma = 2.200000 + +[Sun] +SUNcolor = 254,240,234 +SUNmulti = 1.586010 +SUNu = 0.492000 +SUNv = 0.352000 + diff --git a/examples/pill_demo/res/ibl/HDR_111_Parking_Lot_2_Env.hdr b/examples/pill_demo/res/ibl/HDR_111_Parking_Lot_2_Env.hdr new file mode 100644 index 0000000000000000000000000000000000000000..58a75ab65939857c159fbe63b6c4f5309f8cc62f GIT binary patch literal 298285 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 Uy>@!-%WJn2)8B&Jt3UsL0Y4o1pa1{> literal 0 HcmV?d00001 diff --git a/examples/pill_demo/res/ibl/HDR_111_Parking_Lot_2_Prev.jpg b/examples/pill_demo/res/ibl/HDR_111_Parking_Lot_2_Prev.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3c596cb0a237f72555f61946286a7f802de2a868 GIT binary patch literal 76947 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 pr6}s@6PYQP#-)50>(ra@ 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 z