summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/3d/visual_debug.cc11
-rw-r--r--src/audio/audio.cc6
-rw-r--r--src/audio/audio.h3
-rw-r--r--src/gpu/demo_effects.h55
-rw-r--r--src/gpu/effect.h5
-rw-r--r--src/gpu/effects/chroma_aberration_effect.cc1
-rw-r--r--src/gpu/effects/circle_mask_effect.cc24
-rw-r--r--src/gpu/effects/circle_mask_effect.h8
-rw-r--r--src/gpu/effects/distort_effect.cc26
-rw-r--r--src/gpu/effects/fade_effect.cc19
-rw-r--r--src/gpu/effects/fade_effect.h3
-rw-r--r--src/gpu/effects/flash_cube_effect.h2
-rw-r--r--src/gpu/effects/gaussian_blur_effect.cc1
-rw-r--r--src/gpu/effects/heptagon_effect.cc24
-rw-r--r--src/gpu/effects/moving_ellipse_effect.cc9
-rw-r--r--src/gpu/effects/particle_spray_effect.cc1
-rw-r--r--src/gpu/effects/particles_effect.cc1
-rw-r--r--src/gpu/effects/passthrough_effect.cc1
-rw-r--r--src/gpu/effects/post_process_helper.cc8
-rw-r--r--src/gpu/effects/post_process_helper.h8
-rw-r--r--src/gpu/effects/shaders.cc22
-rw-r--r--src/gpu/effects/shaders.h7
-rw-r--r--src/gpu/effects/solarize_effect.cc5
-rw-r--r--src/gpu/effects/theme_modulation_effect.cc19
-rw-r--r--src/gpu/effects/theme_modulation_effect.h3
-rw-r--r--src/gpu/effects/vignette_effect.cc5
-rw-r--r--src/gpu/gpu.cc11
-rw-r--r--src/gpu/stub_gpu.cc83
-rw-r--r--src/gpu/texture_manager.cc586
-rw-r--r--src/gpu/texture_manager.h68
-rw-r--r--src/gpu/uniform_helper.h1
-rw-r--r--src/main.cc32
-rw-r--r--src/platform/platform.h5
-rw-r--r--src/platform/stub_platform.cc53
-rw-r--r--src/platform/stub_types.h159
-rw-r--r--src/stub_main.cc13
-rw-r--r--src/test_demo.cc39
-rw-r--r--src/tests/test_3d_render.cc39
-rw-r--r--src/tests/test_demo_effects.cc3
-rw-r--r--src/tests/test_effect_base.cc3
-rw-r--r--src/tests/test_file_watcher.cc63
-rw-r--r--src/tests/test_gpu_composite.cc124
-rw-r--r--src/tests/test_gpu_procedural.cc117
-rw-r--r--src/tests/test_post_process_helper.cc8
-rw-r--r--src/tests/test_shader_compilation.cc7
-rw-r--r--src/util/asset_manager.cc20
-rw-r--r--src/util/asset_manager.h6
-rw-r--r--src/util/file_watcher.cc44
-rw-r--r--src/util/file_watcher.h33
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, &params,
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, &params,
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_