From 72c0cbcb38732b698d33d52459fed67af56ee2ec Mon Sep 17 00:00:00 2001 From: skal Date: Sat, 31 Jan 2026 15:36:45 +0100 Subject: 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. --- src/gpu/effect.cc | 203 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 src/gpu/effect.cc (limited to 'src/gpu/effect.cc') 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 +#include + +// --- 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, 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 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 -- cgit v1.2.3