diff options
Diffstat (limited to 'src')
49 files changed, 1620 insertions, 174 deletions
diff --git a/src/3d/visual_debug.cc b/src/3d/visual_debug.cc index 77311f6..cd4ccce 100644 --- a/src/3d/visual_debug.cc +++ b/src/3d/visual_debug.cc @@ -26,7 +26,7 @@ void VisualDebug::init(WGPUDevice device, WGPUTextureFormat format) { WGPUBufferDescriptor ub_desc = {}; ub_desc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst; - ub_desc.size = sizeof(mat4); + ub_desc.size = sizeof(GlobalUniforms); uniform_buffer_ = wgpuDeviceCreateBuffer(device_, &ub_desc); } @@ -340,9 +340,12 @@ void VisualDebug::add_trajectory(const std::vector<vec3>& points, } void VisualDebug::update_buffers(const mat4& view_proj) { - // Update Uniforms + // Update Uniforms - fill entire GlobalUniforms structure + GlobalUniforms uniforms = {}; + uniforms.view_proj = view_proj; + // Other fields zeroed (not used by visual debug shader) wgpuQueueWriteBuffer(wgpuDeviceGetQueue(device_), uniform_buffer_, 0, - &view_proj, sizeof(mat4)); + &uniforms, sizeof(GlobalUniforms)); // Update Vertices size_t required_size = lines_.size() * 2 * sizeof(float) * 6; @@ -385,7 +388,7 @@ void VisualDebug::update_buffers(const mat4& view_proj) { WGPUBindGroupEntry bg_entry = {}; bg_entry.binding = 0; bg_entry.buffer = uniform_buffer_; - bg_entry.size = sizeof(mat4); + bg_entry.size = sizeof(GlobalUniforms); WGPUBindGroupDescriptor bg_desc = {}; bg_desc.layout = bind_group_layout_; diff --git a/src/audio/audio.cc b/src/audio/audio.cc index 2f485a6..c5bd3d9 100644 --- a/src/audio/audio.cc +++ b/src/audio/audio.cc @@ -65,9 +65,11 @@ void audio_start() { g_audio_backend->start(); } -void audio_render_ahead(float music_time, float dt) { +void audio_render_ahead(float music_time, float dt, float target_fill) { // Target: maintain look-ahead buffer - const float target_lookahead = (float)RING_BUFFER_LOOKAHEAD_MS / 1000.0f; + const float target_lookahead = (target_fill < 0.0f) + ? (float)RING_BUFFER_LOOKAHEAD_MS / 1000.0f + : target_fill; // Render in small chunks to keep synth time synchronized with tracker // Chunk size: one frame's worth of audio (~16.6ms @ 60fps) diff --git a/src/audio/audio.h b/src/audio/audio.h index e063a57..778d312 100644 --- a/src/audio/audio.h +++ b/src/audio/audio.h @@ -24,7 +24,8 @@ void audio_init(); void audio_start(); // Starts the audio device callback // Ring buffer audio rendering (main thread fills buffer) -void audio_render_ahead(float music_time, float dt); +// target_fill: Target buffer fill time in seconds (default: RING_BUFFER_LOOKAHEAD_MS/1000) +void audio_render_ahead(float music_time, float dt, float target_fill = -1.0f); // Get current playback time (in seconds) based on samples consumed // This is the ring buffer READ position (what's being played NOW) diff --git a/src/gpu/demo_effects.h b/src/gpu/demo_effects.h index 54bf657..ff7e017 100644 --- a/src/gpu/demo_effects.h +++ b/src/gpu/demo_effects.h @@ -7,12 +7,14 @@ #include "3d/scene.h" #include "effect.h" #include "gpu/effects/circle_mask_effect.h" -#include "gpu/effects/fade_effect.h" // FadeEffect with full definition +#include "gpu/effects/fade_effect.h" // FadeEffect with full definition #include "gpu/effects/flash_effect.h" // FlashEffect with params support #include "gpu/effects/post_process_helper.h" #include "gpu/effects/rotating_cube_effect.h" #include "gpu/effects/shaders.h" #include "gpu/effects/theme_modulation_effect.h" // ThemeModulationEffect with full definition +#include "gpu/effects/hybrid_3d_effect.h" +#include "gpu/effects/flash_cube_effect.h" #include "gpu/gpu.h" #include "gpu/texture_manager.h" #include "gpu/uniform_helper.h" @@ -49,7 +51,6 @@ class ParticlesEffect : public Effect { ComputePass compute_pass_; RenderPass render_pass_; GpuBuffer particles_buffer_; - UniformBuffer<CommonPostProcessUniforms> uniforms_; }; class PassthroughEffect : public PostProcessEffect { @@ -58,7 +59,6 @@ class PassthroughEffect : public PostProcessEffect { void update_bind_group(WGPUTextureView input_view) override; private: - UniformBuffer<CommonPostProcessUniforms> uniforms_; }; class MovingEllipseEffect : public Effect { @@ -83,7 +83,6 @@ class ParticleSprayEffect : public Effect { ComputePass compute_pass_; RenderPass render_pass_; GpuBuffer particles_buffer_; - UniformBuffer<CommonPostProcessUniforms> uniforms_; }; // Parameters for GaussianBlurEffect (set at construction time) @@ -106,7 +105,6 @@ class GaussianBlurEffect : public PostProcessEffect { private: GaussianBlurParams params_; - UniformBuffer<CommonPostProcessUniforms> uniforms_; UniformBuffer<GaussianBlurParams> params_buffer_; }; @@ -118,7 +116,6 @@ class SolarizeEffect : public PostProcessEffect { void update_bind_group(WGPUTextureView input_view) override; private: - UniformBuffer<CommonPostProcessUniforms> uniforms_; }; // Parameters for VignetteEffect @@ -137,7 +134,6 @@ class VignetteEffect : public PostProcessEffect { private: VignetteParams params_; - UniformBuffer<CommonPostProcessUniforms> uniforms_; UniformBuffer<VignetteParams> params_buffer_; }; @@ -160,48 +156,33 @@ class ChromaAberrationEffect : public PostProcessEffect { private: ChromaAberrationParams params_; - UniformBuffer<CommonPostProcessUniforms> uniforms_; UniformBuffer<ChromaAberrationParams> params_buffer_; }; -class Hybrid3DEffect : public Effect { - public: - Hybrid3DEffect(const GpuContext& ctx); - void init(MainSequence* demo) override; - void render(WGPURenderPassEncoder pass, float time, float beat, - float intensity, float aspect_ratio) override; - - private: - Renderer3D renderer_; - TextureManager texture_manager_; - Scene scene_; - Camera camera_; - int width_ = 1280; - int height_ = 720; +// Parameters for DistortEffect +struct DistortParams { + float strength = 0.01f; // Default distortion strength + float speed = 1.0f; // Default distortion speed }; +static_assert(sizeof(DistortParams) == 8, "DistortParams must be 8 bytes for WGSL alignment"); -class FlashCubeEffect : public Effect { +class DistortEffect : public PostProcessEffect { public: - FlashCubeEffect(const GpuContext& ctx); - void init(MainSequence* demo) override; - void resize(int width, int height) override; + DistortEffect(const GpuContext& ctx); + DistortEffect(const GpuContext& ctx, const DistortParams& params); void render(WGPURenderPassEncoder pass, float time, float beat, float intensity, float aspect_ratio) override; + void update_bind_group(WGPUTextureView input_view) override; private: - Renderer3D renderer_; - TextureManager texture_manager_; - Scene scene_; - Camera camera_; - int width_ = 1280; - int height_ = 720; - float last_beat_; - float flash_intensity_; + DistortParams params_; + UniformBuffer<DistortParams> params_buffer_; }; -// ThemeModulationEffect now defined in gpu/effects/theme_modulation_effect.h (included above) -// FadeEffect now defined in gpu/effects/fade_effect.h (included above) -// FlashEffect now defined in gpu/effects/flash_effect.h (included above) +// ThemeModulationEffect now defined in gpu/effects/theme_modulation_effect.h +// (included above) FadeEffect now defined in gpu/effects/fade_effect.h +// (included above) FlashEffect now defined in gpu/effects/flash_effect.h +// (included above) // Auto-generated functions void LoadTimeline(MainSequence& main_seq, const GpuContext& ctx); diff --git a/src/gpu/effect.h b/src/gpu/effect.h index 6fdb0f4..8f35f3c 100644 --- a/src/gpu/effect.h +++ b/src/gpu/effect.h @@ -1,5 +1,7 @@ #pragma once #include "gpu/gpu.h" +#include "gpu/effects/post_process_helper.h" +#include "gpu/uniform_helper.h" #include <algorithm> #include <map> #include <memory> @@ -12,6 +14,7 @@ class PostProcessEffect; class Effect { public: Effect(const GpuContext& ctx) : ctx_(ctx) { + uniforms_.init(ctx.device); } virtual ~Effect() = default; virtual void init(MainSequence* demo) { @@ -43,7 +46,7 @@ class Effect { protected: const GpuContext& ctx_; - GpuBuffer uniforms_; + UniformBuffer<CommonPostProcessUniforms> uniforms_; int width_ = 1280; int height_ = 720; }; diff --git a/src/gpu/effects/chroma_aberration_effect.cc b/src/gpu/effects/chroma_aberration_effect.cc index 7f41153..af3acc5 100644 --- a/src/gpu/effects/chroma_aberration_effect.cc +++ b/src/gpu/effects/chroma_aberration_effect.cc @@ -18,7 +18,6 @@ ChromaAberrationEffect::ChromaAberrationEffect( : PostProcessEffect(ctx), params_(params) { pipeline_ = create_post_process_pipeline(ctx_.device, ctx_.format, chroma_aberration_shader_wgsl); - uniforms_.init(ctx_.device); params_buffer_.init(ctx_.device); } diff --git a/src/gpu/effects/circle_mask_effect.cc b/src/gpu/effects/circle_mask_effect.cc index 5b71086..ca80cf9 100644 --- a/src/gpu/effects/circle_mask_effect.cc +++ b/src/gpu/effects/circle_mask_effect.cc @@ -3,6 +3,7 @@ // Generates circular mask and renders green background outside circle. #include "gpu/effects/circle_mask_effect.h" +#include "gpu/effects/shader_composer.h" #include "generated/assets.h" CircleMaskEffect::CircleMaskEffect(const GpuContext& ctx, float radius) @@ -30,9 +31,7 @@ void CircleMaskEffect::init(MainSequence* demo) { demo_->register_auxiliary_texture("circle_mask", width, height); - compute_uniforms_.init(ctx_.device); compute_params_.init(ctx_.device); - render_uniforms_.init(ctx_.device); WGPUSamplerDescriptor sampler_desc = {}; sampler_desc.addressModeU = WGPUAddressMode_ClampToEdge; @@ -49,9 +48,12 @@ void CircleMaskEffect::init(MainSequence* demo) { const char* render_shader = (const char*)GetAsset( AssetId::ASSET_CIRCLE_MASK_RENDER_SHADER, &render_size); + // Compose shaders to resolve #include directives + std::string composed_compute = ShaderComposer::Get().Compose({}, compute_shader); + WGPUShaderSourceWGSL compute_wgsl = {}; compute_wgsl.chain.sType = WGPUSType_ShaderSourceWGSL; - compute_wgsl.code = str_view(compute_shader); + compute_wgsl.code = str_view(composed_compute.c_str()); WGPUShaderModuleDescriptor compute_desc = {}; compute_desc.nextInChain = &compute_wgsl.chain; @@ -82,11 +84,11 @@ void CircleMaskEffect::init(MainSequence* demo) { const WGPUBindGroupEntry compute_entries[] = { {.binding = 0, - .buffer = compute_uniforms_.get().buffer, + .buffer = uniforms_.get().buffer, .size = sizeof(CommonPostProcessUniforms)}, {.binding = 1, .buffer = compute_params_.get().buffer, - .size = sizeof(EffectParams)}, + .size = sizeof(CircleMaskParams)}, }; const WGPUBindGroupDescriptor compute_bg_desc = { .layout = wgpuRenderPipelineGetBindGroupLayout(compute_pipeline_, 0), @@ -96,9 +98,11 @@ void CircleMaskEffect::init(MainSequence* demo) { compute_bind_group_ = wgpuDeviceCreateBindGroup(ctx_.device, &compute_bg_desc); + std::string composed_render = ShaderComposer::Get().Compose({}, render_shader); + WGPUShaderSourceWGSL render_wgsl = {}; render_wgsl.chain.sType = WGPUSType_ShaderSourceWGSL; - render_wgsl.code = str_view(render_shader); + render_wgsl.code = str_view(composed_render.c_str()); WGPUShaderModuleDescriptor render_desc = {}; render_desc.nextInChain = &render_wgsl.chain; @@ -139,7 +143,7 @@ void CircleMaskEffect::init(MainSequence* demo) { {.binding = 0, .textureView = mask_view}, {.binding = 1, .sampler = mask_sampler_}, {.binding = 2, - .buffer = render_uniforms_.get().buffer, + .buffer = uniforms_.get().buffer, .size = sizeof(CommonPostProcessUniforms)}, }; const WGPUBindGroupDescriptor render_bg_desc = { @@ -160,9 +164,9 @@ void CircleMaskEffect::compute(WGPUCommandEncoder encoder, float time, .beat = beat, .audio_intensity = intensity, }; - compute_uniforms_.update(ctx_.queue, uniforms); + uniforms_.update(ctx_.queue, uniforms); - const EffectParams params = { + const CircleMaskParams params = { .radius = radius_, }; compute_params_.update(ctx_.queue, params); @@ -199,7 +203,7 @@ void CircleMaskEffect::render(WGPURenderPassEncoder pass, float time, .beat = beat, .audio_intensity = intensity, }; - render_uniforms_.update(ctx_.queue, uniforms); + uniforms_.update(ctx_.queue, uniforms); wgpuRenderPassEncoderSetPipeline(pass, render_pipeline_); wgpuRenderPassEncoderSetBindGroup(pass, 0, render_bind_group_, 0, nullptr); diff --git a/src/gpu/effects/circle_mask_effect.h b/src/gpu/effects/circle_mask_effect.h index ac44210..2ddbb11 100644 --- a/src/gpu/effects/circle_mask_effect.h +++ b/src/gpu/effects/circle_mask_effect.h @@ -21,23 +21,23 @@ class CircleMaskEffect : public Effect { float intensity, float aspect_ratio) override; private: - struct EffectParams { + struct CircleMaskParams { float radius; float _pad[3]; }; + static_assert(sizeof(CircleMaskParams) == 16, + "CircleMaskParams must be 16 bytes for WGSL alignment"); MainSequence* demo_ = nullptr; float radius_; WGPURenderPipeline compute_pipeline_ = nullptr; WGPUBindGroup compute_bind_group_ = nullptr; - UniformBuffer<CommonPostProcessUniforms> compute_uniforms_; - UniformBuffer<EffectParams> compute_params_; + UniformBuffer<CircleMaskParams> compute_params_; WGPURenderPipeline render_pipeline_ = nullptr; WGPUBindGroup render_bind_group_ = nullptr; WGPUSampler mask_sampler_ = nullptr; - UniformBuffer<CommonPostProcessUniforms> render_uniforms_; }; #endif /* CIRCLE_MASK_EFFECT_H_ */ diff --git a/src/gpu/effects/distort_effect.cc b/src/gpu/effects/distort_effect.cc index d11dfd7..52a8ec7 100644 --- a/src/gpu/effects/distort_effect.cc +++ b/src/gpu/effects/distort_effect.cc @@ -9,31 +9,35 @@ DistortEffect::DistortEffect(const GpuContext& ctx) : DistortEffect(ctx, DistortParams()) { } -DistortEffect::DistEffect(const GpuContext& ctx, const DistortParams& params) +DistortEffect::DistortEffect(const GpuContext& ctx, const DistortParams& params) : PostProcessEffect(ctx), params_(params) { - uniforms_ = - gpu_create_buffer(ctx_.device, sizeof(DistortUniforms), - WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst); + params_buffer_.init(ctx_.device); pipeline_ = create_post_process_pipeline(ctx_.device, ctx_.format, distort_shader_wgsl); } void DistortEffect::render(WGPURenderPassEncoder pass, float t, float b, float i, float a) { - DistortUniforms u = { + // Populate CommonPostProcessUniforms + const CommonPostProcessUniforms common_u = { + .resolution = {(float)width_, (float)height_}, + .aspect_ratio = a, .time = t, .beat = b, - .intensity = i, - .aspect_ratio = a, - .width = (float)width_, - .height = (float)height_, + .audio_intensity = i, + }; + uniforms_.update(ctx_.queue, common_u); + + // Populate DistortParams + const DistortParams distort_p = { .strength = params_.strength, .speed = params_.speed, }; - wgpuQueueWriteBuffer(ctx_.queue, uniforms_.buffer, 0, &u, sizeof(u)); + params_buffer_.update(ctx_.queue, distort_p); + PostProcessEffect::render(pass, t, b, i, a); } void DistortEffect::update_bind_group(WGPUTextureView v) { - pp_update_bind_group(ctx_.device, pipeline_, &bind_group_, v, {}, uniforms_); + pp_update_bind_group(ctx_.device, pipeline_, &bind_group_, v, uniforms_.get(), params_buffer_); }
\ No newline at end of file diff --git a/src/gpu/effects/fade_effect.cc b/src/gpu/effects/fade_effect.cc index 3efc583..39b54e0 100644 --- a/src/gpu/effects/fade_effect.cc +++ b/src/gpu/effects/fade_effect.cc @@ -5,6 +5,12 @@ #include "gpu/effects/post_process_helper.h" #include <cmath> +struct FadeParams { + float fade_amount; + float _pad[3]; +}; +static_assert(sizeof(FadeParams) == 16, "FadeParams must be 16 bytes for WGSL alignment"); + FadeEffect::FadeEffect(const GpuContext& ctx) : PostProcessEffect(ctx) { const char* shader_code = R"( struct VertexOutput { @@ -22,7 +28,7 @@ FadeEffect::FadeEffect(const GpuContext& ctx) : PostProcessEffect(ctx) { audio_intensity: f32, }; - struct EffectParams { + struct FadeParams { fade_amount: f32, _pad0: f32, _pad1: f32, @@ -32,7 +38,7 @@ FadeEffect::FadeEffect(const GpuContext& ctx) : PostProcessEffect(ctx) { @group(0) @binding(0) var inputSampler: sampler; @group(0) @binding(1) var inputTexture: texture_2d<f32>; @group(0) @binding(2) var<uniform> uniforms: CommonUniforms; - @group(0) @binding(3) var<uniform> params: EffectParams; + @group(0) @binding(3) var<uniform> params: FadeParams; @vertex fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { @@ -57,14 +63,13 @@ FadeEffect::FadeEffect(const GpuContext& ctx) : PostProcessEffect(ctx) { pipeline_ = create_post_process_pipeline(ctx_.device, ctx_.format, shader_code); - common_uniforms_.init(ctx_.device); params_buffer_ = gpu_create_buffer( ctx_.device, 16, WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst); } void FadeEffect::update_bind_group(WGPUTextureView input_view) { pp_update_bind_group(ctx_.device, pipeline_, &bind_group_, input_view, - common_uniforms_.get(), params_buffer_); + uniforms_.get(), params_buffer_); } void FadeEffect::render(WGPURenderPassEncoder pass, float time, float beat, @@ -76,7 +81,7 @@ void FadeEffect::render(WGPURenderPassEncoder pass, float time, float beat, .beat = beat, .audio_intensity = intensity, }; - common_uniforms_.update(ctx_.queue, u); + uniforms_.update(ctx_.queue, u); // Example fade pattern: fade in at start, fade out at end // Customize this based on your needs @@ -90,8 +95,8 @@ void FadeEffect::render(WGPURenderPassEncoder pass, float time, float beat, fade_amount = fmaxf(fade_amount, 0.0f); } - float params[4] = {fade_amount, 0.0f, 0.0f, 0.0f}; - wgpuQueueWriteBuffer(ctx_.queue, params_buffer_.buffer, 0, params, + FadeParams params = {fade_amount, {0.0f, 0.0f, 0.0f}}; + wgpuQueueWriteBuffer(ctx_.queue, params_buffer_.buffer, 0, ¶ms, sizeof(params)); wgpuRenderPassEncoderSetPipeline(pass, pipeline_); diff --git a/src/gpu/effects/fade_effect.h b/src/gpu/effects/fade_effect.h index 22b8f76..178c360 100644 --- a/src/gpu/effects/fade_effect.h +++ b/src/gpu/effects/fade_effect.h @@ -4,9 +4,9 @@ #pragma once #include "gpu/effect.h" +#include "gpu/effects/post_process_helper.h" #include "gpu/gpu.h" #include "gpu/uniform_helper.h" -#include "gpu/effects/post_process_helper.h" class FadeEffect : public PostProcessEffect { public: @@ -16,6 +16,5 @@ class FadeEffect : public PostProcessEffect { void update_bind_group(WGPUTextureView input_view) override; private: - UniformBuffer<CommonPostProcessUniforms> common_uniforms_; GpuBuffer params_buffer_; }; diff --git a/src/gpu/effects/flash_cube_effect.h b/src/gpu/effects/flash_cube_effect.h index 7089af2..5faeb00 100644 --- a/src/gpu/effects/flash_cube_effect.h +++ b/src/gpu/effects/flash_cube_effect.h @@ -22,8 +22,6 @@ class FlashCubeEffect : public Effect { TextureManager texture_manager_; Scene scene_; Camera camera_; - int width_ = 1280; - int height_ = 720; float last_beat_ = 0.0f; float flash_intensity_ = 0.0f; }; diff --git a/src/gpu/effects/gaussian_blur_effect.cc b/src/gpu/effects/gaussian_blur_effect.cc index 0cc4821..697be88 100644 --- a/src/gpu/effects/gaussian_blur_effect.cc +++ b/src/gpu/effects/gaussian_blur_effect.cc @@ -18,7 +18,6 @@ GaussianBlurEffect::GaussianBlurEffect(const GpuContext& ctx, : PostProcessEffect(ctx), params_(params) { pipeline_ = create_post_process_pipeline(ctx_.device, ctx_.format, gaussian_blur_shader_wgsl); - uniforms_.init(ctx_.device); params_buffer_.init(ctx_.device); } diff --git a/src/gpu/effects/heptagon_effect.cc b/src/gpu/effects/heptagon_effect.cc index b77ec53..7b0702d 100644 --- a/src/gpu/effects/heptagon_effect.cc +++ b/src/gpu/effects/heptagon_effect.cc @@ -5,39 +5,25 @@ #include "gpu/gpu.h" #include "util/mini_math.h" -// Match CommonUniforms struct from main_shader.wgsl. -// Padded to 32 bytes for WGSL alignment rules. -struct HeptagonUniforms { - vec2 resolution; // 8 bytes - float _pad0[2]; // 8 bytes padding to align next float - float aspect_ratio; // 4 bytes - float time; // 4 bytes - float beat; // 4 bytes - float audio_intensity; // 4 bytes -}; -static_assert(sizeof(HeptagonUniforms) == 32, - "HeptagonUniforms must be 32 bytes for WGSL alignment"); - // --- HeptagonEffect --- HeptagonEffect::HeptagonEffect(const GpuContext& ctx) : Effect(ctx) { - uniforms_ = - gpu_create_buffer(ctx_.device, sizeof(HeptagonUniforms), - WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst); - ResourceBinding bindings[] = {{uniforms_, WGPUBufferBindingType_Uniform}}; + // uniforms_ is initialized by Effect base class + ResourceBinding bindings[] = {{uniforms_.get(), WGPUBufferBindingType_Uniform}}; pass_ = gpu_create_render_pass(ctx_.device, ctx_.format, main_shader_wgsl, bindings, 1); pass_.vertex_count = 21; } void HeptagonEffect::render(WGPURenderPassEncoder pass, float t, float b, float i, float a) { - HeptagonUniforms u = { + CommonPostProcessUniforms u = { .resolution = {(float)width_, (float)height_}, + ._pad = {0.0f, 0.0f}, .aspect_ratio = a, .time = t, .beat = b, .audio_intensity = i, }; - wgpuQueueWriteBuffer(ctx_.queue, uniforms_.buffer, 0, &u, sizeof(u)); + uniforms_.update(ctx_.queue, u); wgpuRenderPassEncoderSetPipeline(pass, pass_.pipeline); wgpuRenderPassEncoderSetBindGroup(pass, 0, pass_.bind_group, 0, nullptr); wgpuRenderPassEncoderDraw(pass, pass_.vertex_count, 1, 0, 0); diff --git a/src/gpu/effects/moving_ellipse_effect.cc b/src/gpu/effects/moving_ellipse_effect.cc index 945f807..9866f20 100644 --- a/src/gpu/effects/moving_ellipse_effect.cc +++ b/src/gpu/effects/moving_ellipse_effect.cc @@ -7,10 +7,8 @@ // --- MovingEllipseEffect --- MovingEllipseEffect::MovingEllipseEffect(const GpuContext& ctx) : Effect(ctx) { - uniforms_ = - gpu_create_buffer(ctx_.device, sizeof(CommonPostProcessUniforms), - WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst); - ResourceBinding bindings[] = {{uniforms_, WGPUBufferBindingType_Uniform}}; + // uniforms_ is initialized by Effect base class + ResourceBinding bindings[] = {{uniforms_.get(), WGPUBufferBindingType_Uniform}}; pass_ = gpu_create_render_pass(ctx_.device, ctx_.format, ellipse_shader_wgsl, bindings, 1); pass_.vertex_count = 3; @@ -19,12 +17,13 @@ void MovingEllipseEffect::render(WGPURenderPassEncoder pass, float t, float b, float i, float a) { const CommonPostProcessUniforms u = { .resolution = {(float)width_, (float)height_}, + ._pad = {0.0f, 0.0f}, .aspect_ratio = a, .time = t, .beat = b, .audio_intensity = i, }; - wgpuQueueWriteBuffer(ctx_.queue, uniforms_.buffer, 0, &u, sizeof(u)); + uniforms_.update(ctx_.queue, u); wgpuRenderPassEncoderSetPipeline(pass, pass_.pipeline); wgpuRenderPassEncoderSetBindGroup(pass, 0, pass_.bind_group, 0, nullptr); wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); diff --git a/src/gpu/effects/particle_spray_effect.cc b/src/gpu/effects/particle_spray_effect.cc index 3fd2590..a435884 100644 --- a/src/gpu/effects/particle_spray_effect.cc +++ b/src/gpu/effects/particle_spray_effect.cc @@ -8,7 +8,6 @@ // --- ParticleSprayEffect --- ParticleSprayEffect::ParticleSprayEffect(const GpuContext& ctx) : Effect(ctx) { - uniforms_.init(ctx_.device); std::vector<Particle> init_p(NUM_PARTICLES); for (Particle& p : init_p) p.pos[3] = 0.0f; diff --git a/src/gpu/effects/particles_effect.cc b/src/gpu/effects/particles_effect.cc index 01f90a5..cd0df74 100644 --- a/src/gpu/effects/particles_effect.cc +++ b/src/gpu/effects/particles_effect.cc @@ -8,7 +8,6 @@ // --- ParticlesEffect --- ParticlesEffect::ParticlesEffect(const GpuContext& ctx) : Effect(ctx) { - uniforms_.init(ctx_.device); std::vector<Particle> init_p(NUM_PARTICLES); particles_buffer_ = gpu_create_buffer( ctx_.device, sizeof(Particle) * NUM_PARTICLES, diff --git a/src/gpu/effects/passthrough_effect.cc b/src/gpu/effects/passthrough_effect.cc index 93cf948..01d557a 100644 --- a/src/gpu/effects/passthrough_effect.cc +++ b/src/gpu/effects/passthrough_effect.cc @@ -7,7 +7,6 @@ // --- PassthroughEffect --- PassthroughEffect::PassthroughEffect(const GpuContext& ctx) : PostProcessEffect(ctx) { - uniforms_.init(ctx_.device); pipeline_ = create_post_process_pipeline(ctx_.device, ctx_.format, passthrough_shader_wgsl); } diff --git a/src/gpu/effects/post_process_helper.cc b/src/gpu/effects/post_process_helper.cc index 74e052d..e99467f 100644 --- a/src/gpu/effects/post_process_helper.cc +++ b/src/gpu/effects/post_process_helper.cc @@ -4,16 +4,19 @@ #include "post_process_helper.h" #include "../demo_effects.h" #include "gpu/gpu.h" +#include "gpu/effects/shader_composer.h" #include <cstring> // Helper to create a standard post-processing pipeline WGPURenderPipeline create_post_process_pipeline(WGPUDevice device, WGPUTextureFormat format, const char* shader_code) { + std::string composed_shader = ShaderComposer::Get().Compose({}, shader_code); + WGPUShaderModuleDescriptor shader_desc = {}; WGPUShaderSourceWGSL wgsl_src = {}; wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL; - wgsl_src.code = str_view(shader_code); + wgsl_src.code = str_view(composed_shader.c_str()); shader_desc.nextInChain = &wgsl_src.chain; WGPUShaderModule shader_module = wgpuDeviceCreateShaderModule(device, &shader_desc); @@ -94,7 +97,8 @@ void pp_update_bind_group(WGPUDevice device, WGPURenderPipeline pipeline, bge[2].buffer = uniforms.buffer; bge[2].size = uniforms.size; bge[3].binding = PP_BINDING_EFFECT_PARAMS; - bge[3].buffer = effect_params.buffer ? effect_params.buffer : g_dummy_buffer.buffer; + bge[3].buffer = + effect_params.buffer ? effect_params.buffer : g_dummy_buffer.buffer; bge[3].size = effect_params.buffer ? effect_params.size : g_dummy_buffer.size; WGPUBindGroupDescriptor bgd = { .layout = bgl, .entryCount = 4, .entries = bge}; diff --git a/src/gpu/effects/post_process_helper.h b/src/gpu/effects/post_process_helper.h index 77b184f..23cde0e 100644 --- a/src/gpu/effects/post_process_helper.h +++ b/src/gpu/effects/post_process_helper.h @@ -19,10 +19,10 @@ static_assert(sizeof(CommonPostProcessUniforms) == 32, "CommonPostProcessUniforms must be 32 bytes for WGSL alignment"); // Standard post-process bind group layout (group 0): -#define PP_BINDING_SAMPLER 0 // Sampler for input texture -#define PP_BINDING_TEXTURE 1 // Input texture (previous render pass) -#define PP_BINDING_UNIFORMS 2 // Custom uniforms buffer -#define PP_BINDING_EFFECT_PARAMS 3 // Effect-specific parameters +#define PP_BINDING_SAMPLER 0 // Sampler for input texture +#define PP_BINDING_TEXTURE 1 // Input texture (previous render pass) +#define PP_BINDING_UNIFORMS 2 // Custom uniforms buffer +#define PP_BINDING_EFFECT_PARAMS 3 // Effect-specific parameters // Helper to create a standard post-processing pipeline // Uniforms are accessible to both vertex and fragment shaders diff --git a/src/gpu/effects/shaders.cc b/src/gpu/effects/shaders.cc index 2e1cfe5..625c5b6 100644 --- a/src/gpu/effects/shaders.cc +++ b/src/gpu/effects/shaders.cc @@ -99,6 +99,28 @@ const char* chroma_aberration_shader_wgsl = SafeGetAsset(AssetId::ASSET_SHADER_CHROMA_ABERRATION); +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); + +#if !defined(STRIP_GPU_COMPOSITE) +const char* gen_blend_compute_wgsl = + + SafeGetAsset(AssetId::ASSET_SHADER_COMPUTE_GEN_BLEND); + +const char* gen_mask_compute_wgsl = + + SafeGetAsset(AssetId::ASSET_SHADER_COMPUTE_GEN_MASK); +#endif + 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 50b4f32..68b8834 100644 --- a/src/gpu/effects/shaders.h +++ b/src/gpu/effects/shaders.h @@ -18,3 +18,10 @@ extern const char* solarize_shader_wgsl; 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; +#if !defined(STRIP_GPU_COMPOSITE) +extern const char* gen_blend_compute_wgsl; +extern const char* gen_mask_compute_wgsl; +#endif diff --git a/src/gpu/effects/solarize_effect.cc b/src/gpu/effects/solarize_effect.cc index d74d708..4f47218 100644 --- a/src/gpu/effects/solarize_effect.cc +++ b/src/gpu/effects/solarize_effect.cc @@ -6,7 +6,6 @@ // --- SolarizeEffect --- SolarizeEffect::SolarizeEffect(const GpuContext& ctx) : PostProcessEffect(ctx) { - uniforms_.init(ctx.device); pipeline_ = create_post_process_pipeline(ctx_.device, ctx_.format, solarize_shader_wgsl); } @@ -23,6 +22,6 @@ void SolarizeEffect::render(WGPURenderPassEncoder pass, float t, float b, PostProcessEffect::render(pass, t, b, i, a); } void SolarizeEffect::update_bind_group(WGPUTextureView v) { - pp_update_bind_group(ctx_.device, pipeline_, &bind_group_, v, - uniforms_.get(), {}); + pp_update_bind_group(ctx_.device, pipeline_, &bind_group_, v, uniforms_.get(), + {}); } diff --git a/src/gpu/effects/theme_modulation_effect.cc b/src/gpu/effects/theme_modulation_effect.cc index f9ae636..b1eff90 100644 --- a/src/gpu/effects/theme_modulation_effect.cc +++ b/src/gpu/effects/theme_modulation_effect.cc @@ -6,6 +6,12 @@ #include "gpu/effects/shaders.h" #include <cmath> +struct ThemeModulationParams { + float theme_brightness; + float _pad[3]; +}; +static_assert(sizeof(ThemeModulationParams) == 16, "ThemeModulationParams must be 16 bytes for WGSL alignment"); + ThemeModulationEffect::ThemeModulationEffect(const GpuContext& ctx) : PostProcessEffect(ctx) { const char* shader_code = R"( @@ -24,7 +30,7 @@ ThemeModulationEffect::ThemeModulationEffect(const GpuContext& ctx) audio_intensity: f32, }; - struct EffectParams { + struct ThemeModulationParams { theme_brightness: f32, _pad0: f32, _pad1: f32, @@ -34,7 +40,7 @@ ThemeModulationEffect::ThemeModulationEffect(const GpuContext& ctx) @group(0) @binding(0) var inputSampler: sampler; @group(0) @binding(1) var inputTexture: texture_2d<f32>; @group(0) @binding(2) var<uniform> uniforms: CommonUniforms; - @group(0) @binding(3) var<uniform> params: EffectParams; + @group(0) @binding(3) var<uniform> params: ThemeModulationParams; @vertex fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { @@ -61,14 +67,13 @@ ThemeModulationEffect::ThemeModulationEffect(const GpuContext& ctx) pipeline_ = create_post_process_pipeline(ctx_.device, ctx_.format, shader_code); - common_uniforms_.init(ctx_.device); params_buffer_ = gpu_create_buffer( ctx_.device, 16, WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst); } void ThemeModulationEffect::update_bind_group(WGPUTextureView input_view) { pp_update_bind_group(ctx_.device, pipeline_, &bind_group_, input_view, - common_uniforms_.get(), params_buffer_); + uniforms_.get(), params_buffer_); } void ThemeModulationEffect::render(WGPURenderPassEncoder pass, float time, @@ -81,7 +86,7 @@ void ThemeModulationEffect::render(WGPURenderPassEncoder pass, float time, .beat = beat, .audio_intensity = intensity, }; - common_uniforms_.update(ctx_.queue, u); + uniforms_.update(ctx_.queue, u); // Alternate between bright and dark every 4 seconds (2 pattern changes) // Music patterns change every 2 seconds at 120 BPM @@ -97,8 +102,8 @@ void ThemeModulationEffect::render(WGPURenderPassEncoder pass, float time, bright_value + (dark_value - bright_value) * transition; // Update params buffer - float params[4] = {theme_brightness, 0.0f, 0.0f, 0.0f}; - wgpuQueueWriteBuffer(ctx_.queue, params_buffer_.buffer, 0, params, + ThemeModulationParams params = {theme_brightness, {0.0f, 0.0f, 0.0f}}; + wgpuQueueWriteBuffer(ctx_.queue, params_buffer_.buffer, 0, ¶ms, sizeof(params)); // Render diff --git a/src/gpu/effects/theme_modulation_effect.h b/src/gpu/effects/theme_modulation_effect.h index 107529b..713347b 100644 --- a/src/gpu/effects/theme_modulation_effect.h +++ b/src/gpu/effects/theme_modulation_effect.h @@ -5,8 +5,8 @@ #pragma once #include "gpu/effect.h" -#include "gpu/uniform_helper.h" #include "gpu/effects/post_process_helper.h" +#include "gpu/uniform_helper.h" class ThemeModulationEffect : public PostProcessEffect { public: @@ -16,6 +16,5 @@ class ThemeModulationEffect : public PostProcessEffect { void update_bind_group(WGPUTextureView input_view) override; private: - UniformBuffer<CommonPostProcessUniforms> common_uniforms_; GpuBuffer params_buffer_; }; diff --git a/src/gpu/effects/vignette_effect.cc b/src/gpu/effects/vignette_effect.cc index a4967dd..bba0372 100644 --- a/src/gpu/effects/vignette_effect.cc +++ b/src/gpu/effects/vignette_effect.cc @@ -12,7 +12,6 @@ VignetteEffect::VignetteEffect(const GpuContext& ctx) VignetteEffect::VignetteEffect(const GpuContext& ctx, const VignetteParams& params) : PostProcessEffect(ctx), params_(params) { - uniforms_.init(ctx_.device); params_buffer_.init(ctx_.device); pipeline_ = create_post_process_pipeline(ctx_.device, ctx_.format, vignette_shader_wgsl); @@ -33,6 +32,6 @@ void VignetteEffect::render(WGPURenderPassEncoder pass, float t, float b, } void VignetteEffect::update_bind_group(WGPUTextureView v) { - pp_update_bind_group(ctx_.device, pipeline_, &bind_group_, v, - uniforms_.get(), params_buffer_.get()); + pp_update_bind_group(ctx_.device, pipeline_, &bind_group_, v, uniforms_.get(), + params_buffer_.get()); } diff --git a/src/gpu/gpu.cc b/src/gpu/gpu.cc index fde241d..e89a2f0 100644 --- a/src/gpu/gpu.cc +++ b/src/gpu/gpu.cc @@ -5,6 +5,7 @@ #include "gpu.h" #include "effect.h" #include "gpu/effects/shaders.h" +#include "gpu/effects/shader_composer.h" #include "platform/platform.h" #include <cassert> @@ -55,10 +56,13 @@ RenderPass gpu_create_render_pass(WGPUDevice device, WGPUTextureFormat format, ResourceBinding* bindings, int num_bindings) { RenderPass pass = {}; + // Compose shader to resolve #include directives + std::string composed_shader = ShaderComposer::Get().Compose({}, shader_code); + // Create Shader Module WGPUShaderSourceWGSL wgsl_src = {}; wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL; - wgsl_src.code = str_view(shader_code); + wgsl_src.code = str_view(composed_shader.c_str()); WGPUShaderModuleDescriptor shader_desc = {}; shader_desc.nextInChain = &wgsl_src.chain; WGPUShaderModule shader_module = @@ -156,9 +160,12 @@ ComputePass gpu_create_compute_pass(WGPUDevice device, const char* shader_code, int num_bindings) { ComputePass pass = {}; + // Compose shader to resolve #include directives + std::string composed_shader = ShaderComposer::Get().Compose({}, shader_code); + WGPUShaderSourceWGSL wgsl_src = {}; wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL; - wgsl_src.code = str_view(shader_code); + wgsl_src.code = str_view(composed_shader.c_str()); WGPUShaderModuleDescriptor shader_desc = {}; shader_desc.nextInChain = &wgsl_src.chain; WGPUShaderModule shader_module = diff --git a/src/gpu/stub_gpu.cc b/src/gpu/stub_gpu.cc new file mode 100644 index 0000000..0b4185c --- /dev/null +++ b/src/gpu/stub_gpu.cc @@ -0,0 +1,83 @@ +// Stub GPU implementation for size measurement builds. +// All functions are no-ops. Binary compiles but does NOT run. +// This file is only compiled when STRIP_EXTERNAL_LIBS is defined. + +#if defined(STRIP_EXTERNAL_LIBS) + +#include "gpu.h" +#include "platform/stub_types.h" + +GpuBuffer gpu_create_buffer(WGPUDevice device, size_t size, uint32_t usage, + const void* data) { + (void)device; + (void)size; + (void)usage; + (void)data; + return {nullptr, 0}; +} + +RenderPass gpu_create_render_pass(WGPUDevice device, WGPUTextureFormat format, + const char* shader_code, + ResourceBinding* bindings, int num_bindings) { + (void)device; + (void)format; + (void)shader_code; + (void)bindings; + (void)num_bindings; + return {nullptr, nullptr, 0, 0}; +} + +ComputePass gpu_create_compute_pass(WGPUDevice device, const char* shader_code, + ResourceBinding* bindings, + int num_bindings) { + (void)device; + (void)shader_code; + (void)bindings; + (void)num_bindings; + return {nullptr, nullptr, 0, 0, 0}; +} + +void gpu_init(PlatformState* platform_state) { + (void)platform_state; +} + +void gpu_draw(float audio_peak, float aspect_ratio, float time, float beat) { + (void)audio_peak; + (void)aspect_ratio; + (void)time; + (void)beat; +} + +void gpu_resize(int width, int height) { + (void)width; + (void)height; +} + +void gpu_shutdown() { +} + +const GpuContext* gpu_get_context() { + static GpuContext ctx = {nullptr, nullptr, WGPUTextureFormat_BGRA8Unorm}; + return &ctx; +} + +MainSequence* gpu_get_main_sequence() { + return nullptr; +} + +#if !defined(STRIP_ALL) +void gpu_simulate_until(float time, float bpm) { + (void)time; + (void)bpm; +} + +void gpu_add_custom_effect(Effect* effect, float start_time, float end_time, + int priority) { + (void)effect; + (void)start_time; + (void)end_time; + (void)priority; +} +#endif + +#endif // STRIP_EXTERNAL_LIBS diff --git a/src/gpu/texture_manager.cc b/src/gpu/texture_manager.cc index 0c30c94..dfa6315 100644 --- a/src/gpu/texture_manager.cc +++ b/src/gpu/texture_manager.cc @@ -2,7 +2,10 @@ // It implements the TextureManager. #include "gpu/texture_manager.h" +#include "gpu/effects/shader_composer.h" +#include "platform/platform.h" #include <cstdio> +#include <cstring> #include <vector> #if defined(DEMO_CROSS_COMPILE_WIN32) @@ -26,6 +29,22 @@ void TextureManager::shutdown() { wgpuTextureRelease(pair.second.texture); } textures_.clear(); + + for (auto& pair : compute_pipelines_) { + if (pair.second.pipeline) { + wgpuComputePipelineRelease(pair.second.pipeline); + } + } + compute_pipelines_.clear(); + +#if !defined(STRIP_GPU_COMPOSITE) + for (auto& pair : samplers_) { + if (pair.second) { + wgpuSamplerRelease(pair.second); + } + } + samplers_.clear(); +#endif } void TextureManager::create_procedural_texture( @@ -112,3 +131,570 @@ WGPUTextureView TextureManager::get_texture_view(const std::string& name) { } return nullptr; } + +WGPUComputePipeline TextureManager::get_or_create_compute_pipeline( + const std::string& func_name, const char* shader_code, + size_t uniform_size, int num_input_textures) { + auto it = compute_pipelines_.find(func_name); + if (it != compute_pipelines_.end()) { + return it->second.pipeline; + } + + // Create new pipeline + ShaderComposer& composer = ShaderComposer::Get(); + std::string resolved_shader = composer.Compose({}, shader_code); + + 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); + + // Dynamic bind group layout + // Binding 0: output storage texture + // Binding 1: uniform buffer + // Binding 2 to (2 + num_input_textures - 1): input textures + // Binding (2 + num_input_textures): sampler (if inputs > 0) + const int max_entries = 2 + num_input_textures + (num_input_textures > 0 ? 1 : 0); + std::vector<WGPUBindGroupLayoutEntry> bgl_entries(max_entries); + + // Binding 0: Output storage texture + 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; + + // Binding 1: Uniform buffer + bgl_entries[1].binding = 1; + bgl_entries[1].visibility = WGPUShaderStage_Compute; + bgl_entries[1].buffer.type = WGPUBufferBindingType_Uniform; + bgl_entries[1].buffer.minBindingSize = uniform_size; + + // Binding 2+: Input textures + for (int i = 0; i < num_input_textures; ++i) { + bgl_entries[2 + i].binding = 2 + i; + bgl_entries[2 + i].visibility = WGPUShaderStage_Compute; + bgl_entries[2 + i].texture.sampleType = WGPUTextureSampleType_Float; + bgl_entries[2 + i].texture.viewDimension = WGPUTextureViewDimension_2D; + } + + // Binding N: Sampler (if inputs exist) + if (num_input_textures > 0) { + bgl_entries[2 + num_input_textures].binding = 2 + num_input_textures; + bgl_entries[2 + num_input_textures].visibility = WGPUShaderStage_Compute; + bgl_entries[2 + num_input_textures].sampler.type = WGPUSamplerBindingType_Filtering; + } + + WGPUBindGroupLayoutDescriptor bgl_desc = {}; + bgl_desc.entryCount = max_entries; + bgl_desc.entries = bgl_entries.data(); + 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"); + + WGPUComputePipeline pipeline = + wgpuDeviceCreateComputePipeline(device_, &pipeline_desc); + + wgpuPipelineLayoutRelease(pipeline_layout); + wgpuBindGroupLayoutRelease(bind_group_layout); + wgpuShaderModuleRelease(shader_module); + + // Cache pipeline + ComputePipelineInfo info = {pipeline, shader_code, uniform_size, num_input_textures}; + compute_pipelines_[func_name] = info; + + return pipeline; +} + +void TextureManager::dispatch_compute(const std::string& func_name, + WGPUTexture target, + const GpuProceduralParams& params, + const void* uniform_data, + size_t uniform_size) { + auto it = compute_pipelines_.find(func_name); + if (it == compute_pipelines_.end()) { + return; // Pipeline not created yet + } + + WGPUComputePipeline pipeline = it->second.pipeline; + + // Create uniform buffer + WGPUBufferDescriptor buf_desc = {}; + buf_desc.size = uniform_size; + 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, uniform_size); + memcpy(mapped, uniform_data, uniform_size); + wgpuBufferUnmap(uniform_buf); + + // Create storage texture view + 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); + + // Create bind group layout entries (must match pipeline) + 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 = uniform_size; + + WGPUBindGroupLayoutDescriptor bgl_desc = {}; + bgl_desc.entryCount = 2; + bgl_desc.entries = bgl_entries; + WGPUBindGroupLayout bind_group_layout = + wgpuDeviceCreateBindGroupLayout(device_, &bgl_desc); + + // Create bind group + 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 = uniform_size; + + 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); + + // Dispatch compute + WGPUCommandEncoderDescriptor enc_desc = {}; + WGPUCommandEncoder encoder = + wgpuDeviceCreateCommandEncoder(device_, &enc_desc); + WGPUComputePassEncoder pass = + wgpuCommandEncoderBeginComputePass(encoder, nullptr); + wgpuComputePassEncoderSetPipeline(pass, 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); + + // Cleanup + wgpuCommandBufferRelease(cmd); + wgpuCommandEncoderRelease(encoder); + wgpuComputePassEncoderRelease(pass); + wgpuBindGroupRelease(bind_group); + wgpuBindGroupLayoutRelease(bind_group_layout); + wgpuBufferRelease(uniform_buf); + wgpuTextureViewRelease(target_view); +} + +void TextureManager::create_gpu_noise_texture( + const std::string& name, const GpuProceduralParams& params) { + extern const char* gen_noise_compute_wgsl; + get_or_create_compute_pipeline("gen_noise", gen_noise_compute_wgsl, 16); + + 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); + + struct NoiseParams { + uint32_t width; + uint32_t height; + float seed; + float frequency; + }; + NoiseParams uniforms = {(uint32_t)params.width, (uint32_t)params.height, + params.params[0], params.params[1]}; + dispatch_compute("gen_noise", texture, params, &uniforms, sizeof(NoiseParams)); + + 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 noise texture: %s (%dx%d)\n", name.c_str(), + params.width, params.height); +#endif +} + +void TextureManager::create_gpu_perlin_texture( + const std::string& name, const GpuProceduralParams& params) { + extern const char* gen_perlin_compute_wgsl; + get_or_create_compute_pipeline("gen_perlin", gen_perlin_compute_wgsl, 32); + + 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); + + struct PerlinParams { + uint32_t width; + uint32_t height; + float seed; + float frequency; + float amplitude; + float amplitude_decay; + uint32_t octaves; + float _pad0; + }; + PerlinParams uniforms = { + (uint32_t)params.width, + (uint32_t)params.height, + params.params[0], + params.params[1], + params.num_params > 2 ? params.params[2] : 1.0f, + params.num_params > 3 ? params.params[3] : 0.5f, + params.num_params > 4 ? (uint32_t)params.params[4] : 4u, + 0.0f}; + dispatch_compute("gen_perlin", texture, params, &uniforms, + sizeof(PerlinParams)); + + 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::create_gpu_grid_texture( + const std::string& name, const GpuProceduralParams& params) { + extern const char* gen_grid_compute_wgsl; + get_or_create_compute_pipeline("gen_grid", gen_grid_compute_wgsl, 16); + + 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); + + struct GridParams { + uint32_t width; + uint32_t height; + uint32_t grid_size; + uint32_t thickness; + }; + GridParams uniforms = { + (uint32_t)params.width, (uint32_t)params.height, + params.num_params > 0 ? (uint32_t)params.params[0] : 32u, + params.num_params > 1 ? (uint32_t)params.params[1] : 2u}; + dispatch_compute("gen_grid", texture, params, &uniforms, sizeof(GridParams)); + + 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_GPU_COMPOSITE) +WGPUSampler TextureManager::get_or_create_sampler(SamplerType type) { + auto it = samplers_.find(type); + if (it != samplers_.end()) { + return it->second; + } + + WGPUSamplerDescriptor desc = {}; + desc.lodMinClamp = 0.0f; + desc.lodMaxClamp = 1.0f; + desc.maxAnisotropy = 1; + + switch (type) { + case SamplerType::LinearClamp: + desc.addressModeU = WGPUAddressMode_ClampToEdge; + desc.addressModeV = WGPUAddressMode_ClampToEdge; + desc.magFilter = WGPUFilterMode_Linear; + desc.minFilter = WGPUFilterMode_Linear; + desc.mipmapFilter = WGPUMipmapFilterMode_Linear; + break; + case SamplerType::LinearRepeat: + desc.addressModeU = WGPUAddressMode_Repeat; + desc.addressModeV = WGPUAddressMode_Repeat; + desc.magFilter = WGPUFilterMode_Linear; + desc.minFilter = WGPUFilterMode_Linear; + desc.mipmapFilter = WGPUMipmapFilterMode_Linear; + break; + case SamplerType::NearestClamp: + desc.addressModeU = WGPUAddressMode_ClampToEdge; + desc.addressModeV = WGPUAddressMode_ClampToEdge; + desc.magFilter = WGPUFilterMode_Nearest; + desc.minFilter = WGPUFilterMode_Nearest; + desc.mipmapFilter = WGPUMipmapFilterMode_Nearest; + break; + case SamplerType::NearestRepeat: + desc.addressModeU = WGPUAddressMode_Repeat; + desc.addressModeV = WGPUAddressMode_Repeat; + desc.magFilter = WGPUFilterMode_Nearest; + desc.minFilter = WGPUFilterMode_Nearest; + desc.mipmapFilter = WGPUMipmapFilterMode_Nearest; + break; + } + + WGPUSampler sampler = wgpuDeviceCreateSampler(device_, &desc); + samplers_[type] = sampler; + return sampler; +} + +void TextureManager::dispatch_composite( + const std::string& func_name, WGPUTexture target, + const GpuProceduralParams& params, const void* uniform_data, + size_t uniform_size, const std::vector<WGPUTextureView>& input_views, + SamplerType sampler_type) { + auto it = compute_pipelines_.find(func_name); + if (it == compute_pipelines_.end()) { + return; // Pipeline not created yet + } + + WGPUComputePipeline pipeline = it->second.pipeline; + int num_inputs = (int)input_views.size(); + + // Create uniform buffer + WGPUBufferDescriptor buf_desc = {}; + buf_desc.size = uniform_size; + 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, uniform_size); + memcpy(mapped, uniform_data, uniform_size); + wgpuBufferUnmap(uniform_buf); + + // Create storage texture view + 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); + + // Dynamic bind group + const int max_entries = 2 + num_inputs + (num_inputs > 0 ? 1 : 0); + std::vector<WGPUBindGroupEntry> bg_entries(max_entries); + + // Binding 0: Output texture + bg_entries[0].binding = 0; + bg_entries[0].textureView = target_view; + + // Binding 1: Uniform buffer + bg_entries[1].binding = 1; + bg_entries[1].buffer = uniform_buf; + bg_entries[1].size = uniform_size; + + // Binding 2+: Input textures + for (int i = 0; i < num_inputs; ++i) { + bg_entries[2 + i].binding = 2 + i; + bg_entries[2 + i].textureView = input_views[i]; + } + + // Binding N: Sampler + if (num_inputs > 0) { + bg_entries[2 + num_inputs].binding = 2 + num_inputs; + bg_entries[2 + num_inputs].sampler = get_or_create_sampler(sampler_type); + } + + // Create bind group layout (must match pipeline) + const int layout_entries_count = 2 + num_inputs + (num_inputs > 0 ? 1 : 0); + std::vector<WGPUBindGroupLayoutEntry> bgl_entries(layout_entries_count); + + 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 = uniform_size; + + for (int i = 0; i < num_inputs; ++i) { + bgl_entries[2 + i].binding = 2 + i; + bgl_entries[2 + i].visibility = WGPUShaderStage_Compute; + bgl_entries[2 + i].texture.sampleType = WGPUTextureSampleType_Float; + bgl_entries[2 + i].texture.viewDimension = WGPUTextureViewDimension_2D; + } + + if (num_inputs > 0) { + bgl_entries[2 + num_inputs].binding = 2 + num_inputs; + bgl_entries[2 + num_inputs].visibility = WGPUShaderStage_Compute; + bgl_entries[2 + num_inputs].sampler.type = WGPUSamplerBindingType_Filtering; + } + + WGPUBindGroupLayoutDescriptor bgl_desc = {}; + bgl_desc.entryCount = layout_entries_count; + bgl_desc.entries = bgl_entries.data(); + WGPUBindGroupLayout bind_group_layout = + wgpuDeviceCreateBindGroupLayout(device_, &bgl_desc); + + WGPUBindGroupDescriptor bg_desc = {}; + bg_desc.layout = bind_group_layout; + bg_desc.entryCount = max_entries; + bg_desc.entries = bg_entries.data(); + WGPUBindGroup bind_group = wgpuDeviceCreateBindGroup(device_, &bg_desc); + + // Dispatch compute + WGPUCommandEncoderDescriptor enc_desc = {}; + WGPUCommandEncoder encoder = + wgpuDeviceCreateCommandEncoder(device_, &enc_desc); + WGPUComputePassEncoder pass = + wgpuCommandEncoderBeginComputePass(encoder, nullptr); + wgpuComputePassEncoderSetPipeline(pass, 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); + + // Cleanup + wgpuCommandBufferRelease(cmd); + wgpuCommandEncoderRelease(encoder); + wgpuComputePassEncoderRelease(pass); + wgpuBindGroupRelease(bind_group); + wgpuBindGroupLayoutRelease(bind_group_layout); + wgpuBufferRelease(uniform_buf); + wgpuTextureViewRelease(target_view); +} + +void TextureManager::create_gpu_composite_texture( + const std::string& name, const std::string& shader_func, + const char* shader_code, const void* uniform_data, size_t uniform_size, + int width, int height, const std::vector<std::string>& input_names, + SamplerType sampler) { + // Create pipeline if needed + get_or_create_compute_pipeline(shader_func, shader_code, uniform_size, + (int)input_names.size()); + + // Resolve input texture views + std::vector<WGPUTextureView> input_views; + input_views.reserve(input_names.size()); + for (const auto& input_name : input_names) { + WGPUTextureView view = get_texture_view(input_name); + if (!view) { + fprintf(stderr, "Error: Input texture not found: %s\n", + input_name.c_str()); + return; + } + input_views.push_back(view); + } + + // Create output texture + WGPUTextureDescriptor tex_desc = {}; + tex_desc.usage = + WGPUTextureUsage_StorageBinding | WGPUTextureUsage_TextureBinding; + tex_desc.dimension = WGPUTextureDimension_2D; + tex_desc.size = {(uint32_t)width, (uint32_t)height, 1}; + tex_desc.format = WGPUTextureFormat_RGBA8Unorm; + tex_desc.mipLevelCount = 1; + tex_desc.sampleCount = 1; + WGPUTexture texture = wgpuDeviceCreateTexture(device_, &tex_desc); + + // Dispatch composite shader + GpuProceduralParams params = {width, height, nullptr, 0}; + dispatch_composite(shader_func, texture, params, uniform_data, uniform_size, + input_views, sampler); + + // Create view + 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); + + // Store + GpuTexture gpu_tex; + gpu_tex.texture = texture; + gpu_tex.view = view; + gpu_tex.width = width; + gpu_tex.height = height; + textures_[name] = gpu_tex; + +#if !defined(STRIP_ALL) + printf("Generated GPU composite texture: %s (%dx%d, %zu inputs)\n", + name.c_str(), width, height, input_names.size()); +#endif +} +#endif // !defined(STRIP_GPU_COMPOSITE) + +#if !defined(STRIP_ALL) +WGPUTextureView TextureManager::get_or_generate_gpu_texture( + const std::string& name, const GpuProceduralParams& params) { + auto it = textures_.find(name); + if (it != textures_.end()) { + return it->second.view; + } + create_gpu_noise_texture(name, params); + return textures_[name].view; +} +#endif diff --git a/src/gpu/texture_manager.h b/src/gpu/texture_manager.h index 23fdbe8..5a2b9f8 100644 --- a/src/gpu/texture_manager.h +++ b/src/gpu/texture_manager.h @@ -23,6 +23,13 @@ struct GpuTexture { int height; }; +struct GpuProceduralParams { + int width; + int height; + const float* params; + int num_params; +}; + class TextureManager { public: void init(WGPUDevice device, WGPUQueue queue); @@ -36,11 +43,72 @@ class TextureManager { void create_texture(const std::string& name, int width, int height, const uint8_t* data); + // 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_GPU_COMPOSITE) + enum class SamplerType { + LinearClamp, + LinearRepeat, + NearestClamp, + NearestRepeat + }; + + // GPU composite generation (multi-input textures) + void create_gpu_composite_texture(const std::string& name, + const std::string& shader_func, + const char* shader_code, + const void* uniform_data, + size_t uniform_size, + int width, int height, + const std::vector<std::string>& input_names, + SamplerType sampler = SamplerType::LinearClamp); +#endif + +#if !defined(STRIP_ALL) + // On-demand lazy generation (stripped in final builds) + WGPUTextureView get_or_generate_gpu_texture(const std::string& name, + const GpuProceduralParams& params); +#endif + // Retrieves a texture view by name (returns nullptr if not found) WGPUTextureView get_texture_view(const std::string& name); private: + struct ComputePipelineInfo { + WGPUComputePipeline pipeline; + const char* shader_code; + size_t uniform_size; + int num_input_textures; + }; + + WGPUComputePipeline get_or_create_compute_pipeline(const std::string& func_name, + const char* shader_code, + size_t uniform_size, + int num_input_textures = 0); + void dispatch_compute(const std::string& func_name, WGPUTexture target, + const GpuProceduralParams& params, const void* uniform_data, + size_t uniform_size); + +#if !defined(STRIP_GPU_COMPOSITE) + void dispatch_composite(const std::string& func_name, WGPUTexture target, + const GpuProceduralParams& params, + const void* uniform_data, size_t uniform_size, + const std::vector<WGPUTextureView>& input_views, + SamplerType sampler_type); +#endif + WGPUDevice device_; WGPUQueue queue_; std::map<std::string, GpuTexture> textures_; + std::map<std::string, ComputePipelineInfo> compute_pipelines_; +#if !defined(STRIP_GPU_COMPOSITE) + WGPUSampler get_or_create_sampler(SamplerType type); + std::map<SamplerType, WGPUSampler> samplers_; +#endif }; diff --git a/src/gpu/uniform_helper.h b/src/gpu/uniform_helper.h index 151153f..8556c98 100644 --- a/src/gpu/uniform_helper.h +++ b/src/gpu/uniform_helper.h @@ -5,7 +5,6 @@ #pragma once #include "gpu/gpu.h" -#include <cstring> // Generic uniform buffer helper // Usage: diff --git a/src/main.cc b/src/main.cc index 4c44a78..b4091e7 100644 --- a/src/main.cc +++ b/src/main.cc @@ -11,6 +11,7 @@ #include "audio/tracker.h" #if !defined(STRIP_ALL) #include "audio/backend/wav_dump_backend.h" +#include "util/file_watcher.h" #include <vector> #endif #include "generated/assets.h" // Include generated asset header @@ -32,6 +33,7 @@ int main(int argc, char** argv) { bool dump_wav = false; bool tempo_test_enabled = false; const char* wav_output_file = "audio_dump.wav"; + bool hot_reload_enabled = false; #if !defined(STRIP_ALL) for (int i = 1; i < argc; ++i) { @@ -57,6 +59,9 @@ int main(int argc, char** argv) { } } else if (strcmp(argv[i], "--tempo") == 0) { tempo_test_enabled = true; + } else if (strcmp(argv[i], "--hot-reload") == 0) { + hot_reload_enabled = true; + printf("Hot-reload enabled (watching config files)\n"); } } #else @@ -160,17 +165,23 @@ int main(int argc, char** argv) { } #endif /* !defined(STRIP_ALL) */ - // PRE-FILL: Fill ring buffer with initial 200ms before starting audio device - // This prevents underrun on first callback - g_audio_engine.update(g_music_time, 1.0f / 60.0f); - audio_render_ahead(g_music_time, - 1.0f / 60.0f); // Fill buffer with lookahead + // Pre-fill using same pattern as main loop (100ms) + fill_audio_buffer(0.1f, 0.0); - // Start audio (or render to WAV file) audio_start(); g_last_audio_time = audio_get_playback_time(); // Initialize after start #if !defined(STRIP_ALL) + // Hot-reload setup + FileWatcher file_watcher; + if (hot_reload_enabled) { + file_watcher.add_file("assets/final/demo_assets.txt"); + file_watcher.add_file("assets/demo.seq"); + file_watcher.add_file("assets/music.track"); + } +#endif + +#if !defined(STRIP_ALL) // In WAV dump mode, run headless simulation and write audio to file if (dump_wav) { printf("Running WAV dump simulation...\n"); @@ -259,6 +270,15 @@ int main(int argc, char** argv) { // context fill_audio_buffer(audio_dt, current_physical_time); +#if !defined(STRIP_ALL) + // Hot-reload: Check for file changes + if (hot_reload_enabled && file_watcher.check_changes()) { + printf("\n[Hot-Reload] Config files changed - rebuild required\n"); + printf("[Hot-Reload] Run: cmake --build build -j4 && ./build/demo64k\n"); + file_watcher.reset(); + } +#endif + // --- Graphics Update --- const float aspect_ratio = platform_state.aspect_ratio; diff --git a/src/platform/platform.h b/src/platform/platform.h index 0a98850..7bcee9d 100644 --- a/src/platform/platform.h +++ b/src/platform/platform.h @@ -7,7 +7,10 @@ #include <cstring> // WebGPU specific headers and shims -#if defined(DEMO_CROSS_COMPILE_WIN32) +#if defined(STRIP_EXTERNAL_LIBS) +#include "stub_types.h" + +#elif defined(DEMO_CROSS_COMPILE_WIN32) #include <webgpu/webgpu.h> #include <webgpu/wgpu.h> diff --git a/src/platform/stub_platform.cc b/src/platform/stub_platform.cc new file mode 100644 index 0000000..61473a0 --- /dev/null +++ b/src/platform/stub_platform.cc @@ -0,0 +1,53 @@ +// Stub platform implementation for size measurement builds. +// All functions are no-ops. Binary compiles but does NOT run. +// This file is only compiled when STRIP_EXTERNAL_LIBS is defined. + +#if defined(STRIP_EXTERNAL_LIBS) + +#include "platform.h" +#include "stub_types.h" + +// Forward declare GLFWwindow stub +struct GLFWwindow {}; + +PlatformState platform_init(bool fullscreen, int width, int height) { + (void)fullscreen; + PlatformState state = {}; + state.width = width; + state.height = height; + state.aspect_ratio = (float)width / (float)height; + state.window = nullptr; + state.time = 0.0; + state.is_fullscreen = false; + return state; +} + +void platform_shutdown(PlatformState* state) { + (void)state; +} + +void platform_poll(PlatformState* state) { + (void)state; +} + +bool platform_should_close(PlatformState* state) { + (void)state; + return false; +} + +void platform_toggle_fullscreen(PlatformState* state) { + (void)state; +} + +WGPUSurface platform_create_wgpu_surface(WGPUInstance instance, + PlatformState* state) { + (void)instance; + (void)state; + return nullptr; +} + +double platform_get_time() { + return 0.0; +} + +#endif // STRIP_EXTERNAL_LIBS diff --git a/src/platform/stub_types.h b/src/platform/stub_types.h new file mode 100644 index 0000000..f532e04 --- /dev/null +++ b/src/platform/stub_types.h @@ -0,0 +1,159 @@ +// Minimal WebGPU type definitions for size measurement builds. +// All types are opaque pointers, all descriptor structs are empty. +// This file is only used when STRIP_EXTERNAL_LIBS is defined. + +#pragma once + +#if defined(STRIP_EXTERNAL_LIBS) + +#include <cstdint> +#include <cstring> + +// Opaque handle types +typedef void* WGPUInstance; +typedef void* WGPUAdapter; +typedef void* WGPUSurface; +typedef void* WGPUDevice; +typedef void* WGPUQueue; +typedef void* WGPUBuffer; +typedef void* WGPUTexture; +typedef void* WGPUTextureView; +typedef void* WGPUSampler; +typedef void* WGPUShaderModule; +typedef void* WGPUBindGroupLayout; +typedef void* WGPUPipelineLayout; +typedef void* WGPUBindGroup; +typedef void* WGPURenderPipeline; +typedef void* WGPUComputePipeline; +typedef void* WGPUCommandEncoder; +typedef void* WGPURenderPassEncoder; +typedef void* WGPUComputePassEncoder; +typedef void* WGPUCommandBuffer; +typedef void* WGPUQuerySet; +typedef void* WGPURenderBundle; +typedef void* WGPURenderBundleEncoder; + +// Enums (minimal values) +typedef enum { + WGPUTextureFormat_Undefined = 0, + WGPUTextureFormat_BGRA8Unorm = 1, + WGPUTextureFormat_RGBA8Unorm = 2, +} WGPUTextureFormat; + +typedef enum { + WGPUBufferBindingType_Uniform = 0, + WGPUBufferBindingType_Storage = 1, + WGPUBufferBindingType_ReadOnlyStorage = 2, +} WGPUBufferBindingType; + +typedef enum { + WGPULoadOp_Clear = 0, + WGPULoadOp_Load = 1, +} WGPULoadOp; + +typedef enum { + WGPUStoreOp_Store = 0, + WGPUStoreOp_Discard = 1, +} WGPUStoreOp; + +typedef enum { + WGPUSurfaceGetCurrentTextureStatus_Success = 0, + WGPUSurfaceGetCurrentTextureStatus_Timeout = 1, + WGPUSurfaceGetCurrentTextureStatus_Outdated = 2, + WGPUSurfaceGetCurrentTextureStatus_Lost = 3, +} WGPUSurfaceGetCurrentTextureStatus; + +// Buffer usage flags +#define WGPUBufferUsage_MapRead 0x00000001 +#define WGPUBufferUsage_MapWrite 0x00000002 +#define WGPUBufferUsage_CopySrc 0x00000004 +#define WGPUBufferUsage_CopyDst 0x00000008 +#define WGPUBufferUsage_Index 0x00000010 +#define WGPUBufferUsage_Vertex 0x00000020 +#define WGPUBufferUsage_Uniform 0x00000040 +#define WGPUBufferUsage_Storage 0x00000080 +#define WGPUBufferUsage_Indirect 0x00000100 +#define WGPUBufferUsage_QueryResolve 0x00000200 + +// Descriptor structs (all empty) +struct WGPUInstanceDescriptor {}; +struct WGPUAdapterInfo {}; +struct WGPUSurfaceConfiguration {}; +struct WGPUDeviceDescriptor {}; +struct WGPUBufferDescriptor {}; +struct WGPUTextureDescriptor {}; +struct WGPUTextureViewDescriptor {}; +struct WGPUSamplerDescriptor {}; +struct WGPUShaderModuleDescriptor {}; +struct WGPUShaderSourceWGSL {}; +struct WGPUShaderModuleWGSLDescriptor {}; +struct WGPUBindGroupLayoutDescriptor {}; +struct WGPUBindGroupLayoutEntry {}; +struct WGPUPipelineLayoutDescriptor {}; +struct WGPUBindGroupDescriptor {}; +struct WGPUBindGroupEntry {}; +struct WGPURenderPipelineDescriptor {}; +struct WGPUComputePipelineDescriptor {}; +struct WGPUVertexState {}; +struct WGPUFragmentState {}; +struct WGPUColorTargetState {}; +struct WGPUBlendState {}; +struct WGPUPrimitiveState {}; +struct WGPUMultisampleState {}; +struct WGPUDepthStencilState {}; +struct WGPURenderPassDescriptor {}; +struct WGPURenderPassColorAttachment { + WGPUTextureView view; + WGPULoadOp loadOp; + WGPUStoreOp storeOp; + struct { float r, g, b, a; } clearValue; + uint32_t depthSlice; +}; +struct WGPUComputePassDescriptor {}; +struct WGPUCommandEncoderDescriptor {}; +struct WGPUCommandBufferDescriptor {}; +struct WGPUSurfaceTexture { + WGPUTexture texture; + WGPUSurfaceGetCurrentTextureStatus status; +}; +struct WGPUColor { float r, g, b, a; }; +struct WGPUExtent3D { uint32_t width, height, depthOrArrayLayers; }; +struct WGPUOrigin3D { uint32_t x, y, z; }; +struct WGPUImageCopyTexture {}; +struct WGPUImageCopyBuffer {}; +struct WGPUTextureDataLayout {}; +struct WGPUBufferBindingLayout {}; +struct WGPUSamplerBindingLayout {}; +struct WGPUTextureBindingLayout {}; +struct WGPUStorageTextureBindingLayout {}; + +// String view helper (for compatibility) +struct WGPUStringView { + const char* data; + size_t length; +}; + +static inline WGPUStringView str_view(const char* str) { + if (!str) return {nullptr, 0}; + return {str, strlen(str)}; +} + +static inline WGPUStringView label_view(const char* str) { + (void)str; + return {nullptr, 0}; +} + +// Platform shims (no-ops) +static inline void platform_wgpu_wait_any(WGPUInstance) {} +static inline void platform_wgpu_set_error_callback(WGPUDevice, void*) {} + +// Constants +#define WGPU_DEPTH_SLICE_UNDEFINED 0xffffffff +#define WGPUOptionalBool_True true +#define WGPUOptionalBool_False false + +// Callback types (empty) +typedef void (*WGPUErrorCallback)(void*, void*, const char*); +typedef void (*WGPUUncapturedErrorCallback)(void*, void*, const char*); + +#endif // STRIP_EXTERNAL_LIBS diff --git a/src/stub_main.cc b/src/stub_main.cc new file mode 100644 index 0000000..8540fcd --- /dev/null +++ b/src/stub_main.cc @@ -0,0 +1,13 @@ +// Stub main for size measurement builds. +// Binary compiles but does NOT run. +// This file is only compiled when STRIP_EXTERNAL_LIBS is defined. + +#if defined(STRIP_EXTERNAL_LIBS) + +int main(int argc, char** argv) { + (void)argc; + (void)argv; + return 0; +} + +#endif // STRIP_EXTERNAL_LIBS diff --git a/src/test_demo.cc b/src/test_demo.cc index a438bbc..b8e9381 100644 --- a/src/test_demo.cc +++ b/src/test_demo.cc @@ -32,15 +32,23 @@ class PeakMeterEffect : public PostProcessEffect { }; struct Uniforms { - peak_value: f32, + resolution: vec2<f32>, _pad0: f32, _pad1: f32, - _pad2: f32, + aspect_ratio: f32, + time: f32, + beat: f32, + audio_intensity: f32, + }; + + struct EffectParams { + unused: f32, }; @group(0) @binding(0) var inputSampler: sampler; @group(0) @binding(1) var inputTexture: texture_2d<f32>; @group(0) @binding(2) var<uniform> uniforms: Uniforms; + @group(0) @binding(3) var<uniform> params: EffectParams; @vertex fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { @@ -69,7 +77,7 @@ class PeakMeterEffect : public PostProcessEffect { // Optimization: Return bar color early (avoids texture sampling for ~5% of pixels) if (in_bar_y && in_bar_x) { let uv_x = (input.uv.x - bar_x_min) / (bar_x_max - bar_x_min); - let factor = step(uv_x, uniforms.peak_value); + let factor = step(uv_x, uniforms.audio_intensity); return mix(vec4<f32>(0.0, 0.0, 0.0, 1.0), vec4<f32>(1.0, 0.0, 0.0,1.0), factor); } @@ -80,24 +88,26 @@ class PeakMeterEffect : public PostProcessEffect { pipeline_ = create_post_process_pipeline(ctx_.device, ctx_.format, shader_code); - uniforms_ = gpu_create_buffer( - ctx_.device, 16, WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst); } void update_bind_group(WGPUTextureView input_view) { pp_update_bind_group(ctx_.device, pipeline_, &bind_group_, input_view, - uniforms_, {}); + uniforms_.get(), {}); } void render(WGPURenderPassEncoder pass, float time, float beat, float peak_value, float aspect_ratio) { (void)time; (void)beat; - (void)aspect_ratio; - float uniforms[4] = {peak_value, 0.0f, 0.0f, 0.0f}; - wgpuQueueWriteBuffer(ctx_.queue, uniforms_.buffer, 0, uniforms, - sizeof(uniforms)); + CommonPostProcessUniforms u = { + .resolution = {(float)width_, (float)height_}, + .aspect_ratio = aspect_ratio, + .time = time, + .beat = beat, + .audio_intensity = peak_value, + }; + uniforms_.update(ctx_.queue, u); wgpuRenderPassEncoderSetPipeline(pass, pipeline_); wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 0, nullptr); @@ -209,6 +219,9 @@ int main(int argc, char** argv) { platform_state = platform_init(fullscreen_enabled, width, height); gpu_init(&platform_state); + // Load timeline from test_demo.seq + LoadTimeline(*gpu_get_main_sequence(), *gpu_get_context()); + // Add peak meter visualization effect (renders as final post-process) #if !defined(STRIP_ALL) const GpuContext* gpu_ctx = gpu_get_context(); @@ -253,9 +266,9 @@ int main(int argc, char** argv) { audio_render_ahead(g_music_time, audio_dt * g_tempo_scale); }; - // Pre-fill audio buffer - g_audio_engine.update(g_music_time, 1.0f / 60.0f); - audio_render_ahead(g_music_time, 1.0f / 60.0f); + // Pre-fill using same pattern as main loop (100ms) + fill_audio_buffer(0.1f, 0.0); + audio_start(); g_last_audio_time = audio_get_playback_time(); 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); diff --git a/src/tests/test_demo_effects.cc b/src/tests/test_demo_effects.cc index d0163c2..0d2b09a 100644 --- a/src/tests/test_demo_effects.cc +++ b/src/tests/test_demo_effects.cc @@ -197,6 +197,9 @@ static void test_effect_type_classification() { int main() { fprintf(stdout, "=== Demo Effects Tests ===\n"); + extern void InitShaderComposer(); + InitShaderComposer(); + test_post_process_effects(); test_scene_effects(); test_effect_type_classification(); diff --git a/src/tests/test_effect_base.cc b/src/tests/test_effect_base.cc index e280e05..612e9da 100644 --- a/src/tests/test_effect_base.cc +++ b/src/tests/test_effect_base.cc @@ -249,6 +249,9 @@ static void test_pixel_helpers() { int main() { fprintf(stdout, "=== Effect Base Tests ===\n"); + extern void InitShaderComposer(); + InitShaderComposer(); + test_webgpu_fixture(); test_offscreen_render_target(); test_effect_construction(); diff --git a/src/tests/test_file_watcher.cc b/src/tests/test_file_watcher.cc new file mode 100644 index 0000000..ac13afd --- /dev/null +++ b/src/tests/test_file_watcher.cc @@ -0,0 +1,63 @@ +// test_file_watcher.cc - Unit tests for file change detection + +#include "util/file_watcher.h" +#include <cstdio> +#include <fstream> +#include <unistd.h> + +#if !defined(STRIP_ALL) + +int main() { + // Create a temporary test file + const char* test_file = "/tmp/test_watcher_file.txt"; + { + std::ofstream f(test_file); + f << "initial content\n"; + } + + FileWatcher watcher; + watcher.add_file(test_file); + + // Initial check - no changes yet + bool changed = watcher.check_changes(); + if (changed) { + fprintf(stderr, "FAIL: Expected no changes on first check\n"); + return 1; + } + + // Sleep to ensure mtime changes (some filesystems have 1s granularity) + sleep(1); + + // Modify the file + { + std::ofstream f(test_file, std::ios::app); + f << "modified\n"; + } + + // Check for changes + changed = watcher.check_changes(); + if (!changed) { + fprintf(stderr, "FAIL: Expected changes after file modification\n"); + return 1; + } + + // Reset and check again - should be no changes + watcher.reset(); + changed = watcher.check_changes(); + if (changed) { + fprintf(stderr, "FAIL: Expected no changes after reset\n"); + return 1; + } + + printf("PASS: FileWatcher tests\n"); + return 0; +} + +#else + +int main() { + printf("SKIP: FileWatcher tests (STRIP_ALL build)\n"); + return 0; +} + +#endif diff --git a/src/tests/test_gpu_composite.cc b/src/tests/test_gpu_composite.cc new file mode 100644 index 0000000..e5ac788 --- /dev/null +++ b/src/tests/test_gpu_composite.cc @@ -0,0 +1,124 @@ +// This file is part of the 64k demo project. +// Tests GPU composite texture generation (Phase 4). + +#include "gpu/gpu.h" +#include "gpu/texture_manager.h" +#include "platform/platform.h" +#include <cstdint> +#include <cstdio> +#include <vector> + +#if !defined(STRIP_GPU_COMPOSITE) + +int main() { + printf("GPU Composite Test: Starting...\n"); + + // Initialize GPU + PlatformState platform = platform_init(false, 256, 256); + if (!platform.window) { + fprintf(stderr, "Error: Failed to create window\n"); + return 1; + } + + gpu_init(&platform); + const GpuContext* ctx = gpu_get_context(); + + extern void InitShaderComposer(); + InitShaderComposer(); + + TextureManager tex_mgr; + tex_mgr.init(ctx->device, ctx->queue); + + // Create base textures + float noise_params_a[2] = {1234.0f, 4.0f}; + GpuProceduralParams noise_a = {256, 256, noise_params_a, 2}; + tex_mgr.create_gpu_noise_texture("noise_a", noise_a); + + float noise_params_b[2] = {5678.0f, 8.0f}; + GpuProceduralParams noise_b = {256, 256, noise_params_b, 2}; + tex_mgr.create_gpu_noise_texture("noise_b", noise_b); + + float grid_params[2] = {32.0f, 2.0f}; + GpuProceduralParams grid = {256, 256, grid_params, 2}; + tex_mgr.create_gpu_grid_texture("grid", grid); + + printf("SUCCESS: Base textures created (noise_a, noise_b, grid)\n"); + + // Test blend composite + extern const char* gen_blend_compute_wgsl; + struct { + uint32_t width, height; + float blend_factor, _pad0; + } blend_uni = {256, 256, 0.5f, 0.0f}; + + std::vector<std::string> blend_inputs = {"noise_a", "noise_b"}; + tex_mgr.create_gpu_composite_texture("blended", "gen_blend", + gen_blend_compute_wgsl, &blend_uni, + sizeof(blend_uni), 256, 256, blend_inputs); + + WGPUTextureView blended_view = tex_mgr.get_texture_view("blended"); + if (!blended_view) { + fprintf(stderr, "Error: Blended texture not created\n"); + tex_mgr.shutdown(); + gpu_shutdown(); + return 1; + } + printf("SUCCESS: Blend composite created (noise_a + noise_b)\n"); + + // Test mask composite + extern const char* gen_mask_compute_wgsl; + struct { + uint32_t width, height; + } mask_uni = {256, 256}; + + std::vector<std::string> mask_inputs = {"noise_a", "grid"}; + tex_mgr.create_gpu_composite_texture("masked", "gen_mask", gen_mask_compute_wgsl, + &mask_uni, sizeof(mask_uni), 256, 256, + mask_inputs); + + WGPUTextureView masked_view = tex_mgr.get_texture_view("masked"); + if (!masked_view) { + fprintf(stderr, "Error: Masked texture not created\n"); + tex_mgr.shutdown(); + gpu_shutdown(); + return 1; + } + printf("SUCCESS: Mask composite created (noise_a * grid)\n"); + + // Test multi-stage composite (composite of composite) + struct { + uint32_t width, height; + float blend_factor, _pad0; + } blend2_uni = {256, 256, 0.7f, 0.0f}; + + std::vector<std::string> blend2_inputs = {"blended", "masked"}; + tex_mgr.create_gpu_composite_texture("final", "gen_blend", + gen_blend_compute_wgsl, &blend2_uni, + sizeof(blend2_uni), 256, 256, blend2_inputs); + + WGPUTextureView final_view = tex_mgr.get_texture_view("final"); + if (!final_view) { + fprintf(stderr, "Error: Multi-stage composite not created\n"); + tex_mgr.shutdown(); + gpu_shutdown(); + return 1; + } + printf("SUCCESS: Multi-stage composite (composite of composites)\n"); + + // Cleanup + tex_mgr.shutdown(); + gpu_shutdown(); + platform_shutdown(&platform); + + printf("All GPU composite tests passed!\n"); + return 0; +} + +#else + +int main() { + printf("GPU Composite Test: SKIPPED (STRIP_GPU_COMPOSITE defined)\n"); + return 0; +} + +#endif diff --git a/src/tests/test_gpu_procedural.cc b/src/tests/test_gpu_procedural.cc new file mode 100644 index 0000000..f1bade0 --- /dev/null +++ b/src/tests/test_gpu_procedural.cc @@ -0,0 +1,117 @@ +// This file is part of the 64k demo project. +// Tests GPU procedural texture generation. + +#include "gpu/gpu.h" +#include "gpu/texture_manager.h" +#include "platform/platform.h" +#include <cstdio> + +int main() { + printf("GPU Procedural Test: Starting...\n"); + + // Minimal GPU initialization for testing + PlatformState platform = platform_init(false, 256, 256); + if (!platform.window) { + fprintf(stderr, "Error: Failed to create window\n"); + return 1; + } + + gpu_init(&platform); + const GpuContext* ctx = gpu_get_context(); + + // Initialize shader composer (needed for #include resolution) + extern void InitShaderComposer(); + InitShaderComposer(); + + // Create TextureManager + TextureManager tex_mgr; + tex_mgr.init(ctx->device, ctx->queue); + + // Test GPU noise generation + GpuProceduralParams params = {}; + params.width = 256; + params.height = 256; + float proc_params[2] = {0.0f, 4.0f}; // seed, frequency + params.params = proc_params; + params.num_params = 2; + + tex_mgr.create_gpu_noise_texture("test_noise", params); + + // Verify texture exists + WGPUTextureView view = tex_mgr.get_texture_view("test_noise"); + if (!view) { + fprintf(stderr, "Error: GPU noise texture not created\n"); + tex_mgr.shutdown(); + gpu_shutdown(); + return 1; + } + printf("SUCCESS: GPU noise texture created (256x256)\n"); + + // Test pipeline caching (create second noise texture) + tex_mgr.create_gpu_noise_texture("test_noise_2", params); + WGPUTextureView view2 = tex_mgr.get_texture_view("test_noise_2"); + if (!view2) { + fprintf(stderr, "Error: Second GPU noise texture not created\n"); + tex_mgr.shutdown(); + gpu_shutdown(); + return 1; + } + printf("SUCCESS: Pipeline caching works (second noise texture)\n"); + + // Test GPU perlin generation + float perlin_params[5] = {42.0f, 4.0f, 1.0f, 0.5f, 6.0f}; + GpuProceduralParams perlin = {512, 256, perlin_params, 5}; + tex_mgr.create_gpu_perlin_texture("test_perlin", perlin); + WGPUTextureView perlin_view = tex_mgr.get_texture_view("test_perlin"); + if (!perlin_view) { + fprintf(stderr, "Error: GPU perlin texture not created\n"); + tex_mgr.shutdown(); + gpu_shutdown(); + return 1; + } + printf("SUCCESS: GPU perlin texture created (512x256)\n"); + + // Test GPU grid generation + float grid_params[2] = {32.0f, 2.0f}; + GpuProceduralParams grid = {256, 256, grid_params, 2}; + tex_mgr.create_gpu_grid_texture("test_grid", grid); + WGPUTextureView grid_view = tex_mgr.get_texture_view("test_grid"); + if (!grid_view) { + fprintf(stderr, "Error: GPU grid texture not created\n"); + tex_mgr.shutdown(); + gpu_shutdown(); + return 1; + } + printf("SUCCESS: GPU grid texture created (256x256)\n"); + + // Test multiple pipelines coexist + printf("SUCCESS: All three GPU generators work (unified pipeline system)\n"); + + // Test variable-size textures + float noise_small[2] = {999.0f, 8.0f}; + GpuProceduralParams small = {128, 64, noise_small, 2}; + tex_mgr.create_gpu_noise_texture("noise_128x64", small); + if (!tex_mgr.get_texture_view("noise_128x64")) { + fprintf(stderr, "Error: Variable-size texture (128x64) not created\n"); + tex_mgr.shutdown(); + gpu_shutdown(); + return 1; + } + + float noise_large[2] = {777.0f, 2.0f}; + GpuProceduralParams large = {1024, 512, noise_large, 2}; + tex_mgr.create_gpu_noise_texture("noise_1024x512", large); + if (!tex_mgr.get_texture_view("noise_1024x512")) { + fprintf(stderr, "Error: Variable-size texture (1024x512) not created\n"); + tex_mgr.shutdown(); + gpu_shutdown(); + return 1; + } + printf("SUCCESS: Variable-size textures work (128x64, 1024x512)\n"); + + // Cleanup + tex_mgr.shutdown(); + gpu_shutdown(); + platform_shutdown(&platform); + return 0; +} diff --git a/src/tests/test_post_process_helper.cc b/src/tests/test_post_process_helper.cc index 104bbc3..36d193e 100644 --- a/src/tests/test_post_process_helper.cc +++ b/src/tests/test_post_process_helper.cc @@ -182,14 +182,14 @@ static void test_bind_group_update() { // Create initial bind group WGPUBindGroup bind_group = nullptr; - pp_update_bind_group(fixture.device(), pipeline, &bind_group, view1, - uniforms, dummy_effect_params_buffer); + pp_update_bind_group(fixture.device(), pipeline, &bind_group, view1, uniforms, + dummy_effect_params_buffer); assert(bind_group != nullptr && "Initial bind group should be created"); fprintf(stdout, " ✓ Initial bind group created\n"); // Update bind group (should release old and create new) - pp_update_bind_group(fixture.device(), pipeline, &bind_group, view2, - uniforms, dummy_effect_params_buffer); + pp_update_bind_group(fixture.device(), pipeline, &bind_group, view2, uniforms, + dummy_effect_params_buffer); assert(bind_group != nullptr && "Updated bind group should be created"); fprintf(stdout, " ✓ Bind group updated successfully\n"); diff --git a/src/tests/test_shader_compilation.cc b/src/tests/test_shader_compilation.cc index e2c0adc..a322e8a 100644 --- a/src/tests/test_shader_compilation.cc +++ b/src/tests/test_shader_compilation.cc @@ -115,16 +115,19 @@ static bool test_shader_compilation(const char* name, const char* shader_code) { return true; // Not a failure, just skipped } + // Compose shader to resolve #include directives + std::string composed_shader = ShaderComposer::Get().Compose({}, shader_code); + #if defined(DEMO_CROSS_COMPILE_WIN32) WGPUShaderModuleWGSLDescriptor wgsl_desc = {}; wgsl_desc.chain.sType = WGPUSType_ShaderModuleWGSLDescriptor; - wgsl_desc.code = shader_code; + wgsl_desc.code = composed_shader.c_str(); WGPUShaderModuleDescriptor shader_desc = {}; shader_desc.nextInChain = (const WGPUChainedStruct*)&wgsl_desc.chain; #else WGPUShaderSourceWGSL wgsl_desc = {}; wgsl_desc.chain.sType = WGPUSType_ShaderSourceWGSL; - wgsl_desc.code = str_view(shader_code); + wgsl_desc.code = str_view(composed_shader.c_str()); WGPUShaderModuleDescriptor shader_desc = {}; shader_desc.nextInChain = (const WGPUChainedStruct*)&wgsl_desc.chain; #endif diff --git a/src/util/asset_manager.cc b/src/util/asset_manager.cc index a0e2a97..5067ebe 100644 --- a/src/util/asset_manager.cc +++ b/src/util/asset_manager.cc @@ -189,3 +189,23 @@ void DropAsset(AssetId asset_id, const uint8_t* asset) { } // For static assets, no dynamic memory to free. } + +#if !defined(STRIP_ALL) +// Hot-reload: Clear asset cache to force reload from disk +// Note: This only works for assets that read from disk at runtime. +// Compiled-in assets cannot be hot-reloaded. +bool ReloadAssetsFromFile(const char* config_path) { + (void)config_path; // Unused - just for API consistency + + // Clear cache to force reload + for (size_t i = 0; i < (size_t)AssetId::ASSET_LAST_ID; ++i) { + if (g_asset_cache[i].is_procedural && g_asset_cache[i].data) { + delete[] g_asset_cache[i].data; + } + g_asset_cache[i] = {}; + } + + fprintf(stderr, "[ReloadAssets] Cache cleared\n"); + return true; +} +#endif // !defined(STRIP_ALL) diff --git a/src/util/asset_manager.h b/src/util/asset_manager.h index 1e0638c..1714c21 100644 --- a/src/util/asset_manager.h +++ b/src/util/asset_manager.h @@ -10,6 +10,7 @@ struct AssetRecord { size_t size; // Size of the asset data bool is_procedural; // True if data was dynamically allocated by a procedural // generator + bool is_gpu_procedural; // True if GPU compute shader generates texture const char* proc_func_name_str; // Name of procedural generation function // (string literal) const float* proc_params; // Parameters for procedural generation (static, @@ -28,3 +29,8 @@ void DropAsset(AssetId asset_id, const uint8_t* asset); // Returns the AssetId for a given asset name, or AssetId::ASSET_LAST_ID if not // found. AssetId GetAssetIdByName(const char* name); + +#if !defined(STRIP_ALL) +// Hot-reload: Parse and reload assets from config file (debug only) +bool ReloadAssetsFromFile(const char* config_path); +#endif diff --git a/src/util/file_watcher.cc b/src/util/file_watcher.cc new file mode 100644 index 0000000..22eb824 --- /dev/null +++ b/src/util/file_watcher.cc @@ -0,0 +1,44 @@ +// file_watcher.cc - Hot-reload file change detection (debug only) + +#include "file_watcher.h" + +#if !defined(STRIP_ALL) + +#include <sys/stat.h> + +void FileWatcher::add_file(const char* path) { + WatchEntry entry; + entry.path = path; + entry.last_mtime = get_mtime(path); + entry.changed = false; + files_.push_back(entry); +} + +bool FileWatcher::check_changes() { + bool any_changed = false; + for (auto& entry : files_) { + time_t current_mtime = get_mtime(entry.path.c_str()); + if (current_mtime != entry.last_mtime && current_mtime != 0) { + entry.changed = true; + entry.last_mtime = current_mtime; + any_changed = true; + } + } + return any_changed; +} + +void FileWatcher::reset() { + for (auto& entry : files_) { + entry.changed = false; + } +} + +time_t FileWatcher::get_mtime(const char* path) { + struct stat st; + if (stat(path, &st) == 0) { + return st.st_mtime; + } + return 0; +} + +#endif // !defined(STRIP_ALL) diff --git a/src/util/file_watcher.h b/src/util/file_watcher.h new file mode 100644 index 0000000..2766a43 --- /dev/null +++ b/src/util/file_watcher.h @@ -0,0 +1,33 @@ +// file_watcher.h - Hot-reload file change detection (debug only) + +#ifndef FILE_WATCHER_H_ +#define FILE_WATCHER_H_ + +#if !defined(STRIP_ALL) + +#include <string> +#include <vector> +#include <ctime> + +class FileWatcher { + public: + FileWatcher() = default; + + void add_file(const char* path); + bool check_changes(); + void reset(); + + private: + struct WatchEntry { + std::string path; + time_t last_mtime; + bool changed; + }; + + std::vector<WatchEntry> files_; + time_t get_mtime(const char* path); +}; + +#endif // !defined(STRIP_ALL) + +#endif // FILE_WATCHER_H_ |
