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/effect.cc | 308 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 171 insertions(+), 137 deletions(-) (limited to 'src/gpu/effect.cc') diff --git a/src/gpu/effect.cc b/src/gpu/effect.cc index 040b523..0ab476b 100644 --- a/src/gpu/effect.cc +++ b/src/gpu/effect.cc @@ -2,11 +2,23 @@ // It implements the Sequence management logic. #include "effect.h" +#include "gpu/demo_effects.h" +#include "gpu/gpu.h" #include #include +#include + +// --- PostProcessEffect --- +void PostProcessEffect::render(WGPURenderPassEncoder pass, float, float, float, + float) { + if (pipeline_ && bind_group_) { + wgpuRenderPassEncoderSetPipeline(pass, pipeline_); + wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 0, nullptr); + wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); // Fullscreen triangle + } +} // --- Sequence Implementation --- - void Sequence::init(MainSequence *demo) { for (auto &item : items_) { if (!item.effect->is_initialized) { @@ -23,8 +35,8 @@ void Sequence::add_effect(std::shared_ptr effect, float start_time, } void Sequence::sort_items() { - if (is_sorted_) return; - // Sort by priority ascending (0 draws first, 100 draws on top) + if (is_sorted_) + return; std::sort(items_.begin(), items_.end(), [](const SequenceItem &a, const SequenceItem &b) { return a.priority < b.priority; @@ -37,40 +49,27 @@ void Sequence::update_active_list(float seq_time) { 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; - } + if (should_be_active && !item.active) { + item.effect->start(); + item.active = true; + } else if (!should_be_active && item.active) { + item.effect->end(); + item.active = false; } } } -void Sequence::dispatch_compute(WGPUCommandEncoder encoder, float seq_time, - float beat, float intensity, - float aspect_ratio) { +void Sequence::collect_active_effects( + std::vector &scene_effects, + std::vector &post_effects) { 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); + if (item.effect->is_post_process()) { + post_effects.push_back(&item); + } else { + scene_effects.push_back(&item); + } } } } @@ -86,19 +85,49 @@ void Sequence::reset() { // --- MainSequence Implementation --- -void MainSequence::init(WGPUDevice d, WGPUQueue q, WGPUTextureFormat f) { +MainSequence::MainSequence() = default; +MainSequence::~MainSequence() = default; + +void MainSequence::create_framebuffers(int width, int height) { + WGPUTextureDescriptor desc = {}; + desc.usage = + WGPUTextureUsage_RenderAttachment | WGPUTextureUsage_TextureBinding; + desc.dimension = WGPUTextureDimension_2D; + desc.size = {(uint32_t)width, (uint32_t)height, 1}; + desc.format = format; + desc.mipLevelCount = 1; + desc.sampleCount = 1; + + framebuffer_a_ = wgpuDeviceCreateTexture(device, &desc); + framebuffer_b_ = wgpuDeviceCreateTexture(device, &desc); + + WGPUTextureViewDescriptor view_desc = {}; + view_desc.dimension = WGPUTextureViewDimension_2D; + view_desc.format = format; + view_desc.mipLevelCount = 1; + view_desc.arrayLayerCount = 1; + + framebuffer_view_a_ = wgpuTextureCreateView(framebuffer_a_, &view_desc); + framebuffer_view_b_ = wgpuTextureCreateView(framebuffer_b_, &view_desc); +} + +void MainSequence::init(WGPUDevice d, WGPUQueue q, WGPUTextureFormat f, + int width, int height) { device = d; queue = q; format = f; - + + create_framebuffers(width, height); + passthrough_effect_ = std::make_unique(device, format); + for (auto &entry : sequences_) { entry.seq->init(this); } } -void MainSequence::add_sequence(std::shared_ptr seq, float start_time, int priority) { +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; @@ -107,135 +136,140 @@ void MainSequence::add_sequence(std::shared_ptr seq, float start_time, 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 + WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(device, nullptr); - 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 + std::vector scene_effects; + std::vector post_effects; 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. + float seq_time = global_time - entry.start_time; + entry.seq->update_active_list(seq_time); + entry.seq->collect_active_effects(scene_effects, post_effects); } } - - for (auto &entry : sequences_) { - if (global_time >= entry.start_time) { - entry.seq->dispatch_compute(encoder, global_time - entry.start_time, beat, peak, aspect_ratio); - } + std::sort(scene_effects.begin(), scene_effects.end(), + [](const SequenceItem *a, const SequenceItem *b) { + return a->priority < b->priority; + }); + std::sort(post_effects.begin(), post_effects.end(), + [](const SequenceItem *a, const SequenceItem *b) { + return a->priority < b->priority; + }); + + // 1. Compute + for (const auto &item : scene_effects) { + item->effect->compute(encoder, global_time - item->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}; - + // 2. Scene Pass (to A) + WGPURenderPassColorAttachment scene_attachment = {}; + scene_attachment.view = framebuffer_view_a_; + scene_attachment.loadOp = WGPULoadOp_Clear; + scene_attachment.storeOp = WGPUStoreOp_Store; + scene_attachment.clearValue = {0, 0, 0, 1}; #if !defined(DEMO_CROSS_COMPILE_WIN32) - color_attachment.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; + scene_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); - } + WGPURenderPassDescriptor scene_desc = {.colorAttachmentCount = 1, + .colorAttachments = &scene_attachment}; + WGPURenderPassEncoder scene_pass = + wgpuCommandEncoderBeginRenderPass(encoder, &scene_desc); + for (const auto &item : scene_effects) { + item->effect->render(scene_pass, global_time - item->start_time, beat, peak, + aspect_ratio); + } + wgpuRenderPassEncoderEnd(scene_pass); + + // 3. Post Chain + if (post_effects.empty()) { + WGPUSurfaceTexture st; + wgpuSurfaceGetCurrentTexture(surface, &st); + WGPUTextureView final_view = wgpuTextureCreateView(st.texture, nullptr); + passthrough_effect_->update_bind_group(framebuffer_view_a_); + + WGPURenderPassColorAttachment final_attachment = { + .view = final_view, .loadOp = WGPULoadOp_Load, .storeOp = WGPUStoreOp_Store}; + WGPURenderPassDescriptor final_desc = {.colorAttachmentCount = 1, + .colorAttachments = + &final_attachment}; + WGPURenderPassEncoder final_pass = + wgpuCommandEncoderBeginRenderPass(encoder, &final_desc); + passthrough_effect_->render(final_pass, 0, 0, 0, aspect_ratio); + wgpuRenderPassEncoderEnd(final_pass); + + wgpuTextureViewRelease(final_view); + wgpuSurfacePresent(surface); + wgpuTextureRelease(st.texture); + } else { + WGPUTextureView current_input = framebuffer_view_a_; + for (size_t i = 0; i < post_effects.size(); ++i) { + bool is_last = (i == post_effects.size() - 1); + WGPUSurfaceTexture st; + if (is_last) + wgpuSurfaceGetCurrentTexture(surface, &st); + + WGPUTextureView current_output = + is_last ? wgpuTextureCreateView(st.texture, nullptr) + : (current_input == framebuffer_view_a_ ? framebuffer_view_b_ + : framebuffer_view_a_); + + PostProcessEffect *pp = + static_cast(post_effects[i]->effect.get()); + pp->update_bind_group(current_input); + + WGPURenderPassColorAttachment pp_attachment = { + .view = current_output, .loadOp = WGPULoadOp_Load, .storeOp = WGPUStoreOp_Store}; + WGPURenderPassDescriptor pp_desc = {.colorAttachmentCount = 1, + .colorAttachments = &pp_attachment}; + WGPURenderPassEncoder pp_pass = + wgpuCommandEncoderBeginRenderPass(encoder, &pp_desc); + pp->render(pp_pass, global_time - post_effects[i]->start_time, beat, peak, + aspect_ratio); + wgpuRenderPassEncoderEnd(pp_pass); + + if (is_last) { + wgpuTextureViewRelease(current_output); + wgpuSurfacePresent(surface); + wgpuTextureRelease(st.texture); + } + current_input = current_output; } - - wgpuRenderPassEncoderEnd(pass); } - WGPUCommandBufferDescriptor cmd_desc = {}; - WGPUCommandBuffer commands = wgpuCommandEncoderFinish(encoder, &cmd_desc); + WGPUCommandBuffer commands = wgpuCommandEncoderFinish(encoder, nullptr); wgpuQueueSubmit(queue, 1, &commands); - wgpuSurfacePresent(surface); +} - wgpuTextureViewRelease(view); - wgpuTextureRelease(surface_texture.texture); +void MainSequence::shutdown() { + if (framebuffer_view_a_) wgpuTextureViewRelease(framebuffer_view_a_); + if (framebuffer_a_) wgpuTextureRelease(framebuffer_a_); + if (framebuffer_view_b_) wgpuTextureViewRelease(framebuffer_view_b_); + if (framebuffer_b_) wgpuTextureRelease(framebuffer_b_); + for (auto &entry : sequences_) { + entry.seq->reset(); + } } -void MainSequence::simulate_until(float target_time, float step_rate) { #ifndef STRIP_ALL - // Assuming 128 BPM as per main.cc. - // Ideally this should be passed in or shared. +void MainSequence::simulate_until(float target_time, float step_rate) { const float bpm = 128.0f; - const float aspect_ratio = 16.0f / 9.0f; // Dummy aspect - + const float aspect_ratio = 16.0f / 9.0f; for (float t = 0.0f; t < target_time; t += step_rate) { - WGPUCommandEncoderDescriptor encoder_desc = {}; - WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(device, &encoder_desc); - + WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(device, nullptr); float beat = fmodf(t * bpm / 60.0f, 1.0f); - - // Update active lists + std::vector scene_effects, post_effects; for (auto &entry : sequences_) { if (t >= entry.start_time) { entry.seq->update_active_list(t - entry.start_time); + entry.seq->collect_active_effects(scene_effects, post_effects); } } - - // Dispatch compute - for (auto &entry : sequences_) { - if (t >= entry.start_time) { - // peak = 0.0f during simulation (no audio analysis) - entry.seq->dispatch_compute(encoder, t - entry.start_time, beat, 0.0f, aspect_ratio); - } + for (const auto &item : scene_effects) { + item->effect->compute(encoder, t - item->start_time, beat, 0.0f, aspect_ratio); } - - WGPUCommandBufferDescriptor cmd_desc = {}; - WGPUCommandBuffer commands = wgpuCommandEncoderFinish(encoder, &cmd_desc); + WGPUCommandBuffer commands = wgpuCommandEncoderFinish(encoder, nullptr); wgpuQueueSubmit(queue, 1, &commands); } -#else - (void)target_time; - (void)step_rate; -#endif } - -void MainSequence::shutdown() { - for (auto &entry : sequences_) { - entry.seq->reset(); - } - sequences_.clear(); -} \ No newline at end of file +#endif -- cgit v1.2.3