summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-05-20 22:44:44 +0200
committerskal <pascal.massimino@gmail.com>2026-05-20 22:44:44 +0200
commit5d20c892dedce7bc7486acbd72fbd35da69e413e (patch)
tree05e04d5e689504c2421cd5772e91a42ee69608ab /src
parent6ef8f578817ee0134fd5867ca3b80590e3eb2368 (diff)
fix: code review cleanup — bugs, dead code, factorization (-167 lines)
Bugs: - B1: fix dead tempo debug (prev_tempo captured after assignment) - B2: fix ReloadAssetsFromFile leak for disk-loaded assets; simplify DropAsset - B3: fix get_free_pool_slot leak (unregister synth + free data on reuse) - B4: volatile -> std::atomic with acquire/release in miniaudio_backend, synth - B5: fix unaligned reads in scene_loader (memcpy-based read_f32/read_u32) - B6: fix shader module + BGL + pipeline layout leaks in gpu.cc, pipeline_builder Dead code: - D1: remove unused particle_defs.h - D3: remove create_post_process_pipeline_simple (zero callers) - D4: remove empty gpu_draw() - D5: remove write-only Hybrid3D::initialized_ - D6: remove legacy pending buffer path in audio.cc Factorization: - F1: Effect::run_fullscreen_pass() replaces boilerplate in 5 effects - F2: particle_common.wgsl snippet, #include in 3 WGSL shaders - F3: gpu_create_shader_module() helper, used in 3 call sites - F5: get_world_aabb() shared between bvh.cc and physics.cc - F6: samples_to_seconds() replaces 6 inline expressions - F7: gpu_create_linear/nearest_sampler use SamplerCache; add nearest() preset 37/37 tests passing. handoff(Claude): code review batch — all items verified, no regressions.
Diffstat (limited to 'src')
-rw-r--r--src/3d/bvh.cc16
-rw-r--r--src/3d/bvh.h3
-rw-r--r--src/3d/physics.cc26
-rw-r--r--src/3d/scene_loader.cc77
-rw-r--r--src/app/main.cc4
-rw-r--r--src/audio/audio.cc59
-rw-r--r--src/audio/backend/miniaudio_backend.cc20
-rw-r--r--src/audio/backend/miniaudio_backend.h8
-rw-r--r--src/audio/ring_buffer.h5
-rw-r--r--src/audio/synth.cc51
-rw-r--r--src/audio/tracker.cc10
-rw-r--r--src/effects/hybrid3_d_effect.cc2
-rw-r--r--src/effects/hybrid3_d_effect.h1
-rw-r--r--src/effects/particle_compute.wgsl8
-rw-r--r--src/effects/particle_defs.h13
-rw-r--r--src/effects/particle_render.wgsl8
-rw-r--r--src/effects/particle_spray_compute.wgsl8
-rw-r--r--src/effects/peak_meter_effect.cc17
-rw-r--r--src/effects/placeholder_effect.cc15
-rw-r--r--src/effects/scene1_effect.cc15
-rw-r--r--src/effects/scene2_effect.cc15
-rw-r--r--src/effects/shaders.cc2
-rw-r--r--src/gpu/effect.cc25
-rw-r--r--src/gpu/effect.h7
-rw-r--r--src/gpu/gpu.cc71
-rw-r--r--src/gpu/gpu.h7
-rw-r--r--src/gpu/gpu_headless.cc9
-rw-r--r--src/gpu/gpu_stub.cc9
-rw-r--r--src/gpu/pipeline_builder.cc11
-rw-r--r--src/gpu/post_process_helper.cc34
-rw-r--r--src/gpu/post_process_helper.h5
-rw-r--r--src/gpu/sampler_cache.h4
-rw-r--r--src/gpu/wgsl_effect.cc17
-rw-r--r--src/shaders/compute/particle_common.wgsl7
-rw-r--r--src/util/asset_manager.cc36
35 files changed, 232 insertions, 393 deletions
diff --git a/src/3d/bvh.cc b/src/3d/bvh.cc
index 5f7abef..129016c 100644
--- a/src/3d/bvh.cc
+++ b/src/3d/bvh.cc
@@ -4,14 +4,6 @@
#include "3d/bvh.h"
#include <algorithm>
-namespace {
-
-struct ObjectInfo {
- int index;
- AABB aabb;
- vec3 centroid;
-};
-
AABB get_world_aabb(const Object3D& obj) {
BoundingVolume local = obj.get_local_bounds();
mat4 model = obj.get_model_matrix();
@@ -35,6 +27,14 @@ AABB get_world_aabb(const Object3D& obj) {
return world;
}
+namespace {
+
+struct ObjectInfo {
+ int index;
+ AABB aabb;
+ vec3 centroid;
+};
+
int build_recursive(std::vector<BVHNode>& nodes,
std::vector<ObjectInfo>& obj_info, int start, int end) {
int node_idx = (int)nodes.size();
diff --git a/src/3d/bvh.h b/src/3d/bvh.h
index 97e9a06..af4b152 100644
--- a/src/3d/bvh.h
+++ b/src/3d/bvh.h
@@ -63,6 +63,9 @@ class BVH {
void query(const AABB& box, std::vector<int>& out_indices) const;
};
+// Compute world-space AABB by transforming local bounds corners
+AABB get_world_aabb(const Object3D& obj);
+
class BVHBuilder {
public:
static void build(BVH& out_bvh, const std::vector<Object3D>& objects);
diff --git a/src/3d/physics.cc b/src/3d/physics.cc
index 2aa101d..db27e95 100644
--- a/src/3d/physics.cc
+++ b/src/3d/physics.cc
@@ -6,31 +6,7 @@
#include "3d/sdf_cpu.h"
#include <algorithm>
-namespace {
-// Helper to get world AABB (copied from bvh.cc or shared)
-AABB get_world_aabb(const Object3D& obj) {
- BoundingVolume local = obj.get_local_bounds();
- mat4 model = obj.get_model_matrix();
-
- vec3 corners[8] = {
- {local.min.x, local.min.y, local.min.z},
- {local.max.x, local.min.y, local.min.z},
- {local.min.x, local.max.y, local.min.z},
- {local.max.x, local.max.y, local.min.z},
- {local.min.x, local.min.y, local.max.z},
- {local.max.x, local.min.y, local.max.z},
- {local.min.x, local.max.y, local.max.z},
- {local.max.x, local.max.y, local.max.z},
- };
-
- AABB world;
- for (int i = 0; i < 8; ++i) {
- vec4 p = model * vec4(corners[i].x, corners[i].y, corners[i].z, 1.0f);
- world.expand(p.xyz());
- }
- return world;
-}
-} // namespace
+// get_world_aabb() is declared in bvh.h
float PhysicsSystem::sample_sdf(const Object3D& obj, vec3 world_p) {
mat4 inv_model = obj.get_model_matrix().inverse();
diff --git a/src/3d/scene_loader.cc b/src/3d/scene_loader.cc
index eb20954..d5c1879 100644
--- a/src/3d/scene_loader.cc
+++ b/src/3d/scene_loader.cc
@@ -11,6 +11,18 @@
namespace SceneLoader {
+// Safe unaligned read helpers
+static inline uint32_t read_u32(const uint8_t* p) {
+ uint32_t v;
+ std::memcpy(&v, p, sizeof(v));
+ return v;
+}
+static inline float read_f32(const uint8_t* p) {
+ float v;
+ std::memcpy(&v, p, sizeof(v));
+ return v;
+}
+
bool LoadScene(Scene& scene, const uint8_t* data, size_t size) {
if (!data || size < 16) { // Header size check
printf("SceneLoader: Data too small\n");
@@ -25,11 +37,11 @@ bool LoadScene(Scene& scene, const uint8_t* data, size_t size) {
size_t offset = 4;
- uint32_t num_objects = *(const uint32_t*)(data + offset);
+ uint32_t num_objects = read_u32(data + offset);
offset += 4;
- uint32_t num_cameras = *(const uint32_t*)(data + offset);
+ uint32_t num_cameras = read_u32(data + offset);
offset += 4;
- uint32_t num_lights = *(const uint32_t*)(data + offset);
+ uint32_t num_lights = read_u32(data + offset);
offset += 4;
// printf("SceneLoader: Loading %d objects, %d cameras, %d lights\n",
@@ -45,49 +57,33 @@ bool LoadScene(Scene& scene, const uint8_t* data, size_t size) {
if (offset + 4 > size)
return false;
- uint32_t type_val = *(const uint32_t*)(data + offset);
+ uint32_t type_val = read_u32(data + offset);
offset += 4;
ObjectType type = (ObjectType)type_val;
if (offset + 12 + 16 + 12 + 16 > size)
return false; // Transforms + Color
- float px = *(const float*)(data + offset);
- offset += 4;
- float py = *(const float*)(data + offset);
- offset += 4;
- float pz = *(const float*)(data + offset);
- offset += 4;
+ float px = read_f32(data + offset); offset += 4;
+ float py = read_f32(data + offset); offset += 4;
+ float pz = read_f32(data + offset); offset += 4;
vec3 pos(px, py, pz);
- float rx = *(const float*)(data + offset);
- offset += 4;
- float ry = *(const float*)(data + offset);
- offset += 4;
- float rz = *(const float*)(data + offset);
- offset += 4;
- float rw = *(const float*)(data + offset);
- offset += 4;
+ float rx = read_f32(data + offset); offset += 4;
+ float ry = read_f32(data + offset); offset += 4;
+ float rz = read_f32(data + offset); offset += 4;
+ float rw = read_f32(data + offset); offset += 4;
quat rot(rx, ry, rz, rw);
- float sx = *(const float*)(data + offset);
- offset += 4;
- float sy = *(const float*)(data + offset);
- offset += 4;
- float sz = *(const float*)(data + offset);
- offset += 4;
+ float sx = read_f32(data + offset); offset += 4;
+ float sy = read_f32(data + offset); offset += 4;
+ float sz = read_f32(data + offset); offset += 4;
vec3 scale(sx, sy, sz);
- // Color components (cr, cg, cb, ca)
- float cr = *(const float*)(data + offset);
- offset += 4;
- float cg = *(const float*)(data + offset);
- offset += 4;
- float cb = *(const float*)(data + offset);
- offset += 4;
- // Read ca, advance offset AFTER reading ca, then construct color
- float ca = *(const float*)(data + offset);
- offset += 4; // Offset is now after ca
+ float cr = read_f32(data + offset); offset += 4;
+ float cg = read_f32(data + offset); offset += 4;
+ float cb = read_f32(data + offset); offset += 4;
+ float ca = read_f32(data + offset); offset += 4;
vec4 color(cr, cg, cb, ca);
// Plane Distance (if type == PLANE)
@@ -96,7 +92,7 @@ bool LoadScene(Scene& scene, const uint8_t* data, size_t size) {
// Check bounds before reading plane_distance
if (offset + 4 > size)
return false;
- plane_distance = *(const float*)(data + offset);
+ plane_distance = read_f32(data + offset);
offset += 4; // Advance offset after reading plane_distance
}
@@ -105,7 +101,7 @@ bool LoadScene(Scene& scene, const uint8_t* data, size_t size) {
// either after ca (if not PLANE) or after plane_distance (if PLANE).
if (offset + 4 > size)
return false;
- uint32_t name_len = *(const uint32_t*)(data + offset);
+ uint32_t name_len = read_u32(data + offset);
offset += 4;
AssetId mesh_id = (AssetId)0; // Default or INVALID (if 0 is invalid)
@@ -131,12 +127,9 @@ bool LoadScene(Scene& scene, const uint8_t* data, size_t size) {
// Physics properties
if (offset + 4 + 4 + 4 > size)
return false;
- float mass = *(const float*)(data + offset);
- offset += 4;
- float restitution = *(const float*)(data + offset);
- offset += 4;
- uint32_t is_static_u32 = *(const uint32_t*)(data + offset);
- offset += 4;
+ float mass = read_f32(data + offset); offset += 4;
+ float restitution = read_f32(data + offset); offset += 4;
+ uint32_t is_static_u32 = read_u32(data + offset); offset += 4;
bool is_static = (is_static_u32 != 0);
// Create Object3D
diff --git a/src/app/main.cc b/src/app/main.cc
index c85877b..4cddc8a 100644
--- a/src/app/main.cc
+++ b/src/app/main.cc
@@ -142,6 +142,8 @@ int main(int argc, char** argv) {
static float g_last_audio_time = 0.0f;
auto fill_audio_buffer = [&](float audio_dt, double physical_time) {
+ const float prev_tempo = g_tempo_scale;
+
// Calculate tempo scale if --tempo flag enabled
if (tempo_test_enabled) {
const float t = (float)physical_time;
@@ -161,8 +163,6 @@ int main(int argc, char** argv) {
g_tempo_scale = 1.0f; // No tempo variation
}
- const float prev_tempo = g_tempo_scale;
-
#if !defined(STRIP_ALL)
// Debug output when tempo changes significantly
if (fabsf(g_tempo_scale - prev_tempo) > 0.05f) {
diff --git a/src/audio/audio.cc b/src/audio/audio.cc
index 3b98452..91dd05b 100644
--- a/src/audio/audio.cc
+++ b/src/audio/audio.cc
@@ -28,12 +28,6 @@ static void clip_samples(float* buf, int count) {
// Global ring buffer for audio streaming
static AudioRingBuffer g_ring_buffer;
-// Pending write buffer for partially written samples
-// Maximum size: one chunk (533 frames @ 60fps = 1066 samples stereo)
-#define MAX_PENDING_SAMPLES 2048
-static float g_pending_buffer[MAX_PENDING_SAMPLES];
-static int g_pending_samples = 0; // How many samples are waiting to be written
-
// Global backend pointer for audio abstraction
static AudioBackend* g_audio_backend = nullptr;
static MiniaudioBackend g_default_backend;
@@ -56,9 +50,6 @@ void audio_init() {
// In production code, use AudioEngine::init() which manages initialization
// order.
- // Clear pending buffer
- g_pending_samples = 0;
-
// Use default backend if none set
if (g_audio_backend == nullptr) {
g_audio_backend = &g_default_backend;
@@ -74,8 +65,7 @@ float audio_get_required_prefill_time() {
bool audio_is_prefilled() {
const int buffered = g_ring_buffer.available_read();
- const float buffered_time =
- (float)buffered / (RING_BUFFER_SAMPLE_RATE * RING_BUFFER_CHANNELS);
+ const float buffered_time = samples_to_seconds(buffered);
const float required = audio_get_required_prefill_time();
return buffered_time >= (required - 0.001f); // 1ms tolerance
}
@@ -89,9 +79,7 @@ void audio_start() {
#if !defined(STRIP_ALL)
if (!audio_is_prefilled()) {
const int buffered = g_ring_buffer.available_read();
- const float buffered_ms = (float)buffered /
- (RING_BUFFER_SAMPLE_RATE * RING_BUFFER_CHANNELS) *
- 1000.0f;
+ const float buffered_ms = samples_to_seconds(buffered) * 1000.0f;
printf("WARNING: Audio buffer not pre-filled (%.1fms < %.1fms)\n",
buffered_ms, audio_get_required_prefill_time() * 1000.0f);
}
@@ -116,40 +104,9 @@ void audio_render_ahead(float music_time, float dt, float target_fill) {
// Keep rendering small chunks until buffer is full enough
while (true) {
- // First, try to flush any pending samples from previous partial writes
- if (g_pending_samples > 0) {
- const int written =
- g_ring_buffer.write(g_pending_buffer, g_pending_samples);
-
- if (written > 0) {
- // Some or all samples were written
- // Move remaining samples to front of buffer
- const int remaining = g_pending_samples - written;
- if (remaining > 0) {
- for (int i = 0; i < remaining; ++i) {
- g_pending_buffer[i] = g_pending_buffer[written + i];
- }
- }
- g_pending_samples = remaining;
-
- // Notify backend (for testing/tracking)
-#if !defined(STRIP_ALL)
- if (g_audio_backend != nullptr) {
- g_audio_backend->on_frames_rendered(written / RING_BUFFER_CHANNELS);
- }
-#endif
- }
-
- // If still have pending samples, buffer is full - wait for consumption
- if (g_pending_samples > 0)
- break;
- }
-
// Check current buffer state
const int buffered_samples = g_ring_buffer.available_read();
- const float buffered_time =
- (float)buffered_samples /
- (RING_BUFFER_SAMPLE_RATE * RING_BUFFER_CHANNELS);
+ const float buffered_time = samples_to_seconds(buffered_samples);
// Stop if buffer is full enough
if (buffered_time >= target_lookahead)
@@ -238,20 +195,16 @@ float audio_get_playback_time() {
(float)(elapsed * RING_BUFFER_SAMPLE_RATE * RING_BUFFER_CHANNELS);
const float total_samples =
(float)last_callback_samples + interpolated_samples;
- return total_samples / (RING_BUFFER_SAMPLE_RATE * RING_BUFFER_CHANNELS);
+ return samples_to_seconds((int64_t)total_samples);
}
}
// Fallback: coarse ring buffer time (before first callback or no backend)
- const int64_t total_samples = g_ring_buffer.get_total_read();
- return (float)total_samples /
- (RING_BUFFER_SAMPLE_RATE * RING_BUFFER_CHANNELS);
+ return samples_to_seconds(g_ring_buffer.get_total_read());
}
float audio_get_render_time() {
- const int64_t total_samples = g_ring_buffer.get_total_written();
- return (float)total_samples /
- (RING_BUFFER_SAMPLE_RATE * RING_BUFFER_CHANNELS);
+ return samples_to_seconds(g_ring_buffer.get_total_written());
}
float audio_get_realtime_peak() {
diff --git a/src/audio/backend/miniaudio_backend.cc b/src/audio/backend/miniaudio_backend.cc
index 312d36e..8871d10 100644
--- a/src/audio/backend/miniaudio_backend.cc
+++ b/src/audio/backend/miniaudio_backend.cc
@@ -12,11 +12,11 @@
// Real-time peak measured at actual playback time
// Updated in audio_callback when samples are read from ring buffer
-volatile float MiniaudioBackend::realtime_peak_ = 0.0f;
+std::atomic<float> MiniaudioBackend::realtime_peak_{0.0f};
// Smooth playback time interpolation
-volatile double MiniaudioBackend::last_callback_time_ = 0.0;
-volatile int64_t MiniaudioBackend::last_callback_samples_ = 0;
+std::atomic<double> MiniaudioBackend::last_callback_time_{0.0};
+std::atomic<int64_t> MiniaudioBackend::last_callback_samples_{0};
// Static callback for miniaudio (C API requirement)
void MiniaudioBackend::audio_callback(ma_device* pDevice, void* pOutput,
@@ -148,8 +148,10 @@ void MiniaudioBackend::audio_callback(ma_device* pDevice, void* pOutput,
// Update smooth playback time tracking (absolute time, no epoch needed)
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
- last_callback_time_ = ts.tv_sec + ts.tv_nsec / 1e9;
- last_callback_samples_ = ring_buffer->get_total_read();
+ last_callback_time_.store(ts.tv_sec + ts.tv_nsec / 1e9,
+ std::memory_order_release);
+ last_callback_samples_.store(ring_buffer->get_total_read(),
+ std::memory_order_release);
#if defined(DEBUG_LOG_RING_BUFFER)
if (actually_read < samples_to_read) {
@@ -280,11 +282,11 @@ void MiniaudioBackend::shutdown() {
}
float MiniaudioBackend::get_realtime_peak() {
- return realtime_peak_;
+ return realtime_peak_.load(std::memory_order_acquire);
}
void MiniaudioBackend::get_callback_state(double* out_time,
- int64_t* out_samples) {
- *out_time = last_callback_time_;
- *out_samples = last_callback_samples_;
+ int64_t* out_samples) {
+ *out_time = last_callback_time_.load(std::memory_order_acquire);
+ *out_samples = last_callback_samples_.load(std::memory_order_acquire);
}
diff --git a/src/audio/backend/miniaudio_backend.h b/src/audio/backend/miniaudio_backend.h
index 953a0c0..01fa790 100644
--- a/src/audio/backend/miniaudio_backend.h
+++ b/src/audio/backend/miniaudio_backend.h
@@ -6,6 +6,8 @@
#include "../audio_backend.h"
#include "miniaudio.h"
+#include <atomic>
+#include <cstdint>
// Production audio backend using miniaudio library
// Manages real hardware audio device and playback
@@ -31,11 +33,11 @@ class MiniaudioBackend : public AudioBackend {
// Real-time peak measured at actual playback time (not pre-buffer)
// Updated in audio_callback when samples are read from ring buffer
- static volatile float realtime_peak_;
+ static std::atomic<float> realtime_peak_;
// Smooth playback time interpolation (updated in callback)
- static volatile double last_callback_time_; // Absolute CLOCK_MONOTONIC time
- static volatile int64_t last_callback_samples_;
+ static std::atomic<double> last_callback_time_; // Absolute CLOCK_MONOTONIC time
+ static std::atomic<int64_t> last_callback_samples_;
// Static callback required by miniaudio C API
static void audio_callback(ma_device* pDevice, void* pOutput,
diff --git a/src/audio/ring_buffer.h b/src/audio/ring_buffer.h
index 1a21542..ba8cb49 100644
--- a/src/audio/ring_buffer.h
+++ b/src/audio/ring_buffer.h
@@ -19,6 +19,11 @@
RING_BUFFER_CHANNELS) / \
1000)
+// Convert sample count to seconds (accounting for stereo channels)
+inline float samples_to_seconds(int64_t samples) {
+ return (float)samples / (RING_BUFFER_SAMPLE_RATE * RING_BUFFER_CHANNELS);
+}
+
class AudioRingBuffer {
public:
AudioRingBuffer();
diff --git a/src/audio/synth.cc b/src/audio/synth.cc
index 0161385..d584c62 100644
--- a/src/audio/synth.cc
+++ b/src/audio/synth.cc
@@ -5,7 +5,9 @@
#include "synth.h"
#include "audio/dct.h"
#include "audio/ola.h"
+#include "audio/ring_buffer.h"
#include "util/debug.h"
+#include <atomic>
#include <math.h>
#include <string.h> // For memset
@@ -32,18 +34,18 @@ struct Voice {
int start_sample_offset; // Samples to wait before producing audio output
- const volatile float* active_spectral_data;
+ const float* active_spectral_data;
};
static struct {
Spectrogram spectrograms[MAX_SPECTROGRAMS];
- const volatile float* active_spectrogram_data[MAX_SPECTROGRAMS];
+ std::atomic<const float*> active_spectrogram_data[MAX_SPECTROGRAMS];
bool spectrogram_registered[MAX_SPECTROGRAMS];
} g_synth_data;
static Voice g_voices[MAX_VOICES];
-static volatile float g_current_output_peak =
- 0.0f; // Global peak for visualization
+static std::atomic<float> g_current_output_peak{
+ 0.0f}; // Global peak for visualization
static float g_tempo_scale = 1.0f; // Playback speed multiplier
#if !defined(STRIP_ALL)
@@ -53,7 +55,7 @@ static float g_elapsed_time_sec = 0.0f; // Tracks elapsed time for event hooks
void synth_init() {
memset(&g_synth_data, 0, sizeof(g_synth_data));
memset(g_voices, 0, sizeof(g_voices));
- g_current_output_peak = 0.0f;
+ g_current_output_peak.store(0.0f, std::memory_order_relaxed);
#if !defined(STRIP_ALL)
g_elapsed_time_sec = 0.0f;
#endif /* !defined(STRIP_ALL) */
@@ -108,7 +110,8 @@ int synth_register_spectrogram(const Spectrogram* spec) {
for (int i = 0; i < MAX_SPECTROGRAMS; ++i) {
if (!g_synth_data.spectrogram_registered[i]) {
g_synth_data.spectrograms[i] = *spec;
- g_synth_data.active_spectrogram_data[i] = spec->spectral_data_a;
+ g_synth_data.active_spectrogram_data[i].store(
+ spec->spectral_data_a, std::memory_order_release);
g_synth_data.spectrogram_registered[i] = true;
return i;
}
@@ -128,8 +131,9 @@ float* synth_begin_update(int spectrogram_id) {
return nullptr;
}
- const volatile float* active_ptr =
- g_synth_data.active_spectrogram_data[spectrogram_id];
+ const float* active_ptr =
+ g_synth_data.active_spectrogram_data[spectrogram_id].load(
+ std::memory_order_acquire);
if (active_ptr == g_synth_data.spectrograms[spectrogram_id].spectral_data_a) {
return (float*)(g_synth_data.spectrograms[spectrogram_id].spectral_data_b);
@@ -144,18 +148,17 @@ void synth_commit_update(int spectrogram_id) {
return;
}
- const volatile float* old_active_ptr =
- g_synth_data.active_spectrogram_data[spectrogram_id];
+ const float* old_active_ptr =
+ g_synth_data.active_spectrogram_data[spectrogram_id].load(
+ std::memory_order_acquire);
const float* new_active_ptr =
(old_active_ptr ==
g_synth_data.spectrograms[spectrogram_id].spectral_data_a)
? g_synth_data.spectrograms[spectrogram_id].spectral_data_b
: g_synth_data.spectrograms[spectrogram_id].spectral_data_a;
- // Atomic swap using GCC/Clang builtins for thread safety
- __atomic_store_n(
- (const float**)&g_synth_data.active_spectrogram_data[spectrogram_id],
- new_active_ptr, __ATOMIC_RELEASE);
+ g_synth_data.active_spectrogram_data[spectrogram_id].store(
+ new_active_ptr, std::memory_order_release);
}
void synth_trigger_voice(int spectrogram_id, float volume, float pan,
@@ -215,7 +218,8 @@ void synth_trigger_voice(int spectrogram_id, float volume, float pan,
v.fractional_pos = 0.0f;
v.start_sample_offset = start_offset_samples;
v.active_spectral_data =
- g_synth_data.active_spectrogram_data[spectrogram_id];
+ g_synth_data.active_spectrogram_data[spectrogram_id].load(
+ std::memory_order_acquire);
#if !defined(STRIP_ALL)
// Notify backend of voice trigger event (for testing/tracking)
@@ -233,7 +237,7 @@ void synth_trigger_voice(int spectrogram_id, float volume, float pan,
void synth_render(float* output_buffer, int num_frames) {
// Faster decay for more responsive visuals
- g_current_output_peak *= 0.90f;
+ float peak = g_current_output_peak.load(std::memory_order_relaxed) * 0.90f;
for (int i = 0; i < num_frames; ++i) {
float left_sample = 0.0f;
@@ -258,7 +262,8 @@ void synth_render(float* output_buffer, int num_frames) {
// Fetch the latest active spectrogram pointer for this voice
v.active_spectral_data =
- g_synth_data.active_spectrogram_data[v.spectrogram_id];
+ g_synth_data.active_spectrogram_data[v.spectrogram_id].load(
+ std::memory_order_acquire);
const float* spectral_frame = (const float*)v.active_spectral_data +
(v.current_spectral_frame * DCT_SIZE);
@@ -286,14 +291,14 @@ void synth_render(float* output_buffer, int num_frames) {
output_buffer[i * 2 + 1] = right_sample;
// Update the peak with the new max (attack)
- g_current_output_peak = fmaxf(
- g_current_output_peak, fmaxf(fabsf(left_sample), fabsf(right_sample)));
+ peak = fmaxf(peak, fmaxf(fabsf(left_sample), fabsf(right_sample)));
}
+ g_current_output_peak.store(peak, std::memory_order_release);
+
#if !defined(STRIP_ALL)
- // Update elapsed time for event tracking (32000 Hz sample rate)
- const float sample_rate = 32000.0f;
- g_elapsed_time_sec += (float)num_frames / sample_rate;
+ // Update elapsed time for event tracking
+ g_elapsed_time_sec += (float)num_frames / RING_BUFFER_SAMPLE_RATE;
#endif /* !defined(STRIP_ALL) */
}
@@ -308,5 +313,5 @@ int synth_get_active_voice_count() {
}
float synth_get_output_peak() {
- return g_current_output_peak;
+ return g_current_output_peak.load(std::memory_order_acquire);
}
diff --git a/src/audio/tracker.cc b/src/audio/tracker.cc
index 95b8022..a511a62 100644
--- a/src/audio/tracker.cc
+++ b/src/audio/tracker.cc
@@ -255,9 +255,17 @@ static int get_free_pool_slot() {
}
// If all slots are active, reuse the oldest one (round-robin)
- // This automatically handles cleanup of old patterns
+ // Clean up the old slot before reuse
const int slot = g_next_pool_slot;
g_next_pool_slot = (g_next_pool_slot + 1) % MAX_SPECTROGRAMS;
+
+ if (g_spec_pool[slot].synth_id >= 0) {
+ synth_unregister_spectrogram(g_spec_pool[slot].synth_id);
+ }
+ delete[] g_spec_pool[slot].data;
+ g_spec_pool[slot].data = nullptr;
+ g_spec_pool[slot].synth_id = -1;
+ g_spec_pool[slot].active = false;
return slot;
}
diff --git a/src/effects/hybrid3_d_effect.cc b/src/effects/hybrid3_d_effect.cc
index 33a2d73..bef82b4 100644
--- a/src/effects/hybrid3_d_effect.cc
+++ b/src/effects/hybrid3_d_effect.cc
@@ -42,8 +42,6 @@ Hybrid3D::Hybrid3D(const GpuContext& ctx,
renderer_.set_noise_texture(dummy_texture_view_);
renderer_.set_sky_texture(dummy_texture_view_);
- initialized_ = true;
-
// Setup simple scene (1 center cube + 8 surrounding objects)
scene_.clear();
Object3D center(ObjectType::BOX);
diff --git a/src/effects/hybrid3_d_effect.h b/src/effects/hybrid3_d_effect.h
index 13fd7df..88047dd 100644
--- a/src/effects/hybrid3_d_effect.h
+++ b/src/effects/hybrid3_d_effect.h
@@ -24,7 +24,6 @@ class Hybrid3D : public Effect {
Renderer3D renderer_;
Scene scene_;
Camera camera_;
- bool initialized_ = false;
std::string depth_node_;
WGPUTexture dummy_texture_;
WGPUTextureView dummy_texture_view_;
diff --git a/src/effects/particle_compute.wgsl b/src/effects/particle_compute.wgsl
index f3e8051..148a2c3 100644
--- a/src/effects/particle_compute.wgsl
+++ b/src/effects/particle_compute.wgsl
@@ -1,11 +1,5 @@
// Particle simulation (compute shader) - V2
-struct Particle {
- pos: vec4f,
- vel: vec4f,
- rot: vec4f,
- color: vec4f,
-};
-
+#include "particle_common"
#include "sequence_uniforms"
@group(0) @binding(0) var<storage, read_write> particles: array<Particle>;
diff --git a/src/effects/particle_defs.h b/src/effects/particle_defs.h
deleted file mode 100644
index dcbb830..0000000
--- a/src/effects/particle_defs.h
+++ /dev/null
@@ -1,13 +0,0 @@
-// This file is part of the 64k demo project.
-// It defines common structures for particle-based effects.
-
-#pragma once
-
-static const int NUM_PARTICLES = 10000;
-
-struct Particle {
- float pos[4];
- float vel[4];
- float rot[4];
- float color[4];
-};
diff --git a/src/effects/particle_render.wgsl b/src/effects/particle_render.wgsl
index ef0db42..66b0b9c 100644
--- a/src/effects/particle_render.wgsl
+++ b/src/effects/particle_render.wgsl
@@ -1,11 +1,5 @@
// Particle rendering (vertex + fragment) - V2
-struct Particle {
- pos: vec4f,
- vel: vec4f,
- rot: vec4f,
- color: vec4f,
-};
-
+#include "particle_common"
#include "sequence_uniforms"
@group(0) @binding(0) var<storage, read> particles: array<Particle>;
diff --git a/src/effects/particle_spray_compute.wgsl b/src/effects/particle_spray_compute.wgsl
index 7bdae88..84a51f4 100644
--- a/src/effects/particle_spray_compute.wgsl
+++ b/src/effects/particle_spray_compute.wgsl
@@ -1,10 +1,4 @@
-struct Particle {
- pos: vec4f,
- vel: vec4f,
- rot: vec4f,
- color: vec4f,
-};
-
+#include "particle_common"
#include "common_uniforms"
@group(0) @binding(0) var<storage, read_write> particles: array<Particle>;
diff --git a/src/effects/peak_meter_effect.cc b/src/effects/peak_meter_effect.cc
index d462fa0..c2ef42e 100644
--- a/src/effects/peak_meter_effect.cc
+++ b/src/effects/peak_meter_effect.cc
@@ -78,19 +78,6 @@ void PeakMeter::render(WGPUCommandEncoder encoder,
pp_update_bind_group(ctx_.device, pipeline_, &bind_group_, input_view,
uniforms_buffer_.get(), {nullptr, 0});
- WGPURenderPassColorAttachment color_attachment = {};
- gpu_init_color_attachment(color_attachment, output_view);
- color_attachment.loadOp = WGPULoadOp_Load;
-
- WGPURenderPassDescriptor pass_desc = {};
- pass_desc.colorAttachmentCount = 1;
- pass_desc.colorAttachments = &color_attachment;
-
- WGPURenderPassEncoder pass =
- wgpuCommandEncoderBeginRenderPass(encoder, &pass_desc);
- wgpuRenderPassEncoderSetPipeline(pass, pipeline_);
- wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 0, nullptr);
- wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0);
- wgpuRenderPassEncoderEnd(pass);
- wgpuRenderPassEncoderRelease(pass);
+ run_fullscreen_pass(encoder, pipeline_, bind_group_, output_view,
+ WGPULoadOp_Load);
}
diff --git a/src/effects/placeholder_effect.cc b/src/effects/placeholder_effect.cc
index beb5f33..4221a74 100644
--- a/src/effects/placeholder_effect.cc
+++ b/src/effects/placeholder_effect.cc
@@ -32,18 +32,5 @@ void Placeholder::render(WGPUCommandEncoder encoder,
pp_update_bind_group(ctx_.device, pipeline_, &bind_group_, input_view,
uniforms_buffer_.get(), {nullptr, 0});
- WGPURenderPassColorAttachment color_attachment = {};
- gpu_init_color_attachment(color_attachment, output_view);
-
- WGPURenderPassDescriptor pass_desc = {};
- pass_desc.colorAttachmentCount = 1;
- pass_desc.colorAttachments = &color_attachment;
-
- WGPURenderPassEncoder pass =
- wgpuCommandEncoderBeginRenderPass(encoder, &pass_desc);
- wgpuRenderPassEncoderSetPipeline(pass, pipeline_);
- wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 0, nullptr);
- wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0);
- wgpuRenderPassEncoderEnd(pass);
- wgpuRenderPassEncoderRelease(pass);
+ run_fullscreen_pass(encoder, pipeline_, bind_group_, output_view);
}
diff --git a/src/effects/scene1_effect.cc b/src/effects/scene1_effect.cc
index 0aae94a..bf99fc7 100644
--- a/src/effects/scene1_effect.cc
+++ b/src/effects/scene1_effect.cc
@@ -49,18 +49,5 @@ void Scene1::render(WGPUCommandEncoder encoder,
dummy_texture_view_.get(), uniforms_buffer_.get(),
camera_params_.get());
- WGPURenderPassColorAttachment color_attachment = {};
- gpu_init_color_attachment(color_attachment, output_view);
-
- WGPURenderPassDescriptor pass_desc = {};
- pass_desc.colorAttachmentCount = 1;
- pass_desc.colorAttachments = &color_attachment;
-
- WGPURenderPassEncoder pass =
- wgpuCommandEncoderBeginRenderPass(encoder, &pass_desc);
- wgpuRenderPassEncoderSetPipeline(pass, pipeline_.get());
- wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_.get(), 0, nullptr);
- wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); // Fullscreen triangle
- wgpuRenderPassEncoderEnd(pass);
- wgpuRenderPassEncoderRelease(pass);
+ run_fullscreen_pass(encoder, pipeline_.get(), bind_group_.get(), output_view);
}
diff --git a/src/effects/scene2_effect.cc b/src/effects/scene2_effect.cc
index 8c05574..92e5ecd 100644
--- a/src/effects/scene2_effect.cc
+++ b/src/effects/scene2_effect.cc
@@ -30,18 +30,5 @@ void Scene2Effect::render(WGPUCommandEncoder encoder,
dummy_texture_view_.get(), uniforms_buffer_.get(),
{nullptr, 0});
- WGPURenderPassColorAttachment color_attachment = {};
- gpu_init_color_attachment(color_attachment, output_view);
-
- WGPURenderPassDescriptor pass_desc = {};
- pass_desc.colorAttachmentCount = 1;
- pass_desc.colorAttachments = &color_attachment;
-
- WGPURenderPassEncoder pass =
- wgpuCommandEncoderBeginRenderPass(encoder, &pass_desc);
- wgpuRenderPassEncoderSetPipeline(pass, pipeline_.get());
- wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_.get(), 0, nullptr);
- wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0);
- wgpuRenderPassEncoderEnd(pass);
- wgpuRenderPassEncoderRelease(pass);
+ run_fullscreen_pass(encoder, pipeline_.get(), bind_group_.get(), output_view);
}
diff --git a/src/effects/shaders.cc b/src/effects/shaders.cc
index 8b625ee..7ca66fa 100644
--- a/src/effects/shaders.cc
+++ b/src/effects/shaders.cc
@@ -61,6 +61,8 @@ void InitShaderComposer() {
AssetId::ASSET_SHADER_RENDER_NTSC_COMMON);
register_if_exists("render/fullscreen_vs",
AssetId::ASSET_SHADER_RENDER_FULLSCREEN_VS);
+ register_if_exists("particle_common",
+ AssetId::ASSET_SHADER_COMPUTE_PARTICLE_COMMON);
register_if_exists("render/fullscreen_uv_vs",
AssetId::ASSET_SHADER_RENDER_FULLSCREEN_UV_VS);
register_if_exists("math/color", AssetId::ASSET_SHADER_MATH_COLOR);
diff --git a/src/gpu/effect.cc b/src/gpu/effect.cc
index 1257090..2e93a11 100644
--- a/src/gpu/effect.cc
+++ b/src/gpu/effect.cc
@@ -2,6 +2,7 @@
#include "gpu/effect.h"
#include "gpu/gpu.h"
+#include "gpu/sampler_cache.h"
#include "gpu/sequence.h"
#include "util/fatal_error.h"
@@ -72,6 +73,30 @@ std::string Effect::find_downstream_output(
return "";
}
+void Effect::run_fullscreen_pass(WGPUCommandEncoder encoder,
+ WGPURenderPipeline pipeline,
+ WGPUBindGroup bind_group,
+ WGPUTextureView output_view,
+ WGPULoadOp load_op) {
+ HEADLESS_RETURN_IF_NULL(encoder);
+
+ WGPURenderPassColorAttachment color_attachment = {};
+ gpu_init_color_attachment(color_attachment, output_view);
+ color_attachment.loadOp = load_op;
+
+ WGPURenderPassDescriptor pass_desc = {};
+ pass_desc.colorAttachmentCount = 1;
+ pass_desc.colorAttachments = &color_attachment;
+
+ WGPURenderPassEncoder pass =
+ wgpuCommandEncoderBeginRenderPass(encoder, &pass_desc);
+ wgpuRenderPassEncoderSetPipeline(pass, pipeline);
+ wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group, 0, nullptr);
+ wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0);
+ wgpuRenderPassEncoderEnd(pass);
+ wgpuRenderPassEncoderRelease(pass);
+}
+
void Effect::create_linear_sampler() {
sampler_.set(gpu_create_linear_sampler(ctx_.device));
}
diff --git a/src/gpu/effect.h b/src/gpu/effect.h
index 70ba9be..47dd3c2 100644
--- a/src/gpu/effect.h
+++ b/src/gpu/effect.h
@@ -79,6 +79,13 @@ class Effect {
// consumer). Returns "" if no such effect exists or it has no outputs.
std::string find_downstream_output(const std::vector<EffectDAGNode>& dag) const;
+ // Helper: Run a fullscreen triangle pass (pipeline + bind_group → output)
+ static void run_fullscreen_pass(WGPUCommandEncoder encoder,
+ WGPURenderPipeline pipeline,
+ WGPUBindGroup bind_group,
+ WGPUTextureView output_view,
+ WGPULoadOp load_op = WGPULoadOp_Clear);
+
// Helper: Create linear sampler (call in subclass constructor if needed)
void create_linear_sampler();
diff --git a/src/gpu/gpu.cc b/src/gpu/gpu.cc
index 0be5afe..f5f1515 100644
--- a/src/gpu/gpu.cc
+++ b/src/gpu/gpu.cc
@@ -5,6 +5,7 @@
#include "gpu.h"
#include "effects/shaders.h"
#include "generated/timeline.h"
+#include "gpu/sampler_cache.h"
#include "gpu/shader_composer.h"
#include "platform/platform.h"
@@ -124,26 +125,28 @@ WGPUTextureView gpu_create_texture_view_2d(WGPUTexture texture,
return wgpuTextureCreateView(texture, &view_desc);
}
+WGPUShaderModule gpu_create_shader_module(WGPUDevice device,
+ const char* wgsl_code,
+ const char* label) {
+ WGPUShaderSourceWGSL wgsl_src = {};
+ wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL;
+ wgsl_src.code = str_view(wgsl_code);
+ WGPUShaderModuleDescriptor desc = {};
+ desc.label = label_view(label);
+ desc.nextInChain = &wgsl_src.chain;
+ return wgpuDeviceCreateShaderModule(device, &desc);
+}
+
WGPUSampler gpu_create_linear_sampler(WGPUDevice device) {
- WGPUSamplerDescriptor desc = {};
- desc.addressModeU = WGPUAddressMode_ClampToEdge;
- desc.addressModeV = WGPUAddressMode_ClampToEdge;
- desc.addressModeW = WGPUAddressMode_ClampToEdge;
- desc.magFilter = WGPUFilterMode_Linear;
- desc.minFilter = WGPUFilterMode_Linear;
- desc.mipmapFilter = WGPUMipmapFilterMode_Nearest;
- desc.maxAnisotropy = 1;
- return wgpuDeviceCreateSampler(device, &desc);
+ WGPUSampler s = SamplerCache::Get().get_or_create(device, SamplerCache::clamp());
+ if (s) wgpuSamplerAddRef(s); // Caller owns a reference (for RAII wrappers)
+ return s;
}
WGPUSampler gpu_create_nearest_sampler(WGPUDevice device) {
- WGPUSamplerDescriptor desc = {};
- desc.addressModeU = WGPUAddressMode_ClampToEdge;
- desc.addressModeV = WGPUAddressMode_ClampToEdge;
- desc.magFilter = WGPUFilterMode_Nearest;
- desc.minFilter = WGPUFilterMode_Nearest;
- desc.maxAnisotropy = 1;
- return wgpuDeviceCreateSampler(device, &desc);
+ WGPUSampler s = SamplerCache::Get().get_or_create(device, SamplerCache::nearest());
+ if (s) wgpuSamplerAddRef(s); // Caller owns a reference (for RAII wrappers)
+ return s;
}
TextureWithView gpu_create_dummy_scene_texture(WGPUDevice device) {
@@ -167,15 +170,8 @@ RenderPass gpu_create_render_pass(WGPUDevice device, WGPUTextureFormat format,
// 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(composed_shader.c_str());
- WGPUShaderModuleDescriptor shader_desc = {};
- shader_desc.label = label_view("render_shader");
- shader_desc.nextInChain = &wgsl_src.chain;
WGPUShaderModule shader_module =
- wgpuDeviceCreateShaderModule(device, &shader_desc);
+ gpu_create_shader_module(device, composed_shader.c_str(), "render_shader");
// Create Bind Group Layout & Bind Group
std::vector<WGPUBindGroupLayoutEntry> bgl_entries;
@@ -257,6 +253,10 @@ RenderPass gpu_create_render_pass(WGPUDevice device, WGPUTextureFormat format,
pass.pipeline = wgpuDeviceCreateRenderPipeline(device, &pipeline_desc);
+ wgpuShaderModuleRelease(shader_module);
+ wgpuPipelineLayoutRelease(pipeline_layout);
+ wgpuBindGroupLayoutRelease(bind_group_layout);
+
return pass;
}
@@ -268,14 +268,8 @@ ComputePass gpu_create_compute_pass(WGPUDevice device, const char* shader_code,
// 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(composed_shader.c_str());
- WGPUShaderModuleDescriptor shader_desc = {};
- shader_desc.label = label_view("compute_shader");
- shader_desc.nextInChain = &wgsl_src.chain;
WGPUShaderModule shader_module =
- wgpuDeviceCreateShaderModule(device, &shader_desc);
+ gpu_create_shader_module(device, composed_shader.c_str(), "compute_shader");
std::vector<WGPUBindGroupLayoutEntry> bgl_entries;
std::vector<WGPUBindGroupEntry> bg_entries;
@@ -319,6 +313,11 @@ ComputePass gpu_create_compute_pass(WGPUDevice device, const char* shader_code,
pipeline_desc.compute.entryPoint = str_view("main");
pass.pipeline = wgpuDeviceCreateComputePipeline(device, &pipeline_desc);
+
+ wgpuShaderModuleRelease(shader_module);
+ wgpuPipelineLayoutRelease(pipeline_layout);
+ wgpuBindGroupLayoutRelease(bind_group_layout);
+
return pass;
}
@@ -439,16 +438,6 @@ void gpu_init(PlatformState* platform_state) {
}
-void gpu_draw(float audio_peak, float aspect_ratio, float time, float beat_time,
- float beat_phase) {
- // Rendering is driven externally via the sequence pipeline
- (void)audio_peak;
- (void)aspect_ratio;
- (void)time;
- (void)beat_time;
- (void)beat_phase;
-}
-
void gpu_resize(int width, int height) {
if (width <= 0 || height <= 0)
return;
diff --git a/src/gpu/gpu.h b/src/gpu/gpu.h
index bbced41..c5d0123 100644
--- a/src/gpu/gpu.h
+++ b/src/gpu/gpu.h
@@ -34,8 +34,6 @@ struct RenderPass {
};
void gpu_init(PlatformState* platform_state);
-void gpu_draw(float audio_peak, float aspect_ratio, float time, float beat_time,
- float beat_phase);
void gpu_resize(int width, int height);
void gpu_shutdown();
@@ -92,6 +90,11 @@ gpu_create_render_pass(WGPUDevice device,
const char* shader_code, ResourceBinding* bindings,
int num_bindings);
+// Create a shader module from WGSL source code
+WGPUShaderModule gpu_create_shader_module(WGPUDevice device,
+ const char* wgsl_code,
+ const char* label = "shader");
+
// Common sampler configurations
WGPUSampler gpu_create_linear_sampler(WGPUDevice device);
WGPUSampler gpu_create_nearest_sampler(WGPUDevice device);
diff --git a/src/gpu/gpu_headless.cc b/src/gpu/gpu_headless.cc
index 547ca18..c60da00 100644
--- a/src/gpu/gpu_headless.cc
+++ b/src/gpu/gpu_headless.cc
@@ -48,15 +48,6 @@ void gpu_init(PlatformState* platform_state) {
}
}
-void gpu_draw(float audio_peak, float aspect_ratio, float time, float beat_time,
- float beat_phase) {
- (void)audio_peak;
- (void)aspect_ratio;
- (void)time;
- (void)beat_time;
- (void)beat_phase;
-}
-
void gpu_resize(int width, int height) {
(void)width;
(void)height;
diff --git a/src/gpu/gpu_stub.cc b/src/gpu/gpu_stub.cc
index d889666..246c3a6 100644
--- a/src/gpu/gpu_stub.cc
+++ b/src/gpu/gpu_stub.cc
@@ -41,15 +41,6 @@ void gpu_init(PlatformState* platform_state) {
(void)platform_state;
}
-void gpu_draw(float audio_peak, float aspect_ratio, float time, float beat_time,
- float beat_phase) {
- (void)audio_peak;
- (void)aspect_ratio;
- (void)time;
- (void)beat_time;
- (void)beat_phase;
-}
-
void gpu_resize(int width, int height) {
(void)width;
(void)height;
diff --git a/src/gpu/pipeline_builder.cc b/src/gpu/pipeline_builder.cc
index 2d9ec07..acd2ae9 100644
--- a/src/gpu/pipeline_builder.cc
+++ b/src/gpu/pipeline_builder.cc
@@ -1,5 +1,6 @@
// WGPU render pipeline builder - implementation
#include "gpu/pipeline_builder.h"
+#include "gpu/gpu.h"
#include "util/fatal_error.h"
RenderPipelineBuilder::RenderPipelineBuilder(WGPUDevice device)
@@ -15,12 +16,8 @@ RenderPipelineBuilder& RenderPipelineBuilder::shader(const char* wgsl,
shader_text_ = compose ? ShaderComposer::Get().Compose({}, wgsl) : wgsl;
if (device_ == nullptr)
return *this;
- WGPUShaderSourceWGSL wgsl_src{};
- wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL;
- wgsl_src.code = str_view(shader_text_.c_str());
- WGPUShaderModuleDescriptor shader_desc{};
- shader_desc.nextInChain = &wgsl_src.chain;
- shader_module_ = wgpuDeviceCreateShaderModule(device_, &shader_desc);
+ shader_module_ =
+ gpu_create_shader_module(device_, shader_text_.c_str(), "pipeline_shader");
desc_.vertex.module = shader_module_;
desc_.vertex.entryPoint = str_view("vs_main");
return *this;
@@ -90,5 +87,7 @@ WGPURenderPipeline RenderPipelineBuilder::build() {
WGPURenderPipeline pipeline = wgpuDeviceCreateRenderPipeline(device_, &desc_);
wgpuPipelineLayoutRelease(layout);
+ if (shader_module_)
+ wgpuShaderModuleRelease(shader_module_);
return pipeline;
}
diff --git a/src/gpu/post_process_helper.cc b/src/gpu/post_process_helper.cc
index 79fda20..871a238 100644
--- a/src/gpu/post_process_helper.cc
+++ b/src/gpu/post_process_helper.cc
@@ -39,33 +39,13 @@ WGPURenderPipeline create_post_process_pipeline(WGPUDevice device,
return pipeline;
}
-// Helper to create a simple post-processing pipeline (no effect params)
-WGPURenderPipeline
-create_post_process_pipeline_simple(WGPUDevice device, WGPUTextureFormat format,
- const char* shader_code) {
- // Headless mode: skip pipeline creation (compiled out in STRIP_ALL)
- HEADLESS_RETURN_VAL_IF_NULL(device, nullptr);
-
- WGPUBindGroupLayout bgl =
- BindGroupLayoutBuilder()
- .sampler(PP_BINDING_SAMPLER, WGPUShaderStage_Fragment)
- .texture(PP_BINDING_TEXTURE, WGPUShaderStage_Fragment)
- .uniform(PP_BINDING_UNIFORMS,
- WGPUShaderStage_Vertex | WGPUShaderStage_Fragment)
- .build(device);
-
- const std::string composed_shader =
- ShaderComposer::Get().Compose({}, shader_code);
-
- WGPURenderPipeline pipeline = RenderPipelineBuilder(device)
- .shader(composed_shader.c_str())
- .bind_group_layout(bgl)
- .format(format)
- .build();
-
- wgpuBindGroupLayoutRelease(bgl);
- return pipeline;
-}
+// NOTE: create_post_process_pipeline_simple() was removed (zero callers).
+// If a 3-binding pipeline is needed in the future, add a `bool use_effect_params`
+// parameter to create_post_process_pipeline() instead.
+// Example:
+// WGPURenderPipeline p = create_post_process_pipeline(device, format, code);
+// // Then pass {nullptr, 0} as effect_params to pp_update_bind_group —
+// // the dummy buffer fallback handles it automatically.
void gpu_upload_mat4(WGPUQueue queue, WGPUBuffer buffer, size_t offset,
const mat4& m) {
diff --git a/src/gpu/post_process_helper.h b/src/gpu/post_process_helper.h
index 178a4d5..8f2bd21 100644
--- a/src/gpu/post_process_helper.h
+++ b/src/gpu/post_process_helper.h
@@ -17,11 +17,6 @@ WGPURenderPipeline create_post_process_pipeline(WGPUDevice device,
WGPUTextureFormat format,
const char* shader_code);
-// No effect params, 3 bindings only
-WGPURenderPipeline create_post_process_pipeline_simple(WGPUDevice device,
- WGPUTextureFormat format,
- const char* shader_code);
-
void pp_update_bind_group(WGPUDevice device, WGPURenderPipeline pipeline,
WGPUBindGroup* bind_group, WGPUTextureView input_view,
GpuBuffer uniforms, GpuBuffer effect_params);
diff --git a/src/gpu/sampler_cache.h b/src/gpu/sampler_cache.h
index 0a2afa0..562db29 100644
--- a/src/gpu/sampler_cache.h
+++ b/src/gpu/sampler_cache.h
@@ -51,4 +51,8 @@ class SamplerCache {
return {WGPUAddressMode_ClampToEdge, WGPUAddressMode_ClampToEdge,
WGPUFilterMode_Linear, WGPUFilterMode_Linear, 1};
}
+ static SamplerSpec nearest() {
+ return {WGPUAddressMode_ClampToEdge, WGPUAddressMode_ClampToEdge,
+ WGPUFilterMode_Nearest, WGPUFilterMode_Nearest, 1};
+ }
};
diff --git a/src/gpu/wgsl_effect.cc b/src/gpu/wgsl_effect.cc
index 0ce4730..1cb0ecb 100644
--- a/src/gpu/wgsl_effect.cc
+++ b/src/gpu/wgsl_effect.cc
@@ -36,19 +36,6 @@ void WgslEffect::render(WGPUCommandEncoder encoder,
input_view, uniforms_buffer_.get(),
params_buffer_.get());
- WGPURenderPassColorAttachment color_attachment = {};
- gpu_init_color_attachment(color_attachment, output_view);
- color_attachment.loadOp = load_op_;
-
- WGPURenderPassDescriptor pass_desc = {};
- pass_desc.colorAttachmentCount = 1;
- pass_desc.colorAttachments = &color_attachment;
-
- WGPURenderPassEncoder pass =
- wgpuCommandEncoderBeginRenderPass(encoder, &pass_desc);
- wgpuRenderPassEncoderSetPipeline(pass, pipeline_.get());
- wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_.get(), 0, nullptr);
- wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0);
- wgpuRenderPassEncoderEnd(pass);
- wgpuRenderPassEncoderRelease(pass);
+ run_fullscreen_pass(encoder, pipeline_.get(), bind_group_.get(), output_view,
+ load_op_);
}
diff --git a/src/shaders/compute/particle_common.wgsl b/src/shaders/compute/particle_common.wgsl
new file mode 100644
index 0000000..0c36be8
--- /dev/null
+++ b/src/shaders/compute/particle_common.wgsl
@@ -0,0 +1,7 @@
+// Shared particle struct for compute and render shaders
+struct Particle {
+ pos: vec4f,
+ vel: vec4f,
+ rot: vec4f,
+ color: vec4f,
+};
diff --git a/src/util/asset_manager.cc b/src/util/asset_manager.cc
index 264ddda..82c07be 100644
--- a/src/util/asset_manager.cc
+++ b/src/util/asset_manager.cc
@@ -245,29 +245,25 @@ void DropAsset(AssetId asset_id, const uint8_t* asset) {
return; // Invalid asset_id
}
- // Check if the asset is in cache and is procedural, and if the pointer
- // matches. This prevents accidentally freeing static data or freeing twice.
- if (g_asset_cache[index].data == asset &&
- (g_asset_cache[index].type == AssetType::PROC ||
- g_asset_cache[index].type == AssetType::PROC_GPU)) {
- delete[] g_asset_cache[index].data;
- g_asset_cache[index] = {}; // Zero out the struct to force re-generation
- }
- // Heap-allocated decompressed buffer (compression != NONE): cache owns it.
- if (g_asset_cache[index].data == asset &&
- g_asset_cache[index].compression != AssetCompression::NONE) {
- delete[] g_asset_cache[index].data;
- g_asset_cache[index] = {};
+ // Check if the asset is in cache and the pointer matches.
+ // This prevents accidentally freeing static data or freeing twice.
+ if (g_asset_cache[index].data != asset) {
+ return; // Pointer mismatch — not our allocation
}
+
+ // Heap-owned: procedural, compressed, or disk-loaded (debug)
+ if (g_asset_cache[index].type == AssetType::PROC ||
+ g_asset_cache[index].type == AssetType::PROC_GPU ||
+ g_asset_cache[index].compression != AssetCompression::NONE
#if !defined(STRIP_ALL)
- if (g_asset_cache[index].data == asset &&
- (g_asset_cache[index].type == AssetType::SPEC ||
- g_asset_cache[index].type == AssetType::MP3 ||
- g_asset_cache[index].type == AssetType::WGSL)) {
+ || g_asset_cache[index].type == AssetType::SPEC ||
+ g_asset_cache[index].type == AssetType::MP3 ||
+ g_asset_cache[index].type == AssetType::WGSL
+#endif
+ ) {
delete[] g_asset_cache[index].data;
g_asset_cache[index] = {};
}
-#endif
// For static assets, no dynamic memory to free.
}
@@ -283,7 +279,9 @@ bool ReloadAssetsFromFile(const char* config_path) {
const AssetRecord& e = g_asset_cache[i];
if (e.data &&
(e.type == AssetType::PROC || e.type == AssetType::PROC_GPU ||
- e.compression != AssetCompression::NONE)) {
+ e.compression != AssetCompression::NONE ||
+ e.type == AssetType::SPEC || e.type == AssetType::MP3 ||
+ e.type == AssetType::WGSL)) {
delete[] e.data;
}
g_asset_cache[i] = {};