Files
wsg/README_DETAILS.md
T
Jérôme Bousquié d4c2d93fc5 eng doc
2026-09-25 20:06:10 +02:00

11 KiB
Raw Blame History

WSG — Detailed Documentation

Technical content from the main README: status, architecture, API reference, workflows, roadmap.

Status

Area State
Manual workflow (Context + Renderer + PipelineCache) ✅ Working (advanced — fine-grained control)
App / AppBuilder / AppHandler event-loop facade ✅ Working — window, events, frame presentation, automatic scene rendering
Scene resource/entity registry ✅ Working — auto-rendered in one batched pass (App::render_scene)
GPU-driven two-pass pipeline (Compute → indirect draw) ✅ Working (Phase 3) — render_scene + shadow pass 100% indirect; opt-in frustum culling
3D infrastructure (uniform bind groups, MVP + camera) ✅ Working — per-frame camera + per-entity world matrices in shared uniforms
Shadows (shadow mapping) ✅ Working — directional/spot, slope-scaled bias, PCF 3×3
HDR + Tone Mapping ✅ Working — offscreen Rgba16Float, ACES/Reinhard, opt-in
LOD (Level of Detail) ✅ Working — quadric decimation, hysteresis, multi-level buffer
Mesh module (primitives + import) ✅ Working — feature-gated primitives, OBJ parser
Bloom ✅ Working — threshold + separable blur + composite, HDR required
Fog ✅ Working — 3 modes (linear, exp, exp²), runtime switchable
MSAA ✅ Working — 4× multisample, resolve pass
DoF ✅ Working — CoC + disc blur, focus presets
PBR (metallic/roughness + normal maps) ✅ Working — Cook-Torrance, GGX, IBL, derivative tangent

Note: standard_shader.wgsl (Phong + PBR, with an explicit unlit mode) is the single shader the library ships. Flat 2D drawing is its unlit variant (Renderer::set_unlit(true)).

Architecture

Layer model

  • Manager layer (Context) — owns the GPU hardware lifecycle (Instance → Surface → Adapter → Device → Queue). Created once at startup; configure() sets up the swapchain, Frame wraps each frame's surface texture + view.
  • Executor layer (Renderer) — binds a Material pipeline + Mesh buffers into a RenderPass and submits. render_scene batches all entities into one encoder + one submit per frame.
  • Supporting pieces — PipelineCache (shader → compiled RenderPipeline, Arc-shared), Material, Geometry/Mesh/Vertex, Scene (string-ID registry), Camera/Transform.

GPU-driven pipeline (Phase 3)

A Compute Pass derives each entity's world matrix and fills per-entity indirect draw arguments (with opt-in frustum culling), then the main and shadow render passes issue one indirect draw per active slot.

Spec: docs/tech/ARCHI_CPU_GPU.md · User guide: docs/user/cameras/gpu-driven.md

Module layout

lib/src/
├── lib.rs              # crate root, re-exports
├── prelude.rs          # glob re-exports
├── app.rs              # App + AppBuilder
├── handler.rs          # AppHandler trait
├── camera.rs           # Camera, CameraController
├── input.rs            # InputState
├── lights.rs           # Lights, Light, LightType, directional_light, …
├── core/
│   ├── context.rs      # GPU lifecycle (Instance/Surface/Adapter/Device/Queue)
│   ├── renderer.rs     # RenderPass execution, shadow pass, HDR/TM pass
│   ├── frame.rs        # Per-frame RAII (surface texture + view)
│   ├── geometry.rs     # Geometry (positions/normals/UVs/indices) + BBox
│   ├── transform.rs    # Transform (translation/rotation/scale)
│   ├── frustum.rs      # Frustum (6 planes, sphere/box culling)
│   ├── lod.rs          # LOD decimation (quadric edge collapse)
│   ├── shadow.rs       # ShadowConfig (map size, bias, PCF)
│   ├── hdr.rs          # ToneMapper enum (Aces/Reinhard)
│   ├── bloom.rs        # BloomConfig + BloomPipeline
│   ├── msaa.rs         # MsaaConfig
│   ├── fog.rs          # FogConfig + FogMode
│   └── dof.rs          # DoFConfig + DoFPipeline
├── mesh/
│   ├── mod.rs          # Re-exports flat
│   ├── primitives/     # 6 feature-gated generators
│   └── import/         # OBJ parser + glTF stub
├── pipeline/           # PipelineCache (shader → RenderPipeline)
├── resources/          # Mesh, Material, Texture, Uniform, Vertex
├── scene/              # Scene (registry), Entity
└── utils/              # Conf constants, WsgError

Quick reference (types)

Concept Type Responsibility
App / AppBuilder Facade Window + event loop + frame + auto scene render
AppHandler Trait setup() / update() / render() callbacks
Scene Struct Registry: shaders, materials, meshes, entities, lights, camera
Context Struct GPU hardware (Instance, Surface, Adapter, Device, Queue)
Renderer Struct RenderPass execution (scene, shadow, HDR/TM, bloom, DoF)
PipelineCache Struct Shader → compiled RenderPipeline cache
Material Struct Shader ID + texture + pipeline + PBR params
Geometry Struct CPU vertex data (positions/normals/UVs/colors/indices)
Mesh / Vertex Struct GPU geometry / interleaved upload tuple
Frame Struct Per-frame RAII (surface texture + view)
Camera / Transform Struct Camera math + per-entity transform
CameraController Struct Orbital camera (orbit/zoom/reset/apply_to)
InputState Struct Unified keyboard/mouse (pressed/held/released, delta, scroll)
Texture Struct GPU image (Rgba8UnormSrgb) + sampler
Lights / Light Struct Light list (directional/point/spot, MAX=8) + ambient
ShadowConfig Struct Shadow map size, bias, PCF taps, scene radius
ToneMapper Enum ACES Filmic / Reinhard
BloomConfig Struct Threshold, intensity, H/V passes
MsaaConfig Struct Sample count (1 = disabled)
FogConfig Struct Mode, near/far, density, color
DoFConfig Struct Focus distance, aperture, max blur
BBox Struct Axis-aligned bounding box (min/max)
Frustum Struct 6 planes, sphere/box culling
use wsg_lib::prelude::*;
use wsg_lib::app::AppBuilder;
use wsg_lib::utils::WsgError;

struct MyScene;

impl AppHandler for MyScene {
    fn setup(&mut self, app: &mut wsg_lib::App) {
        app.scene
            .register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
            .unwrap();
        app.scene.add_material_shader("mat", "standard").unwrap();
        app.scene.create_mesh("cube", cube(1.0), Some("mat")).unwrap();
        app.scene.add_entity("my_cube", "cube").unwrap();
    }
    fn update(&mut self, app: &mut wsg_lib::App) {
        // your per-frame logic
    }
    // render() default: app.render_scene(frame.view()) — auto-draws everything
}

#[pollster::main]
async fn main() -> Result<(), WsgError> {
    let app = AppBuilder::new().title("WSG").build().await?;
    app.run(MyScene);
    Ok(())
}

Scene methods return Result<_, String> — typed-error unification is on the roadmap.

Manual workflow (advanced)

Bypass the App facade and drive Context, Renderer and PipelineCache yourself:

use std::sync::Arc;
use winit::event_loop::EventLoop;
use winit::window::WindowBuilder;
use wsg_lib::core::{Context, Frame, Renderer};
use wsg_lib::pipeline::PipelineCache;
use wsg_lib::resources::{Geometry, Material, Mesh};
use wsg_lib::utils;

fn main() {
    let event_loop = EventLoop::new().unwrap();
    let window = Arc::new(WindowBuilder::new().build(&event_loop).unwrap());
    let context = pollster::block_on(Context::new(window.clone())).expect("GPU init");
    let format = context.configure(&context.adapter, 800, 600).expect("surface config");

    let mut renderer = Renderer::new(&context, format, 800, 600);
    let mut cache = PipelineCache::new(Arc::new(context.device.clone()));
    cache.register_shader("standard", utils::STANDARD_SHADER_PATH).unwrap();

    let material = Material::new(renderer.format(), "standard", &mut cache);
    let geometry = Geometry::new(vec![-0.5f32, 0.5, 0.0, 0.5, 0.5, 0.0, 0.5, -0.5, 0.0, -0.5, -0.5, 0.0])
        .with_indices(vec![0, 1, 2, 0, 2, 3]);
    let mesh = Mesh::from_geometry(renderer.device(), Arc::new(geometry), None);

    event_loop.run(|event, elwt| {
        match event {
            winit::event::Event::AboutToWait => window.request_redraw(),
            winit::event::Event::WindowEvent { event: winit::event::WindowEvent::RedrawRequested, .. } => {
                if let Some(frame) = Frame::try_new(&context.surface) {
                    renderer.render(frame.view(), &mesh, &material);
                    renderer.present(frame);
                }
            }
            winit::event::Event::WindowEvent { event: winit::event::WindowEvent::CloseRequested, .. } => elwt.exit(),
            _ => {}
        }
    }).unwrap();
}

Features

Feature Default Provides
prim-cube ✅ cube(size)
prim-plane ✅ plane(w, d, seg_x, seg_z)
prim-sphere ✅ uv_sphere(…), icosphere(…)
prim-cylinder ✅ cylinder(…)
prim-cone ✅ cone(…)
prim-torus ✅ torus(…)
all-prims ✅ (default) All 6 primitives
import-obj ⬜ load_obj(path), parse_obj(str)
import-gltf ⬜ load_gltf(path) (stub)

Design principle: opt-in = zero cost

Feature How to enable If NOT enabled
Shadows scene.set_shadow_caster(Some(idx)) No shadow map, no depth pass, no PCF
HDR + TM AppBuilder::with_hdr(ToneMapper::Aces) No offscreen texture, no TM pass
Bloom AppBuilder::with_bloom(BloomConfig::…) No bloom textures, no passes
MSAA AppBuilder::with_msaa(MsaaConfig { sample_count: 4 }) Single sample, no resolve
Fog AppBuilder::with_fog(FogConfig::…) No fog uniforms
DoF AppBuilder::with_dof(DoFConfig::…) No CoC/blur textures
GPU-driven culling AppBuilder::with_gpu_driven(true) No compute pipeline, no indirect buffers
LOD scene.create_mesh_with_lod(…, levels) Single-level mesh
Primitives Cargo feature prim-* Not compiled
File import Cargo feature import-* Not compiled

Roadmap

Phase Status
1 — Foundations (window, render loop, Context) ✅
2 — 3D infrastructure (Geometry, Mesh, Material, Pipeline) ✅
3 — GPU-driven (compute pass, indirect draws, culling) ✅
4 — Advanced rendering (shadows, HDR/TM, lights) ✅
5 — Polish (LOD, camera controller, input, demo) ✅
6 — Post-MVP (bloom, PBR, fog, DoF, MSAA, cascaded shadows, SSAO) 🔄

Documentation

Where What
docs/user/ User guide
docs/tech/ Internal architecture
docs/ROADMAP.md Roadmap
docs/PLAN.md Recipe book (step history)
cargo doc -p wsg-lib --no-deps API reference (rustdoc)