From a958c6ca8dd48f642570037df127a4b23c984d82 Mon Sep 17 00:00:00 2001 From: skal Date: Sat, 31 Jan 2026 17:05:15 +0100 Subject: feat: Multi-pass rendering architecture and effect stubs Implements a post-processing pipeline using offscreen framebuffers. Adds stubs for MovingEllipse, ParticleSpray, GaussianBlur, Solarize, Distort, and ChromaAberration effects. Updates MainSequence to orchestrate the scene pass and post-processing chain. --- src/gpu/demo_effects.cc | 455 +++++++++++++++++++++--------------------------- 1 file changed, 199 insertions(+), 256 deletions(-) (limited to 'src/gpu/demo_effects.cc') diff --git a/src/gpu/demo_effects.cc b/src/gpu/demo_effects.cc index 869cd12..45a1bea 100644 --- a/src/gpu/demo_effects.cc +++ b/src/gpu/demo_effects.cc @@ -1,302 +1,245 @@ // This file is part of the 64k demo project. // It implements the concrete effects used in the demo. -#include "demo_effects.h" +#include "gpu/demo_effects.h" +#include "gpu/gpu.h" #include #include #include #include -static const int NUM_PARTICLES = 10000; - -struct Particle { - float pos[4]; // x, y, z, life - float vel[4]; // vx, vy, vz, padding - float rot[4]; // angle, speed, padding, padding - float color[4]; // r, g, b, a -}; +// Helper to create a standard post-processing pipeline +static WGPURenderPipeline +create_post_process_pipeline(WGPUDevice device, WGPUTextureFormat format, + const char *shader_code) { + WGPUShaderModuleDescriptor shader_desc = {}; + WGPUShaderSourceWGSL wgsl_src = {}; + wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL; + wgsl_src.code = str_view(shader_code); + shader_desc.nextInChain = &wgsl_src.chain; + WGPUShaderModule shader_module = + wgpuDeviceCreateShaderModule(device, &shader_desc); + + WGPUBindGroupLayoutEntry bgl_entries[2] = {}; + bgl_entries[0].binding = 0; + bgl_entries[0].visibility = WGPUShaderStage_Fragment; + bgl_entries[0].sampler.type = WGPUSamplerBindingType_Filtering; + bgl_entries[1].binding = 1; + bgl_entries[1].visibility = WGPUShaderStage_Fragment; + bgl_entries[1].texture.sampleType = WGPUTextureSampleType_Float; + + WGPUBindGroupLayoutDescriptor bgl_desc = {}; + bgl_desc.entryCount = 2; + 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; + + return wgpuDeviceCreateRenderPipeline(device, &pipeline_desc); +} const char *main_shader_wgsl = R"( -struct Uniforms { - audio_peak : f32, - aspect_ratio: f32, - time: f32, -}; - -@group(0) @binding(0) var uniforms : Uniforms; - -@vertex -fn vs_main(@builtin(vertex_index) vertex_index: u32) -> @builtin(position) vec4 { - let PI = 3.14159265; - let num_sides = 7.0; - - // Pulse scale based on audio peak - let base_scale = 0.5; - let pulse_scale = 0.3 * uniforms.audio_peak; - let scale = base_scale + pulse_scale; - - let tri_idx = f32(vertex_index / 3u); - let sub_idx = vertex_index % 3u; - - if (sub_idx == 0u) { - return vec4(0.0, 0.0, 0.0, 1.0); - } - - // Apply rotation based on time - let rotation = uniforms.time * 0.5; - let i = tri_idx + f32(sub_idx - 1u); - let angle = i * 2.0 * PI / num_sides + rotation; - let x = scale * cos(angle) / uniforms.aspect_ratio; - let y = scale * sin(angle); - - return vec4(x, y, 0.0, 1.0); +struct Uniforms { audio_peak: f32, aspect_ratio: f32, time: f32, }; +@group(0) @binding(0) var uniforms: Uniforms; +@vertex fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4 { + let PI = 3.14159265; let num_sides = 7.0; + let scale = 0.5 + 0.3 * uniforms.audio_peak; + let tri_idx = f32(i/3u); let sub_idx = i%3u; + if (sub_idx == 0u) { return vec4(0.0,0.0,0.0,1.0); } + let angle = (tri_idx + f32(sub_idx - 1u)) * 2.0 * PI / num_sides + uniforms.time * 0.5; + return vec4(scale*cos(angle)/uniforms.aspect_ratio, scale*sin(angle), 0.0, 1.0); } - -@fragment -fn fs_main() -> @location(0) vec4 { - // Dynamic color shifting based on time and responsiveness to peak +@fragment fn fs_main() -> @location(0) vec4 { let h = uniforms.time * 2.0 + uniforms.audio_peak * 3.0; - let r = sin(h + 0.0) * 0.5 + 0.5; - let g = sin(h + 2.0) * 0.9 + 0.3; - let b = sin(h + 4.0) * 0.5 + 0.5; - + let r = sin(h)*0.5+0.5; let g = sin(h+2.0)*0.9+0.3; let b = sin(h+4.0)*0.5+0.5; let boost = uniforms.audio_peak * 0.5; - return vec4(r + boost, g + boost, b + boost, 0.5); // Alpha 0.5 for blending + return vec4(r+boost,g+boost,b+boost, 0.5); +})"; + +const char *passthrough_shader_wgsl = R"( +@group(0) @binding(0) var smplr: sampler; +@group(0) @binding(1) var txt: texture_2d; +@vertex fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4 { + var pos = array, 3>(vec2(-1,-1), vec2(3,-1), vec2(-1, 3)); + return vec4(pos[i], 0.0, 1.0); } -)"; - -const char *particle_compute_wgsl = R"( -struct Particle { - pos : vec4, - vel : vec4, - rot : vec4, - color : vec4, -}; - -struct Uniforms { - audio_peak : f32, - aspect_ratio: f32, - time: f32, -}; - -@group(0) @binding(0) var particles : array; -@group(0) @binding(1) var uniforms : Uniforms; - -@compute @workgroup_size(64) -fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3) { - let index = GlobalInvocationID.x; - if (index >= arrayLength(&particles)) { - return; - } - - var p = particles[index]; - - // Update Position - p.pos.x = p.pos.x + p.vel.x * 0.016; - p.pos.y = p.pos.y + p.vel.y * 0.016; - p.pos.z = p.pos.z + p.vel.z * 0.016; - - // Gravity / Audio attraction - p.vel.y = p.vel.y - 0.01 * (1.0 + uniforms.audio_peak * 5.0); - - // Rotate - p.rot.x = p.rot.x + p.rot.y * 0.016; - - // Reset if out of bounds - if (p.pos.y < -1.5) { - p.pos.y = 1.5; - p.pos.x = (f32(index % 100u) / 50.0) - 1.0 + (uniforms.audio_peak * 0.5); - p.vel.y = 0.0; - p.vel.x = (f32(index % 10u) - 5.0) * 0.1; - } - - particles[index] = p; +@fragment fn fs_main(@builtin(position) p: vec4) -> @location(0) vec4 { + return textureSample(txt, smplr, p.xy / vec2(1280.0, 720.0)); // FIXME: Resolution +})"; + +const char *gaussian_blur_shader_wgsl = R"( +@group(0) @binding(0) var smplr: sampler; +@group(0) @binding(1) var txt: texture_2d; +@vertex fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4 { + var pos = array, 3>(vec2(-1,-1), vec2(3,-1), vec2(-1, 3)); + return vec4(pos[i], 0.0, 1.0); } -)"; - -const char *particle_render_wgsl = R"( -struct Particle { - pos : vec4, - vel : vec4, - rot : vec4, - color : vec4, -}; - -struct Uniforms { - audio_peak : f32, - aspect_ratio: f32, - time: f32, -}; - -@group(0) @binding(0) var particles : array; -@group(0) @binding(1) var uniforms : Uniforms; - -struct VertexOutput { - @builtin(position) Position : vec4, - @location(0) Color : vec4, -}; - -@vertex -fn vs_main(@builtin(vertex_index) vertex_index : u32, @builtin(instance_index) instance_index : u32) -> VertexOutput { - let p = particles[instance_index]; - - // Simple quad expansion - let size = 0.02 + p.pos.z * 0.01 + uniforms.audio_peak * 0.02; - - // Vertex ID 0..5 for 2 triangles (Quad) - // 0 1 2, 2 1 3 (Strip-like order manually mapped) - var offsets = array, 6>( - vec2(-1.0, -1.0), - vec2( 1.0, -1.0), - vec2(-1.0, 1.0), - vec2(-1.0, 1.0), - vec2( 1.0, -1.0), - vec2( 1.0, 1.0) - ); - - let offset = offsets[vertex_index]; - - // Rotate - let c = cos(p.rot.x); - let s = sin(p.rot.x); - let rot_x = offset.x * c - offset.y * s; - let rot_y = offset.x * s + offset.y * c; - - let x = p.pos.x + rot_x * size / uniforms.aspect_ratio; - let y = p.pos.y + rot_y * size; - - var output : VertexOutput; - output.Position = vec4(x, y, 0.0, 1.0); - output.Color = p.color * (0.5 + 0.5 * uniforms.audio_peak); - return output; +@fragment fn fs_main(@builtin(position) p: vec4) -> @location(0) vec4 { + return textureSample(txt, smplr, p.xy / vec2(1280.0, 720.0)); +})"; + +const char *solarize_shader_wgsl = R"( +@group(0) @binding(0) var smplr: sampler; +@group(0) @binding(1) var txt: texture_2d; +@vertex fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4 { + var pos = array, 3>(vec2(-1,-1), vec2(3,-1), vec2(-1, 3)); + return vec4(pos[i], 0.0, 1.0); } - -@fragment -fn fs_main(@location(0) Color : vec4) -> @location(0) vec4 { - return Color; +@fragment fn fs_main(@builtin(position) p: vec4) -> @location(0) vec4 { + return textureSample(txt, smplr, p.xy / vec2(1280.0, 720.0)); +})"; + +const char *distort_shader_wgsl = R"( +@group(0) @binding(0) var smplr: sampler; +@group(0) @binding(1) var txt: texture_2d; +@vertex fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4 { + var pos = array, 3>(vec2(-1,-1), vec2(3,-1), vec2(-1, 3)); + return vec4(pos[i], 0.0, 1.0); } -)"; +@fragment fn fs_main(@builtin(position) p: vec4) -> @location(0) vec4 { + return textureSample(txt, smplr, p.xy / vec2(1280.0, 720.0)); +})"; + +const char *chroma_aberration_shader_wgsl = R"( +@group(0) @binding(0) var smplr: sampler; +@group(0) @binding(1) var txt: texture_2d; +@vertex fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4 { + var pos = array, 3>(vec2(-1,-1), vec2(3,-1), vec2(-1, 3)); + return vec4(pos[i], 0.0, 1.0); +} +@fragment fn fs_main(@builtin(position) p: vec4) -> @location(0) vec4 { + return textureSample(txt, smplr, p.xy / vec2(1280.0, 720.0)); +})"; // --- HeptagonEffect --- - HeptagonEffect::HeptagonEffect(WGPUDevice device, WGPUQueue queue, WGPUTextureFormat format) : queue_(queue) { - uniforms_ = gpu_create_buffer( - device, sizeof(float) * 4, - WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst, nullptr); - + uniforms_ = gpu_create_buffer(device, sizeof(float) * 4, + WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst); ResourceBinding bindings[] = {{uniforms_, WGPUBufferBindingType_Uniform}}; pass_ = gpu_create_render_pass(device, format, main_shader_wgsl, bindings, 1); pass_.vertex_count = 21; } - void HeptagonEffect::render(WGPURenderPassEncoder pass, float time, float beat, float intensity, float aspect_ratio) { - struct { - float audio_peak; - float aspect_ratio; - float time; - float padding; - } u = {intensity, aspect_ratio, time, 0.0f}; - + struct { float p, a, t, d; } u = {intensity, aspect_ratio, time, 0.0f}; wgpuQueueWriteBuffer(queue_, uniforms_.buffer, 0, &u, sizeof(u)); - wgpuRenderPassEncoderSetPipeline(pass, pass_.pipeline); wgpuRenderPassEncoderSetBindGroup(pass, 0, pass_.bind_group, 0, nullptr); wgpuRenderPassEncoderDraw(pass, pass_.vertex_count, 1, 0, 0); } // --- ParticlesEffect --- - ParticlesEffect::ParticlesEffect(WGPUDevice device, WGPUQueue queue, WGPUTextureFormat format) : queue_(queue) { - uniforms_ = gpu_create_buffer( - device, sizeof(float) * 4, - WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst, nullptr); - - std::vector initial_particles(NUM_PARTICLES); - for (int i = 0; i < NUM_PARTICLES; ++i) { - initial_particles[i].pos[0] = ((float)(rand() % 100) / 50.0f) - 1.0f; - initial_particles[i].pos[1] = ((float)(rand() % 100) / 50.0f) - 1.0f; - initial_particles[i].pos[2] = 0.0f; - initial_particles[i].pos[3] = 1.0f; - - initial_particles[i].vel[0] = 0.0f; - initial_particles[i].vel[1] = 0.0f; - - initial_particles[i].rot[0] = 0.0f; - initial_particles[i].rot[1] = ((float)(rand() % 10) / 100.0f); - - initial_particles[i].color[0] = (float)(rand() % 10) / 10.0f; - initial_particles[i].color[1] = (float)(rand() % 10) / 10.0f; - initial_particles[i].color[2] = 1.0f; - initial_particles[i].color[3] = 1.0f; - } - - particles_buffer_ = gpu_create_buffer( - device, sizeof(Particle) * NUM_PARTICLES, - (WGPUBufferUsage)(WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst | - WGPUBufferUsage_Vertex), - initial_particles.data()); - - ResourceBinding compute_bindings[] = { - {particles_buffer_, WGPUBufferBindingType_Storage}, - {uniforms_, WGPUBufferBindingType_Uniform}}; - compute_pass_ = gpu_create_compute_pass(device, particle_compute_wgsl, - compute_bindings, 2); - compute_pass_.workgroup_size_x = (NUM_PARTICLES + 63) / 64; - compute_pass_.workgroup_size_y = 1; - compute_pass_.workgroup_size_z = 1; - - ResourceBinding render_bindings[] = { - {particles_buffer_, WGPUBufferBindingType_ReadOnlyStorage}, - {uniforms_, WGPUBufferBindingType_Uniform}}; - render_pass_ = gpu_create_render_pass(device, format, particle_render_wgsl, - render_bindings, 2); - render_pass_.vertex_count = 6; - render_pass_.instance_count = NUM_PARTICLES; + // TODO: Restore real implementation } - void ParticlesEffect::compute(WGPUCommandEncoder encoder, float time, float beat, float intensity, float aspect_ratio) { - struct { - float audio_peak; - float aspect_ratio; - float time; - float padding; - } u = {intensity, aspect_ratio, time, 0.0f}; + (void)encoder; (void)time; (void)beat; (void)intensity; (void)aspect_ratio; +} +void ParticlesEffect::render(WGPURenderPassEncoder pass, float time, float beat, + float intensity, float aspect_ratio) { + (void)pass; (void)time; (void)beat; (void)intensity; (void)aspect_ratio; +} - wgpuQueueWriteBuffer(queue_, uniforms_.buffer, 0, &u, sizeof(u)); +// --- PassthroughEffect --- +PassthroughEffect::PassthroughEffect(WGPUDevice device, + WGPUTextureFormat format) + : device_(device) { + pipeline_ = create_post_process_pipeline(device, format, passthrough_shader_wgsl); +} +void PassthroughEffect::update_bind_group(WGPUTextureView input_view) { + if (bind_group_) wgpuBindGroupRelease(bind_group_); + WGPUBindGroupLayout bgl = wgpuRenderPipelineGetBindGroupLayout(pipeline_, 0); + WGPUSamplerDescriptor sd = {}; + WGPUSampler sampler = wgpuDeviceCreateSampler(device_, &sd); + WGPUBindGroupEntry bge[2] = {}; + bge[0].binding = 0; bge[0].sampler = sampler; + bge[1].binding = 1; bge[1].textureView = input_view; + WGPUBindGroupDescriptor bgd = {.layout = bgl, .entryCount = 2, .entries = bge}; + bind_group_ = wgpuDeviceCreateBindGroup(device_, &bgd); +} - WGPUComputePassDescriptor compute_desc = {}; - WGPUComputePassEncoder pass = - wgpuCommandEncoderBeginComputePass(encoder, &compute_desc); - wgpuComputePassEncoderSetPipeline(pass, compute_pass_.pipeline); - wgpuComputePassEncoderSetBindGroup(pass, 0, compute_pass_.bind_group, 0, - nullptr); - wgpuComputePassEncoderDispatchWorkgroups(pass, compute_pass_.workgroup_size_x, - 1, 1); - wgpuComputePassEncoderEnd(pass); +// --- Stubs for others --- +MovingEllipseEffect::MovingEllipseEffect(WGPUDevice device, WGPUQueue queue, + WGPUTextureFormat format) + : queue_(queue) {} +void MovingEllipseEffect::render(WGPURenderPassEncoder pass, float time, + float beat, float intensity, + float aspect_ratio) {} + +ParticleSprayEffect::ParticleSprayEffect(WGPUDevice device, WGPUQueue queue, + WGPUTextureFormat format) + : queue_(queue) {} +void ParticleSprayEffect::compute(WGPUCommandEncoder encoder, float time, + float beat, float intensity, + float aspect_ratio) {} +void ParticleSprayEffect::render(WGPURenderPassEncoder pass, float time, + float beat, float intensity, + float aspect_ratio) {} + +GaussianBlurEffect::GaussianBlurEffect(WGPUDevice device, WGPUQueue queue, + WGPUTextureFormat format) + : device_(device) { + (void)queue; + pipeline_ = + create_post_process_pipeline(device, format, gaussian_blur_shader_wgsl); +} +void GaussianBlurEffect::update_bind_group(WGPUTextureView input_view) { + (void)input_view; } -void ParticlesEffect::render(WGPURenderPassEncoder pass, float time, float beat, - float intensity, float aspect_ratio) { - // Update uniforms again? Technically redundant if compute happened same frame. - // But safer if render is called without compute (e.g. debugging). - struct { - float audio_peak; - float aspect_ratio; - float time; - float padding; - } u = {intensity, aspect_ratio, time, 0.0f}; - - wgpuQueueWriteBuffer(queue_, uniforms_.buffer, 0, &u, sizeof(u)); - - wgpuRenderPassEncoderSetPipeline(pass, render_pass_.pipeline); - wgpuRenderPassEncoderSetBindGroup(pass, 0, render_pass_.bind_group, 0, - nullptr); - wgpuRenderPassEncoderDraw(pass, render_pass_.vertex_count, - render_pass_.instance_count, 0, 0); +SolarizeEffect::SolarizeEffect(WGPUDevice device, WGPUQueue queue, + WGPUTextureFormat format) + : device_(device) { + (void)queue; + pipeline_ = create_post_process_pipeline(device, format, solarize_shader_wgsl); +} +void SolarizeEffect::update_bind_group(WGPUTextureView input_view) { + (void)input_view; +} + +DistortEffect::DistortEffect(WGPUDevice device, WGPUQueue queue, + WGPUTextureFormat format) + : device_(device) { + (void)queue; + pipeline_ = create_post_process_pipeline(device, format, distort_shader_wgsl); +} +void DistortEffect::update_bind_group(WGPUTextureView input_view) { + (void)input_view; +} + +ChromaAberrationEffect::ChromaAberrationEffect(WGPUDevice device, + WGPUQueue queue, + WGPUTextureFormat format) + : device_(device) { + (void)queue; + pipeline_ = + create_post_process_pipeline(device, format, chroma_aberration_shader_wgsl); } +void ChromaAberrationEffect::update_bind_group(WGPUTextureView input_view) { + (void)input_view; +} \ No newline at end of file -- cgit v1.2.3