summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt2
-rw-r--r--PROJECT_CONTEXT.md6
-rw-r--r--src/gpu/demo_effects.cc314
-rw-r--r--src/gpu/demo_effects.h40
-rw-r--r--src/gpu/effect.cc203
-rw-r--r--src/gpu/effect.h117
-rw-r--r--src/gpu/gpu.cc358
-rw-r--r--src/gpu/gpu.h10
-rw-r--r--src/main.cc3
9 files changed, 716 insertions, 337 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7030152..c7b7508 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -159,6 +159,8 @@ add_executable(demo64k
src/main.cc
src/platform.cc
src/gpu/gpu.cc
+ src/gpu/effect.cc
+ src/gpu/demo_effects.cc
src/audio/audio.cc
src/audio/gen.cc
src/audio/fdct.cc
diff --git a/PROJECT_CONTEXT.md b/PROJECT_CONTEXT.md
index ddde463..24b1517 100644
--- a/PROJECT_CONTEXT.md
+++ b/PROJECT_CONTEXT.md
@@ -35,8 +35,12 @@ Incoming tasks in no particular order:
- [x] 7. compile wgpu-native in optimized mode (not unoptimized)
- [x] 8. add a #define STRIP_ALL to remove all unnecessary code for the final build (for instance, command-line args parsing, or unnecessary options, constant parameters to function calls, etc.)
- [x] 9. work on the compact in-line and off-line asset system (@ASSET_SYSTEM.md)
-- [x] 10. optimize spectool to remove trailing zeroes from a spec file before saving it
+- 10. optimize spectool to remove trailing zeroes from a spec file before saving it
- [x] 11. implement a general [time / timer / beat] system in the demo, for effects timing
+- 12. Implement 'Sequence' and 'Effect' system:
+ - `Effect` interface: `start()`, `draw(time, beat, intensity)`, `end()`.
+ - `Sequence` manager: Handles a list of potentially overlapping effects.
+ - `time` is relative to sequence start. `beat` is a [0..1] pulse. `intensity` is signal strength (e.g. music level).
## Session Decisions and Current State
diff --git a/src/gpu/demo_effects.cc b/src/gpu/demo_effects.cc
new file mode 100644
index 0000000..5fc7c15
--- /dev/null
+++ b/src/gpu/demo_effects.cc
@@ -0,0 +1,314 @@
+// This file is part of the 64k demo project.
+// It implements the concrete effects used in the demo.
+
+#include "demo_effects.h"
+#include <cmath>
+#include <cstdlib>
+#include <cstring>
+#include <vector>
+
+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
+};
+
+const char *main_shader_wgsl = R"(
+struct Uniforms {
+ audio_peak : f32,
+ aspect_ratio: f32,
+ time: f32,
+};
+
+@group(0) @binding(0) var<uniform> uniforms : Uniforms;
+
+@vertex
+fn vs_main(@builtin(vertex_index) vertex_index: u32) -> @builtin(position) vec4<f32> {
+ 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<f32>(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<f32>(x, y, 0.0, 1.0);
+}
+
+@fragment
+fn fs_main() -> @location(0) vec4<f32> {
+ // Dynamic color shifting based on time and responsiveness to peak
+ 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 boost = uniforms.audio_peak * 0.5;
+ return vec4<f32>(r + boost, g + boost, b + boost, 0.5); // Alpha 0.5 for blending
+}
+)";
+
+const char *particle_compute_wgsl = R"(
+struct Particle {
+ pos : vec4<f32>,
+ vel : vec4<f32>,
+ rot : vec4<f32>,
+ color : vec4<f32>,
+};
+
+struct Uniforms {
+ audio_peak : f32,
+ aspect_ratio: f32,
+ time: f32,
+};
+
+@group(0) @binding(0) var<storage, read_write> particles : array<Particle>;
+@group(0) @binding(1) var<uniform> uniforms : Uniforms;
+
+@compute @workgroup_size(64)
+fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) {
+ 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;
+}
+)";
+
+const char *particle_render_wgsl = R"(
+struct Particle {
+ pos : vec4<f32>,
+ vel : vec4<f32>,
+ rot : vec4<f32>,
+ color : vec4<f32>,
+};
+
+struct Uniforms {
+ audio_peak : f32,
+ aspect_ratio: f32,
+ time: f32,
+};
+
+@group(0) @binding(0) var<storage, read> particles : array<Particle>;
+@group(0) @binding(1) var<uniform> uniforms : Uniforms;
+
+struct VertexOutput {
+ @builtin(position) Position : vec4<f32>,
+ @location(0) Color : vec4<f32>,
+};
+
+@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<vec2<f32>, 6>(
+ vec2<f32>(-1.0, -1.0),
+ vec2<f32>( 1.0, -1.0),
+ vec2<f32>(-1.0, 1.0),
+ vec2<f32>(-1.0, 1.0),
+ vec2<f32>( 1.0, -1.0),
+ vec2<f32>( 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<f32>(x, y, 0.0, 1.0);
+ output.Color = p.color * (0.5 + 0.5 * uniforms.audio_peak);
+ return output;
+}
+
+@fragment
+fn fs_main(@location(0) Color : vec4<f32>) -> @location(0) vec4<f32> {
+ return Color;
+}
+)";
+
+// --- HeptagonEffect ---
+
+HeptagonEffect::HeptagonEffect(WGPUDevice device, WGPUQueue queue,
+ WGPUTextureFormat format)
+ : queue_(queue) {
+ uniforms_ = gpu_create_buffer(
+ device, sizeof(float) * 4,
+ WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst, nullptr);
+
+ 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};
+
+ 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<Particle> 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;
+}
+
+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};
+
+ wgpuQueueWriteBuffer(queue_, uniforms_.buffer, 0, &u, sizeof(u));
+
+ 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);
+}
+
+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);
+}
+
+std::shared_ptr<Sequence> create_demo_sequence(WGPUDevice device,
+ WGPUQueue queue,
+ WGPUTextureFormat format) {
+ auto seq = std::make_shared<Sequence>();
+ // Overlap them for now to replicate original behavior
+ seq->add_effect(std::make_shared<HeptagonEffect>(device, queue, format), 0.0f,
+ 1000.0f);
+ seq->add_effect(std::make_shared<ParticlesEffect>(device, queue, format),
+ 0.0f, 1000.0f);
+ return seq;
+}
diff --git a/src/gpu/demo_effects.h b/src/gpu/demo_effects.h
new file mode 100644
index 0000000..81321d9
--- /dev/null
+++ b/src/gpu/demo_effects.h
@@ -0,0 +1,40 @@
+// This file is part of the 64k demo project.
+// It declares the concrete effects used in the demo.
+
+#pragma once
+#include "effect.h"
+#include "gpu.h"
+#include <memory>
+
+class HeptagonEffect : public Effect {
+public:
+ HeptagonEffect(WGPUDevice device, WGPUQueue queue, WGPUTextureFormat format);
+ void render(WGPURenderPassEncoder pass, float time, float beat,
+ float intensity, float aspect_ratio) override;
+
+private:
+ WGPUQueue queue_;
+ RenderPass pass_;
+ GpuBuffer uniforms_;
+};
+
+class ParticlesEffect : public Effect {
+public:
+ ParticlesEffect(WGPUDevice device, WGPUQueue queue, WGPUTextureFormat format);
+ void compute(WGPUCommandEncoder encoder, float time, float beat,
+ float intensity, float aspect_ratio) override;
+ void render(WGPURenderPassEncoder pass, float time, float beat,
+ float intensity, float aspect_ratio) override;
+
+private:
+ WGPUQueue queue_;
+ ComputePass compute_pass_;
+ RenderPass render_pass_;
+ GpuBuffer particles_buffer_;
+ GpuBuffer uniforms_;
+};
+
+// Factory
+std::shared_ptr<Sequence> create_demo_sequence(WGPUDevice device,
+ WGPUQueue queue,
+ WGPUTextureFormat format);
diff --git a/src/gpu/effect.cc b/src/gpu/effect.cc
new file mode 100644
index 0000000..cfef7f9
--- /dev/null
+++ b/src/gpu/effect.cc
@@ -0,0 +1,203 @@
+// This file is part of the 64k demo project.
+// It implements the Sequence management logic.
+
+#include "effect.h"
+#include <algorithm>
+#include <cstdio>
+
+// --- Sequence Implementation ---
+
+void Sequence::init(MainSequence *demo) {
+ for (auto &item : items_) {
+ if (!item.effect->is_initialized) {
+ item.effect->init(demo);
+ item.effect->is_initialized = true;
+ }
+ }
+}
+
+void Sequence::add_effect(std::shared_ptr<Effect> effect, float start_time,
+ float end_time, int priority) {
+ items_.push_back({effect, start_time, end_time, priority, false});
+ is_sorted_ = false;
+}
+
+void Sequence::sort_items() {
+ if (is_sorted_) return;
+ // Sort by priority ascending (0 draws first, 100 draws on top)
+ std::sort(items_.begin(), items_.end(),
+ [](const SequenceItem &a, const SequenceItem &b) {
+ return a.priority < b.priority;
+ });
+ is_sorted_ = true;
+}
+
+void Sequence::update_active_list(float seq_time) {
+ for (auto &item : items_) {
+ bool should_be_active =
+ (seq_time >= item.start_time && seq_time < item.end_time);
+
+ if (should_be_active) {
+ if (!item.active) {
+ item.effect->start();
+ item.active = true;
+ }
+ } else {
+ if (item.active) {
+ item.effect->end();
+ item.active = false;
+ }
+ }
+ }
+}
+
+void Sequence::dispatch_compute(WGPUCommandEncoder encoder, float seq_time,
+ float beat, float intensity,
+ float aspect_ratio) {
+ sort_items();
+ for (auto &item : items_) {
+ if (item.active) {
+ item.effect->compute(encoder, seq_time - item.start_time, beat, intensity,
+ aspect_ratio);
+ }
+ }
+}
+
+void Sequence::dispatch_render(WGPURenderPassEncoder pass, float seq_time,
+ float beat, float intensity,
+ float aspect_ratio) {
+ sort_items(); // Should be sorted already but safe to check
+ for (auto &item : items_) {
+ if (item.active) {
+ item.effect->render(pass, seq_time - item.start_time, beat, intensity,
+ aspect_ratio);
+ }
+ }
+}
+
+void Sequence::reset() {
+ for (auto &item : items_) {
+ if (item.active) {
+ item.effect->end();
+ item.active = false;
+ }
+ }
+}
+
+// --- MainSequence Implementation ---
+
+void MainSequence::init(WGPUDevice d, WGPUQueue q, WGPUTextureFormat f) {
+ device = d;
+ queue = q;
+ format = f;
+
+ for (auto &entry : sequences_) {
+ entry.seq->init(this);
+ }
+}
+
+void MainSequence::add_sequence(std::shared_ptr<Sequence> seq, float start_time, int priority) {
+ sequences_.push_back({seq, start_time, priority});
+ // Sort sequences by priority
+ std::sort(sequences_.begin(), sequences_.end(),
+ [](const ActiveSequence &a, const ActiveSequence &b) {
+ return a.priority < b.priority;
+ });
+}
+
+void MainSequence::render_frame(float global_time, float beat, float peak,
+ float aspect_ratio, WGPUSurface surface) {
+ WGPUSurfaceTexture surface_texture;
+ wgpuSurfaceGetCurrentTexture(surface, &surface_texture);
+
+#if defined(DEMO_CROSS_COMPILE_WIN32)
+ #define STATUS_OPTIMAL WGPUSurfaceGetCurrentTextureStatus_Success
+ #define STATUS_SUBOPTIMAL WGPUSurfaceGetCurrentTextureStatus_Success
+#else
+ #define STATUS_OPTIMAL WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal
+ #define STATUS_SUBOPTIMAL WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal
+#endif
+
+ if (surface_texture.status != STATUS_OPTIMAL &&
+ surface_texture.status != STATUS_SUBOPTIMAL) {
+ return;
+ }
+
+ WGPUTextureView view = wgpuTextureCreateView(surface_texture.texture, nullptr);
+
+ WGPUCommandEncoderDescriptor encoder_desc = {};
+ WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(device, &encoder_desc);
+
+ // 1. Update & Compute Phase
+ for (auto &entry : sequences_) {
+ // Check if sequence is active (start_time <= global_time)
+ // We assume sequences run until end of demo or have internal end?
+ // User said "Sequence ... overlap". Implicitly they might have duration but here we just check start.
+ // Let's assume they are active if time >= start.
+ // Effects inside sequence handle duration.
+ if (global_time >= entry.start_time) {
+ float seq_time = global_time - entry.start_time;
+ entry.seq->update_active_list(seq_time);
+
+ // Pass generic aspect ratio 16:9 for compute?
+ // Or wait for render. Particles compute uses it.
+ // We can get it from surface texture size if we want?
+ // Let's pass 1.777f for now or fetch.
+ // gpu_draw used to pass it. We need it here.
+ // Wait, render_frame doesn't take aspect_ratio. gpu_draw did.
+ // I should add aspect_ratio to render_frame or calculate it from surface.
+ }
+ }
+
+ for (auto &entry : sequences_) {
+ if (global_time >= entry.start_time) {
+ entry.seq->dispatch_compute(encoder, global_time - entry.start_time, beat, peak, aspect_ratio);
+ }
+ }
+
+ // 2. Render Phase
+ {
+ WGPURenderPassColorAttachment color_attachment = {};
+ color_attachment.view = view;
+ color_attachment.loadOp = WGPULoadOp_Clear;
+ color_attachment.storeOp = WGPUStoreOp_Store;
+
+ // Clear color logic could be dynamic or part of a "BackgroundEffect"?
+ // For now hardcode.
+ float flash = peak * 0.2f;
+ color_attachment.clearValue = {0.05 + flash, 0.1 + flash, 0.2 + flash, 1.0};
+
+#if !defined(DEMO_CROSS_COMPILE_WIN32)
+ color_attachment.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
+#endif
+
+ WGPURenderPassDescriptor render_pass_desc = {};
+ render_pass_desc.colorAttachmentCount = 1;
+ render_pass_desc.colorAttachments = &color_attachment;
+
+ WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(encoder, &render_pass_desc);
+
+ for (auto &entry : sequences_) {
+ if (global_time >= entry.start_time) {
+ entry.seq->dispatch_render(pass, global_time - entry.start_time, beat, peak, aspect_ratio);
+ }
+ }
+
+ wgpuRenderPassEncoderEnd(pass);
+ }
+
+ WGPUCommandBufferDescriptor cmd_desc = {};
+ WGPUCommandBuffer commands = wgpuCommandEncoderFinish(encoder, &cmd_desc);
+ wgpuQueueSubmit(queue, 1, &commands);
+ wgpuSurfacePresent(surface);
+
+ wgpuTextureViewRelease(view);
+ wgpuTextureRelease(surface_texture.texture);
+}
+
+void MainSequence::shutdown() {
+ for (auto &entry : sequences_) {
+ entry.seq->reset();
+ }
+ sequences_.clear();
+} \ No newline at end of file
diff --git a/src/gpu/effect.h b/src/gpu/effect.h
new file mode 100644
index 0000000..50b2d72
--- /dev/null
+++ b/src/gpu/effect.h
@@ -0,0 +1,117 @@
+// This file is part of the 64k demo project.
+// It defines the Effect interface and Sequence management system.
+// Used for choreographing visual effects.
+
+#pragma once
+
+#include <vector>
+#include <memory>
+#include <algorithm>
+
+#if defined(DEMO_CROSS_COMPILE_WIN32)
+#include <webgpu/webgpu.h>
+#else
+#include <webgpu.h>
+#endif
+
+class MainSequence;
+
+// Abstract base class for all visual effects
+class Effect {
+public:
+ virtual ~Effect() = default;
+
+ // One-time setup (load assets, create buffers).
+ // Idempotent: safe to call multiple times if effect is shared.
+ virtual void init(MainSequence *demo) { (void)demo; }
+
+ // Called when the effect starts playing in a sequence segment.
+ virtual void start() {}
+
+ // Dispatch compute shaders.
+ virtual void compute(WGPUCommandEncoder encoder, float time, float beat,
+ float intensity, float aspect_ratio) {
+ (void)encoder;
+ (void)time;
+ (void)beat;
+ (void)intensity;
+ (void)aspect_ratio;
+ }
+
+ // Record render commands.
+ virtual void render(WGPURenderPassEncoder pass, float time, float beat,
+ float intensity, float aspect_ratio) = 0;
+
+ // Called when the effect finishes in a sequence segment.
+ virtual void end() {}
+
+ bool is_initialized = false;
+};
+
+struct SequenceItem {
+ std::shared_ptr<Effect> effect;
+ float start_time; // Relative to Sequence start
+ float end_time; // Relative to Sequence start
+ int priority; // Render order within sequence (higher = later/top)
+ bool active;
+};
+
+class Sequence {
+public:
+ int priority = 0; // Render order of this sequence (higher = later/top)
+
+ void init(MainSequence *demo);
+
+ // Add an effect to the sequence.
+ // start_time, end_time: Relative to sequence start.
+ // priority: Drawing order within this sequence.
+ void add_effect(std::shared_ptr<Effect> effect, float start_time,
+ float end_time, int priority = 0);
+
+ // Updates active state of effects based on sequence-local time.
+ // seq_time: Time relative to sequence start.
+ void update_active_list(float seq_time);
+
+ // Calls compute() on all active effects (sorted by priority).
+ void dispatch_compute(WGPUCommandEncoder encoder, float seq_time, float beat,
+ float intensity, float aspect_ratio);
+
+ // Calls render() on all active effects (sorted by priority).
+ void dispatch_render(WGPURenderPassEncoder pass, float seq_time, float beat,
+ float intensity, float aspect_ratio);
+
+ void reset();
+
+private:
+ std::vector<SequenceItem> items_;
+ bool is_sorted_ = false;
+ void sort_items();
+};
+
+class MainSequence {
+public:
+ WGPUDevice device;
+ WGPUQueue queue;
+ WGPUTextureFormat format;
+
+ void init(WGPUDevice device, WGPUQueue queue, WGPUTextureFormat format);
+
+ // Add a sequence to the demo.
+ // start_time: Global time when this sequence starts.
+ // priority: Layering order (higher = on top).
+ void add_sequence(std::shared_ptr<Sequence> seq, float start_time, int priority = 0);
+
+ // Renders the full frame: updates sequences, runs compute, runs render pass.
+ void render_frame(float global_time, float beat, float peak,
+ float aspect_ratio, WGPUSurface surface);
+
+ void shutdown();
+
+private:
+ struct ActiveSequence {
+ std::shared_ptr<Sequence> seq;
+ float start_time;
+ int priority;
+ };
+ std::vector<ActiveSequence> sequences_;
+};
diff --git a/src/gpu/gpu.cc b/src/gpu/gpu.cc
index 1d3c24f..0655bb3 100644
--- a/src/gpu/gpu.cc
+++ b/src/gpu/gpu.cc
@@ -4,6 +4,8 @@
#include "gpu.h"
#include "platform.h"
+#include "demo_effects.h"
+#include "effect.h"
#include <GLFW/glfw3.h>
#include <math.h>
@@ -91,33 +93,19 @@ static WGPUQueue g_queue = nullptr;
static WGPUSurface g_surface = nullptr;
static WGPUSurfaceConfiguration g_config = {};
-// We keep the main render pass as a global for now
-static RenderPass g_main_pass;
-static GpuBuffer g_uniform_buffer_struct;
-
-// Particle System Globals
-static ComputePass g_particle_compute_pass;
-static RenderPass g_particle_render_pass;
-static GpuBuffer g_particle_buffer;
-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
-};
+static MainSequence g_main_sequence;
// --- Helper Functions ---
-GpuBuffer gpu_create_buffer(size_t size, uint32_t usage, const void *data) {
+GpuBuffer gpu_create_buffer(WGPUDevice device, size_t size, uint32_t usage,
+ const void *data) {
WGPUBufferDescriptor desc = {};
desc.label = label_view("GpuBuffer");
desc.usage = (WGPUBufferUsage)usage; // Cast for C++ strictness with enums
desc.size = size;
desc.mappedAtCreation = (data != nullptr); // Map if we have initial data
- WGPUBuffer buffer = wgpuDeviceCreateBuffer(g_device, &desc);
+ WGPUBuffer buffer = wgpuDeviceCreateBuffer(device, &desc);
if (data) {
void *ptr = wgpuBufferGetMappedRange(buffer, 0, size);
@@ -128,7 +116,8 @@ GpuBuffer gpu_create_buffer(size_t size, uint32_t usage, const void *data) {
return {buffer, size};
}
-RenderPass gpu_create_render_pass(const char *shader_code,
+RenderPass gpu_create_render_pass(WGPUDevice device, WGPUTextureFormat format,
+ const char *shader_code,
ResourceBinding *bindings, int num_bindings) {
RenderPass pass = {};
@@ -139,7 +128,7 @@ RenderPass gpu_create_render_pass(const char *shader_code,
WGPUShaderModuleDescriptor shader_desc = {};
shader_desc.nextInChain = &wgsl_src.chain;
WGPUShaderModule shader_module =
- wgpuDeviceCreateShaderModule(g_device, &shader_desc);
+ wgpuDeviceCreateShaderModule(device, &shader_desc);
// Create Bind Group Layout & Bind Group
std::vector<WGPUBindGroupLayoutEntry> bgl_entries;
@@ -164,24 +153,24 @@ RenderPass gpu_create_render_pass(const char *shader_code,
bgl_desc.entryCount = (uint32_t)bgl_entries.size();
bgl_desc.entries = bgl_entries.data();
WGPUBindGroupLayout bind_group_layout =
- wgpuDeviceCreateBindGroupLayout(g_device, &bgl_desc);
+ wgpuDeviceCreateBindGroupLayout(device, &bgl_desc);
WGPUBindGroupDescriptor bg_desc = {};
bg_desc.layout = bind_group_layout;
bg_desc.entryCount = (uint32_t)bg_entries.size();
bg_desc.entries = bg_entries.data();
- pass.bind_group = wgpuDeviceCreateBindGroup(g_device, &bg_desc);
+ pass.bind_group = wgpuDeviceCreateBindGroup(device, &bg_desc);
// Pipeline Layout
WGPUPipelineLayoutDescriptor pl_desc = {};
pl_desc.bindGroupLayoutCount = 1;
pl_desc.bindGroupLayouts = &bind_group_layout;
WGPUPipelineLayout pipeline_layout =
- wgpuDeviceCreatePipelineLayout(g_device, &pl_desc);
+ wgpuDeviceCreatePipelineLayout(device, &pl_desc);
// Render Pipeline
WGPUColorTargetState color_target = {};
- color_target.format = g_config.format; // Use global swapchain format
+ color_target.format = format; // Use passed format
color_target.writeMask = WGPUColorWriteMask_All;
color_target.blend = nullptr;
@@ -210,12 +199,12 @@ RenderPass gpu_create_render_pass(const char *shader_code,
pipeline_desc.multisample.mask = 0xFFFFFFFF;
pipeline_desc.fragment = &fragment_state;
- pass.pipeline = wgpuDeviceCreateRenderPipeline(g_device, &pipeline_desc);
+ pass.pipeline = wgpuDeviceCreateRenderPipeline(device, &pipeline_desc);
return pass;
}
-ComputePass gpu_create_compute_pass(const char *shader_code,
+ComputePass gpu_create_compute_pass(WGPUDevice device, const char *shader_code,
ResourceBinding *bindings,
int num_bindings) {
ComputePass pass = {};
@@ -226,7 +215,7 @@ ComputePass gpu_create_compute_pass(const char *shader_code,
WGPUShaderModuleDescriptor shader_desc = {};
shader_desc.nextInChain = &wgsl_src.chain;
WGPUShaderModule shader_module =
- wgpuDeviceCreateShaderModule(g_device, &shader_desc);
+ wgpuDeviceCreateShaderModule(device, &shader_desc);
std::vector<WGPUBindGroupLayoutEntry> bgl_entries;
std::vector<WGPUBindGroupEntry> bg_entries;
@@ -250,26 +239,26 @@ ComputePass gpu_create_compute_pass(const char *shader_code,
bgl_desc.entryCount = (uint32_t)bgl_entries.size();
bgl_desc.entries = bgl_entries.data();
WGPUBindGroupLayout bind_group_layout =
- wgpuDeviceCreateBindGroupLayout(g_device, &bgl_desc);
+ wgpuDeviceCreateBindGroupLayout(device, &bgl_desc);
WGPUBindGroupDescriptor bg_desc = {};
bg_desc.layout = bind_group_layout;
bg_desc.entryCount = (uint32_t)bg_entries.size();
bg_desc.entries = bg_entries.data();
- pass.bind_group = wgpuDeviceCreateBindGroup(g_device, &bg_desc);
+ pass.bind_group = wgpuDeviceCreateBindGroup(device, &bg_desc);
WGPUPipelineLayoutDescriptor pl_desc = {};
pl_desc.bindGroupLayoutCount = 1;
pl_desc.bindGroupLayouts = &bind_group_layout;
WGPUPipelineLayout pipeline_layout =
- wgpuDeviceCreatePipelineLayout(g_device, &pl_desc);
+ wgpuDeviceCreatePipelineLayout(device, &pl_desc);
WGPUComputePipelineDescriptor pipeline_desc = {};
pipeline_desc.layout = pipeline_layout;
pipeline_desc.compute.module = shader_module;
pipeline_desc.compute.entryPoint = str_view("main");
- pass.pipeline = wgpuDeviceCreateComputePipeline(g_device, &pipeline_desc);
+ pass.pipeline = wgpuDeviceCreateComputePipeline(device, &pipeline_desc);
return pass;
}
@@ -364,169 +353,6 @@ static void handle_request_device(WGPURequestDeviceStatus status,
#endif
#endif
-// ... (Shaders omitted for brevity, they are unchanged) ...
-
-const char *main_shader_wgsl = R"(
-struct Uniforms {
- audio_peak : f32,
- aspect_ratio: f32,
- time: f32,
-};
-
-@group(0) @binding(0) var<uniform> uniforms : Uniforms;
-
-@vertex
-fn vs_main(@builtin(vertex_index) vertex_index: u32) -> @builtin(position) vec4<f32> {
- 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<f32>(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<f32>(x, y, 0.0, 1.0);
-}
-
-@fragment
-fn fs_main() -> @location(0) vec4<f32> {
- // Dynamic color shifting based on time and responsiveness to peak
- 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 boost = uniforms.audio_peak * 0.5;
- return vec4<f32>(r + boost, g + boost, b + boost, 0.5); // Alpha 0.5 for blending
-}
-)";
-
-const char *particle_compute_wgsl = R"(
-struct Particle {
- pos : vec4<f32>,
- vel : vec4<f32>,
- rot : vec4<f32>,
- color : vec4<f32>,
-};
-
-struct Uniforms {
- audio_peak : f32,
- aspect_ratio: f32,
- time: f32,
-};
-
-@group(0) @binding(0) var<storage, read_write> particles : array<Particle>;
-@group(0) @binding(1) var<uniform> uniforms : Uniforms;
-
-@compute @workgroup_size(64)
-fn main(@builtin(global_invocation_id) GlobalInvocationID : vec3<u32>) {
- 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;
-}
-)";
-
-const char *particle_render_wgsl = R"(
-struct Particle {
- pos : vec4<f32>,
- vel : vec4<f32>,
- rot : vec4<f32>,
- color : vec4<f32>,
-};
-
-struct Uniforms {
- audio_peak : f32,
- aspect_ratio: f32,
- time: f32,
-};
-
-@group(0) @binding(0) var<storage, read> particles : array<Particle>;
-@group(0) @binding(1) var<uniform> uniforms : Uniforms;
-
-struct VertexOutput {
- @builtin(position) Position : vec4<f32>,
- @location(0) Color : vec4<f32>,
-};
-
-@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<vec2<f32>, 6>(
- vec2<f32>(-1.0, -1.0),
- vec2<f32>( 1.0, -1.0),
- vec2<f32>(-1.0, 1.0),
- vec2<f32>(-1.0, 1.0),
- vec2<f32>( 1.0, -1.0),
- vec2<f32>( 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<f32>(x, y, 0.0, 1.0);
- output.Color = p.color * (0.5 + 0.5 * uniforms.audio_peak);
- return output;
-}
-
-@fragment
-fn fs_main(@location(0) Color : vec4<f32>) -> @location(0) vec4<f32> {
- return Color;
-}
-)";
-
void gpu_init(GLFWwindow *window) {
g_instance = wgpuCreateInstance(nullptr);
g_surface = platform_create_wgpu_surface(g_instance);
@@ -589,146 +415,16 @@ void gpu_init(GLFWwindow *window) {
g_config.alphaMode = WGPUCompositeAlphaMode_Opaque;
wgpuSurfaceConfigure(g_surface, &g_config);
- // Initialize Uniforms
- g_uniform_buffer_struct = gpu_create_buffer(
- sizeof(float) * 4, WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst,
- nullptr);
-
- // Initialize Main Pass
- ResourceBinding main_bindings[] = {
- {g_uniform_buffer_struct, WGPUBufferBindingType_Uniform}};
- g_main_pass = gpu_create_render_pass(main_shader_wgsl, main_bindings, 1);
- g_main_pass.vertex_count = 21;
+ g_main_sequence.init(g_device, g_queue, g_config.format);
- // Initialize Particles
- std::vector<Particle> 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;
- }
-
- g_particle_buffer = gpu_create_buffer(
- sizeof(Particle) * NUM_PARTICLES,
- (WGPUBufferUsage)(WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst |
- WGPUBufferUsage_Vertex),
- initial_particles.data());
-
- // Initialize Particle Compute Pass
- ResourceBinding compute_bindings[] = {
- {g_particle_buffer, WGPUBufferBindingType_Storage},
- {g_uniform_buffer_struct, WGPUBufferBindingType_Uniform}};
- g_particle_compute_pass =
- gpu_create_compute_pass(particle_compute_wgsl, compute_bindings, 2);
- g_particle_compute_pass.workgroup_size_x = (NUM_PARTICLES + 63) / 64;
- g_particle_compute_pass.workgroup_size_y = 1;
- g_particle_compute_pass.workgroup_size_z = 1;
-
- // Initialize Particle Render Pass
- ResourceBinding render_bindings[] = {
- {g_particle_buffer, WGPUBufferBindingType_ReadOnlyStorage},
- {g_uniform_buffer_struct, WGPUBufferBindingType_Uniform}};
- g_particle_render_pass =
- gpu_create_render_pass(particle_render_wgsl, render_bindings, 2);
- g_particle_render_pass.vertex_count = 6;
- g_particle_render_pass.instance_count = NUM_PARTICLES;
+ auto seq = create_demo_sequence(g_device, g_queue, g_config.format);
+ g_main_sequence.add_sequence(seq, 0.0f, 0);
}
-void gpu_draw(float audio_peak, float aspect_ratio, float time) {
- WGPUSurfaceTexture surface_texture;
- wgpuSurfaceGetCurrentTexture(g_surface, &surface_texture);
- if (surface_texture.status !=
- WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal &&
- surface_texture.status !=
- WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal)
- return;
-
- WGPUTextureView view =
- wgpuTextureCreateView(surface_texture.texture, nullptr);
-
- struct {
- float audio_peak;
- float aspect_ratio;
- float time;
- float padding;
- } uniforms = {audio_peak, aspect_ratio, time, 0.0f};
- wgpuQueueWriteBuffer(g_queue, g_uniform_buffer_struct.buffer, 0, &uniforms,
- sizeof(uniforms));
-
- WGPUCommandEncoderDescriptor encoder_desc = {};
- WGPUCommandEncoder encoder =
- wgpuDeviceCreateCommandEncoder(g_device, &encoder_desc);
-
- // --- Compute Pass ---
- {
- WGPUComputePassDescriptor compute_desc = {};
- compute_desc.label = label_view("Particle Compute");
- WGPUComputePassEncoder compute_pass =
- wgpuCommandEncoderBeginComputePass(encoder, &compute_desc);
- wgpuComputePassEncoderSetPipeline(compute_pass,
- g_particle_compute_pass.pipeline);
- wgpuComputePassEncoderSetBindGroup(
- compute_pass, 0, g_particle_compute_pass.bind_group, 0, nullptr);
- wgpuComputePassEncoderDispatchWorkgroups(
- compute_pass, g_particle_compute_pass.workgroup_size_x, 1, 1);
- wgpuComputePassEncoderEnd(compute_pass);
- }
-
- // --- Render Pass ---
- {
- WGPURenderPassColorAttachment color_attachment = {};
- color_attachment.view = view;
- color_attachment.loadOp = WGPULoadOp_Clear;
- color_attachment.storeOp = WGPUStoreOp_Store;
- float flash = audio_peak * 0.2f;
- color_attachment.clearValue = {0.05 + flash, 0.1 + flash, 0.2 + flash, 1.0};
-#if !defined(DEMO_CROSS_COMPILE_WIN32)
- color_attachment.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
-#endif
-
- WGPURenderPassDescriptor render_pass_desc = {};
- render_pass_desc.colorAttachmentCount = 1;
- render_pass_desc.colorAttachments = &color_attachment;
-
- WGPURenderPassEncoder pass =
- wgpuCommandEncoderBeginRenderPass(encoder, &render_pass_desc);
-
- // Draw Main Object
- wgpuRenderPassEncoderSetPipeline(pass, g_main_pass.pipeline);
- wgpuRenderPassEncoderSetBindGroup(pass, 0, g_main_pass.bind_group, 0,
- nullptr);
- wgpuRenderPassEncoderDraw(pass, g_main_pass.vertex_count, 1, 0, 0);
-
- // Draw Particles
- wgpuRenderPassEncoderSetPipeline(pass, g_particle_render_pass.pipeline);
- wgpuRenderPassEncoderSetBindGroup(
- pass, 0, g_particle_render_pass.bind_group, 0, nullptr);
- wgpuRenderPassEncoderDraw(pass, g_particle_render_pass.vertex_count,
- g_particle_render_pass.instance_count, 0, 0);
-
- wgpuRenderPassEncoderEnd(pass);
- }
-
- WGPUCommandBufferDescriptor cmd_desc = {};
- WGPUCommandBuffer commands = wgpuCommandEncoderFinish(encoder, &cmd_desc);
- wgpuQueueSubmit(g_queue, 1, &commands);
- wgpuSurfacePresent(g_surface);
-
- wgpuTextureViewRelease(view);
- wgpuTextureRelease(surface_texture.texture);
+void gpu_draw(float audio_peak, float aspect_ratio, float time, float beat) {
+ g_main_sequence.render_frame(time, beat, audio_peak, aspect_ratio, g_surface);
}
void gpu_shutdown() {
-}
+ g_main_sequence.shutdown();
+} \ No newline at end of file
diff --git a/src/gpu/gpu.h b/src/gpu/gpu.h
index a78c433..f9c322c 100644
--- a/src/gpu/gpu.h
+++ b/src/gpu/gpu.h
@@ -32,7 +32,7 @@ struct RenderPass {
};
void gpu_init(GLFWwindow *window);
-void gpu_draw(float audio_peak, float aspect_ratio, float time);
+void gpu_draw(float audio_peak, float aspect_ratio, float time, float beat);
void gpu_shutdown();
// Helper functions (exposed for internal/future use)
@@ -42,10 +42,12 @@ struct ResourceBinding {
// WGPUBufferBindingType_Storage
};
-GpuBuffer gpu_create_buffer(size_t size, uint32_t usage,
+GpuBuffer gpu_create_buffer(WGPUDevice device, size_t size, uint32_t usage,
const void *data = nullptr);
-ComputePass gpu_create_compute_pass(const char *shader_code,
+ComputePass gpu_create_compute_pass(WGPUDevice device, const char *shader_code,
ResourceBinding *bindings,
int num_bindings);
-RenderPass gpu_create_render_pass(const char *shader_code,
+RenderPass gpu_create_render_pass(WGPUDevice device,
+ WGPUTextureFormat format, // Needed for render pipeline
+ const char *shader_code,
ResourceBinding *bindings, int num_bindings);
diff --git a/src/main.cc b/src/main.cc
index 4f99230..fb0b412 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -212,7 +212,8 @@ int main(int argc, char **argv) {
float raw_peak = synth_get_output_peak();
float visual_peak = fminf(raw_peak * 8.0f, 1.0f);
- gpu_draw(visual_peak, aspect_ratio, (float)current_time);
+ float beat = fmodf((float)current_time * DEMO_BPM / 60.0f, 1.0f);
+ gpu_draw(visual_peak, aspect_ratio, (float)current_time, beat);
audio_update();
}