summaryrefslogtreecommitdiff
path: root/src/gpu/effect.cc
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-01-31 15:36:45 +0100
committerskal <pascal.massimino@gmail.com>2026-01-31 15:36:45 +0100
commit72c0cbcb38732b698d33d52459fed67af56ee2ec (patch)
tree83cabb089d76c7dda49ac786ad829b6f37d17bbf /src/gpu/effect.cc
parente96f282d77bd114493c8d9097c7fb7eaaad2338b (diff)
feat: Implement Sequence and Effect system for demo choreography
Refactors the rendering pipeline into a modular Sequence/Effect system. 'MainSequence' manages the final frame rendering and a list of 'Sequence' layers. 'Sequence' manages a timeline of 'Effect' objects (start/end/priority). Concrete effects (Heptagon, Particles) are moved to 'demo_effects.cc'. Updates main loop to pass beat and aspect ratio.
Diffstat (limited to 'src/gpu/effect.cc')
-rw-r--r--src/gpu/effect.cc203
1 files changed, 203 insertions, 0 deletions
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