From 744bcadfe8f4bb1b2d4f1daf9f880fa511d65405 Mon Sep 17 00:00:00 2001 From: skal Date: Mon, 9 Feb 2026 13:59:07 +0100 Subject: feat(gpu): Phase 2 - Add gen_perlin and gen_grid GPU compute shaders Complete Phase 2 implementation: - gen_perlin.wgsl: FBM with configurable octaves, amplitude decay - gen_grid.wgsl: Grid pattern with configurable spacing/thickness - TextureManager extensions: create_gpu_perlin_texture(), create_gpu_grid_texture() - Asset packer now validates gen_noise, gen_perlin, gen_grid for PROC_GPU() - 3 compute pipelines (lazy-init on first use) Shader parameters: - gen_perlin: seed, frequency, amplitude, amplitude_decay, octaves (32 bytes) - gen_grid: width, height, grid_size, thickness (16 bytes) test_3d_render migration: - Replaced CPU sky texture (gen_perlin) with GPU version - Replaced CPU noise texture (gen_noise) with GPU version - Added new GPU grid texture (256x256, 32px grid, 2px lines) Size impact: - gen_perlin.wgsl: ~200 bytes (compressed) - gen_grid.wgsl: ~100 bytes (compressed) - Total Phase 2 code: ~300 bytes - Cumulative (Phase 1+2): ~600 bytes Testing: - All 34 tests passing (100%) - test_gpu_procedural validates all generators - test_3d_render uses 3 GPU textures (noise, perlin, grid) Next: Phase 3 - Variable dimensions, async generation, pipeline caching Co-Authored-By: Claude Sonnet 4.5 --- src/gpu/effects/shaders.cc | 8 + src/gpu/effects/shaders.h | 2 + src/gpu/texture_manager.cc | 354 ++++++++++++++++++++++++++++++++++++++++++++ src/gpu/texture_manager.h | 10 ++ src/tests/test_3d_render.cc | 39 +++-- 5 files changed, 399 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/gpu/effects/shaders.cc b/src/gpu/effects/shaders.cc index 5c6dd37..6ed82d5 100644 --- a/src/gpu/effects/shaders.cc +++ b/src/gpu/effects/shaders.cc @@ -103,6 +103,14 @@ const char* gen_noise_compute_wgsl = SafeGetAsset(AssetId::ASSET_SHADER_COMPUTE_GEN_NOISE); +const char* gen_perlin_compute_wgsl = + + SafeGetAsset(AssetId::ASSET_SHADER_COMPUTE_GEN_PERLIN); + +const char* gen_grid_compute_wgsl = + + SafeGetAsset(AssetId::ASSET_SHADER_COMPUTE_GEN_GRID); + const char* vignette_shader_wgsl = SafeGetAsset(AssetId::ASSET_SHADER_VIGNETTE); diff --git a/src/gpu/effects/shaders.h b/src/gpu/effects/shaders.h index b629e30..a0f91da 100644 --- a/src/gpu/effects/shaders.h +++ b/src/gpu/effects/shaders.h @@ -19,3 +19,5 @@ extern const char* distort_shader_wgsl; extern const char* chroma_aberration_shader_wgsl; extern const char* vignette_shader_wgsl; extern const char* gen_noise_compute_wgsl; +extern const char* gen_perlin_compute_wgsl; +extern const char* gen_grid_compute_wgsl; diff --git a/src/gpu/texture_manager.cc b/src/gpu/texture_manager.cc index aff106a..9a19957 100644 --- a/src/gpu/texture_manager.cc +++ b/src/gpu/texture_manager.cc @@ -22,6 +22,8 @@ void TextureManager::init(WGPUDevice device, WGPUQueue queue) { device_ = device; queue_ = queue; noise_compute_pipeline_ = nullptr; + perlin_compute_pipeline_ = nullptr; + grid_compute_pipeline_ = nullptr; } void TextureManager::shutdown() { @@ -34,6 +36,14 @@ void TextureManager::shutdown() { wgpuComputePipelineRelease(noise_compute_pipeline_); noise_compute_pipeline_ = nullptr; } + if (perlin_compute_pipeline_) { + wgpuComputePipelineRelease(perlin_compute_pipeline_); + perlin_compute_pipeline_ = nullptr; + } + if (grid_compute_pipeline_) { + wgpuComputePipelineRelease(grid_compute_pipeline_); + grid_compute_pipeline_ = nullptr; + } } void TextureManager::create_procedural_texture( @@ -297,6 +307,350 @@ void TextureManager::create_gpu_noise_texture( #endif } +void TextureManager::dispatch_perlin_compute(WGPUTexture target, + const GpuProceduralParams& params) { + // Lazy-init compute pipeline + if (!perlin_compute_pipeline_) { + extern const char* gen_perlin_compute_wgsl; + ShaderComposer& composer = ShaderComposer::Get(); + std::string resolved_shader = composer.Compose({}, gen_perlin_compute_wgsl); + + WGPUShaderSourceWGSL wgsl_src = {}; + wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL; + wgsl_src.code = str_view(resolved_shader.c_str()); + WGPUShaderModuleDescriptor shader_desc = {}; + shader_desc.nextInChain = &wgsl_src.chain; + WGPUShaderModule shader_module = + wgpuDeviceCreateShaderModule(device_, &shader_desc); + + WGPUBindGroupLayoutEntry bgl_entries[2] = {}; + bgl_entries[0].binding = 0; + bgl_entries[0].visibility = WGPUShaderStage_Compute; + bgl_entries[0].storageTexture.access = WGPUStorageTextureAccess_WriteOnly; + bgl_entries[0].storageTexture.format = WGPUTextureFormat_RGBA8Unorm; + bgl_entries[0].storageTexture.viewDimension = WGPUTextureViewDimension_2D; + bgl_entries[1].binding = 1; + bgl_entries[1].visibility = WGPUShaderStage_Compute; + bgl_entries[1].buffer.type = WGPUBufferBindingType_Uniform; + bgl_entries[1].buffer.minBindingSize = 32; // sizeof(PerlinParams) + + WGPUBindGroupLayoutDescriptor bgl_desc = {}; + bgl_desc.entryCount = 2; + bgl_desc.entries = bgl_entries; + WGPUBindGroupLayout bind_group_layout = + wgpuDeviceCreateBindGroupLayout(device_, &bgl_desc); + + WGPUPipelineLayoutDescriptor pl_desc = {}; + pl_desc.bindGroupLayoutCount = 1; + pl_desc.bindGroupLayouts = &bind_group_layout; + WGPUPipelineLayout pipeline_layout = + wgpuDeviceCreatePipelineLayout(device_, &pl_desc); + + WGPUComputePipelineDescriptor pipeline_desc = {}; + pipeline_desc.layout = pipeline_layout; + pipeline_desc.compute.module = shader_module; + pipeline_desc.compute.entryPoint = str_view("main"); + + perlin_compute_pipeline_ = + wgpuDeviceCreateComputePipeline(device_, &pipeline_desc); + + wgpuPipelineLayoutRelease(pipeline_layout); + wgpuBindGroupLayoutRelease(bind_group_layout); + wgpuShaderModuleRelease(shader_module); + } + + // Create uniform buffer + struct PerlinParams { + uint32_t width; + uint32_t height; + float seed; + float frequency; + float amplitude; + float amplitude_decay; + uint32_t octaves; + float _pad0; + }; + PerlinParams uniform_data = { + (uint32_t)params.width, + (uint32_t)params.height, + params.params[0], // seed + params.params[1], // frequency + params.num_params > 2 ? params.params[2] : 1.0f, // amplitude + params.num_params > 3 ? params.params[3] : 0.5f, // amplitude_decay + params.num_params > 4 ? (uint32_t)params.params[4] : 4u, // octaves + 0.0f // padding + }; + + WGPUBufferDescriptor buf_desc = {}; + buf_desc.size = sizeof(PerlinParams); + buf_desc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst; + buf_desc.mappedAtCreation = WGPUOptionalBool_True; + WGPUBuffer uniform_buf = wgpuDeviceCreateBuffer(device_, &buf_desc); + void* mapped = wgpuBufferGetMappedRange(uniform_buf, 0, sizeof(PerlinParams)); + memcpy(mapped, &uniform_data, sizeof(PerlinParams)); + wgpuBufferUnmap(uniform_buf); + + WGPUTextureViewDescriptor view_desc = {}; + view_desc.format = WGPUTextureFormat_RGBA8Unorm; + view_desc.dimension = WGPUTextureViewDimension_2D; + view_desc.mipLevelCount = 1; + view_desc.arrayLayerCount = 1; + WGPUTextureView target_view = wgpuTextureCreateView(target, &view_desc); + + WGPUBindGroupLayoutEntry bgl_entries[2] = {}; + bgl_entries[0].binding = 0; + bgl_entries[0].visibility = WGPUShaderStage_Compute; + bgl_entries[0].storageTexture.access = WGPUStorageTextureAccess_WriteOnly; + bgl_entries[0].storageTexture.format = WGPUTextureFormat_RGBA8Unorm; + bgl_entries[0].storageTexture.viewDimension = WGPUTextureViewDimension_2D; + bgl_entries[1].binding = 1; + bgl_entries[1].visibility = WGPUShaderStage_Compute; + bgl_entries[1].buffer.type = WGPUBufferBindingType_Uniform; + bgl_entries[1].buffer.minBindingSize = 32; + + WGPUBindGroupLayoutDescriptor bgl_desc = {}; + bgl_desc.entryCount = 2; + bgl_desc.entries = bgl_entries; + WGPUBindGroupLayout bind_group_layout = + wgpuDeviceCreateBindGroupLayout(device_, &bgl_desc); + + WGPUBindGroupEntry bg_entries[2] = {}; + bg_entries[0].binding = 0; + bg_entries[0].textureView = target_view; + bg_entries[1].binding = 1; + bg_entries[1].buffer = uniform_buf; + bg_entries[1].size = sizeof(PerlinParams); + + WGPUBindGroupDescriptor bg_desc = {}; + bg_desc.layout = bind_group_layout; + bg_desc.entryCount = 2; + bg_desc.entries = bg_entries; + WGPUBindGroup bind_group = wgpuDeviceCreateBindGroup(device_, &bg_desc); + + WGPUCommandEncoderDescriptor enc_desc = {}; + WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(device_, &enc_desc); + WGPUComputePassEncoder pass = wgpuCommandEncoderBeginComputePass(encoder, nullptr); + wgpuComputePassEncoderSetPipeline(pass, perlin_compute_pipeline_); + wgpuComputePassEncoderSetBindGroup(pass, 0, bind_group, 0, nullptr); + wgpuComputePassEncoderDispatchWorkgroups(pass, (params.width + 7) / 8, + (params.height + 7) / 8, 1); + wgpuComputePassEncoderEnd(pass); + + WGPUCommandBufferDescriptor cmd_desc = {}; + WGPUCommandBuffer cmd = wgpuCommandEncoderFinish(encoder, &cmd_desc); + wgpuQueueSubmit(queue_, 1, &cmd); + + wgpuCommandBufferRelease(cmd); + wgpuCommandEncoderRelease(encoder); + wgpuComputePassEncoderRelease(pass); + wgpuBindGroupRelease(bind_group); + wgpuBindGroupLayoutRelease(bind_group_layout); + wgpuBufferRelease(uniform_buf); + wgpuTextureViewRelease(target_view); +} + +void TextureManager::create_gpu_perlin_texture( + const std::string& name, const GpuProceduralParams& params) { + WGPUTextureDescriptor tex_desc = {}; + tex_desc.usage = + WGPUTextureUsage_StorageBinding | WGPUTextureUsage_TextureBinding; + tex_desc.dimension = WGPUTextureDimension_2D; + tex_desc.size = {(uint32_t)params.width, (uint32_t)params.height, 1}; + tex_desc.format = WGPUTextureFormat_RGBA8Unorm; + tex_desc.mipLevelCount = 1; + tex_desc.sampleCount = 1; + WGPUTexture texture = wgpuDeviceCreateTexture(device_, &tex_desc); + + dispatch_perlin_compute(texture, params); + + WGPUTextureViewDescriptor view_desc = {}; + view_desc.format = WGPUTextureFormat_RGBA8Unorm; + view_desc.dimension = WGPUTextureViewDimension_2D; + view_desc.mipLevelCount = 1; + view_desc.arrayLayerCount = 1; + WGPUTextureView view = wgpuTextureCreateView(texture, &view_desc); + + GpuTexture gpu_tex; + gpu_tex.texture = texture; + gpu_tex.view = view; + gpu_tex.width = params.width; + gpu_tex.height = params.height; + textures_[name] = gpu_tex; + +#if !defined(STRIP_ALL) + printf("Generated GPU perlin texture: %s (%dx%d)\n", name.c_str(), + params.width, params.height); +#endif +} + +void TextureManager::dispatch_grid_compute(WGPUTexture target, + const GpuProceduralParams& params) { + // Lazy-init compute pipeline + if (!grid_compute_pipeline_) { + extern const char* gen_grid_compute_wgsl; + ShaderComposer& composer = ShaderComposer::Get(); + std::string resolved_shader = composer.Compose({}, gen_grid_compute_wgsl); + + WGPUShaderSourceWGSL wgsl_src = {}; + wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL; + wgsl_src.code = str_view(resolved_shader.c_str()); + WGPUShaderModuleDescriptor shader_desc = {}; + shader_desc.nextInChain = &wgsl_src.chain; + WGPUShaderModule shader_module = + wgpuDeviceCreateShaderModule(device_, &shader_desc); + + WGPUBindGroupLayoutEntry bgl_entries[2] = {}; + bgl_entries[0].binding = 0; + bgl_entries[0].visibility = WGPUShaderStage_Compute; + bgl_entries[0].storageTexture.access = WGPUStorageTextureAccess_WriteOnly; + bgl_entries[0].storageTexture.format = WGPUTextureFormat_RGBA8Unorm; + bgl_entries[0].storageTexture.viewDimension = WGPUTextureViewDimension_2D; + bgl_entries[1].binding = 1; + bgl_entries[1].visibility = WGPUShaderStage_Compute; + bgl_entries[1].buffer.type = WGPUBufferBindingType_Uniform; + bgl_entries[1].buffer.minBindingSize = 16; // sizeof(GridParams) + + WGPUBindGroupLayoutDescriptor bgl_desc = {}; + bgl_desc.entryCount = 2; + bgl_desc.entries = bgl_entries; + WGPUBindGroupLayout bind_group_layout = + wgpuDeviceCreateBindGroupLayout(device_, &bgl_desc); + + WGPUPipelineLayoutDescriptor pl_desc = {}; + pl_desc.bindGroupLayoutCount = 1; + pl_desc.bindGroupLayouts = &bind_group_layout; + WGPUPipelineLayout pipeline_layout = + wgpuDeviceCreatePipelineLayout(device_, &pl_desc); + + WGPUComputePipelineDescriptor pipeline_desc = {}; + pipeline_desc.layout = pipeline_layout; + pipeline_desc.compute.module = shader_module; + pipeline_desc.compute.entryPoint = str_view("main"); + + grid_compute_pipeline_ = + wgpuDeviceCreateComputePipeline(device_, &pipeline_desc); + + wgpuPipelineLayoutRelease(pipeline_layout); + wgpuBindGroupLayoutRelease(bind_group_layout); + wgpuShaderModuleRelease(shader_module); + } + + // Create uniform buffer + struct GridParams { + uint32_t width; + uint32_t height; + uint32_t grid_size; + uint32_t thickness; + }; + GridParams uniform_data = { + (uint32_t)params.width, + (uint32_t)params.height, + params.num_params > 0 ? (uint32_t)params.params[0] : 32u, // grid_size + params.num_params > 1 ? (uint32_t)params.params[1] : 2u // thickness + }; + + WGPUBufferDescriptor buf_desc = {}; + buf_desc.size = sizeof(GridParams); + buf_desc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst; + buf_desc.mappedAtCreation = WGPUOptionalBool_True; + WGPUBuffer uniform_buf = wgpuDeviceCreateBuffer(device_, &buf_desc); + void* mapped = wgpuBufferGetMappedRange(uniform_buf, 0, sizeof(GridParams)); + memcpy(mapped, &uniform_data, sizeof(GridParams)); + wgpuBufferUnmap(uniform_buf); + + WGPUTextureViewDescriptor view_desc = {}; + view_desc.format = WGPUTextureFormat_RGBA8Unorm; + view_desc.dimension = WGPUTextureViewDimension_2D; + view_desc.mipLevelCount = 1; + view_desc.arrayLayerCount = 1; + WGPUTextureView target_view = wgpuTextureCreateView(target, &view_desc); + + WGPUBindGroupLayoutEntry bgl_entries[2] = {}; + bgl_entries[0].binding = 0; + bgl_entries[0].visibility = WGPUShaderStage_Compute; + bgl_entries[0].storageTexture.access = WGPUStorageTextureAccess_WriteOnly; + bgl_entries[0].storageTexture.format = WGPUTextureFormat_RGBA8Unorm; + bgl_entries[0].storageTexture.viewDimension = WGPUTextureViewDimension_2D; + bgl_entries[1].binding = 1; + bgl_entries[1].visibility = WGPUShaderStage_Compute; + bgl_entries[1].buffer.type = WGPUBufferBindingType_Uniform; + bgl_entries[1].buffer.minBindingSize = 16; + + WGPUBindGroupLayoutDescriptor bgl_desc = {}; + bgl_desc.entryCount = 2; + bgl_desc.entries = bgl_entries; + WGPUBindGroupLayout bind_group_layout = + wgpuDeviceCreateBindGroupLayout(device_, &bgl_desc); + + WGPUBindGroupEntry bg_entries[2] = {}; + bg_entries[0].binding = 0; + bg_entries[0].textureView = target_view; + bg_entries[1].binding = 1; + bg_entries[1].buffer = uniform_buf; + bg_entries[1].size = sizeof(GridParams); + + WGPUBindGroupDescriptor bg_desc = {}; + bg_desc.layout = bind_group_layout; + bg_desc.entryCount = 2; + bg_desc.entries = bg_entries; + WGPUBindGroup bind_group = wgpuDeviceCreateBindGroup(device_, &bg_desc); + + WGPUCommandEncoderDescriptor enc_desc = {}; + WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(device_, &enc_desc); + WGPUComputePassEncoder pass = wgpuCommandEncoderBeginComputePass(encoder, nullptr); + wgpuComputePassEncoderSetPipeline(pass, grid_compute_pipeline_); + wgpuComputePassEncoderSetBindGroup(pass, 0, bind_group, 0, nullptr); + wgpuComputePassEncoderDispatchWorkgroups(pass, (params.width + 7) / 8, + (params.height + 7) / 8, 1); + wgpuComputePassEncoderEnd(pass); + + WGPUCommandBufferDescriptor cmd_desc = {}; + WGPUCommandBuffer cmd = wgpuCommandEncoderFinish(encoder, &cmd_desc); + wgpuQueueSubmit(queue_, 1, &cmd); + + wgpuCommandBufferRelease(cmd); + wgpuCommandEncoderRelease(encoder); + wgpuComputePassEncoderRelease(pass); + wgpuBindGroupRelease(bind_group); + wgpuBindGroupLayoutRelease(bind_group_layout); + wgpuBufferRelease(uniform_buf); + wgpuTextureViewRelease(target_view); +} + +void TextureManager::create_gpu_grid_texture( + const std::string& name, const GpuProceduralParams& params) { + WGPUTextureDescriptor tex_desc = {}; + tex_desc.usage = + WGPUTextureUsage_StorageBinding | WGPUTextureUsage_TextureBinding; + tex_desc.dimension = WGPUTextureDimension_2D; + tex_desc.size = {(uint32_t)params.width, (uint32_t)params.height, 1}; + tex_desc.format = WGPUTextureFormat_RGBA8Unorm; + tex_desc.mipLevelCount = 1; + tex_desc.sampleCount = 1; + WGPUTexture texture = wgpuDeviceCreateTexture(device_, &tex_desc); + + dispatch_grid_compute(texture, params); + + WGPUTextureViewDescriptor view_desc = {}; + view_desc.format = WGPUTextureFormat_RGBA8Unorm; + view_desc.dimension = WGPUTextureViewDimension_2D; + view_desc.mipLevelCount = 1; + view_desc.arrayLayerCount = 1; + WGPUTextureView view = wgpuTextureCreateView(texture, &view_desc); + + GpuTexture gpu_tex; + gpu_tex.texture = texture; + gpu_tex.view = view; + gpu_tex.width = params.width; + gpu_tex.height = params.height; + textures_[name] = gpu_tex; + +#if !defined(STRIP_ALL) + printf("Generated GPU grid texture: %s (%dx%d)\n", name.c_str(), + params.width, params.height); +#endif +} + #if !defined(STRIP_ALL) WGPUTextureView TextureManager::get_or_generate_gpu_texture( const std::string& name, const GpuProceduralParams& params) { diff --git a/src/gpu/texture_manager.h b/src/gpu/texture_manager.h index 0cffe0c..b2dea84 100644 --- a/src/gpu/texture_manager.h +++ b/src/gpu/texture_manager.h @@ -46,6 +46,10 @@ class TextureManager { // GPU procedural generation void create_gpu_noise_texture(const std::string& name, const GpuProceduralParams& params); + void create_gpu_perlin_texture(const std::string& name, + const GpuProceduralParams& params); + void create_gpu_grid_texture(const std::string& name, + const GpuProceduralParams& params); #if !defined(STRIP_ALL) // On-demand lazy generation (stripped in final builds) @@ -59,8 +63,14 @@ class TextureManager { private: void dispatch_noise_compute(WGPUTexture target, const GpuProceduralParams& params); + void dispatch_perlin_compute(WGPUTexture target, + const GpuProceduralParams& params); + void dispatch_grid_compute(WGPUTexture target, + const GpuProceduralParams& params); WGPUDevice device_; WGPUQueue queue_; std::map textures_; WGPUComputePipeline noise_compute_pipeline_ = nullptr; + WGPUComputePipeline perlin_compute_pipeline_ = nullptr; + WGPUComputePipeline grid_compute_pipeline_ = nullptr; }; diff --git a/src/tests/test_3d_render.cc b/src/tests/test_3d_render.cc index fa13a43..eee46ba 100644 --- a/src/tests/test_3d_render.cc +++ b/src/tests/test_3d_render.cc @@ -220,25 +220,36 @@ int main(int argc, char** argv) { g_renderer.resize(platform_state.width, platform_state.height); g_textures.init(g_device, g_queue); - ProceduralTextureDef noise_def; - noise_def.width = 256; - noise_def.height = 256; - noise_def.gen_func = gen_periodic_noise; - noise_def.params.push_back(1234.0f); - noise_def.params.push_back(16.0f); - g_textures.create_procedural_texture("noise", noise_def); + // GPU Noise texture (replaces CPU procedural) + GpuProceduralParams noise_params = {}; + noise_params.width = 256; + noise_params.height = 256; + float noise_vals[2] = {1234.0f, 16.0f}; + noise_params.params = noise_vals; + noise_params.num_params = 2; + g_textures.create_gpu_noise_texture("noise", noise_params); g_renderer.set_noise_texture(g_textures.get_texture_view("noise")); - ProceduralTextureDef sky_def; - sky_def.width = 512; - sky_def.height = 256; - sky_def.gen_func = procedural::gen_perlin; - sky_def.params = {42.0f, 4.0f, 1.0f, 0.5f, 6.0f}; - g_textures.create_procedural_texture("sky", sky_def); - + // GPU Perlin texture for sky (replaces CPU procedural) + GpuProceduralParams sky_params = {}; + sky_params.width = 512; + sky_params.height = 256; + float sky_vals[5] = {42.0f, 4.0f, 1.0f, 0.5f, 6.0f}; + sky_params.params = sky_vals; + sky_params.num_params = 5; + g_textures.create_gpu_perlin_texture("sky", sky_params); g_renderer.set_sky_texture(g_textures.get_texture_view("sky")); + // GPU Grid texture (new!) + GpuProceduralParams grid_params = {}; + grid_params.width = 256; + grid_params.height = 256; + float grid_vals[2] = {32.0f, 2.0f}; // grid_size, thickness + grid_params.params = grid_vals; + grid_params.num_params = 2; + g_textures.create_gpu_grid_texture("grid", grid_params); + setup_scene(); g_camera.position = vec3(0, 5, 10); -- cgit v1.2.3