From f6324b0b5d65aef6e713e8b902a6b689659dd27f Mon Sep 17 00:00:00 2001 From: skal Date: Sun, 8 Feb 2026 21:20:58 +0100 Subject: feat(gpu): Add auxiliary texture masking system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements MainSequence auxiliary texture registry to support inter-effect texture sharing within a single frame. Primary use case: screen-space partitioning where multiple effects render to complementary regions. Architecture: - MainSequence::register_auxiliary_texture(name, width, height) Creates named texture that persists for entire frame - MainSequence::get_auxiliary_view(name) Retrieves texture view for reading/writing Use case example: - Effect1: Generate mask (1 = Effect1 region, 0 = Effect2 region) - Effect1: Render scene A where mask = 1 - Effect2: Reuse mask, render scene B where mask = 0 - Result: Both scenes composited to same framebuffer Implementation details: - Added std::map to MainSequence - Texture lifecycle managed by MainSequence (create/resize/shutdown) - Memory impact: ~4-8 MB per mask (acceptable for 2-3 masks) - Size impact: ~100 lines (~500 bytes code) Changes: - src/gpu/effect.h: Added auxiliary texture registry API - src/gpu/effect.cc: Implemented registry with FATAL_CHECK validation - doc/MASKING_SYSTEM.md: Complete architecture documentation - doc/HOWTO.md: Added auxiliary texture usage example Also fixed: - test_demo_effects.cc: Corrected EXPECTED_POST_PROCESS_COUNT (9→8) Pre-existing bug: DistortEffect was counted but not tested Testing: - All 33 tests pass (100%) - No functional changes to existing effects - Zero regressions See doc/MASKING_SYSTEM.md for detailed design rationale and examples. --- doc/MASKING_SYSTEM.md | 240 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 doc/MASKING_SYSTEM.md (limited to 'doc/MASKING_SYSTEM.md') diff --git a/doc/MASKING_SYSTEM.md b/doc/MASKING_SYSTEM.md new file mode 100644 index 0000000..d468d48 --- /dev/null +++ b/doc/MASKING_SYSTEM.md @@ -0,0 +1,240 @@ +# Auxiliary Texture Masking System + +## Overview + +The auxiliary texture masking system allows effects to share textures within a single frame render. Primary use case: **screen-space partitioning** where multiple effects render to complementary regions of the framebuffer. + +## Use Case + +**Problem:** Render two different 3D scenes to different regions of the screen (split-screen, portals, picture-in-picture). + +**Solution:** +- Effect1 generates a mask (1 = Effect1's region, 0 = Effect2's region) +- Effect1 renders scene A where mask = 1 +- Effect2 reuses the mask and renders scene B where mask = 0 +- Both render to the same framebuffer in the same frame + +## Architecture Choice: Mask Texture vs Stencil Buffer + +### Option 1: Stencil Buffer (NOT CHOSEN) +**Pros:** Hardware-accelerated, fast early-rejection +**Cons:** 8-bit limitation, complex pipeline config, hard to debug + +### Option 2: Mask Texture (CHOSEN) +**Pros:** +- Flexible (soft edges, gradients, any format) +- Debuggable (visualize mask as texture) +- Reusable (multiple effects can read same mask) +- Simple pipeline setup + +**Cons:** +- Requires auxiliary texture (~4 MB for 1280x720 RGBA8) +- Fragment shader discard (slightly slower than stencil) + +**Verdict:** Mask texture flexibility and debuggability outweigh performance cost. + +## Implementation + +### MainSequence Auxiliary Texture Registry + +```cpp +class MainSequence { + public: + // Register a named auxiliary texture (call once in effect init) + void register_auxiliary_texture(const char* name, int width, int height); + + // Get texture view for reading/writing (call every frame) + WGPUTextureView get_auxiliary_view(const char* name); + + private: + struct AuxiliaryTexture { + WGPUTexture texture; + WGPUTextureView view; + int width, height; + }; + std::map auxiliary_textures_; +}; +``` + +### Effect Lifecycle + +``` +Init Phase (once): + Effect1.init(): register_auxiliary_texture("mask_1", width, height) + Effect2.init(): (no registration - reuses Effect1's mask) + +Compute Phase (every frame): + Effect1.compute(): Generate mask to get_auxiliary_view("mask_1") + +Scene Pass (every frame, shared render pass): + Effect1.render(): Sample mask, discard if < 0.5, render scene A + Effect2.render(): Sample mask, discard if > 0.5, render scene B +``` + +### Render Flow Diagram + +``` +Frame N: +┌───────────────────────────────────────────────────────────┐ +│ Compute Phase: │ +│ Effect1.compute() │ +│ └─ Generate mask → auxiliary_textures_["mask_1"] │ +│ │ +├───────────────────────────────────────────────────────────┤ +│ Scene Pass (all effects share framebuffer A + depth): │ +│ Effect1.render() [priority 5] │ +│ ├─ Sample auxiliary_textures_["mask_1"] │ +│ ├─ Discard fragments where mask < 0.5 │ +│ └─ Render 3D scene A → framebuffer A │ +│ │ +│ Effect2.render() [priority 10] │ +│ ├─ Sample auxiliary_textures_["mask_1"] │ +│ ├─ Discard fragments where mask > 0.5 │ +│ └─ Render 3D scene B → framebuffer A │ +│ │ +│ Result: framebuffer A contains both scenes, partitioned │ +├───────────────────────────────────────────────────────────┤ +│ Post-Process Chain: │ +│ A ⟷ B ⟷ Screen │ +└───────────────────────────────────────────────────────────┘ +``` + +## Example: Circular Portal Effect + +### Effect1: Render Scene A (inside portal) + +```cpp +class PortalSceneEffect : public Effect { + public: + PortalSceneEffect(const GpuContext& ctx) : Effect(ctx) {} + + void init(MainSequence* demo) override { + demo_ = demo; + demo->register_auxiliary_texture("portal_mask", width_, height_); + // ... create pipelines + } + + void compute(WGPUCommandEncoder encoder, ...) override { + // Generate circular mask (portal region) + WGPUTextureView mask_view = demo_->get_auxiliary_view("portal_mask"); + // ... render fullscreen quad with circular mask shader + } + + void render(WGPURenderPassEncoder pass, ...) override { + // Render 3D scene, discard outside portal + WGPUTextureView mask_view = demo_->get_auxiliary_view("portal_mask"); + // ... bind mask, render scene with mask test + } +}; +``` + +### Effect2: Render Scene B (outside portal) + +```cpp +class OutsideSceneEffect : public Effect { + public: + OutsideSceneEffect(const GpuContext& ctx) : Effect(ctx) {} + + void init(MainSequence* demo) override { + demo_ = demo; + // Don't register - reuse PortalSceneEffect's mask + } + + void render(WGPURenderPassEncoder pass, ...) override { + // Render 3D scene, discard inside portal + WGPUTextureView mask_view = demo_->get_auxiliary_view("portal_mask"); + // ... bind mask, render scene with inverted mask test + } +}; +``` + +### Mask Generation Shader + +```wgsl +// portal_mask.wgsl +@group(0) @binding(0) var uniforms: MaskUniforms; + +@fragment fn fs_main(@builtin(position) p: vec4) -> @location(0) vec4 { + let uv = p.xy / uniforms.resolution; + let center = vec2(0.5, 0.5); + let radius = 0.3; + + let dist = length(uv - center); + let mask = f32(dist < radius); // 1.0 inside circle, 0.0 outside + + return vec4(mask, mask, mask, 1.0); +} +``` + +### Scene Rendering with Mask + +```wgsl +// scene_with_mask.wgsl +@group(0) @binding(2) var mask_sampler: sampler; +@group(0) @binding(3) var mask_texture: texture_2d; + +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { + // Sample mask + let screen_uv = in.position.xy / uniforms.resolution; + let mask_value = textureSample(mask_texture, mask_sampler, screen_uv).r; + + // Effect1: Discard outside portal (mask = 0) + if (mask_value < 0.5) { + discard; + } + + // Effect2: Invert test - discard inside portal (mask = 1) + // if (mask_value > 0.5) { discard; } + + // Render scene + return compute_scene_color(in); +} +``` + +## Memory Impact + +Each auxiliary texture: **width × height × 4 bytes** +- 1280×720 RGBA8: ~3.7 MB +- 1920×1080 RGBA8: ~8.3 MB + +For 2-3 masks: 10-25 MB total (acceptable overhead). + +## Use Cases + +1. **Split-screen**: Vertical/horizontal partition +2. **Portals**: Circular/arbitrary shape windows to other scenes +3. **Picture-in-picture**: Small viewport in corner +4. **Masked transitions**: Wipe effects between scenes +5. **Shadow maps**: Pre-generated in compute, used in render +6. **Reflection probes**: Generated once, reused by multiple objects + +## Alternatives Considered + +### Effect-Owned Texture (No MainSequence changes) +```cpp +auto effect1 = std::make_shared(...); +auto effect2 = std::make_shared(...); +effect2->set_mask_source(effect1->get_mask_view()); +``` + +**Pros:** No MainSequence changes +**Cons:** Manual wiring, effects coupled, less flexible + +**Verdict:** Not chosen. Registry approach is cleaner and more maintainable. + +## Future Extensions + +- **Multi-channel masks**: RGBA mask for 4 independent regions +- **Mipmap support**: For hierarchical queries +- **Compression**: Quantize masks to R8 (1 byte per pixel) +- **Enum-based lookup**: Replace string keys for size optimization + +## Size Impact + +- MainSequence changes: ~100 lines (~500 bytes code) +- std::map usage: ~1 KB overhead (low priority for CRT removal) +- Runtime memory: 4-8 MB per mask (acceptable) + +--- + +*Document created: February 8, 2026* -- cgit v1.2.3