From 9d1d4df877f96f1970dce2ab30cfae49d3d796e1 Mon Sep 17 00:00:00 2001 From: skal Date: Mon, 16 Feb 2026 08:26:45 +0100 Subject: feat(sequence): Phase 1 - Sequence v2 foundation - Add Node system with typed buffers (u8x4_norm, f32x4, f16x8, depth24) - Add NodeRegistry with aliasing support for ping-pong optimization - Add SequenceV2 base class with DAG execution - Add EffectV2 base class with multi-input/multi-output - Add comprehensive tests (5 test cases, all passing) - Corrected FATAL_CHECK usage (checks ERROR conditions, not success) Phase 1 complete: Core v2 architecture functional. Next: Phase 2 compiler (seq_compiler_v2.py) handoff(Claude): Phase 1 foundation complete, all tests passing (35/35) --- src/gpu/sequence_v2.cc | 207 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 src/gpu/sequence_v2.cc (limited to 'src/gpu/sequence_v2.cc') diff --git a/src/gpu/sequence_v2.cc b/src/gpu/sequence_v2.cc new file mode 100644 index 0000000..c3f9aea --- /dev/null +++ b/src/gpu/sequence_v2.cc @@ -0,0 +1,207 @@ +// Sequence v2 implementation + +#include "gpu/sequence_v2.h" +#include "gpu/effect_v2.h" +#include "util/fatal_error.h" +#include + +// NodeRegistry implementation + +NodeRegistry::NodeRegistry(WGPUDevice device, int default_width, + int default_height) + : device_(device), default_width_(default_width), + default_height_(default_height) { + // Reserve source/sink as implicit nodes (managed externally by MainSequence) +} + +NodeRegistry::~NodeRegistry() { + for (auto& [name, node] : nodes_) { + if (node.view) { + wgpuTextureViewRelease(node.view); + } + for (auto& mip_view : node.mip_views) { + wgpuTextureViewRelease(mip_view); + } + if (node.texture) { + wgpuTextureRelease(node.texture); + } + } +} + +void NodeRegistry::declare_node(const std::string& name, NodeType type, + int width, int height) { + FATAL_CHECK(nodes_.find(name) != nodes_.end(), + "Node already declared: %s\n", name.c_str()); + + if (width <= 0) + width = default_width_; + if (height <= 0) + height = default_height_; + + Node node; + node.type = type; + node.width = width; + node.height = height; + create_texture(node); + + nodes_[name] = node; +} + +void NodeRegistry::declare_aliased_node(const std::string& name, + const std::string& alias_of) { + FATAL_CHECK(nodes_.find(alias_of) == nodes_.end(), + "Alias target does not exist: %s\n", alias_of.c_str()); + FATAL_CHECK(aliases_.find(name) != aliases_.end(), "Alias already exists: %s\n", + name.c_str()); + + aliases_[name] = alias_of; +} + +WGPUTextureView NodeRegistry::get_view(const std::string& name) { + // Check aliases first + auto alias_it = aliases_.find(name); + if (alias_it != aliases_.end()) { + return get_view(alias_it->second); + } + + auto it = nodes_.find(name); + FATAL_CHECK(it == nodes_.end(), "Node not found: %s\n", name.c_str()); + return it->second.view; +} + +std::vector +NodeRegistry::get_output_views(const std::vector& names) { + std::vector views; + views.reserve(names.size()); + for (const auto& name : names) { + views.push_back(get_view(name)); + } + return views; +} + +void NodeRegistry::resize(int width, int height) { + default_width_ = width; + default_height_ = height; + + for (auto& [name, node] : nodes_) { + // Release old texture + if (node.view) { + wgpuTextureViewRelease(node.view); + } + for (auto& mip_view : node.mip_views) { + wgpuTextureViewRelease(mip_view); + } + if (node.texture) { + wgpuTextureRelease(node.texture); + } + + // Recreate with new dimensions + node.width = width; + node.height = height; + create_texture(node); + } +} + +bool NodeRegistry::has_node(const std::string& name) const { + return nodes_.find(name) != nodes_.end() || + aliases_.find(name) != aliases_.end(); +} + +void NodeRegistry::create_texture(Node& node) { + WGPUTextureFormat format; + WGPUTextureUsage usage; + + switch (node.type) { + case NodeType::U8X4_NORM: + format = WGPUTextureFormat_RGBA8Unorm; + usage = WGPUTextureUsage_RenderAttachment | WGPUTextureUsage_TextureBinding; + break; + case NodeType::F32X4: + format = WGPUTextureFormat_RGBA32Float; + usage = WGPUTextureUsage_RenderAttachment | WGPUTextureUsage_TextureBinding; + break; + case NodeType::F16X8: + format = WGPUTextureFormat_RGBA16Float; // WebGPU doesn't have 8-channel, use RGBA16 + usage = WGPUTextureUsage_RenderAttachment | WGPUTextureUsage_TextureBinding; + break; + case NodeType::DEPTH24: + format = WGPUTextureFormat_Depth24Plus; + usage = WGPUTextureUsage_RenderAttachment; + break; + case NodeType::COMPUTE_F32: + format = WGPUTextureFormat_RGBA32Float; + usage = WGPUTextureUsage_StorageBinding | WGPUTextureUsage_TextureBinding; + break; + } + + WGPUTextureDescriptor desc = {}; + desc.usage = usage; + desc.dimension = WGPUTextureDimension_2D; + desc.size = {static_cast(node.width), + static_cast(node.height), 1}; + desc.format = format; + desc.mipLevelCount = 1; + desc.sampleCount = 1; + + node.texture = wgpuDeviceCreateTexture(device_, &desc); + FATAL_CHECK(node.texture == nullptr, "Failed to create texture\n"); + + WGPUTextureViewDescriptor view_desc = {}; + view_desc.format = format; + view_desc.dimension = WGPUTextureViewDimension_2D; + view_desc.baseMipLevel = 0; + view_desc.mipLevelCount = 1; + view_desc.baseArrayLayer = 0; + view_desc.arrayLayerCount = 1; + view_desc.aspect = (node.type == NodeType::DEPTH24) + ? WGPUTextureAspect_DepthOnly + : WGPUTextureAspect_All; + + node.view = wgpuTextureCreateView(node.texture, &view_desc); + FATAL_CHECK(node.view == nullptr, "Failed to create texture view\n"); +} + +// SequenceV2 implementation + +SequenceV2::SequenceV2(const GpuContext& ctx, int width, int height) + : ctx_(ctx), width_(width), height_(height), + nodes_(ctx.device, width, height) { + uniforms_buffer_.init(ctx.device); +} + +void SequenceV2::preprocess(float seq_time, float beat_time, float beat_phase, + float audio_intensity) { + params_.resolution = {static_cast(width_), static_cast(height_)}; + params_.aspect_ratio = + static_cast(width_) / static_cast(height_); + params_.time = seq_time; + params_.beat_time = beat_time; + params_.beat_phase = beat_phase; + params_.audio_intensity = audio_intensity; + params_._pad = 0.0f; + + uniforms_buffer_.update(ctx_.queue, params_); +} + +void SequenceV2::postprocess(WGPUCommandEncoder encoder) { + (void)encoder; + // Default: No-op (last effect writes to sink directly) +} + +void SequenceV2::render_effects(WGPUCommandEncoder encoder) { + // Execute DAG in topological order (pre-sorted by compiler) + for (const auto& dag_node : effect_dag_) { + dag_node.effect->render(encoder, params_, nodes_); + } +} + +void SequenceV2::resize(int width, int height) { + width_ = width; + height_ = height; + nodes_.resize(width, height); + + // Notify effects + for (auto& dag_node : effect_dag_) { + dag_node.effect->resize(width, height); + } +} -- cgit v1.2.3