summaryrefslogtreecommitdiff
path: root/src/gpu
diff options
context:
space:
mode:
Diffstat (limited to 'src/gpu')
-rw-r--r--src/gpu/bind_group_builder.h111
-rw-r--r--src/gpu/demo_effects.h1
-rw-r--r--src/gpu/effects/cnn_effect.cc106
-rw-r--r--src/gpu/effects/post_process_helper.cc102
-rw-r--r--src/gpu/effects/rotating_cube_effect.cc23
-rw-r--r--src/gpu/effects/scene1_effect.cc28
-rw-r--r--src/gpu/effects/scene1_effect.h19
-rw-r--r--src/gpu/effects/shaders.cc4
-rw-r--r--src/gpu/effects/shaders.h1
-rw-r--r--src/gpu/pipeline_builder.h109
-rw-r--r--src/gpu/sampler_cache.h61
11 files changed, 390 insertions, 175 deletions
diff --git a/src/gpu/bind_group_builder.h b/src/gpu/bind_group_builder.h
new file mode 100644
index 0000000..d63f6e2
--- /dev/null
+++ b/src/gpu/bind_group_builder.h
@@ -0,0 +1,111 @@
+// WGPU bind group builder - reduces boilerplate for bind group creation
+#pragma once
+#include <vector>
+
+// Forward declarations (users must include gpu.h)
+struct WGPUBindGroupLayoutEntry;
+struct WGPUBindGroupEntry;
+struct WGPUDeviceImpl;
+typedef struct WGPUDeviceImpl* WGPUDevice;
+struct WGPUBindGroupLayoutImpl;
+typedef struct WGPUBindGroupLayoutImpl* WGPUBindGroupLayout;
+struct WGPUBindGroupImpl;
+typedef struct WGPUBindGroupImpl* WGPUBindGroup;
+struct WGPUBufferImpl;
+typedef struct WGPUBufferImpl* WGPUBuffer;
+struct WGPUTextureViewImpl;
+typedef struct WGPUTextureViewImpl* WGPUTextureView;
+struct WGPUSamplerImpl;
+typedef struct WGPUSamplerImpl* WGPUSampler;
+typedef uint32_t WGPUShaderStageFlags;
+
+#include "platform/platform.h"
+
+class BindGroupLayoutBuilder {
+ std::vector<WGPUBindGroupLayoutEntry> entries_;
+
+public:
+ BindGroupLayoutBuilder& uniform(uint32_t binding, WGPUShaderStageFlags vis, size_t min_size = 0) {
+ WGPUBindGroupLayoutEntry e{};
+ e.binding = binding;
+ e.visibility = vis;
+ e.buffer.type = WGPUBufferBindingType_Uniform;
+ if (min_size) e.buffer.minBindingSize = min_size;
+ entries_.push_back(e);
+ return *this;
+ }
+
+ BindGroupLayoutBuilder& storage(uint32_t binding, WGPUShaderStageFlags vis, size_t min_size = 0) {
+ WGPUBindGroupLayoutEntry e{};
+ e.binding = binding;
+ e.visibility = vis;
+ e.buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
+ if (min_size) e.buffer.minBindingSize = min_size;
+ entries_.push_back(e);
+ return *this;
+ }
+
+ BindGroupLayoutBuilder& texture(uint32_t binding, WGPUShaderStageFlags vis) {
+ WGPUBindGroupLayoutEntry e{};
+ e.binding = binding;
+ e.visibility = vis;
+ e.texture.sampleType = WGPUTextureSampleType_Float;
+ e.texture.viewDimension = WGPUTextureViewDimension_2D;
+ entries_.push_back(e);
+ return *this;
+ }
+
+ BindGroupLayoutBuilder& sampler(uint32_t binding, WGPUShaderStageFlags vis) {
+ WGPUBindGroupLayoutEntry e{};
+ e.binding = binding;
+ e.visibility = vis;
+ e.sampler.type = WGPUSamplerBindingType_Filtering;
+ entries_.push_back(e);
+ return *this;
+ }
+
+ WGPUBindGroupLayout build(WGPUDevice device) {
+ WGPUBindGroupLayoutDescriptor desc{};
+ desc.entryCount = entries_.size();
+ desc.entries = entries_.data();
+ return wgpuDeviceCreateBindGroupLayout(device, &desc);
+ }
+};
+
+class BindGroupBuilder {
+ std::vector<WGPUBindGroupEntry> entries_;
+
+public:
+ BindGroupBuilder& buffer(uint32_t binding, WGPUBuffer buf, size_t size) {
+ WGPUBindGroupEntry e{};
+ e.binding = binding;
+ e.buffer = buf;
+ e.size = size;
+ entries_.push_back(e);
+ return *this;
+ }
+
+ BindGroupBuilder& texture(uint32_t binding, WGPUTextureView view) {
+ WGPUBindGroupEntry e{};
+ e.binding = binding;
+ e.textureView = view;
+ entries_.push_back(e);
+ return *this;
+ }
+
+ BindGroupBuilder& sampler(uint32_t binding, WGPUSampler samp) {
+ WGPUBindGroupEntry e{};
+ e.binding = binding;
+ e.sampler = samp;
+ entries_.push_back(e);
+ return *this;
+ }
+
+ WGPUBindGroup build(WGPUDevice device, WGPUBindGroupLayout layout) {
+ WGPUBindGroupDescriptor desc{};
+ desc.layout = layout;
+ desc.entryCount = entries_.size();
+ desc.entries = entries_.data();
+ return wgpuDeviceCreateBindGroup(device, &desc);
+ }
+};
diff --git a/src/gpu/demo_effects.h b/src/gpu/demo_effects.h
index 72b3f65..1ccf930 100644
--- a/src/gpu/demo_effects.h
+++ b/src/gpu/demo_effects.h
@@ -15,6 +15,7 @@
#include "gpu/effects/theme_modulation_effect.h" // ThemeModulationEffect with full definition
#include "gpu/effects/hybrid_3d_effect.h"
#include "gpu/effects/flash_cube_effect.h"
+#include "gpu/effects/scene1_effect.h"
#include "gpu/gpu.h"
#include "gpu/texture_manager.h"
#include "gpu/uniform_helper.h"
diff --git a/src/gpu/effects/cnn_effect.cc b/src/gpu/effects/cnn_effect.cc
index 7107bea..d74187c 100644
--- a/src/gpu/effects/cnn_effect.cc
+++ b/src/gpu/effects/cnn_effect.cc
@@ -6,70 +6,30 @@
#include "gpu/effects/shaders.h"
#include "gpu/effects/shader_composer.h"
#include "gpu/effect.h"
+#include "gpu/bind_group_builder.h"
+#include "gpu/sampler_cache.h"
+#include "gpu/pipeline_builder.h"
// Create custom pipeline with 5 bindings (includes original texture)
static WGPURenderPipeline create_cnn_pipeline(WGPUDevice device,
WGPUTextureFormat format,
const char* shader_code) {
- std::string composed_shader = ShaderComposer::Get().Compose({}, shader_code);
+ WGPUBindGroupLayout bgl = BindGroupLayoutBuilder()
+ .sampler(0, WGPUShaderStage_Fragment)
+ .texture(1, WGPUShaderStage_Fragment)
+ .uniform(2, WGPUShaderStage_Vertex | WGPUShaderStage_Fragment)
+ .uniform(3, WGPUShaderStage_Fragment)
+ .texture(4, WGPUShaderStage_Fragment)
+ .build(device);
- WGPUShaderModuleDescriptor shader_desc = {};
- WGPUShaderSourceWGSL wgsl_src = {};
- wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL;
- wgsl_src.code = str_view(composed_shader.c_str());
- shader_desc.nextInChain = &wgsl_src.chain;
- WGPUShaderModule shader_module =
- wgpuDeviceCreateShaderModule(device, &shader_desc);
+ WGPURenderPipeline pipeline = RenderPipelineBuilder(device)
+ .shader(shader_code)
+ .bind_group_layout(bgl)
+ .format(format)
+ .build();
- WGPUBindGroupLayoutEntry bgl_entries[5] = {};
- bgl_entries[0].binding = 0; // sampler
- bgl_entries[0].visibility = WGPUShaderStage_Fragment;
- bgl_entries[0].sampler.type = WGPUSamplerBindingType_Filtering;
- bgl_entries[1].binding = 1; // input texture
- bgl_entries[1].visibility = WGPUShaderStage_Fragment;
- bgl_entries[1].texture.sampleType = WGPUTextureSampleType_Float;
- bgl_entries[1].texture.viewDimension = WGPUTextureViewDimension_2D;
- bgl_entries[2].binding = 2; // uniforms
- bgl_entries[2].visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment;
- bgl_entries[2].buffer.type = WGPUBufferBindingType_Uniform;
- bgl_entries[3].binding = 3; // effect params
- bgl_entries[3].visibility = WGPUShaderStage_Fragment;
- bgl_entries[3].buffer.type = WGPUBufferBindingType_Uniform;
- bgl_entries[4].binding = 4; // original texture
- bgl_entries[4].visibility = WGPUShaderStage_Fragment;
- bgl_entries[4].texture.sampleType = WGPUTextureSampleType_Float;
- bgl_entries[4].texture.viewDimension = WGPUTextureViewDimension_2D;
-
- WGPUBindGroupLayoutDescriptor bgl_desc = {};
- bgl_desc.entryCount = 5;
- bgl_desc.entries = bgl_entries;
- WGPUBindGroupLayout bgl = wgpuDeviceCreateBindGroupLayout(device, &bgl_desc);
-
- WGPUPipelineLayoutDescriptor pl_desc = {};
- pl_desc.bindGroupLayoutCount = 1;
- pl_desc.bindGroupLayouts = &bgl;
- WGPUPipelineLayout pl = wgpuDeviceCreatePipelineLayout(device, &pl_desc);
-
- WGPUColorTargetState color_target = {};
- color_target.format = format;
- color_target.writeMask = WGPUColorWriteMask_All;
-
- WGPUFragmentState fragment_state = {};
- fragment_state.module = shader_module;
- fragment_state.entryPoint = str_view("fs_main");
- fragment_state.targetCount = 1;
- fragment_state.targets = &color_target;
-
- WGPURenderPipelineDescriptor pipeline_desc = {};
- pipeline_desc.layout = pl;
- pipeline_desc.vertex.module = shader_module;
- pipeline_desc.vertex.entryPoint = str_view("vs_main");
- pipeline_desc.fragment = &fragment_state;
- pipeline_desc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
- pipeline_desc.multisample.count = 1;
- pipeline_desc.multisample.mask = 0xFFFFFFFF;
-
- return wgpuDeviceCreateRenderPipeline(device, &pipeline_desc);
+ wgpuBindGroupLayoutRelease(bgl);
+ return pipeline;
}
CNNEffect::CNNEffect(const GpuContext& ctx)
@@ -137,29 +97,13 @@ void CNNEffect::update_bind_group(WGPUTextureView input_view) {
wgpuBindGroupRelease(bind_group_);
WGPUBindGroupLayout bgl = wgpuRenderPipelineGetBindGroupLayout(pipeline_, 0);
- WGPUSamplerDescriptor sd = {};
- sd.magFilter = WGPUFilterMode_Linear;
- sd.minFilter = WGPUFilterMode_Linear;
- sd.maxAnisotropy = 1;
- WGPUSampler sampler = wgpuDeviceCreateSampler(ctx_.device, &sd);
-
- WGPUBindGroupEntry bge[5] = {};
- bge[0].binding = 0;
- bge[0].sampler = sampler;
- bge[1].binding = 1;
- bge[1].textureView = input_view_;
- bge[2].binding = 2;
- bge[2].buffer = uniforms_.get().buffer;
- bge[2].size = uniforms_.get().size;
- bge[3].binding = 3;
- bge[3].buffer = params_buffer_.get().buffer;
- bge[3].size = params_buffer_.get().size;
- bge[4].binding = 4;
- bge[4].textureView = original_view_ ? original_view_ : input_view_;
+ WGPUSampler sampler = SamplerCache::Get().get_or_create(ctx_.device, SamplerCache::linear());
- WGPUBindGroupDescriptor bgd = {};
- bgd.layout = bgl;
- bgd.entryCount = 5;
- bgd.entries = bge;
- bind_group_ = wgpuDeviceCreateBindGroup(ctx_.device, &bgd);
+ bind_group_ = BindGroupBuilder()
+ .sampler(0, sampler)
+ .texture(1, input_view_)
+ .buffer(2, uniforms_.get().buffer, uniforms_.get().size)
+ .buffer(3, params_buffer_.get().buffer, params_buffer_.get().size)
+ .texture(4, original_view_ ? original_view_ : input_view_)
+ .build(ctx_.device, bgl);
}
diff --git a/src/gpu/effects/post_process_helper.cc b/src/gpu/effects/post_process_helper.cc
index e99467f..0c339c7 100644
--- a/src/gpu/effects/post_process_helper.cc
+++ b/src/gpu/effects/post_process_helper.cc
@@ -5,69 +5,30 @@
#include "../demo_effects.h"
#include "gpu/gpu.h"
#include "gpu/effects/shader_composer.h"
+#include "gpu/bind_group_builder.h"
+#include "gpu/sampler_cache.h"
+#include "gpu/pipeline_builder.h"
#include <cstring>
// Helper to create a standard post-processing pipeline
WGPURenderPipeline create_post_process_pipeline(WGPUDevice device,
WGPUTextureFormat format,
const char* shader_code) {
- std::string composed_shader = ShaderComposer::Get().Compose({}, shader_code);
+ WGPUBindGroupLayout bgl = BindGroupLayoutBuilder()
+ .sampler(PP_BINDING_SAMPLER, WGPUShaderStage_Fragment)
+ .texture(PP_BINDING_TEXTURE, WGPUShaderStage_Fragment)
+ .uniform(PP_BINDING_UNIFORMS, WGPUShaderStage_Vertex | WGPUShaderStage_Fragment)
+ .uniform(PP_BINDING_EFFECT_PARAMS, WGPUShaderStage_Fragment)
+ .build(device);
- WGPUShaderModuleDescriptor shader_desc = {};
- WGPUShaderSourceWGSL wgsl_src = {};
- wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL;
- wgsl_src.code = str_view(composed_shader.c_str());
- shader_desc.nextInChain = &wgsl_src.chain;
- WGPUShaderModule shader_module =
- wgpuDeviceCreateShaderModule(device, &shader_desc);
+ WGPURenderPipeline pipeline = RenderPipelineBuilder(device)
+ .shader(shader_code)
+ .bind_group_layout(bgl)
+ .format(format)
+ .build();
- WGPUBindGroupLayoutEntry bgl_entries[4] = {};
- bgl_entries[0].binding = PP_BINDING_SAMPLER;
- bgl_entries[0].visibility = WGPUShaderStage_Fragment;
- bgl_entries[0].sampler.type = WGPUSamplerBindingType_Filtering;
- bgl_entries[1].binding = PP_BINDING_TEXTURE;
- bgl_entries[1].visibility = WGPUShaderStage_Fragment;
- bgl_entries[1].texture.sampleType = WGPUTextureSampleType_Float;
- bgl_entries[1].texture.viewDimension = WGPUTextureViewDimension_2D;
- bgl_entries[2].binding = PP_BINDING_UNIFORMS;
- bgl_entries[2].visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment;
- bgl_entries[2].buffer.type = WGPUBufferBindingType_Uniform;
-
- // Add an entry for effect-specific parameters
- bgl_entries[3].binding = PP_BINDING_EFFECT_PARAMS;
- bgl_entries[3].visibility = WGPUShaderStage_Fragment;
- bgl_entries[3].buffer.type = WGPUBufferBindingType_Uniform;
-
- WGPUBindGroupLayoutDescriptor bgl_desc = {};
- bgl_desc.entryCount = 4;
- bgl_desc.entries = bgl_entries;
- WGPUBindGroupLayout bgl = wgpuDeviceCreateBindGroupLayout(device, &bgl_desc);
-
- WGPUPipelineLayoutDescriptor pl_desc = {};
- pl_desc.bindGroupLayoutCount = 1;
- pl_desc.bindGroupLayouts = &bgl;
- WGPUPipelineLayout pl = wgpuDeviceCreatePipelineLayout(device, &pl_desc);
-
- WGPUColorTargetState color_target = {};
- color_target.format = format;
- color_target.writeMask = WGPUColorWriteMask_All;
-
- WGPUFragmentState fragment_state = {};
- fragment_state.module = shader_module;
- fragment_state.entryPoint = str_view("fs_main");
- fragment_state.targetCount = 1;
- fragment_state.targets = &color_target;
-
- WGPURenderPipelineDescriptor pipeline_desc = {};
- pipeline_desc.layout = pl;
- pipeline_desc.vertex.module = shader_module;
- pipeline_desc.vertex.entryPoint = str_view("vs_main");
- pipeline_desc.fragment = &fragment_state;
- pipeline_desc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
- pipeline_desc.multisample.count = 1;
- pipeline_desc.multisample.mask = 0xFFFFFFFF;
-
- return wgpuDeviceCreateRenderPipeline(device, &pipeline_desc);
+ wgpuBindGroupLayoutRelease(bgl);
+ return pipeline;
}
// --- PostProcess Implementation Helper ---
@@ -82,25 +43,16 @@ void pp_update_bind_group(WGPUDevice device, WGPURenderPipeline pipeline,
if (*bind_group)
wgpuBindGroupRelease(*bind_group);
+
WGPUBindGroupLayout bgl = wgpuRenderPipelineGetBindGroupLayout(pipeline, 0);
- WGPUSamplerDescriptor sd = {};
- sd.magFilter = WGPUFilterMode_Linear;
- sd.minFilter = WGPUFilterMode_Linear;
- sd.maxAnisotropy = 1;
- WGPUSampler sampler = wgpuDeviceCreateSampler(device, &sd);
- WGPUBindGroupEntry bge[4] = {};
- bge[0].binding = PP_BINDING_SAMPLER;
- bge[0].sampler = sampler;
- bge[1].binding = PP_BINDING_TEXTURE;
- bge[1].textureView = input_view;
- bge[2].binding = PP_BINDING_UNIFORMS;
- bge[2].buffer = uniforms.buffer;
- bge[2].size = uniforms.size;
- bge[3].binding = PP_BINDING_EFFECT_PARAMS;
- bge[3].buffer =
- effect_params.buffer ? effect_params.buffer : g_dummy_buffer.buffer;
- bge[3].size = effect_params.buffer ? effect_params.size : g_dummy_buffer.size;
- WGPUBindGroupDescriptor bgd = {
- .layout = bgl, .entryCount = 4, .entries = bge};
- *bind_group = wgpuDeviceCreateBindGroup(device, &bgd);
+ WGPUSampler sampler = SamplerCache::Get().get_or_create(device, SamplerCache::linear());
+
+ *bind_group = BindGroupBuilder()
+ .sampler(PP_BINDING_SAMPLER, sampler)
+ .texture(PP_BINDING_TEXTURE, input_view)
+ .buffer(PP_BINDING_UNIFORMS, uniforms.buffer, uniforms.size)
+ .buffer(PP_BINDING_EFFECT_PARAMS,
+ effect_params.buffer ? effect_params.buffer : g_dummy_buffer.buffer,
+ effect_params.buffer ? effect_params.size : g_dummy_buffer.size)
+ .build(device, bgl);
}
diff --git a/src/gpu/effects/rotating_cube_effect.cc b/src/gpu/effects/rotating_cube_effect.cc
index 8d1f05a..da973e5 100644
--- a/src/gpu/effects/rotating_cube_effect.cc
+++ b/src/gpu/effects/rotating_cube_effect.cc
@@ -5,16 +5,14 @@
#include "gpu/effects/rotating_cube_effect.h"
#include "generated/assets.h"
#include "gpu/effects/shader_composer.h"
+#include "gpu/sampler_cache.h"
#include "util/asset_manager_utils.h"
RotatingCubeEffect::RotatingCubeEffect(const GpuContext& ctx) : Effect(ctx) {
}
RotatingCubeEffect::~RotatingCubeEffect() {
- if (mask_sampler_)
- wgpuSamplerRelease(mask_sampler_);
- if (noise_sampler_)
- wgpuSamplerRelease(noise_sampler_);
+ // Samplers owned by SamplerCache - don't release
if (noise_view_)
wgpuTextureViewRelease(noise_view_);
if (noise_texture_)
@@ -49,21 +47,8 @@ void RotatingCubeEffect::init(MainSequence* demo) {
noise_texture_ = wgpuDeviceCreateTexture(ctx_.device, &tex_desc);
noise_view_ = wgpuTextureCreateView(noise_texture_, nullptr);
- WGPUSamplerDescriptor sampler_desc = {};
- sampler_desc.addressModeU = WGPUAddressMode_Repeat;
- sampler_desc.addressModeV = WGPUAddressMode_Repeat;
- sampler_desc.magFilter = WGPUFilterMode_Linear;
- sampler_desc.minFilter = WGPUFilterMode_Linear;
- sampler_desc.maxAnisotropy = 1;
- noise_sampler_ = wgpuDeviceCreateSampler(ctx_.device, &sampler_desc);
-
- WGPUSamplerDescriptor mask_sampler_desc = {};
- mask_sampler_desc.addressModeU = WGPUAddressMode_ClampToEdge;
- mask_sampler_desc.addressModeV = WGPUAddressMode_ClampToEdge;
- mask_sampler_desc.magFilter = WGPUFilterMode_Linear;
- mask_sampler_desc.minFilter = WGPUFilterMode_Linear;
- mask_sampler_desc.maxAnisotropy = 1;
- mask_sampler_ = wgpuDeviceCreateSampler(ctx_.device, &mask_sampler_desc);
+ noise_sampler_ = SamplerCache::Get().get_or_create(ctx_.device, SamplerCache::linear());
+ mask_sampler_ = SamplerCache::Get().get_or_create(ctx_.device, SamplerCache::clamp());
size_t shader_size;
const char* shader_code =
diff --git a/src/gpu/effects/scene1_effect.cc b/src/gpu/effects/scene1_effect.cc
new file mode 100644
index 0000000..a6733b7
--- /dev/null
+++ b/src/gpu/effects/scene1_effect.cc
@@ -0,0 +1,28 @@
+// This file is part of the 64k demo project.
+// Scene1 effect - ShaderToy conversion (raymarching scene)
+
+#include "gpu/demo_effects.h"
+#include "gpu/gpu.h"
+
+Scene1Effect::Scene1Effect(const GpuContext& ctx) : Effect(ctx) {
+ ResourceBinding bindings[] = {{uniforms_.get(), WGPUBufferBindingType_Uniform}};
+ pass_ = gpu_create_render_pass(ctx_.device, ctx_.format, scene1_shader_wgsl,
+ bindings, 1);
+ pass_.vertex_count = 3;
+}
+
+void Scene1Effect::render(WGPURenderPassEncoder pass, float t, float b,
+ float i, float a) {
+ CommonPostProcessUniforms u = {
+ .resolution = {(float)width_, (float)height_},
+ ._pad = {0.0f, 0.0f},
+ .aspect_ratio = a,
+ .time = t,
+ .beat = b,
+ .audio_intensity = i,
+ };
+ uniforms_.update(ctx_.queue, u);
+ wgpuRenderPassEncoderSetPipeline(pass, pass_.pipeline);
+ wgpuRenderPassEncoderSetBindGroup(pass, 0, pass_.bind_group, 0, nullptr);
+ wgpuRenderPassEncoderDraw(pass, pass_.vertex_count, 1, 0, 0);
+}
diff --git a/src/gpu/effects/scene1_effect.h b/src/gpu/effects/scene1_effect.h
new file mode 100644
index 0000000..dc5c747
--- /dev/null
+++ b/src/gpu/effects/scene1_effect.h
@@ -0,0 +1,19 @@
+// This file is part of the 64k demo project.
+// Scene1 effect - ShaderToy conversion (raymarching scene)
+
+#ifndef SCENE1_EFFECT_H_
+#define SCENE1_EFFECT_H_
+
+#include "gpu/effect.h"
+
+class Scene1Effect : public Effect {
+ public:
+ Scene1Effect(const GpuContext& ctx);
+ void render(WGPURenderPassEncoder pass, float time, float beat,
+ float intensity, float aspect_ratio) override;
+
+ private:
+ RenderPass pass_;
+};
+
+#endif /* SCENE1_EFFECT_H_ */
diff --git a/src/gpu/effects/shaders.cc b/src/gpu/effects/shaders.cc
index 6559bf5..5f78298 100644
--- a/src/gpu/effects/shaders.cc
+++ b/src/gpu/effects/shaders.cc
@@ -98,6 +98,10 @@ const char* solarize_shader_wgsl =
SafeGetAsset(AssetId::ASSET_SHADER_SOLARIZE);
+const char* scene1_shader_wgsl =
+
+ SafeGetAsset(AssetId::ASSET_SHADER_SCENE1);
+
const char* distort_shader_wgsl =
SafeGetAsset(AssetId::ASSET_SHADER_DISTORT);
diff --git a/src/gpu/effects/shaders.h b/src/gpu/effects/shaders.h
index 7acc2a6..03fa48c 100644
--- a/src/gpu/effects/shaders.h
+++ b/src/gpu/effects/shaders.h
@@ -15,6 +15,7 @@ extern const char* ellipse_shader_wgsl;
extern const char* particle_spray_compute_wgsl;
extern const char* gaussian_blur_shader_wgsl;
extern const char* solarize_shader_wgsl;
+extern const char* scene1_shader_wgsl;
extern const char* distort_shader_wgsl;
extern const char* chroma_aberration_shader_wgsl;
extern const char* vignette_shader_wgsl;
diff --git a/src/gpu/pipeline_builder.h b/src/gpu/pipeline_builder.h
new file mode 100644
index 0000000..06b4ceb
--- /dev/null
+++ b/src/gpu/pipeline_builder.h
@@ -0,0 +1,109 @@
+// WGPU render pipeline builder - reduces pipeline creation boilerplate
+#pragma once
+#include <vector>
+#include <string>
+
+// Forward declarations (users must include gpu.h and shader_composer.h)
+struct WGPUDeviceImpl;
+typedef struct WGPUDeviceImpl* WGPUDevice;
+struct WGPUBindGroupLayoutImpl;
+typedef struct WGPUBindGroupLayoutImpl* WGPUBindGroupLayout;
+struct WGPURenderPipelineImpl;
+typedef struct WGPURenderPipelineImpl* WGPURenderPipeline;
+struct WGPUShaderModuleImpl;
+typedef struct WGPUShaderModuleImpl* WGPUShaderModule;
+
+#include "platform/platform.h"
+#include "gpu/effects/shader_composer.h"
+
+class RenderPipelineBuilder {
+ WGPUDevice device_;
+ WGPURenderPipelineDescriptor desc_{};
+ WGPUColorTargetState color_{};
+ WGPUBlendState blend_{};
+ WGPUDepthStencilState depth_{};
+ std::vector<WGPUBindGroupLayout> layouts_;
+ std::string shader_text_;
+ WGPUShaderModule shader_module_ = nullptr;
+ bool has_blend_ = false;
+ bool has_depth_ = false;
+
+public:
+ explicit RenderPipelineBuilder(WGPUDevice device) : device_(device) {
+ desc_.primitive.topology = WGPUPrimitiveTopology_TriangleList;
+ desc_.primitive.cullMode = WGPUCullMode_None;
+ desc_.multisample.count = 1;
+ desc_.multisample.mask = 0xFFFFFFFF;
+ }
+
+ RenderPipelineBuilder& shader(const char* wgsl, bool compose = true) {
+ shader_text_ = compose ? ShaderComposer::Get().Compose({}, wgsl) : wgsl;
+ WGPUShaderSourceWGSL wgsl_src{};
+ wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL;
+ wgsl_src.code = str_view(shader_text_.c_str());
+ WGPUShaderModuleDescriptor shader_desc{};
+ shader_desc.nextInChain = &wgsl_src.chain;
+ shader_module_ = wgpuDeviceCreateShaderModule(device_, &shader_desc);
+ desc_.vertex.module = shader_module_;
+ desc_.vertex.entryPoint = str_view("vs_main");
+ return *this;
+ }
+
+ RenderPipelineBuilder& bind_group_layout(WGPUBindGroupLayout layout) {
+ layouts_.push_back(layout);
+ return *this;
+ }
+
+ RenderPipelineBuilder& format(WGPUTextureFormat fmt) {
+ color_.format = fmt;
+ return *this;
+ }
+
+ RenderPipelineBuilder& blend_alpha() {
+ has_blend_ = true;
+ blend_.color.operation = WGPUBlendOperation_Add;
+ blend_.color.srcFactor = WGPUBlendFactor_SrcAlpha;
+ blend_.color.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha;
+ blend_.alpha.operation = WGPUBlendOperation_Add;
+ blend_.alpha.srcFactor = WGPUBlendFactor_One;
+ blend_.alpha.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha;
+ return *this;
+ }
+
+ RenderPipelineBuilder& depth(WGPUTextureFormat depth_fmt = WGPUTextureFormat_Depth24Plus) {
+ has_depth_ = true;
+ depth_.format = depth_fmt;
+ depth_.depthWriteEnabled = WGPUOptionalBool_True;
+ depth_.depthCompare = WGPUCompareFunction_Less;
+ return *this;
+ }
+
+ RenderPipelineBuilder& cull_back() {
+ desc_.primitive.cullMode = WGPUCullMode_Back;
+ return *this;
+ }
+
+ WGPURenderPipeline build() {
+ color_.writeMask = WGPUColorWriteMask_All;
+ if (has_blend_) color_.blend = &blend_;
+
+ WGPUFragmentState fragment{};
+ fragment.module = shader_module_;
+ fragment.entryPoint = str_view("fs_main");
+ fragment.targetCount = 1;
+ fragment.targets = &color_;
+
+ WGPUPipelineLayoutDescriptor pl_desc{};
+ pl_desc.bindGroupLayoutCount = layouts_.size();
+ pl_desc.bindGroupLayouts = layouts_.data();
+ WGPUPipelineLayout layout = wgpuDeviceCreatePipelineLayout(device_, &pl_desc);
+
+ desc_.layout = layout;
+ desc_.fragment = &fragment;
+ if (has_depth_) desc_.depthStencil = &depth_;
+
+ WGPURenderPipeline pipeline = wgpuDeviceCreateRenderPipeline(device_, &desc_);
+ wgpuPipelineLayoutRelease(layout);
+ return pipeline;
+ }
+};
diff --git a/src/gpu/sampler_cache.h b/src/gpu/sampler_cache.h
new file mode 100644
index 0000000..0f012a8
--- /dev/null
+++ b/src/gpu/sampler_cache.h
@@ -0,0 +1,61 @@
+// Sampler cache - deduplicates samplers across effects
+#pragma once
+#include <map>
+
+// Forward declarations (users must include gpu.h)
+struct WGPUDeviceImpl;
+typedef struct WGPUDeviceImpl* WGPUDevice;
+struct WGPUSamplerImpl;
+typedef struct WGPUSamplerImpl* WGPUSampler;
+
+#include "platform/platform.h"
+
+struct SamplerSpec {
+ WGPUAddressMode u, v;
+ WGPUFilterMode mag, min;
+ uint16_t anisotropy;
+
+ bool operator<(const SamplerSpec& o) const {
+ if (u != o.u) return u < o.u;
+ if (v != o.v) return v < o.v;
+ if (mag != o.mag) return mag < o.mag;
+ if (min != o.min) return min < o.min;
+ return anisotropy < o.anisotropy;
+ }
+};
+
+class SamplerCache {
+ std::map<SamplerSpec, WGPUSampler> cache_;
+ SamplerCache() = default;
+
+public:
+ static SamplerCache& Get() {
+ static SamplerCache instance;
+ return instance;
+ }
+
+ WGPUSampler get_or_create(WGPUDevice device, const SamplerSpec& spec) {
+ auto it = cache_.find(spec);
+ if (it != cache_.end()) return it->second;
+
+ WGPUSamplerDescriptor desc{};
+ desc.addressModeU = spec.u;
+ desc.addressModeV = spec.v;
+ desc.magFilter = spec.mag;
+ desc.minFilter = spec.min;
+ desc.maxAnisotropy = spec.anisotropy;
+ WGPUSampler sampler = wgpuDeviceCreateSampler(device, &desc);
+ cache_[spec] = sampler;
+ return sampler;
+ }
+
+ // Common presets
+ static SamplerSpec linear() {
+ return {WGPUAddressMode_Repeat, WGPUAddressMode_Repeat,
+ WGPUFilterMode_Linear, WGPUFilterMode_Linear, 1};
+ }
+ static SamplerSpec clamp() {
+ return {WGPUAddressMode_ClampToEdge, WGPUAddressMode_ClampToEdge,
+ WGPUFilterMode_Linear, WGPUFilterMode_Linear, 1};
+ }
+};