// GBufferEffect implementation // Rasterizes proxy geometry to MRT G-buffer, then packs into CNN v3 feature textures. #include "gbuffer_effect.h" #include "3d/object.h" #include "gpu/gpu.h" #include "gpu/shader_composer.h" #include "util/fatal_error.h" #include "util/mini_math.h" #include #include // Shader source (loaded from asset at runtime — declared extern by the build system) // For standalone use outside the asset system, the caller must ensure the WGSL // source strings are available. They are declared here as weak-linkable externs. extern const char* gbuf_raster_wgsl; extern const char* gbuf_pack_wgsl; // Maximum number of objects the G-buffer supports per frame. static const int kGBufMaxObjects = 256; // ObjectData struct that mirrors the WGSL layout in gbuf_raster.wgsl and renderer.h struct GBufObjectData { mat4 model; mat4 inv_model; vec4 color; vec4 params; // x = object type, y = plane_distance }; static_assert(sizeof(GBufObjectData) == sizeof(float) * 40, "GBufObjectData must be 160 bytes"); // GlobalUniforms struct mirroring renderer.h struct GBufGlobalUniforms { mat4 view_proj; mat4 inv_view_proj; vec4 camera_pos_time; vec4 params; // x = num_objects vec2 resolution; vec2 padding; }; static_assert(sizeof(GBufGlobalUniforms) == sizeof(float) * 44, "GBufGlobalUniforms must be 176 bytes"); // Helper: create a 1×1 placeholder texture of a given format cleared to `value`. static WGPUTexture create_placeholder_tex(WGPUDevice device, WGPUTextureFormat format, float value) { WGPUTextureDescriptor desc = {}; desc.usage = (WGPUTextureUsage)(WGPUTextureUsage_TextureBinding | WGPUTextureUsage_CopyDst); desc.dimension = WGPUTextureDimension_2D; desc.size = {1, 1, 1}; desc.format = format; desc.mipLevelCount = 1; desc.sampleCount = 1; WGPUTexture tex = wgpuDeviceCreateTexture(device, &desc); return tex; } // Helper: write a single RGBA float pixel to a texture via queue. static void write_placeholder_pixel(WGPUQueue queue, WGPUTexture tex, float r, float g, float b, float a) { const float data[4] = {r, g, b, a}; WGPUTexelCopyTextureInfo dst = {}; dst.texture = tex; dst.mipLevel = 0; dst.origin = {0, 0, 0}; dst.aspect = WGPUTextureAspect_All; WGPUTexelCopyBufferLayout layout = {}; layout.offset = 0; layout.bytesPerRow = 16; // 4 × sizeof(float) layout.rowsPerImage = 1; const WGPUExtent3D extent = {1, 1, 1}; wgpuQueueWriteTexture(queue, &dst, data, sizeof(data), &layout, &extent); } // Create bilinear sampler. static WGPUSampler create_bilinear_sampler(WGPUDevice device) { WGPUSamplerDescriptor desc = {}; desc.addressModeU = WGPUAddressMode_ClampToEdge; desc.addressModeV = WGPUAddressMode_ClampToEdge; desc.magFilter = WGPUFilterMode_Linear; desc.minFilter = WGPUFilterMode_Linear; desc.mipmapFilter = WGPUMipmapFilterMode_Linear; desc.maxAnisotropy = 1; return wgpuDeviceCreateSampler(device, &desc); } // ---- GBufferEffect ---- GBufferEffect::GBufferEffect(const GpuContext& ctx, const std::vector& inputs, const std::vector& outputs, float start_time, float end_time) : Effect(ctx, inputs, outputs, start_time, end_time) { HEADLESS_RETURN_IF_NULL(ctx_.device); // Derive internal node name prefix from the first output name. const std::string& prefix = outputs.empty() ? "gbuf" : outputs[0]; node_albedo_ = prefix + "_albedo"; node_normal_mat_ = prefix + "_normal_mat"; node_depth_ = prefix + "_depth"; node_shadow_ = prefix + "_shadow"; node_transp_ = prefix + "_transp"; node_feat0_ = outputs.size() > 0 ? outputs[0] : prefix + "_feat0"; node_feat1_ = outputs.size() > 1 ? outputs[1] : prefix + "_feat1"; // Allocate GPU buffers for scene data. global_uniforms_buf_ = gpu_create_buffer(ctx_.device, sizeof(GBufGlobalUniforms), WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst); ensure_objects_buffer(kGBufMaxObjects); // Resolution uniform for pack shader. pack_res_uniform_.init(ctx_.device); // Placeholder shadow (1.0 = fully lit) and transp (0.0 = opaque) textures. shadow_placeholder_tex_.set( create_placeholder_tex(ctx_.device, WGPUTextureFormat_RGBA32Float, 1.0f)); write_placeholder_pixel(ctx_.queue, shadow_placeholder_tex_.get(), 1.0f, 0.0f, 0.0f, 1.0f); transp_placeholder_tex_.set( create_placeholder_tex(ctx_.device, WGPUTextureFormat_RGBA32Float, 0.0f)); write_placeholder_pixel(ctx_.queue, transp_placeholder_tex_.get(), 0.0f, 0.0f, 0.0f, 1.0f); WGPUTextureViewDescriptor vd = {}; vd.format = WGPUTextureFormat_RGBA32Float; vd.dimension = WGPUTextureViewDimension_2D; vd.baseMipLevel = 0; vd.mipLevelCount = 1; vd.baseArrayLayer = 0; vd.arrayLayerCount = 1; vd.aspect = WGPUTextureAspect_All; shadow_placeholder_view_.set( wgpuTextureCreateView(shadow_placeholder_tex_.get(), &vd)); transp_placeholder_view_.set( wgpuTextureCreateView(transp_placeholder_tex_.get(), &vd)); create_raster_pipeline(); create_pack_pipeline(); } void GBufferEffect::declare_nodes(NodeRegistry& registry) { registry.declare_node(node_albedo_, NodeType::GBUF_ALBEDO, -1, -1); registry.declare_node(node_normal_mat_, NodeType::GBUF_ALBEDO, -1, -1); registry.declare_node(node_depth_, NodeType::GBUF_DEPTH32, -1, -1); registry.declare_node(node_shadow_, NodeType::GBUF_R8, -1, -1); registry.declare_node(node_transp_, NodeType::GBUF_R8, -1, -1); // feat_tex0 / feat_tex1 are the declared output_nodes_ — they get registered // by the sequence infrastructure; declare them here as well if not already. if (!registry.has_node(node_feat0_)) { registry.declare_node(node_feat0_, NodeType::GBUF_RGBA32UINT, -1, -1); } if (!registry.has_node(node_feat1_)) { registry.declare_node(node_feat1_, NodeType::GBUF_RGBA32UINT, -1, -1); } } void GBufferEffect::set_scene(const Scene* scene, const Camera* camera) { scene_ = scene; camera_ = camera; } void GBufferEffect::render(WGPUCommandEncoder encoder, const UniformsSequenceParams& params, NodeRegistry& nodes) { if (!scene_ || !camera_) { return; } upload_scene_data(*scene_, *camera_, params.time); // Update resolution uniform for pack shader. GBufResUniforms res_uni; res_uni.resolution = params.resolution; res_uni._pad0 = 0.0f; res_uni._pad1 = 0.0f; pack_res_uniform_.update(ctx_.queue, res_uni); WGPUTextureView albedo_view = nodes.get_view(node_albedo_); WGPUTextureView normal_mat_view = nodes.get_view(node_normal_mat_); WGPUTextureView depth_view = nodes.get_view(node_depth_); WGPUTextureView feat0_view = nodes.get_view(node_feat0_); WGPUTextureView feat1_view = nodes.get_view(node_feat1_); // prev_cnn: first input node if available, else dummy. WGPUTextureView prev_view = nullptr; if (!input_nodes_.empty()) { prev_view = nodes.get_view(input_nodes_[0]); } if (!prev_view) { prev_view = dummy_texture_view_.get(); } // --- Pass 1: MRT rasterization --- update_raster_bind_group(nodes); WGPURenderPassColorAttachment color_attachments[2] = {}; // Attachment 0: albedo color_attachments[0].view = albedo_view; color_attachments[0].loadOp = WGPULoadOp_Clear; color_attachments[0].storeOp = WGPUStoreOp_Store; color_attachments[0].clearValue = {0.0f, 0.0f, 0.0f, 1.0f}; color_attachments[0].depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; // Attachment 1: normal_mat color_attachments[1].view = normal_mat_view; color_attachments[1].loadOp = WGPULoadOp_Clear; color_attachments[1].storeOp = WGPUStoreOp_Store; color_attachments[1].clearValue = {0.5f, 0.5f, 0.0f, 0.0f}; color_attachments[1].depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; WGPURenderPassDepthStencilAttachment depth_attachment = {}; depth_attachment.view = depth_view; depth_attachment.depthLoadOp = WGPULoadOp_Clear; depth_attachment.depthStoreOp = WGPUStoreOp_Store; depth_attachment.depthClearValue = 1.0f; depth_attachment.depthReadOnly = false; WGPURenderPassDescriptor raster_pass_desc = {}; raster_pass_desc.colorAttachmentCount = 2; raster_pass_desc.colorAttachments = color_attachments; raster_pass_desc.depthStencilAttachment = &depth_attachment; const int num_objects = (int)(scene_->objects.size() < (size_t)kGBufMaxObjects ? scene_->objects.size() : (size_t)kGBufMaxObjects); if (num_objects > 0 && raster_pipeline_.get() != nullptr) { WGPURenderPassEncoder raster_pass = wgpuCommandEncoderBeginRenderPass(encoder, &raster_pass_desc); wgpuRenderPassEncoderSetPipeline(raster_pass, raster_pipeline_.get()); wgpuRenderPassEncoderSetBindGroup(raster_pass, 0, raster_bind_group_.get(), 0, nullptr); // Draw 36 vertices (proxy box) × num_objects instances. wgpuRenderPassEncoderDraw(raster_pass, 36, (uint32_t)num_objects, 0, 0); wgpuRenderPassEncoderEnd(raster_pass); wgpuRenderPassEncoderRelease(raster_pass); } else { // Clear passes with no draws still need to be submitted. WGPURenderPassEncoder raster_pass = wgpuCommandEncoderBeginRenderPass(encoder, &raster_pass_desc); wgpuRenderPassEncoderEnd(raster_pass); wgpuRenderPassEncoderRelease(raster_pass); } // Pass 2: SDF raymarching — TODO (placeholder: shadow=1, transp=0 already set) // Pass 3: Lighting/shadow — TODO // --- Pass 4: Pack compute --- // Rebuild pack bind group with current node views. // Construct a temporary bilinear sampler for this pass. WGPUSampler bilinear = create_bilinear_sampler(ctx_.device); // Get texture views from nodes. // shadow / transp are GBUF_R8 nodes; use their views. WGPUTextureView shadow_view = nodes.get_view(node_shadow_); WGPUTextureView transp_view = nodes.get_view(node_transp_); // Build pack bind group (bindings 0-9). WGPUBindGroupEntry pack_entries[10] = {}; pack_entries[0].binding = 0; pack_entries[0].buffer = pack_res_uniform_.get().buffer; pack_entries[0].size = sizeof(GBufResUniforms); pack_entries[1].binding = 1; pack_entries[1].textureView = albedo_view; pack_entries[2].binding = 2; pack_entries[2].textureView = normal_mat_view; pack_entries[3].binding = 3; pack_entries[3].textureView = depth_view; pack_entries[4].binding = 4; pack_entries[4].textureView = shadow_view; pack_entries[5].binding = 5; pack_entries[5].textureView = transp_view; pack_entries[6].binding = 6; pack_entries[6].textureView = prev_view; pack_entries[7].binding = 7; pack_entries[7].textureView = feat0_view; pack_entries[8].binding = 8; pack_entries[8].textureView = feat1_view; pack_entries[9].binding = 9; pack_entries[9].sampler = bilinear; WGPUBindGroupLayout pack_bgl = wgpuComputePipelineGetBindGroupLayout(pack_pipeline_.get(), 0); WGPUBindGroupDescriptor pack_bg_desc = {}; pack_bg_desc.layout = pack_bgl; pack_bg_desc.entryCount = 10; pack_bg_desc.entries = pack_entries; WGPUBindGroup pack_bg = wgpuDeviceCreateBindGroup(ctx_.device, &pack_bg_desc); wgpuBindGroupLayoutRelease(pack_bgl); WGPUComputePassDescriptor compute_pass_desc = {}; WGPUComputePassEncoder compute_pass = wgpuCommandEncoderBeginComputePass(encoder, &compute_pass_desc); wgpuComputePassEncoderSetPipeline(compute_pass, pack_pipeline_.get()); wgpuComputePassEncoderSetBindGroup(compute_pass, 0, pack_bg, 0, nullptr); const uint32_t wg_x = ((uint32_t)width_ + 7u) / 8u; const uint32_t wg_y = ((uint32_t)height_ + 7u) / 8u; wgpuComputePassEncoderDispatchWorkgroups(compute_pass, wg_x, wg_y, 1); wgpuComputePassEncoderEnd(compute_pass); wgpuComputePassEncoderRelease(compute_pass); wgpuBindGroupRelease(pack_bg); wgpuSamplerRelease(bilinear); } // ---- private helpers ---- void GBufferEffect::ensure_objects_buffer(int num_objects) { if (num_objects <= objects_buf_capacity_) { return; } if (objects_buf_.buffer) { wgpuBufferRelease(objects_buf_.buffer); } objects_buf_ = gpu_create_buffer( ctx_.device, (size_t)num_objects * sizeof(GBufObjectData), WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst); objects_buf_capacity_ = num_objects; } void GBufferEffect::upload_scene_data(const Scene& scene, const Camera& camera, float time) { const int num_objects = (int)(scene.objects.size() < (size_t)kGBufMaxObjects ? scene.objects.size() : (size_t)kGBufMaxObjects); const mat4 view = camera.get_view_matrix(); const mat4 proj = camera.get_projection_matrix(); const mat4 vp = proj * view; GBufGlobalUniforms gu = {}; gu.view_proj = vp; gu.inv_view_proj = vp.inverse(); gu.camera_pos_time = vec4(camera.position.x, camera.position.y, camera.position.z, time); gu.params = vec4((float)num_objects, 0.0f, 0.0f, 0.0f); gu.resolution = vec2((float)width_, (float)height_); gu.padding = vec2(0.0f, 0.0f); wgpuQueueWriteBuffer(ctx_.queue, global_uniforms_buf_.buffer, 0, &gu, sizeof(GBufGlobalUniforms)); // Upload object data. if (num_objects > 0) { ensure_objects_buffer(num_objects); std::vector obj_data; obj_data.reserve((size_t)num_objects); for (int i = 0; i < num_objects; ++i) { const Object3D& obj = scene.objects[(size_t)i]; const mat4 m = obj.get_model_matrix(); GBufObjectData d; d.model = m; d.inv_model = m.inverse(); d.color = obj.color; d.params = vec4(0.0f, 0.0f, 0.0f, 0.0f); obj_data.push_back(d); } wgpuQueueWriteBuffer(ctx_.queue, objects_buf_.buffer, 0, obj_data.data(), (size_t)num_objects * sizeof(GBufObjectData)); } } void GBufferEffect::create_raster_pipeline() { HEADLESS_RETURN_IF_NULL(ctx_.device); // Load shader source. const char* src = gbuf_raster_wgsl; if (!src) { return; // Asset not loaded yet; pipeline creation deferred. } const std::string composed = ShaderComposer::Get().Compose({"common_uniforms"}, src); WGPUShaderSourceWGSL wgsl_src = {}; wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL; wgsl_src.code = str_view(composed.c_str()); WGPUShaderModuleDescriptor shader_desc = {}; shader_desc.nextInChain = &wgsl_src.chain; WGPUShaderModule shader = wgpuDeviceCreateShaderModule(ctx_.device, &shader_desc); // Bind group layout: B0 = GlobalUniforms, B1 = ObjectsBuffer (storage read) WGPUBindGroupLayoutEntry bgl_entries[2] = {}; bgl_entries[0].binding = 0; bgl_entries[0].visibility = (WGPUShaderStage)(WGPUShaderStage_Vertex | WGPUShaderStage_Fragment); bgl_entries[0].buffer.type = WGPUBufferBindingType_Uniform; bgl_entries[0].buffer.minBindingSize = sizeof(GBufGlobalUniforms); bgl_entries[1].binding = 1; bgl_entries[1].visibility = (WGPUShaderStage)(WGPUShaderStage_Vertex | WGPUShaderStage_Fragment); bgl_entries[1].buffer.type = WGPUBufferBindingType_ReadOnlyStorage; bgl_entries[1].buffer.minBindingSize = sizeof(GBufObjectData); WGPUBindGroupLayoutDescriptor bgl_desc = {}; bgl_desc.entryCount = 2; bgl_desc.entries = bgl_entries; WGPUBindGroupLayout bgl = wgpuDeviceCreateBindGroupLayout(ctx_.device, &bgl_desc); WGPUPipelineLayoutDescriptor pl_desc = {}; pl_desc.bindGroupLayoutCount = 1; pl_desc.bindGroupLayouts = &bgl; WGPUPipelineLayout pl = wgpuDeviceCreatePipelineLayout(ctx_.device, &pl_desc); // Two color targets: albedo (rgba16float) and normal_mat (rgba16float) WGPUColorTargetState color_targets[2] = {}; color_targets[0].format = WGPUTextureFormat_RGBA16Float; color_targets[0].writeMask = WGPUColorWriteMask_All; color_targets[1].format = WGPUTextureFormat_RGBA16Float; color_targets[1].writeMask = WGPUColorWriteMask_All; WGPUFragmentState frag = {}; frag.module = shader; frag.entryPoint = str_view("fs_main"); frag.targetCount = 2; frag.targets = color_targets; WGPUDepthStencilState ds = {}; ds.format = WGPUTextureFormat_Depth32Float; ds.depthWriteEnabled = WGPUOptionalBool_True; ds.depthCompare = WGPUCompareFunction_Less; WGPURenderPipelineDescriptor pipe_desc = {}; pipe_desc.layout = pl; pipe_desc.vertex.module = shader; pipe_desc.vertex.entryPoint = str_view("vs_main"); pipe_desc.fragment = &frag; pipe_desc.depthStencil = &ds; pipe_desc.primitive.topology = WGPUPrimitiveTopology_TriangleList; pipe_desc.primitive.cullMode = WGPUCullMode_Back; pipe_desc.multisample.count = 1; pipe_desc.multisample.mask = 0xFFFFFFFF; raster_pipeline_.set(wgpuDeviceCreateRenderPipeline(ctx_.device, &pipe_desc)); wgpuPipelineLayoutRelease(pl); wgpuBindGroupLayoutRelease(bgl); wgpuShaderModuleRelease(shader); } void GBufferEffect::create_pack_pipeline() { HEADLESS_RETURN_IF_NULL(ctx_.device); const char* src = gbuf_pack_wgsl; if (!src) { return; } const std::string composed = ShaderComposer::Get().Compose({}, src); WGPUShaderSourceWGSL wgsl_src = {}; wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL; wgsl_src.code = str_view(composed.c_str()); WGPUShaderModuleDescriptor shader_desc = {}; shader_desc.nextInChain = &wgsl_src.chain; WGPUShaderModule shader = wgpuDeviceCreateShaderModule(ctx_.device, &shader_desc); // Build explicit bind group layout for bindings 0-9. WGPUBindGroupLayoutEntry bgl_entries[10] = {}; // B0: resolution uniform bgl_entries[0].binding = 0; bgl_entries[0].visibility = WGPUShaderStage_Compute; bgl_entries[0].buffer.type = WGPUBufferBindingType_Uniform; bgl_entries[0].buffer.minBindingSize = sizeof(GBufResUniforms); // B1: gbuf_albedo (texture_2d) bgl_entries[1].binding = 1; bgl_entries[1].visibility = WGPUShaderStage_Compute; bgl_entries[1].texture.sampleType = WGPUTextureSampleType_Float; bgl_entries[1].texture.viewDimension = WGPUTextureViewDimension_2D; // B2: gbuf_normal_mat (texture_2d) bgl_entries[2].binding = 2; bgl_entries[2].visibility = WGPUShaderStage_Compute; bgl_entries[2].texture.sampleType = WGPUTextureSampleType_Float; bgl_entries[2].texture.viewDimension = WGPUTextureViewDimension_2D; // B3: gbuf_depth (texture_depth_2d) bgl_entries[3].binding = 3; bgl_entries[3].visibility = WGPUShaderStage_Compute; bgl_entries[3].texture.sampleType = WGPUTextureSampleType_Depth; bgl_entries[3].texture.viewDimension = WGPUTextureViewDimension_2D; // B4: gbuf_shadow (texture_2d) bgl_entries[4].binding = 4; bgl_entries[4].visibility = WGPUShaderStage_Compute; bgl_entries[4].texture.sampleType = WGPUTextureSampleType_Float; bgl_entries[4].texture.viewDimension = WGPUTextureViewDimension_2D; // B5: gbuf_transp (texture_2d) bgl_entries[5].binding = 5; bgl_entries[5].visibility = WGPUShaderStage_Compute; bgl_entries[5].texture.sampleType = WGPUTextureSampleType_Float; bgl_entries[5].texture.viewDimension = WGPUTextureViewDimension_2D; // B6: prev_cnn (texture_2d) bgl_entries[6].binding = 6; bgl_entries[6].visibility = WGPUShaderStage_Compute; bgl_entries[6].texture.sampleType = WGPUTextureSampleType_Float; bgl_entries[6].texture.viewDimension = WGPUTextureViewDimension_2D; // B7: feat_tex0 (storage texture, write, rgba32uint) bgl_entries[7].binding = 7; bgl_entries[7].visibility = WGPUShaderStage_Compute; bgl_entries[7].storageTexture.access = WGPUStorageTextureAccess_WriteOnly; bgl_entries[7].storageTexture.format = WGPUTextureFormat_RGBA32Uint; bgl_entries[7].storageTexture.viewDimension = WGPUTextureViewDimension_2D; // B8: feat_tex1 (storage texture, write, rgba32uint) bgl_entries[8].binding = 8; bgl_entries[8].visibility = WGPUShaderStage_Compute; bgl_entries[8].storageTexture.access = WGPUStorageTextureAccess_WriteOnly; bgl_entries[8].storageTexture.format = WGPUTextureFormat_RGBA32Uint; bgl_entries[8].storageTexture.viewDimension = WGPUTextureViewDimension_2D; // B9: bilinear sampler bgl_entries[9].binding = 9; bgl_entries[9].visibility = WGPUShaderStage_Compute; bgl_entries[9].sampler.type = WGPUSamplerBindingType_Filtering; WGPUBindGroupLayoutDescriptor bgl_desc = {}; bgl_desc.entryCount = 10; bgl_desc.entries = bgl_entries; WGPUBindGroupLayout bgl = wgpuDeviceCreateBindGroupLayout(ctx_.device, &bgl_desc); WGPUPipelineLayoutDescriptor pl_desc = {}; pl_desc.bindGroupLayoutCount = 1; pl_desc.bindGroupLayouts = &bgl; WGPUPipelineLayout pl = wgpuDeviceCreatePipelineLayout(ctx_.device, &pl_desc); WGPUComputePipelineDescriptor pipe_desc = {}; pipe_desc.layout = pl; pipe_desc.compute.module = shader; pipe_desc.compute.entryPoint = str_view("pack_features"); pack_pipeline_.set(wgpuDeviceCreateComputePipeline(ctx_.device, &pipe_desc)); wgpuPipelineLayoutRelease(pl); wgpuBindGroupLayoutRelease(bgl); wgpuShaderModuleRelease(shader); } void GBufferEffect::update_raster_bind_group(NodeRegistry& nodes) { (void)nodes; // Rebuild each frame since textures may resize. raster_bind_group_.replace(nullptr); if (raster_pipeline_.get() == nullptr) { return; } WGPUBindGroupEntry entries[2] = {}; entries[0].binding = 0; entries[0].buffer = global_uniforms_buf_.buffer; entries[0].size = sizeof(GBufGlobalUniforms); entries[1].binding = 1; entries[1].buffer = objects_buf_.buffer; entries[1].size = (size_t)objects_buf_capacity_ * sizeof(GBufObjectData); WGPUBindGroupLayout bgl = wgpuRenderPipelineGetBindGroupLayout(raster_pipeline_.get(), 0); WGPUBindGroupDescriptor bg_desc = {}; bg_desc.layout = bgl; bg_desc.entryCount = 2; bg_desc.entries = entries; raster_bind_group_.replace(wgpuDeviceCreateBindGroup(ctx_.device, &bg_desc)); wgpuBindGroupLayoutRelease(bgl); } void GBufferEffect::update_pack_bind_group(NodeRegistry& nodes) { (void)nodes; // Pack bind group is rebuilt inline in render() to use current node views. }