feat: renderer: PBR, IBL, Passes - #38
Conversation
# Conflicts: # engine/Cargo.toml # engine/pill_core/Cargo.toml # engine/pill_core/src/utils.rs # engine/pill_engine/Cargo.toml # engine/pill_engine/res/shaders/default_lit_fragment.wgsl # engine/pill_engine/res/shaders/default_unlit_fragment.wgsl # engine/pill_engine/src/ecs/systems/rendering_system.rs # engine/pill_engine/src/engine.rs # engine/pill_engine/src/graphics/dummy_renderer.rs # engine/pill_engine/src/graphics/render_queue.rs # engine/pill_engine/src/graphics/renderer.rs # engine/pill_engine/src/lib.rs # engine/pill_engine/src/renderer/drawers/mesh_drawer.rs # engine/pill_engine/src/renderer/resources/engine_parameters.rs # engine/pill_engine/src/renderer/resources/renderer_mesh.rs # engine/pill_engine/src/renderer/resources/renderer_shader.rs # engine/pill_engine/src/renderer/resources/renderer_texture.rs # engine/pill_engine/src/resources/material.rs # engine/pill_engine/src/resources/shader.rs # engine/pill_engine/src/resources/texture.rs # engine/pill_renderer/Cargo.toml # engine/pill_renderer/src/renderer.rs # engine/pill_renderer/src/resources/renderer_material.rs # engine/pill_renderer/src/resources/renderer_resource_storage.rs
# Conflicts: # engine/Cargo.toml # engine/pill_core/Cargo.toml # engine/pill_core/src/utils.rs # engine/pill_engine/Cargo.toml # engine/pill_engine/res/shaders/default_lit_fragment.wgsl # engine/pill_engine/res/shaders/default_unlit_fragment.wgsl # engine/pill_engine/src/ecs/systems/rendering_system.rs # engine/pill_engine/src/engine.rs # engine/pill_engine/src/graphics/dummy_renderer.rs # engine/pill_engine/src/graphics/render_queue.rs # engine/pill_engine/src/graphics/renderer.rs # engine/pill_engine/src/lib.rs # engine/pill_engine/src/renderer/drawers/mesh_drawer.rs # engine/pill_engine/src/renderer/resources/engine_parameters.rs # engine/pill_engine/src/renderer/resources/renderer_mesh.rs # engine/pill_engine/src/renderer/resources/renderer_shader.rs # engine/pill_engine/src/renderer/resources/renderer_texture.rs # engine/pill_engine/src/resources/material.rs # engine/pill_engine/src/resources/shader.rs # engine/pill_engine/src/resources/texture.rs # engine/pill_renderer/Cargo.toml # engine/pill_renderer/src/renderer.rs # engine/pill_renderer/src/resources/renderer_material.rs # engine/pill_renderer/src/resources/renderer_resource_storage.rs
…kspace path - div_ceil/repeat_n: replace manual bit-alignment arithmetic with .div_ceil() and std::iter::repeat_n in pass_pbr_static.rs and state.rs - needless_range_loop: rewrite mat4_mul inner loop as (0..4).map(...).sum() in glb_to_cooked_mesh.rs - egui native-only: move egui + egui-wgpu from unconditional deps to [target.'cfg(not(target_arch = "wasm32"))'.dependencies]; gate egui_client.rs, EguiClient re-export, RenderStateComponent.egui_client field, and init_default_passes signature/call sites with #[cfg(not(target_arch = "wasm32"))] to restore WASM binary size below 499 KB budget - codesign: gate codesign_adhoc function and call sites with #[cfg(target_os = "macos")] so Linux CI does not try to invoke the tool - workspace path: restore NO_PATH placeholder in engine/Cargo.toml so the launcher can substitute the correct game path on any machine - pill_tunel: add egui = "0.32.1" direct dependency (game.rs uses egui:: types); fix workspace field to NO_PATH
| } | ||
| } | ||
|
|
||
| fn orbit_camera_system(engine: &mut Engine) -> Result<()> { |
There was a problem hiding this comment.
we really ought to have a common place for such systems/components
There was a problem hiding this comment.
Cool, these are just emerging. A code that get's duplicated 3 times is moved to a proper common place.
|
|
||
| #[cfg(target_os = "macos")] | ||
| fn codesign_adhoc(path: &PathBuf) -> Result<()> { | ||
| let status = Command::new("codesign") |
There was a problem hiding this comment.
On macos executables would faild duoe to .dylib (.dll on mac) being not signed, even if it's development version #think_different
|
|
||
| use pill_game::WebGame; | ||
| #[cfg(target_arch = "wasm32")] | ||
| #[global_allocator] |
There was a problem hiding this comment.
what was the allocator situation for Wasm before this allocator was added?
There was a problem hiding this comment.
Allocator for wasm was changed to single threaded allocator with less binary size.
|
|
||
| [dependencies] | ||
| pill_core = {path = "../pill_core"} | ||
| glam = { version = "0.30.8", features = ["serde", "bytemuck"] } |
There was a problem hiding this comment.
why are we pulling it in here? Isn't everything typedeffed already in core?
|
|
||
| /// Parse an RTEX binary header. | ||
| /// Returns `(pixel_bytes, width, height, mip_count, version)`. | ||
| fn decode_rtex_header(bytes: &[u8]) -> (&[u8], u32, u32, u32, u32) { |
There was a problem hiding this comment.
defo not place for this here, move to utils?
There was a problem hiding this comment.
utils in what domain?
There was a problem hiding this comment.
I though for now just a file in the crate - if we start getting a lot of common functions/code across the whole project then maybe a new pill_utils crate?
|
|
||
| /// Provides equirect background texture bytes; used by PassBackground on first-frame init. | ||
| /// Must be called before the first rendered frame (i.e. from `start()`). | ||
| pub fn set_background_texture(&mut self, bytes: Vec<u8>) -> Result<()> { |
There was a problem hiding this comment.
this also has utilish taste to it.
There was a problem hiding this comment.
also, I think the less we require to be called by the user the better - it should all happen automagically or at least minimize it to be called inside start. I think such a requirement is too stark. The game should run if the user does not provide these textures. Also, what if the user wants to change it mid-run?
There was a problem hiding this comment.
Good catch. Engine is expected to have no knowledge about domain specific assets. Remove set_background_texture() and keep it as component state
| pub type RenderQueueKeyType = u64; | ||
|
|
||
| // 64-bit render sort key layout (MSB → LSB): | ||
| // bit: 63 59 58 51 50 43 42 35 34 27 26 19 18 11 10 0 |
There was a problem hiding this comment.
not sure if I don't prefer the old more verbose ASCII diagram
| ) -> Result<()> { | ||
| let shader = engine.get_resource::<Shader>(&self.shader_handle)?; | ||
|
|
||
| // Get texture to be set |
There was a problem hiding this comment.
not sure about removing these comments
| #[cfg(not(target_arch = "wasm32"))] | ||
| fn new(window: Arc<winit::window::Window>, config: EngineConfig) -> Result<Self> { | ||
| info!(LogContext::Rendering => "Initializing {}", "Renderer".module_object_style()); | ||
| let state = pollster::block_on(State::new(window, config))?; |
There was a problem hiding this comment.
will this run the renderer on another thread? I am trying to grasp the point behind having it inited async
| vertex_wgsl: &str, | ||
| fragment_wgsl: &str, | ||
| texture_slots: &HashMap<String, crate::resources::ShaderTextureSlot>, | ||
| parameter_slots: &[(String, crate::resources::ShaderParameterSlot)], |
There was a problem hiding this comment.
Why the other is a hashmap and this is an array? Also, maybe a custom typedef so we avoid long names?
|
|
||
| let frame = self.state.surface.get_current_texture(); | ||
| let frame = match frame { | ||
| std::result::Result::Ok(frame) => frame, |
There was a problem hiding this comment.
I am not sure we need to have qualification everywhere, especially on std types or pill_* ones. Defo good to have it on wgpu etc, but then again we create long chains of types and names and its hard to read. If we use it often, just use it at the top.
There was a problem hiding this comment.
Yeah, we need to decide on one project-wide approach.
But I'm also not sure which to pick
There was a problem hiding this comment.
I think the rules should be simple:
If used often - import, if it's an one-off and is not a::super::long::qualification::type::name then we can do it with the qualification. Otherwise, let's just import at the top and save the future code readers additional strain. LSPs are good enough at providing the full type info if necessary.
| Ok(buffer) | ||
| } | ||
|
|
||
| fn create_pipeline_v2(&mut self, desc: PipelineV2Desc) -> Result<PipelineV2> { |
There was a problem hiding this comment.
v2? why then keep v1?
| fn create_texture_from_pixels( | ||
| &mut self, | ||
| name: &str, | ||
| mip_pixels: &[&[u8]], |
There was a problem hiding this comment.
quite weird type, why it's not contiguous or a separate specialized type?
| module: &vertex_shader, | ||
| entry_point: Some("vs_main"), | ||
| buffers: vertex_layouts, // Specifies structure of vertices that will be passed to the vertex shader | ||
| buffers: vertex_layouts, |
There was a problem hiding this comment.
a lot of comments removed - sure we want that?
| wgpu::VertexAttribute { | ||
| // Vertex texture coordinates | ||
| // slangc maps TEXCOORD0 → @location(4), not 1 | ||
| offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, |
| pub(crate) fn write_parameters_to_buffer( | ||
| queue: &wgpu::Queue, | ||
| buffer: &wgpu::Buffer, | ||
| parameter_slots: &[(String, ShaderParameterSlot)], |
There was a problem hiding this comment.
why this array of pairs?
| // --- Pass API types --- | ||
|
|
||
| /// Read-only view of the world handed to each pass. A pass calls `query::<T>()` for whatever | ||
| /// component types it needs and builds its OWN draw list (opaque, translucent, shadow, …). |
There was a problem hiding this comment.
I think this is in line with what @MattSzymonski is implementing in the ECS refactor. Might be good to commonalize the query-approach to using just a slice of mut engine.
| } | ||
|
|
||
| #[derive(Clone, Debug)] | ||
| pub struct PipelineV2Desc<'a> { |
There was a problem hiding this comment.
I still don't get the V2 - will it be merged without a rename?
| textures: &[(String, MaterialTexture)], | ||
| ) -> Result<()>; | ||
| /// Returns the OS window the renderer is bound to. | ||
| fn get_window(&self) -> std::sync::Arc<winit::window::Window>; |
There was a problem hiding this comment.
Don't we have a coding convention for using qualified type names? If not, it's good to set it as I don't know when we use -import a type and when to use a full qualified type name.
| pub struct RenderQueueItem { | ||
| pub key: RenderQueueKey, | ||
| pub entity_index: u32, | ||
| /// Raw transform copied (no trig) during the cache-warm sequential ECS scan; |
There was a problem hiding this comment.
whats trig in this context? Trigonometry?
There was a problem hiding this comment.
Yes, building LocalMatrix requires computing sin/cos from angle values
| entry_func: "fs_main", | ||
| }, | ||
| vertex_buffers: &[ | ||
| <crate::renderer::resources::RendererMesh as crate::renderer::resources::Vertex>::data_layout_descriptor(), |
| &mut self, | ||
| encoder: &mut wgpu::CommandEncoder, | ||
| renderer: &mut dyn PillRenderer, | ||
| _frame: &wgpu::SurfaceTexture, |
There was a problem hiding this comment.
why do we need this param?
There was a problem hiding this comment.
Some Passes may need surface to get it's format or size
| // Read active camera and transform. | ||
| let active_camera_index = world.active_camera.data().index as usize; | ||
| let active_camera_component = camera_components | ||
| .data |
There was a problem hiding this comment.
why not handle the error gracefully?
| let active_camera_component = camera_components | ||
| .data | ||
| .get(active_camera_index) | ||
| .unwrap() |
| // --- Transform Component --- | ||
|
|
||
| // NOTE: Setting position/rotation/scale directly is not possible since we need to update matrices after each change | ||
| // 36 bytes hot (pos+rot+scale only); model_matrix/normal_matrix were dead weight — GPU |
There was a problem hiding this comment.
Wait, so model_matrix was used only for rendering?
There was a problem hiding this comment.
There is no Scene hierarchy and transform tree, isn't there?
In this case ALU on GPU is faster than Memory Bandwidth of pushing whole matrices through CPU->GPU.
| { | ||
| //entry: HashMapVacantEntry<'a, TypeId, Box<(dyn Any + Send + Sync)>>, | ||
| entry: HashMapVacantEntry<'a, TypeId, Box<dyn Any + Send>>, | ||
| entry: HashMapVacantEntry<'a, TypeId, Box<dyn Any>>, |
There was a problem hiding this comment.
they used to be Send + Sync now they are no longer either? Why?
There was a problem hiding this comment.
Did we ever have threadness implemented, because that's the reason why we add these traits. If we want to have multithreaded iteration over the typemaps then we would probably need to either slice them up in non-overlapping partitions (which is cleaner) or have these traits added.
| @@ -0,0 +1,9 @@ | |||
| // WHY: zero-size marker types so Handle<RendererMeshTag> and Handle<RendererTextureTag> are different types — passing the wrong one is a compile error, not a runtime bug. | |||
| } | ||
| } | ||
|
|
||
| pub(crate) fn lerp3(start: [f32; 3], end: [f32; 3], factor: f32) -> [f32; 3] { |
There was a problem hiding this comment.
can't some of these pill_assets helper functions be shared? We are starting to have a lot of math/util funcitons flying around here
There was a problem hiding this comment.
Agree, math in core looks like good place.
As per previous reply, pill_assets are based on examples using it and as code emerges the functions are moved into proper modules.
|
LGTM, minor remarks. Would really appreciate some documentation at this point, especially since I am no rendering expert and it's not that easy to review it without a deeper understanding. Read through Aaltonen's presentations and at least I get the gist of the rendering optimization thanks to that 😁 |
60k pills @ 42 FPS, macbook M1 1920x180. Single threaded yet, wasm is single threaded too.

https://github.com/KhronosGroup/glTF-Sample-Models/tree/d7a3cc8e51d7c573771ae77a57f16b0662a905c6/2.0/MetalRoughSpheres
https://github.com/KhronosGroup/glTF-Sample-Models/tree/d7a3cc8e51d7c573771ae77a57f16b0662a905c6/2.0/DamagedHelmet
sequenceDiagram autonumber participant ECS as ECS · SoA storage participant GP as Gameplay systems participant RS as RenderingSystem (stateless) participant RN as renderer.render() participant PB as PassPBROpaque participant GPU as GPU note over GP,ECS: 2. Game — mutate world GP->>ECS: pill_particle / hero / drift write Transform[] note over RS,RN: 3. PostGame — stateless plumbing RS->>ECS: borrow active scene RS->>RN: render(active_camera, scene, delta_time, resources) RN->>RN: get_current_texture → create_view → create_command_encoder RN->>RN: WorldQuery::new(active_camera, scene, delta_time, resources) note over RN,GPU: Scene Passes — each Pass queries the world, owns its queue RN->>PB: draw(encoder, frame, view, &world) PB->>ECS: a. QUERY world.query::<T>() → zip transform[]+pbr[], skip None PB->>PB: b. GROUP find-or-create by material+mesh (cut PSO/bind switches) PB->>PB: c. STAGE concat instances → buffer, assign base_offset PB->>GPU: d. UPLOAD write per_draw storage (x1) PB->>GPU: e. PASS begin_render_pass: Clear color=BLACK, depth=1.0 PB->>GPU: f. ENCODE 6 groups → 6x draw_indexed (instanced, 60,001 total) PB->>GPU: g. BG background sub-draw (fullscreen tri @ far plane) GPU->>GPU: vertex shader per instance: model = T·R·S → clip = viewProj·model