Skip to content

PoC renderer - #14

Draft
sp0lsh wants to merge 69 commits into
Pillware:mainfrom
sp0lsh:feat/poc-render
Draft

PoC renderer#14
sp0lsh wants to merge 69 commits into
Pillware:mainfrom
sp0lsh:feat/poc-render

Conversation

@sp0lsh

@sp0lsh sp0lsh commented Oct 14, 2025

Copy link
Copy Markdown
Contributor

Benchmarked with 60k dynamic pills (30k -> 60k since previous benchmark), 30 FPS @ 1920x1200 on Mac M1.

pill_60k

https://youtu.be/o140PuAGH2k

Reference: HypeHype Mobile Rendering Architecture, Aaltonen23

PS: Please mind that it's my first time with Rust

@sp0lsh sp0lsh changed the title Feat/poc render PoC renderer Oct 14, 2025
@@ -0,0 +1,7 @@
pub struct RendererBufferTag;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is core module a good place for such tags? Can't they be somewhere in rendering-related file in engine?

@sp0lsh sp0lsh Nov 30, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think no, because XYZTag is for common ResourceManager implementation. I'd keep it in Core until the engine matures with a couple more demos.

Separate engine ResourceManager and RendererResourceManagers cause many headaches, duplication and additional complexities at this stage.


// --- Borrow-only render query bundle and alias ---

use crate::ecs::{CameraComponent, ComponentStorage, EntityHandle, TransformComponent};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be moved to the top of this file

@@ -158,10 +159,21 @@ impl TransformComponent {
}

fn get_rotation_matrix(&self) -> Matrix3<f32> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice


}

pub fn update_transform_matrices(transform_component: &mut TransformComponent) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another nice one

} No newline at end of file

// Peek system timer without taking ownership (cloned snapshot)
pub fn peek_system_timer(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe name it clone_system_timer?
I'm not used to "peek", but it may be only my thing

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, seconding that - if we clone we clone 😁

key: render_queue_key,
entity_index: entity_handle.data().index as u32,
};
engine.render_queue.push(render_queue_item);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about sorting the queue again after this batch update?

@sp0lsh sp0lsh Nov 30, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sort is done in PassScene (opaques) where context of selected camera view is present.

So sort depends on the pass logic, imagine OpaquePass (front to back) vs TransparentPass (back to front) sampling queue from the same world state

&mut timer
) {
// Build WorldView with raw pointers to avoid borrow conflicts
let world_view = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using pointers in rust is an antipattern.
You should use splitting borrow pattern (disjoined borrows)
Always prefer passing the ownership

@sp0lsh sp0lsh Nov 30, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I've already discovered :P

Pls help C/C++ dev, that wants code fast pixels, with these Rust Shenanigans

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, basically arm yourself with a gun:
https://doc.rust-lang.org/nomicon/borrow-splitting.html

} No newline at end of file
}

fn init_default_resources(engine: &mut Engine) -> Result<(), Error> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be in renderer.rs like all bootstraping logic IMO

@@ -1,25 +1,47 @@
#![cfg_attr(debug_assertions, allow(dead_code, unused_imports))]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General thoughts about proposed shader passess architecture.

Right now all materials are dynamic. Shader resources read external shader files, figure out what uniforms and samplers they need, and then create pipeline objects (descriptors) with the appropriate fields.
Users still have to define shader parameters manually, but we can add proper reflection so this becomes fully automatic.
With this setup, we don’t need to hardcode render passes as Rust structs with fixed fields. I initially explored that approach but ended up going with generic dynamic objects instead.

Additional points / concerns:

  • I really don’t like the idea of having shader code embedded directly in the engine.
    It breaks normal workflows (syntax highlighting, tooling, error messages, file diffs, etc.), and it forces engine rebuilds every time you tweak a shader.
  • IMO we should stick to HLSL, because that’s the market standard. Anything else immediately scares people off (including myself! (while developing this engine I would like to learn shader development as well, but not in <1% market share shader language)). Same situation as wanting to use Rhai (which is great) but picking Lua because that’s what devs expect, unfortunately.
  • A hybrid setup with Rust-GPU + HLSL is too messy and confusing. Two shader languages doubles the mental overhead.
  • Not even sure if Rust-GPU supports hot reload in a practical way. Tooling around it is still pretty limited.
  • External shader files give us all the benefits: hot reload, version control diffs, modding support, cleaner debugging.
  • Most graphics programmers already know HLSL. Keeping things familiar makes onboarding way easier.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also we may end up with an explosion for render pass code files that will duplicate a lot of the same logic (setting up the descriptors, etc)
It looks like this is already happening.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really don’t like the idea of having shader code embedded directly in the engine.
It breaks normal workflows (syntax highlighting, tooling, error messages, file diffs, etc.), and it forces engine rebuilds every time you tweak a shader.

I don't like it too, moreover, It's not put into a proper place yet, and it can be put into files etc.

@sp0lsh sp0lsh Nov 30, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO we should stick to HLSL, because that’s the market standard. Anything else immediately scares people off (including myself! (while developing this engine I would like to learn shader development as well, but not in <1% market share shader language)). Same situation as wanting to use Rhai (which is great) but picking Lua because that’s what devs expect, unfortunately.

Huh? I don't even know how currently used shader language is called, I just went with the existing flow. I've spent more time with GLSL and I've never heard of HLSL being any standard apart from DirectX APIs (which AAA engines may like due to M$ support on XBox).

I do not care as long as the shader compilation pipeline (SPIRV) is simple for targeted platforms (x64, arm, web)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also we may end up with an explosion for render pass code files that will duplicate a lot of the same logic (setting up the descriptors, etc)
It looks like this is already happening.

Duplication at this stage is intended. I copy-pasted with a cold blood to see what code will be the same when exploring the performant modern rendering code. This way it is easier to change before prematurely optimizing the code with complexities. To be cleaned up and deduplicated when reaching the scope of the demo. Until the feature scope is reached, I do not have enough observation to cleanup.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is a living PR, then even better - we can discuss, iterate and conclude with much better code. I cannot add more because I am not a rendering expert. My aim would be to keep it optimized/quite simple (not intimidating with complexity) - something Aaltonen managed to achieve in his presentation.

#[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 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that structs with shader parameters are more elegant...
I was researching this topic and ultimately went with accessing material parameters via their names as strings.
This is the approach that both Unity and Unreal use. It is the most flexible one allowing to have generic material objects instead of hardcoded ones.
This way we can have PBR material, cartoon material and skybox material declared as the same material/shader type, all automatic/dynamic. No separate mental-overhead (engine-hardcoded!) structs required.
Current approach to this is not a random one, it is intentional and long considered.

@sp0lsh sp0lsh Nov 30, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PoC system is based on IdTech6 and HypeHype. The core pass PBR that is most minimal and performant.

Unreal explodes with shader combinations which I dislike as an architecture choice and Unity well... they tried.

But hey, we have encapsulated Passes, so 80% of shaders can benefit from core, fast pipeline. For remaining others 20% we can provide the custom path with string lookups and other thing but do not quote us on performance numbers there.

I like that in Unity I can create an experiment shader and drop on an object - this will be custom pass.
Unreal has PBR materials on nodes that I hate, however PoC PassScene can be extended with similar PBR material interface, however when you know what you are doing, IdTech6 did UberShader for easier low-level shader execution tuning.

If you really want to draw 100% meshes customized, feel free to copy-paste our performant baseline Pass and modify as you like.

Comment thread engine/.cargo/config.toml
@@ -0,0 +1,16 @@
[build]
# Enable SIMD optimizations for math operations
# This will auto-vectorize cgmath operations when possible

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rebase on glam - we probably get auto-vectorization from the get-go

Comment thread engine/.cargo/config.toml
rustflags = ["-C", "target-cpu=native"]

# Alternative: More conservative SIMD flags
# rustflags = ["-C", "target-feature=+avx2,+fma"]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sometimes we might want avx512 etc

Comment thread engine/pill_core/src/pill_slotmap.rs Outdated
use std::fmt::Formatter;
use core::fmt::Debug;
use std::iter::Enumerate;
use std::num::{ NonZeroU32};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rebase after #16, #17, #18 are merged - they remove these obsolete fields anyway

self.counters.insert(label.into(), value);
}

// pub fn increment_counter(&mut self, label: impl Into<String>, delta: u64) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not commit commented out code

use cgmath::{Deg, Matrix3, SquareMatrix, Zero};
use anyhow::{ Result, Context, Error };
use serde::{ Serialize, Deserialize };
use glam::{Mat3, Mat4, Quat, Vec3};

@JDuchniewicz JDuchniewicz Dec 1, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use pill_core names for types since we already merged #15


// Normal matrix: rotation only
let n = Mat3::from_quat(q);
transform_component.normal_matrix = n.to_cols_array_2d();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we already talked about that - please also check if what I replaced in #15 is faster or if this is, just merge the faster alternatives 😄

@@ -0,0 +1,241 @@
# Pill Renderer vNext (wgpu) – Minimal MVP, API-Correct

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move all documentation related entities to docs - even better - annotate the code with documentation?

@@ -0,0 +1,10 @@
Building game project from /Users/mk/dev/demo/Pill-Demo...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need the log file staged?

Comment thread .gitignore
crates/pill_standalone/Cargo.toml
examples/*/Cargo.toml

engine/Cargo.toml

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know why you are doing that, though this is probably not the best solution 😓

The launcher will constantly update the paths in engine/Cargo and show this file as changed. Honestly did not find a good solution for that except calling git add engine/pill_* when staging files.

env_tex_rt,
prefilter_handle,
ibl_brdf_rt,
5,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in general we have a lot of magic constants - I know it is very math-oriented and settings-oriented code, but we still should strive to have as many documented variables as possible. Even better - having one simple place to tweak them (even later we could allow for tweaking at runtime if some of them are changing the visual output of the system).

5,
);
{
let self_ptr: *mut dyn crate::graphics::PillRenderer = &mut *engine.renderer;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would put the imports at the top of the file and call relative imports instead of absolute ones.

pub struct MaterialDesc<'a> {
pub label: &'a str,
// Factors
pub albedo: [f32; 3],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use SIMD types?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Materials will not be involved in much CPU compute. Most often set value, copy to WGPU buffer and upload to GPU (mem bandwidth). On GPU shaders do heavy ALU operations.

So if anything can be squeezed then the material precision -> size of GPU buffer / cache line fit in the loop over many materials.

fn resize(&mut self, new_window_size: winit::dpi::PhysicalSize<u32>);

fn destroy_texture(&mut self, renderer_texture_handle: RendererTextureHandle) -> Result<()>;
// Creates a 256B-aligned uniform buffer (COPY_DST) and returns its handle

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cache-line size?

@sp0lsh sp0lsh Dec 10, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the intent is to be able to create small buffers fitting the cache, suitable for the hot path. Another that on Mac, Metal failed to allocate buffers smaller than 256B, hence the padding alignment for the time being. To be checked on other platforms.


// WorldView raw-pointer based view to avoid borrow conflicts at call site
pub struct WorldView {
pub active_camera: crate::ecs::EntityHandle,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above, can we use Rust's no-pointer approach here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants