diff --git a/docs/AUDIO.md b/docs/AUDIO.md index 647ea66..a14ecaf 100644 --- a/docs/AUDIO.md +++ b/docs/AUDIO.md @@ -28,7 +28,7 @@ flowchart TD SpatialProcessor[SpatialAudioProcessor] end - subgraph SdlStream["SDL3 Audio Stream (main thread)"] + subgraph SdlStream["SDL3 Audio Stream (audio thread)"] AudioDevice[DAC Output / Speakers] Mixer[Stereo Interleaved Mixer] end @@ -47,7 +47,7 @@ flowchart TD ### Thread Safety & Zero-Allocation Streaming The real-time audio thread must never block or allocate heap memory (`malloc`/`new`). - **`AudioRingBuffer`**: A lock-free single-producer single-consumer ring buffer transfers sound trigger requests from the game and physics threads to the audio thread. -- **Main-Thread Streaming**: PCM is generated synchronously in `AudioEngine::listen` on the main thread and written to the SDL3 audio stream; friction sources are plain per-body structs with no cross-thread state (only the debug/override level uses an atomic float). +- **Decoupled Audio Thread Streaming**: PCM is generated asynchronously in `AudioEngine::renderAudio` via SDL3's `SDL_AudioStreamCallback` on a dedicated audio thread. Main-thread hitches (such as heavy shader compilation or window resizing) do not interrupt audio playback. The main thread's `AudioEngine::listen` only performs microsecond-scale state updates under a fast mutex. - **Mutex-Protected Song State**: Fast `std::mutex` guards protect song reference swaps and parameter updates between frames. --- diff --git a/include/weird-audio/AudioEngine.h b/include/weird-audio/AudioEngine.h index ab8e731..ca736bb 100644 --- a/include/weird-audio/AudioEngine.h +++ b/include/weird-audio/AudioEngine.h @@ -1,6 +1,8 @@ #pragma once +#include #include +#include #include #include @@ -18,6 +20,11 @@ namespace WeirdEngine float currentVolume = 0.0f; // For pulsing size float currentFriction = 0.0f; // For static/jitter std::vector waveform; // For oscilloscope effects (snapshot of last 256 samples) + + AudioData() + : waveform(256, 0.0f) + { + } }; class AudioEngine @@ -38,23 +45,33 @@ namespace WeirdEngine void setAudioStream(SDL_AudioStream* stream) { + std::lock_guard lock(m_audioMutex); m_audioStream = stream; } uint32_t getSampleRate() const; uint8_t getChannels() const; - // Per-frame scene audio update and PCM generation + // SDL3 Audio stream callback invoked on the dedicated audio thread + static void SDLCALL audioStreamCallback(void* userdata, SDL_AudioStream* stream, int additional_amount, + int total_amount); + + // Generates PCM audio data directly on the audio thread + void renderAudio(SDL_AudioStream* stream, int additional_amount); + + // Per-frame scene audio synchronization from main thread void listen(Scene& scene); // Volume & Mute controls void mute() { + std::lock_guard lock(m_audioMutex); m_settings.mute = true; } void unmute() { + std::lock_guard lock(m_audioMutex); m_settings.mute = false; } @@ -65,6 +82,7 @@ namespace WeirdEngine void setMasterVolume(float vol) { + std::lock_guard lock(m_audioMutex); m_settings.masterVolume = vol; } @@ -97,6 +115,7 @@ namespace WeirdEngine // Spatial Audio Setting void setSpatialAudioEnabled(bool enabled) { + std::lock_guard lock(m_audioMutex); m_settings.enableSpatialAudio = enabled; m_physicsEngine.setSpatialAudioEnabled(enabled); } @@ -107,13 +126,15 @@ namespace WeirdEngine } // Visualizer data - const AudioData& getAudioData() const + AudioData getAudioData() const { + std::lock_guard lock(m_audioMutex); return m_visualSnapshot; } float getAudioVolume() const { + std::lock_guard lock(m_audioMutex); return m_visualSnapshot.currentVolume; } @@ -124,8 +145,11 @@ namespace WeirdEngine PhysicsAudioEngine m_physicsEngine; SdfMusicEngine m_musicEngine; + mutable std::mutex m_audioMutex; SDL_AudioStream* m_audioStream = nullptr; AudioData m_visualSnapshot; + std::vector m_mixBuffer; + double m_audioTime = 0.0; // Master bus DC-blocking filter state (per channel) float m_dcBlockerX[2] = {0.0f, 0.0f}; diff --git a/include/weird-audio/SdfMusicEngine.h b/include/weird-audio/SdfMusicEngine.h index e74d705..5272a3a 100644 --- a/include/weird-audio/SdfMusicEngine.h +++ b/include/weird-audio/SdfMusicEngine.h @@ -155,13 +155,15 @@ namespace WeirdEngine // Re-sample the shape at the 8 compass points (call after modifying shape parameters in real time) void resampleShape(); - const ShapeMusicalParams& getShapeParameters() const + ShapeMusicalParams getShapeParameters() const { + std::lock_guard lock(m_songMutex); return m_shapeParams; } - const InstrumentRack& getInstrumentRack() const + InstrumentRack getInstrumentRack() const { + std::lock_guard lock(m_songMutex); return m_rack; } @@ -174,61 +176,74 @@ namespace WeirdEngine void setTrackToggles(const MusicTrackToggles& toggles) { + std::lock_guard lock(m_songMutex); m_tracks = toggles; } - const MusicTrackToggles& getTrackToggles() const + MusicTrackToggles getTrackToggles() const { + std::lock_guard lock(m_songMutex); return m_tracks; } TrackPlayState getTrackPlayState(MusicTrack track) const; float getSurgeLevel() const { + std::lock_guard lock(m_songMutex); return m_surgeLevel; } float getDuckingLevel() const { + std::lock_guard lock(m_songMutex); return m_ducking; } // Motion & Domain Fill Inspection float getMotionLevel() const { + std::lock_guard lock(m_songMutex); return m_motionLevel; } float getMotionNorm() const { + std::lock_guard lock(m_songMutex); return m_motionNorm; } float getFillRatio() const { + std::lock_guard lock(m_songMutex); return m_fillRatio; } float getTempoFromMotion() const { + std::lock_guard lock(m_songMutex); return m_tempoFromMotion; } float getVolumeFromFill() const { + std::lock_guard lock(m_songMutex); return m_volumeFromFill; } // Surface Complexity Inspection (scale-normalized curvature spread of the SDF surface) float getComplexityLevel() const { + std::lock_guard lock(m_songMutex); return m_complexityLevel; } float getComplexityNorm() const { + std::lock_guard lock(m_songMutex); return m_complexityNorm; } float getComplexitySaturation() const { + std::lock_guard lock(m_songMutex); return m_complexitySaturation; } // Higher values make the normalized stat cooler (more headroom for complex shapes) void setComplexitySaturation(float saturation) { + std::lock_guard lock(m_songMutex); m_complexitySaturation = std::max(0.1f, saturation); } @@ -236,6 +251,7 @@ namespace WeirdEngine // articulation independently of motion (tempo). 0 = calm, 1 = tense. float getTension() const { + std::lock_guard lock(m_songMutex); return m_tensionFromComplexity; } @@ -246,15 +262,18 @@ namespace WeirdEngine // Current playhead position in beats float getPlayheadBeat() const { + std::lock_guard lock(m_songMutex); return m_currentBeat; } bool isPlaying() const { + std::lock_guard lock(m_songMutex); return m_playing; } void setPlaying(bool playing) { + std::lock_guard lock(m_songMutex); m_playing = playing; } @@ -266,7 +285,7 @@ namespace WeirdEngine std::shared_ptr m_currentSong; std::shared_ptr m_queuedSong; - std::mutex m_songMutex; + mutable std::recursive_mutex m_songMutex; // Sequencer time tracking float m_currentBeat = 0.0f; diff --git a/include/weird-physics/Simulation2D.h b/include/weird-physics/Simulation2D.h index cfb3edc..b2a4256 100644 --- a/include/weird-physics/Simulation2D.h +++ b/include/weird-physics/Simulation2D.h @@ -3,7 +3,10 @@ #include #include #include +#include #include +#include +#include #include #include #include @@ -38,7 +41,8 @@ namespace WeirdEngine UnFix, SetMass, ActivatePending, - AddImpulse + AddImpulse, + Action }; struct PhysicsCommand @@ -46,7 +50,8 @@ namespace WeirdEngine PhysicsCommandType type; SimulationID id; vec2 vectorData; - float floatData; + float floatData = 0.0f; + std::function action; }; enum class CollisionState @@ -58,9 +63,8 @@ namespace WeirdEngine struct PhysicsCollisionEvent { - // CollisionState state; - SimulationID bodyA; - SimulationID bodyB; + SimulationID bodyA = 0; + SimulationID bodyB = 0; vec2 position = vec2(0.0f); vec2 normal = vec2(0.0f); vec2 relativeVelocity = vec2(0.0f); @@ -120,6 +124,9 @@ namespace WeirdEngine } // void setSize(unsigned int size); + // Main-thread lifecycle: creation reserves an ID and queues initialization. + // Activation publishes the completed batch. Removal waits for a physics + // boundary so the caller can immediately apply the last-slot ID remapping. SimulationID generateSimulationID(); void activatePendingBodies(); void removeObject(SimulationID id); @@ -245,7 +252,9 @@ namespace WeirdEngine // destroyed. Do not retain the pointer after the call; read or modify // it through getUserData()/getUserDataAs() instead. setUserData() is // main-thread only; getUserData()/getUserDataAs()/forEachUserData() - // are safe from the physics callbacks without locks. + // may be called from physics callbacks. forEachUserData holds the data + // lock throughout its callback. Raw pointers must not be retained across + // replacement/removal; use forEachUserData for concurrent main-thread edits. void setUserData(SimulationID id, std::unique_ptr data); BodyUserData* getUserData(SimulationID id); @@ -260,24 +269,15 @@ namespace WeirdEngine } // Calls fn(SimulationID, BodyUserData&) for every active body that has - // user data attached. Lock-free from physics callbacks (the step - // already holds the structural mutex); serialized on the main thread. + // user data attached. Protected by m_userDataMutex. Main-thread callbacks + // must not call synchronous physics operations (removal, constraint queries + // or edits returning bool): those wait for the worker, which needs this lock. template void forEachUserData(Fn&& fn) { - if (isPhysicsExecutionContext()) - { - for (SimulationID id = 0; id < m_size; ++id) - { - if (m_userData[id]) - fn(id, *m_userData[id]); - } - return; - } - - std::lock_guard lock(m_structuralMutex); + std::lock_guard lock(m_userDataMutex); for (SimulationID id = 0; id < m_size; ++id) { - if (m_userData[id]) + if (m_bodyActive[id] && m_userData[id]) fn(id, *m_userData[id]); } } @@ -331,17 +331,23 @@ namespace WeirdEngine }; // Serialization support: read constraint data - const std::vector& getDistanceConstraints() const + std::vector getDistanceConstraints() const { - return m_distanceConstraints; + std::vector result; + const_cast(this)->executeSynchronous([&] { result = m_distanceConstraints; }); + return result; } - const std::vector& getGravitationalConstraints() const + std::vector getGravitationalConstraints() const { - return m_gravitationalConstraints; + std::vector result; + const_cast(this)->executeSynchronous([&] { result = m_gravitationalConstraints; }); + return result; } - const std::vector& getFixedObjects() const + std::vector getFixedObjects() const { - return m_fixedObjects; + std::vector result; + const_cast(this)->executeSynchronous([&] { result = m_fixedObjects; }); + return result; } // Serialization support: load raw constraint (bypasses stiffness conversion) @@ -349,11 +355,16 @@ namespace WeirdEngine { if (a == b) return; - m_distanceConstraints.emplace_back(a, b, distance, k); + enqueueAction([=, this] { m_distanceConstraints.emplace_back(a, b, distance, k); }); } private: void process(); + void processCommands(); + void removeBodyAtBoundary(SimulationID id); + void enqueueCommand(PhysicsCommand command); + void enqueueAction(std::function action); + void executeSynchronous(std::function action); void checkCollisions(double& broadPhaseMs, double& narrowPhaseMs, double& shapeEvaluationMs); void solveCollisionsPositionBased(); void applyForces(); @@ -415,14 +426,16 @@ namespace WeirdEngine uint16_t distanceFieldId; CombinationType combinationId; uint16_t groupId; - float parameters[11]; + float parameters[12] = {0.0f}; + float smoothRadius = 1.0f; DistanceFieldObject2D(Entity owner, uint16_t id, CombinationType combinationId, uint16_t groupId, - float* params) + float* params, float smoothRadius = 1.0f) : distanceFieldId(id) , combinationId(combinationId) , groupId(groupId) , owner(owner) + , smoothRadius(smoothRadius) { std::copy(params, params + 8, parameters); // Copy params into parameters } @@ -459,8 +472,11 @@ namespace WeirdEngine vec2* m_continuousForcesWrite; size_t m_maxSize; + // Physics owns initialized slots; the game thread reserves IDs. size_t m_size; - size_t m_allocated; + std::atomic m_allocated; + std::atomic m_activeSize{0}; + std::vector m_bodyActive; float* m_mass; float* m_invMass; @@ -481,6 +497,7 @@ namespace WeirdEngine // Shapes std::unordered_map m_entityToObjectsIdx; std::shared_ptr>> m_sdfs; + std::shared_ptr>> m_sdfsSnapshot; // sim-thread-only copy std::vector m_objects; std::vector m_collisionMap; @@ -519,8 +536,10 @@ namespace WeirdEngine std::mutex m_fixMutex; std::mutex m_externalForcesMutex; std::mutex m_structuralMutex; + mutable std::recursive_mutex m_userDataMutex; std::mutex m_readMutex; std::mutex m_commandMutex; + std::condition_variable m_commandReady; std::vector m_pendingCommands; std::vector m_internalCommands; @@ -564,4 +583,4 @@ namespace WeirdEngine } }; -} // namespace WeirdEngine \ No newline at end of file +} // namespace WeirdEngine diff --git a/include/weird-renderer/core/SDLInitializer.h b/include/weird-renderer/core/SDLInitializer.h index c569aa3..70d750a 100644 --- a/include/weird-renderer/core/SDLInitializer.h +++ b/include/weird-renderer/core/SDLInitializer.h @@ -18,6 +18,7 @@ namespace WeirdEngine SDL_Window* m_window; SDL_GLContext m_glContext; SDL_AudioStream* m_audioStream; + WeirdAudio::AudioEngine& m_audioEngine; // True when rendering through the direct framebuffer EGL backend // (WEIRD_VIDEO_BACKEND=fbdev) instead of SDL's GL context. diff --git a/src/weird-audio/AudioEngine.cpp b/src/weird-audio/AudioEngine.cpp index c1cad84..f17088c 100644 --- a/src/weird-audio/AudioEngine.cpp +++ b/src/weird-audio/AudioEngine.cpp @@ -18,6 +18,7 @@ namespace WeirdEngine bool AudioEngine::init(const WeirdAudio::AudioSettings& settings) { + std::lock_guard lock(m_audioMutex); m_settings = settings; m_physicsEngine.init(SAMPLE_RATE, CHANNELS); @@ -27,6 +28,10 @@ namespace WeirdEngine m_musicEngine.init(SAMPLE_RATE, CHANNELS); m_musicEngine.setVolume(settings.musicVolume); + m_mixBuffer.resize(8192 * CHANNELS, 0.0f); + m_visualSnapshot.waveform.assign(256, 0.0f); + m_audioTime = 0.0; + return true; } @@ -43,7 +48,14 @@ namespace WeirdEngine void AudioEngine::listen(Scene& scene) { if (m_settings.mute || !m_audioStream) + { + auto& audioQueue = scene.getAudioQueue(); + SimpleAudioRequest req; + while (audioQueue.pop(req)) + { + } return; + } // 1. Camera & Listener orientation auto& camera = scene.getCamera(); @@ -52,6 +64,8 @@ namespace WeirdEngine : vec3(0.0f, 0.0f, -1.0f); vec3 listenerUp = glm::length2(camera.up) > 0.001f ? glm::normalize(camera.up) : vec3(0.0f, 1.0f, 0.0f); + std::lock_guard lock(m_audioMutex); + // 2. Physics continuous friction (one voice per selected source) if (scene.isFrictionSoundOverridden()) { @@ -77,102 +91,133 @@ namespace WeirdEngine m_physicsEngine.playSound(req, listenerPos, listenerForward, listenerUp); } + } - // 4. Update procedural music timing & dynamic feedback - m_musicEngine.update(scene.getLastDelta(), scene.getTime()); + void SDLCALL AudioEngine::audioStreamCallback(void* userdata, SDL_AudioStream* stream, int additional_amount, + int /*total_amount*/) + { + auto* self = static_cast(userdata); + if (!self) + return; - // 6. Generate and stream PCM audio to SDL - constexpr int TARGET_BUFFER_BYTES = (SAMPLE_RATE * CHANNELS * sizeof(float) * 14) / 100; - int queuedBytes = SDL_GetAudioStreamQueued(m_audioStream); + self->renderAudio(stream, additional_amount); + } - if (queuedBytes < TARGET_BUFFER_BYTES) + void AudioEngine::renderAudio(SDL_AudioStream* stream, int additional_amount) + { + if (!stream) + return; + + constexpr int bytesPerFrame = CHANNELS * sizeof(float); + int bytesNeeded = additional_amount; + if (bytesNeeded <= 0) { - int bytesToGenerate = TARGET_BUFFER_BYTES - queuedBytes; - uint32_t framesToWrite = static_cast(bytesToGenerate / (CHANNELS * sizeof(float))); - if (framesToWrite > 8192) - { - framesToWrite = 8192; - } + return; + } - if (framesToWrite > 0) - { - std::vector mix(framesToWrite * CHANNELS, 0.0f); + constexpr int MAX_CHUNK_FRAMES = 4096; + int maxBytes = MAX_CHUNK_FRAMES * bytesPerFrame; + if (bytesNeeded > maxBytes) + { + bytesNeeded = maxBytes; + } - // Layer 1: Physics realistic sounds & spatial audio - if (m_settings.enablePhysicsAudio) - { - m_physicsEngine.render(mix.data(), framesToWrite, CHANNELS); - } + uint32_t framesToWrite = static_cast((bytesNeeded + bytesPerFrame - 1) / bytesPerFrame); + if (framesToWrite == 0) + return; - // Layer 2: SDF Procedural Music - if (m_settings.enableMusic) - { - m_musicEngine.render(mix.data(), framesToWrite, CHANNELS); - } + std::lock_guard lock(m_audioMutex); - // Master bus processing: DC Blocker (1-pole highpass at ~15 Hz, R = 0.995) - // Eliminates DC offset so waveforms center symmetrically at 0.0 - constexpr float DC_BLOCK_R = 0.995f; - for (size_t frame = 0; frame < framesToWrite; ++frame) - { - for (size_t ch = 0; ch < CHANNELS; ++ch) - { - size_t idx = frame * CHANNELS + ch; - float in = mix[idx]; - float out = in - m_dcBlockerX[ch] + DC_BLOCK_R * m_dcBlockerY[ch]; - m_dcBlockerX[ch] = in; - // Denormal protection - if (std::abs(out) < 1e-15f) - out = 0.0f; - m_dcBlockerY[ch] = out; - mix[idx] = out; - } - } + // Advance procedural music sample-accurately based on rendered audio frames + double chunkDt = static_cast(framesToWrite) / static_cast(SAMPLE_RATE); + m_audioTime += chunkDt; + m_musicEngine.update(chunkDt, m_audioTime); - // Master bus processing: Volume & Soft-Clipping (tanh) - float masterVol = m_settings.masterVolume; - for (size_t i = 0; i < mix.size(); ++i) - { - mix[i] = std::tanh(mix[i] * masterVol); - } + size_t totalSamples = framesToWrite * CHANNELS; + if (m_mixBuffer.size() < totalSamples) + { + m_mixBuffer.resize(totalSamples, 0.0f); + } + std::fill(m_mixBuffer.begin(), m_mixBuffer.begin() + totalSamples, 0.0f); - // Visual snapshot (RMS volume + waveform capture) - float sumSquares = 0.0f; - for (size_t i = 0; i < mix.size(); i += 4) - { - sumSquares += mix[i] * mix[i]; - } - float rms = std::sqrt(sumSquares / (mix.size() / 4)); - m_visualSnapshot.currentVolume = rms; - m_visualSnapshot.currentFriction = m_physicsEngine.getFrictionLevel(); + if (!m_settings.mute) + { + // Layer 1: Physics realistic sounds & spatial audio + if (m_settings.enablePhysicsAudio) + { + m_physicsEngine.render(m_mixBuffer.data(), framesToWrite, CHANNELS); + } - // Extract mono waveform (continuous tail of the mix buffer) - constexpr size_t WAVEFORM_SAMPLES = 256; - m_visualSnapshot.waveform.resize(WAVEFORM_SAMPLES, 0.0f); + // Layer 2: SDF Procedural Music + if (m_settings.enableMusic) + { + m_musicEngine.render(m_mixBuffer.data(), framesToWrite, CHANNELS); + } - if (framesToWrite >= WAVEFORM_SAMPLES) - { - size_t startFrame = framesToWrite - WAVEFORM_SAMPLES; - for (size_t i = 0; i < WAVEFORM_SAMPLES; ++i) - { - size_t frame = startFrame + i; - m_visualSnapshot.waveform[i] = (mix[frame * CHANNELS] + mix[frame * CHANNELS + 1]) * 0.5f; - } - } - else if (framesToWrite > 0) + // Master bus processing: DC Blocker (1-pole highpass at ~15 Hz, R = 0.995) + constexpr float DC_BLOCK_R = 0.995f; + for (size_t frame = 0; frame < framesToWrite; ++frame) + { + for (size_t ch = 0; ch < CHANNELS; ++ch) { - for (size_t i = 0; i < WAVEFORM_SAMPLES; ++i) - { - size_t frame = (i * framesToWrite) / WAVEFORM_SAMPLES; - m_visualSnapshot.waveform[i] = (mix[frame * CHANNELS] + mix[frame * CHANNELS + 1]) * 0.5f; - } + size_t idx = frame * CHANNELS + ch; + float in = m_mixBuffer[idx]; + float out = in - m_dcBlockerX[ch] + DC_BLOCK_R * m_dcBlockerY[ch]; + m_dcBlockerX[ch] = in; + if (std::abs(out) < 1e-15f) + out = 0.0f; + m_dcBlockerY[ch] = out; + m_mixBuffer[idx] = out; } + } - // Submit to SDL stream - SDL_PutAudioStreamData(m_audioStream, mix.data(), - static_cast(framesToWrite * CHANNELS * sizeof(float))); + // Master bus processing: Volume & Soft-Clipping (tanh) + float masterVol = m_settings.masterVolume; + for (size_t i = 0; i < totalSamples; ++i) + { + m_mixBuffer[i] = std::tanh(m_mixBuffer[i] * masterVol); } } + + // Visual snapshot (RMS volume + waveform capture) + float sumSquares = 0.0f; + for (size_t i = 0; i < totalSamples; i += 4) + { + sumSquares += m_mixBuffer[i] * m_mixBuffer[i]; + } + float rms = std::sqrt(sumSquares / (totalSamples / 4 + 1)); + m_visualSnapshot.currentVolume = rms; + m_visualSnapshot.currentFriction = m_physicsEngine.getFrictionLevel(); + + // Extract mono waveform + constexpr size_t WAVEFORM_SAMPLES = 256; + if (m_visualSnapshot.waveform.size() != WAVEFORM_SAMPLES) + { + m_visualSnapshot.waveform.resize(WAVEFORM_SAMPLES, 0.0f); + } + + if (framesToWrite >= WAVEFORM_SAMPLES) + { + size_t startFrame = framesToWrite - WAVEFORM_SAMPLES; + for (size_t i = 0; i < WAVEFORM_SAMPLES; ++i) + { + size_t frame = startFrame + i; + m_visualSnapshot.waveform[i] = + (m_mixBuffer[frame * CHANNELS] + m_mixBuffer[frame * CHANNELS + 1]) * 0.5f; + } + } + else if (framesToWrite > 0) + { + for (size_t i = 0; i < WAVEFORM_SAMPLES; ++i) + { + size_t frame = (i * framesToWrite) / WAVEFORM_SAMPLES; + m_visualSnapshot.waveform[i] = + (m_mixBuffer[frame * CHANNELS] + m_mixBuffer[frame * CHANNELS + 1]) * 0.5f; + } + } + + // Submit to SDL stream + SDL_PutAudioStreamData(stream, m_mixBuffer.data(), static_cast(totalSamples * sizeof(float))); } } // namespace WeirdAudio diff --git a/src/weird-audio/SdfMusicEngine.cpp b/src/weird-audio/SdfMusicEngine.cpp index e970622..54f3b59 100644 --- a/src/weird-audio/SdfMusicEngine.cpp +++ b/src/weird-audio/SdfMusicEngine.cpp @@ -530,14 +530,14 @@ namespace WeirdEngine void SdfMusicEngine::resampleShape() { - std::lock_guard lock(m_songMutex); + std::lock_guard lock(m_songMutex); sampleShapeParameters(); initDomainSamples(); } void SdfMusicEngine::setSong(std::shared_ptr song, bool beatSynced) { - std::lock_guard lock(m_songMutex); + std::lock_guard lock(m_songMutex); if (!m_currentSong || !beatSynced) { m_currentSong = std::move(song); @@ -552,7 +552,13 @@ namespace WeirdEngine if (isSongEmpty(m_currentSong)) { - m_activeVoices.clear(); + for (auto& voice : m_activeVoices) + { + if (!voice.finished && voice.fadeRate <= 0.0f) + { + voice.fadeRate = (std::max)(voice.fadeGain, 0.01f) / 0.005f; + } + } m_rack = InstrumentRack{}; } else @@ -568,17 +574,19 @@ namespace WeirdEngine std::shared_ptr SdfMusicEngine::getCurrentSong() const { + std::lock_guard lock(m_songMutex); return m_currentSong; } void SdfMusicEngine::queueSong(std::shared_ptr nextSong) { - std::lock_guard lock(m_songMutex); + std::lock_guard lock(m_songMutex); m_queuedSong = std::move(nextSong); } void SdfMusicEngine::setTrackEnabled(MusicTrack track, bool enabled) { + std::lock_guard lock(m_songMutex); switch (track) { case MusicTrack::Lead: @@ -598,6 +606,7 @@ namespace WeirdEngine bool SdfMusicEngine::isTrackEnabled(MusicTrack track) const { + std::lock_guard lock(m_songMutex); switch (track) { case MusicTrack::Lead: @@ -614,6 +623,7 @@ namespace WeirdEngine TrackPlayState SdfMusicEngine::getTrackPlayState(MusicTrack track) const { + std::lock_guard lock(m_songMutex); size_t idx = static_cast(track); if (idx < m_trackPlayStates.size()) { @@ -624,6 +634,7 @@ namespace WeirdEngine void SdfMusicEngine::triggerPositiveFeedback(float intensity) { + std::lock_guard lock(m_songMutex); float clamped = std::clamp(intensity, 0.1f, 2.0f); if (m_currentSong) { @@ -644,6 +655,7 @@ namespace WeirdEngine void SdfMusicEngine::triggerNegativeFeedback(float intensity) { + std::lock_guard lock(m_songMutex); float clamped = std::clamp(intensity, 0.1f, 2.0f); if (m_currentSong) { @@ -672,6 +684,7 @@ namespace WeirdEngine void SdfMusicEngine::triggerDeath() { + std::lock_guard lock(m_songMutex); m_isDead = true; m_deathStage = 1; // Stage 1: Lead stops queuing new notes; active notes fade naturally m_deathTimer = 0.0f; @@ -680,6 +693,7 @@ namespace WeirdEngine bool SdfMusicEngine::isTrackDead(MusicTrack track) const { + std::lock_guard lock(m_songMutex); if (!m_isDead) return false; @@ -700,6 +714,7 @@ namespace WeirdEngine void SdfMusicEngine::duck(float amount) { + std::lock_guard lock(m_songMutex); m_duckTarget = (std::min)(1.0f, m_duckTarget + amount); // Peak hold: sustained danger hold (up to 8.0s) m_duckTimer = (std::min)(8.0f, (std::max)(m_duckTimer, 2.5f) + amount * 3.5f); @@ -722,6 +737,7 @@ namespace WeirdEngine void SdfMusicEngine::surge(float amount) { + std::lock_guard lock(m_songMutex); m_duckTarget = (std::max)(0.0f, m_duckTarget - amount * 0.8f); m_duckTimer = 0.0f; @@ -738,6 +754,7 @@ namespace WeirdEngine void SdfMusicEngine::resetDynamicEffects() { + std::lock_guard lock(m_songMutex); m_ducking = 0.0f; m_duckTarget = 0.0f; m_duckTimer = 0.0f; @@ -762,6 +779,7 @@ namespace WeirdEngine float SdfMusicEngine::quantizeToSongScale(float rawFreq) const { + std::lock_guard lock(m_songMutex); if (!m_currentSong || rawFreq <= 20.0f) return rawFreq > 20.0f ? rawFreq : 440.0f; @@ -785,6 +803,7 @@ namespace WeirdEngine void SdfMusicEngine::update(double deltaTime, double sceneTime) { + std::lock_guard lock(m_songMutex); if (!m_playing || !m_currentSong) return; @@ -913,7 +932,7 @@ namespace WeirdEngine // Check for beat-synced song transition on downbeats (every 4 steps = 1 beat) if (isQuarterBeat && m_queuedSong) { - std::lock_guard lock(m_songMutex); + std::lock_guard lock(m_songMutex); if (m_queuedSong) { m_currentSong = std::move(m_queuedSong); @@ -925,7 +944,13 @@ namespace WeirdEngine if (isSongEmpty(m_currentSong)) { - m_activeVoices.clear(); + for (auto& voice : m_activeVoices) + { + if (!voice.finished && voice.fadeRate <= 0.0f) + { + voice.fadeRate = (std::max)(voice.fadeGain, 0.01f) / 0.005f; + } + } m_rack = InstrumentRack{}; } else @@ -1585,6 +1610,7 @@ namespace WeirdEngine void SdfMusicEngine::playNote(float freq, float amp, float durationSec, int instrument, float pan, float filterCutoff) { + std::lock_guard lock(m_songMutex); // Lead is strictly killed during ducking if (instrument == 0 && (m_duckTimer > 0.0f || m_ducking > 0.01f)) return; @@ -1699,6 +1725,7 @@ namespace WeirdEngine void SdfMusicEngine::render(float* buffer, uint32_t frameCount, uint32_t channels) { + std::lock_guard lock(m_songMutex); if (!m_playing || frameCount == 0 || channels != 2) return; diff --git a/src/weird-physics/Simulation2D.cpp b/src/weird-physics/Simulation2D.cpp index 1a62eba..05f7bec 100644 --- a/src/weird-physics/Simulation2D.cpp +++ b/src/weird-physics/Simulation2D.cpp @@ -1,6 +1,7 @@ #include "weird-physics/Simulation2D.h" #include +#include #include "glm/gtx/norm.hpp" #include "weird-engine/Assert.h" @@ -75,6 +76,7 @@ namespace WeirdEngine , m_diameter(1.0f) , m_diameterSquared(m_diameter * m_diameter) , m_radious(m_diameter / 2.0f) + , m_bodyActive(size, 0) , m_collisionMap(size) , m_head(8191, -1) { @@ -101,6 +103,7 @@ namespace WeirdEngine Simulation2D::~Simulation2D() { + stopSimulationThread(); // Free any user data still attached to live bodies (the simulation // owns these pointers; removed bodies free theirs in removeObject). for (size_t i = 0; i < m_allocated; ++i) @@ -144,8 +147,18 @@ namespace WeirdEngine void Simulation2D::update(double delta) { + // Cap single-frame delta to prevent physics death spiral after stalls + constexpr double MAX_FRAME_DELTA = 0.1; // at most 100ms per frame + double clampedDelta = (std::min)(delta, MAX_FRAME_DELTA); + + const double maximum = static_cast(MAX_STEPS) * m_fixedDeltaTime; + double current = m_simulationDelay.load(); + while (!m_simulationDelay.compare_exchange_weak(current, (std::min)(current + clampedDelta, maximum))) + { + } { - m_simulationDelay += delta; + std::lock_guard lock(m_commandMutex); + m_commandReady.notify_one(); } if (m_simulating) @@ -154,28 +167,63 @@ namespace WeirdEngine process(); } - void Simulation2D::process() + void Simulation2D::enqueueCommand(PhysicsCommand command) { - PhysicsExecutionScope physicsExecution; + if (isPhysicsExecutionContext()) + { + m_internalCommands.push_back(std::move(command)); + return; + } + { + std::lock_guard lock(m_commandMutex); + m_pendingCommands.push_back(std::move(command)); + } + m_commandReady.notify_one(); + } - int steps = 0; + void Simulation2D::enqueueAction(std::function action) + { + enqueueCommand({PhysicsCommandType::Action, 0, vec2(0.0f), 0.0f, std::move(action)}); + } - while (m_simulationDelay >= m_fixedDeltaTime && steps < MAX_STEPS) + void Simulation2D::executeSynchronous(std::function action) + { + if (isPhysicsExecutionContext()) { - std::lock_guard structLock(m_structuralMutex); + action(); + return; + } + auto task = std::make_shared>(std::move(action)); + auto completed = task->get_future(); + enqueueAction([task] { (*task)(); }); + if (!m_simulating) + processCommands(); + completed.get(); + } - std::vector commandsToExecute; - { - std::lock_guard cmdLock(m_commandMutex); - commandsToExecute = std::move(m_pendingCommands); - } - commandsToExecute.insert(commandsToExecute.end(), m_internalCommands.begin(), m_internalCommands.end()); - m_internalCommands.clear(); + void Simulation2D::processCommands() + { + PhysicsExecutionScope physicsExecution; + std::vector commandsToExecute; + commandsToExecute.swap(m_internalCommands); + std::vector pending; + { + std::lock_guard cmdLock(m_commandMutex); + pending.swap(m_pendingCommands); + } + // Callback commands refer to the pre-removal IDs, so apply them before + // game-thread removal fences and their subsequent ID remapping. + commandsToExecute.insert(commandsToExecute.end(), std::make_move_iterator(pending.begin()), + std::make_move_iterator(pending.end())); + { for (const auto& cmd : commandsToExecute) { switch (cmd.type) { + case PhysicsCommandType::Action: + cmd.action(); + break; case PhysicsCommandType::SetVelocity: m_velocities[cmd.id] = cmd.vectorData; break; @@ -215,20 +263,42 @@ namespace WeirdEngine } case PhysicsCommandType::ActivatePending: { - m_size = m_allocated; + std::lock_guard lock(m_userDataMutex); + for (size_t id = 0; id < cmd.id; ++id) + { + if (!m_bodyActive[id]) + { + m_bodyActive[id] = 1; + ++m_activeSize; + } + } break; } case PhysicsCommandType::AddImpulse: { std::lock_guard lock(m_externalForcesMutex); m_impulsesSinceLastUpdate = true; - m_impulses[cmd.id] += cmd.vectorData; + m_impulses[cmd.id] += cmd.vectorData * (cmd.floatData != 0.0f ? m_mass[cmd.id] : 1.0f); break; } default: break; } } + } + } + + void Simulation2D::process() + { + PhysicsExecutionScope physicsExecution; + + int steps = 0; + + while (steps < MAX_STEPS) + { + processCommands(); + if (m_simulationDelay < m_fixedDeltaTime) + break; auto start = std::chrono::high_resolution_clock::now(); @@ -248,6 +318,7 @@ namespace WeirdEngine } } m_pendingShapeUpdates.clear(); + m_sdfsSnapshot = m_sdfs; } double timerBroad = 0, timerNarrow = 0, timerShape = 0; @@ -264,10 +335,12 @@ namespace WeirdEngine integratePredict((float)m_fixedDeltaTime); // 2. Iteratively solve constraints (Push/Pull particles to their exact distances) - - for (int iter = 0; iter < m_relaxationSteps; iter++) { - solveConstraints(); + std::lock_guard structLock(m_structuralMutex); + for (int iter = 0; iter < m_relaxationSteps; iter++) + { + solveConstraints(); + } } // 3. Derive the exact velocity based on how much the constraints moved the particles @@ -286,7 +359,6 @@ namespace WeirdEngine m_stats.integrationMs = m_stats.integrationMs * 0.9 + timerIntegration * 0.1; } - ++steps; { // m_simulationTime += m_fixedDeltaTime; // m_simulationTime.fetch_add(m_fixedDeltaTime); @@ -301,11 +373,13 @@ namespace WeirdEngine // Notify collision callback if (m_stepCallback) { + std::lock_guard dataLock(m_userDataMutex); m_stepCallback(m_callbackUserData); } } } + ++steps; { // std::lock_guard lock(g_simulationTimeMutex); // Lock the mutex m_simulationDelay -= m_fixedDeltaTime; @@ -332,7 +406,7 @@ namespace WeirdEngine { WEIRD_ASSERT(!isPhysicsExecutionContext(), "setUserData() may not be called from physics execution context"); - std::lock_guard lock(m_structuralMutex); + std::lock_guard lock(m_userDataMutex); // Bounds-check against m_allocated, not m_size: bodies can carry user // data before they are activated (ActivatePending) later in the frame. @@ -345,20 +419,10 @@ namespace WeirdEngine BodyUserData* Simulation2D::getUserData(SimulationID id) { - // Inside a physics step the structural mutex is already held, so the - // read is lock-free; on the main thread it is serialized against - // structural changes (removeObject renumbering). - if (isPhysicsExecutionContext()) - { - if (id >= m_allocated) - return nullptr; - return m_userData[id]; - } - - std::lock_guard lock(m_structuralMutex); - if (id >= m_allocated) return nullptr; + + std::lock_guard lock(m_userDataMutex); return m_userData[id]; } @@ -377,7 +441,11 @@ namespace WeirdEngine if (!m_simulating) return; - m_simulating = false; + { + std::lock_guard lock(m_commandMutex); + m_simulating = false; + } + m_commandReady.notify_one(); m_simulationThread.join(); } @@ -418,6 +486,8 @@ namespace WeirdEngine // Insert all particles into the spatial grid for (int i = 0; i < m_size; i++) { + if (!m_bodyActive[i]) + continue; // Calculate which grid cell the particle is in int gx = static_cast(std::floor(m_positions[i].x * invCellSize)); int gy = static_cast(std::floor(m_positions[i].y * invCellSize)); @@ -459,6 +529,8 @@ namespace WeirdEngine // Check for collisions using the grid for (int i = 0; i < m_size; i++) { + if (!m_bodyActive[i]) + continue; int gx = static_cast(std::floor(m_positions[i].x * invCellSize)); int gy = static_cast(std::floor(m_positions[i].y * invCellSize)); @@ -498,11 +570,13 @@ namespace WeirdEngine // Shape collisions for (size_t i = 0; i < m_size; i++) { + if (!m_bodyActive[i]) + continue; vec2& p = m_positions[i]; // Check bool currentCollision = false; - PhysicsShapeCollisionEvent collisionEvent; + PhysicsShapeCollisionEvent collisionEvent{}; collisionEvent.body = static_cast(i); // Static shapes @@ -512,38 +586,40 @@ namespace WeirdEngine if (d < m_radious) { - // Collision normal calculation - float d1 = map(p + vec2(EPSILON, 0.0)) - map(p - vec2(EPSILON, 0.0)); - float d2 = map(p + vec2(0.0, EPSILON)) - map(p - vec2(0.0, EPSILON)); - - // float d1 = d - map(vec2(p.x - EPSILON, p.y)); - // float d2 = d - map(vec2(p.x, p.y - EPSILON)); - + float d1 = map(p + vec2(EPSILON, 0.0f)) - map(p - vec2(EPSILON, 0.0f)); + float d2 = map(p + vec2(0.0f, EPSILON)) - map(p - vec2(0.0f, EPSILON)); vec2 grad(d1, d2); - float gradLenSq = glm::length2(grad); - collisionEvent.normal = gradLenSq > 0.0f ? grad / std::sqrt(gradLenSq) : vec2(0.0f, 1.0f); + float gradLen = glm::length(grad); + collisionEvent.normal = gradLen > 0.0f ? grad / gradLen : vec2(0.0f, 1.0f); WEIRD_ASSERT(!std::isnan(collisionEvent.normal.x) && !std::isnan(collisionEvent.normal.y), "NaN normal in shape collision calculation"); - float distanceAtSurface = map(p - ((m_radious)*collisionEvent.normal)); - if (distanceAtSurface <= - 0.0f) // Bad solution? Check if the distance at approximate contact point is small enough + float distanceAtSurface = map(p - m_radious * collisionEvent.normal); + if (distanceAtSurface <= 0.0f) { - float penetration = (std::min)(-distanceAtSurface, m_radious - d); - currentCollision = true; + float penetration; + if (d >= 0.0f && distanceAtSurface < 0.0f) + { + // The signs confirm a crossing on this segment. Interpolate its position + // using both samples; this is exact when the field varies linearly here. + float surfaceDistance = m_radious * d / (d - distanceAtSurface); + penetration = m_radious - surfaceDistance; + } + else + { + // Preserve the existing response when the center is already inside. + // Gradient scaling is a local approximation, not an exact distance. + float gradMag = gradLen / (2.0f * EPSILON); + float distanceScale = gradMag > 0.05f && gradMag < 1.0f ? gradMag : 1.0f; + penetration = (std::min)(-distanceAtSurface / distanceScale, m_radious - d / distanceScale); + } - collisionEvent.penetration = penetration; + currentCollision = true; + collisionEvent.penetration = std::max(0.0f, penetration); collisionEvent.position = p - (0.5f * collisionEvent.normal); collisionEvent.velocity = m_velocities[i]; - - // TODO: Pre-calculate target plane for relaxation - // collisionEvent.targetPos = p + (penetration * collisionEvent.normal); - - constexpr float DYNAMIC_FRICTION = 0.05f; - collisionEvent.friction = DYNAMIC_FRICTION; - - constexpr float ABSORTION = 10.0f; - collisionEvent.absortion = ABSORTION; + collisionEvent.friction = 0.05f; + collisionEvent.absortion = 10.0f; } } @@ -629,11 +705,11 @@ namespace WeirdEngine float minDistance; }; - // We typically have very few groups; linear lookup avoids hash overhead. - std::vector groups; - groups.reserve(16); - - std::vector globalShapes; + // Retain capacity across samples without limiting scene geometry. + thread_local std::vector groups; + thread_local std::vector globalShapes; + groups.clear(); + globalShapes.clear(); for (int i = 0; i < m_objects.size(); i++) { @@ -644,7 +720,7 @@ namespace WeirdEngine continue; } - if (!m_sdfs || obj.distanceFieldId >= m_sdfs->size()) + if (!m_sdfsSnapshot || obj.distanceFieldId >= m_sdfsSnapshot->size()) { continue; } @@ -658,17 +734,17 @@ namespace WeirdEngine // Distance - float dist = (*m_sdfs)[obj.distanceFieldId]->getValue(params); + float dist = (*m_sdfsSnapshot)[obj.distanceFieldId]->getValue(params); WEIRD_ASSERT(!std::isnan(dist), "NAN in group shape"); float currentMinDistance = d; GroupState* groupState = nullptr; - for (auto& group : groups) + for (size_t g = 0; g < groups.size(); ++g) { - if (group.id == obj.groupId) + if (groups[g].id == obj.groupId) { - groupState = &group; + groupState = &groups[g]; break; } } @@ -701,12 +777,12 @@ namespace WeirdEngine } case CombinationType::SmoothAddition: { - currentMinDistance = fOpUnionSoft(currentMinDistance, dist, 1.0f); + currentMinDistance = fOpUnionSoft(currentMinDistance, dist, obj.smoothRadius); break; } case CombinationType::SmoothSubtraction: { - currentMinDistance = fOpSubSoft(currentMinDistance, dist, 1.0f); + currentMinDistance = fOpSubSoft(currentMinDistance, dist, obj.smoothRadius); break; } default: @@ -722,17 +798,18 @@ namespace WeirdEngine groupState->minDistance = currentMinDistance; } - for (const auto& group : groups) + for (size_t g = 0; g < groups.size(); ++g) { - d = std::min(d, group.minDistance); + d = std::min(d, groups[g].minDistance); } // Apply global shapes as well, but without grouping (they affect everything) - for (int shapeIdx : globalShapes) + for (size_t g = 0; g < globalShapes.size(); ++g) { + int shapeIdx = globalShapes[g]; DistanceFieldObject2D& obj = m_objects[shapeIdx]; - if (!m_sdfs || obj.distanceFieldId >= m_sdfs->size()) + if (!m_sdfsSnapshot || obj.distanceFieldId >= m_sdfsSnapshot->size()) { continue; } @@ -746,7 +823,7 @@ namespace WeirdEngine // Distance - float dist = (*m_sdfs)[obj.distanceFieldId]->getValue(params); + float dist = (*m_sdfsSnapshot)[obj.distanceFieldId]->getValue(params); WEIRD_ASSERT(!std::isnan(dist), "NAN in global shape"); float currentMinDistance = d; @@ -771,12 +848,12 @@ namespace WeirdEngine } case CombinationType::SmoothAddition: { - currentMinDistance = fOpUnionSoft(currentMinDistance, dist, 1.0f); + currentMinDistance = fOpUnionSoft(currentMinDistance, dist, obj.smoothRadius); break; } case CombinationType::SmoothSubtraction: { - currentMinDistance = fOpSubSoft(currentMinDistance, dist, 1.0f); + currentMinDistance = fOpSubSoft(currentMinDistance, dist, obj.smoothRadius); break; } default: @@ -803,6 +880,8 @@ namespace WeirdEngine for (size_t i = 0; i < m_size; i++) { + if (!m_bodyActive[i]) + continue; m_forces[i] += m_continuousForcesRead[i]; } @@ -811,6 +890,8 @@ namespace WeirdEngine m_impulsesSinceLastUpdate = false; for (size_t i = 0; i < m_size; i++) { + if (!m_bodyActive[i]) + continue; m_forces[i] += m_impulses[i]; m_impulses[i] = vec2(0); } @@ -860,8 +941,9 @@ namespace WeirdEngine // Notify collision callback if (m_collisionCallback) { - vec2 contactPos = 0.5f * (m_positions[col.A] + m_positions[col.B]); - PhysicsCollisionEvent event{col.A, col.B, contactPos, normal, vRel, std::abs(impulseMagnitude)}; + vec2 contactPos = m_positions[col.A] + 0.5f * col.AB; + PhysicsCollisionEvent event{col.A, col.B, contactPos, normal, vRel, impulseMagnitude}; + std::lock_guard dataLock(m_userDataMutex); m_collisionCallback(event, m_callbackUserData); // Why am I creating a new event and not saving it?????? } } @@ -872,6 +954,7 @@ namespace WeirdEngine // Send event if (m_shapeCollisionCallback) { + std::lock_guard dataLock(m_userDataMutex); m_shapeCollisionCallback(collisionEvent, m_callbackUserData); // Scene can modify values } @@ -885,12 +968,17 @@ namespace WeirdEngine vec2 vel_t = vel - (v_n * collisionEvent.normal); float speed_t = length(vel_t); + // Penalty normal acceleration + float penSq = collisionEvent.penetration * collisionEvent.penetration; + float normalAcceleration = m_push * penSq; + + // Safety clamp to prevent explosive catapult forces from SDF corner / union distortion + constexpr float MAX_PENALTY_ACCELERATION = 500.0f; + normalAcceleration = std::min(normalAcceleration, MAX_PENALTY_ACCELERATION); + // Apply Tangential Friction if (speed_t > EPSILON) { - // Normal acceleration from the penalty method (calculated below: push * penetration^2) - float normalAcceleration = m_push * collisionEvent.penetration * collisionEvent.penetration; - // Coulomb friction (constant sliding resistance based on normal force) float coulombDrop = collisionEvent.friction * normalAcceleration * m_fixedDeltaTimeF; @@ -919,8 +1007,7 @@ namespace WeirdEngine m_velocities[collisionEvent.body] -= dampingDrop * collisionEvent.normal; // Penalty - vec2 v = collisionEvent.penetration * collisionEvent.penetration * collisionEvent.normal; - vec2 force = m_mass[collisionEvent.body] * m_push * v; + vec2 force = m_mass[collisionEvent.body] * normalAcceleration * collisionEvent.normal; m_forces[collisionEvent.body] += force; } @@ -930,6 +1017,8 @@ namespace WeirdEngine // Apply extra forces for (size_t i = 0; i < m_size; i++) { + if (!m_bodyActive[i]) + continue; vec2& p = m_positions[i]; // vec2& force = m_forces[i]; @@ -946,6 +1035,8 @@ namespace WeirdEngine for (auto it = m_distanceConstraints.begin(); it != m_distanceConstraints.end(); ++it) { DistanceConstraint constraint = *it; + if (!m_bodyActive[constraint.A] || !m_bodyActive[constraint.B]) + continue; vec2 v = m_positions[constraint.B] - m_positions[constraint.A]; float distance = length(v); @@ -994,6 +1085,8 @@ namespace WeirdEngine { for (size_t i = 0; i < m_size; i++) { + if (!m_bodyActive[i]) + continue; // Store current position m_previousPositions[i] = m_positions[i]; @@ -1019,6 +1112,8 @@ namespace WeirdEngine float invTimeStep = 1.0f / timeStep; for (size_t i = 0; i < m_size; i++) { + if (!m_bodyActive[i]) + continue; // How much did the particle actually move after constraints pushed it around? vec2 newVelocity = (m_positions[i] - m_previousPositions[i]) * invTimeStep; @@ -1029,7 +1124,7 @@ namespace WeirdEngine "integrateVelocity resulted in NaN velocity"); } - // Restore your original rendering buffer logic + // Publish pending slots too: their initialized transforms must survive buffer swaps. for (size_t i = 0; i < m_size; i++) { m_positionsAux[i] = m_positions[i]; @@ -1053,53 +1148,67 @@ namespace WeirdEngine SimulationID Simulation2D::generateSimulationID() { - std::lock_guard lock(m_structuralMutex); - std::lock_guard readLock(m_readMutex); - - SimulationID id = static_cast(m_allocated); - - // Initialize particle with safe defaults so the physics - // thread never processes stale/garbage data. - m_positions[id] = vec2(0.0f); - m_positionsRead[id] = vec2(0.0f); - m_positionsAux[id] = vec2(0.0f); - m_previousPositions[id] = vec2(0.0f); - m_velocities[id] = vec2(0.0f); - m_velocitiesRead[id] = vec2(0.0f); - m_velocitiesAux[id] = vec2(0.0f); - m_forces[id] = vec2(0.0f); - m_impulses[id] = vec2(0.0f); - m_continuousForcesRead[id] = vec2(0.0f); - m_continuousForcesWrite[id] = vec2(0.0f); - m_mass[id] = 1.0f; - m_invMass[id] = 1.0f; - m_collisionMap[id] = false; - m_userData[id] = nullptr; - - m_allocated++; + WEIRD_ASSERT(!isPhysicsExecutionContext(), "Body creation is main-thread only"); + const auto id = static_cast(m_allocated.load()); + if (id >= m_maxSize) + throw std::length_error("Simulation2D body capacity exceeded"); + { + std::lock_guard lock(m_readMutex); + m_positionsRead[id] = vec2(0.0f); + m_positionsAux[id] = vec2(0.0f); + m_velocitiesRead[id] = vec2(0.0f); + m_velocitiesAux[id] = vec2(0.0f); + } + ++m_allocated; + enqueueAction( + [this, id] + { + m_positions[id] = vec2(0.0f); + m_previousPositions[id] = vec2(0.0f); + m_velocities[id] = vec2(0.0f); + m_forces[id] = vec2(0.0f); + m_impulses[id] = vec2(0.0f); + m_continuousForcesRead[id] = vec2(0.0f); + m_continuousForcesWrite[id] = vec2(0.0f); + m_mass[id] = 1.0f; + m_invMass[id] = 1.0f; + m_collisionMap[id] = false; + m_positionsAux[id] = vec2(0.0f); + m_velocitiesAux[id] = vec2(0.0f); + std::lock_guard lock(m_userDataMutex); + m_bodyActive[id] = 0; + m_size = id + 1; + }); return id; } void Simulation2D::activatePendingBodies() { - if (m_allocated == m_size) - return; - - std::lock_guard lock(m_commandMutex); - m_pendingCommands.push_back({PhysicsCommandType::ActivatePending}); + // Capture the completed batch, never a live allocation counter on the worker. + enqueueCommand({PhysicsCommandType::ActivatePending, static_cast(m_allocated.load())}); } void Simulation2D::removeObject(SimulationID id) { - std::scoped_lock lock(m_structuralMutex, m_externalForcesMutex, m_fixMutex, m_readMutex); + WEIRD_ASSERT(!isPhysicsExecutionContext(), "Body removal is main-thread only"); + executeSynchronous([this, id] { removeBodyAtBoundary(id); }); + } + + void Simulation2D::removeBodyAtBoundary(SimulationID id) + { + std::scoped_lock lock(m_structuralMutex, m_externalForcesMutex, m_fixMutex, m_readMutex, m_userDataMutex); if (m_size == 0 || id >= m_size) { return; } + if (m_bodyActive[id]) + --m_activeSize; auto toId = id; auto fromId = m_size - 1; + m_bodyActive[toId] = m_bodyActive[fromId]; + m_bodyActive[fromId] = 0; if (toId != fromId) { @@ -1187,7 +1296,7 @@ namespace WeirdEngine size_t Simulation2D::getSize() { - return m_size; + return m_activeSize.load(); } void Simulation2D::addImpulseForce(SimulationID id, const vec2& impulse, bool massIndependent) @@ -1195,50 +1304,35 @@ namespace WeirdEngine WEIRD_ASSERT(id < m_allocated, "addImpulseForce called with invalid simulation id"); WEIRD_ASSERT(!std::isnan(impulse.x) && !std::isnan(impulse.y), "addImpulseForce called with NaN impulse"); - vec2 finalImpulse = impulse * m_simulationFrequency; - if (massIndependent) - finalImpulse *= m_mass[id]; - - if (std::this_thread::get_id() == m_physicsThreadId) - { - m_internalCommands.push_back({PhysicsCommandType::AddImpulse, id, finalImpulse}); - } - else - { - std::lock_guard lock(m_commandMutex); - m_pendingCommands.push_back({PhysicsCommandType::AddImpulse, id, finalImpulse}); - } + enqueueCommand( + {PhysicsCommandType::AddImpulse, id, impulse * m_simulationFrequency, massIndependent ? 1.0f : 0.0f}); } void Simulation2D::setContinuousForce(SimulationID id, const vec2& force, bool massIndependent) { WEIRD_ASSERT(id < m_allocated, "setContinuousForce called with invalid simulation id"); WEIRD_ASSERT(!std::isnan(force.x) && !std::isnan(force.y), "setContinuousForce called with NaN force"); - - // No lock needed, game thread writes directly to the write buffer - if (massIndependent) - m_continuousForcesWrite[id] = force * m_mass[id]; - else - m_continuousForcesWrite[id] = force; + enqueueAction([=, this] { m_continuousForcesWrite[id] = force * (massIndependent ? m_mass[id] : 1.0f); }); } void Simulation2D::swapContinuousForces() { - std::lock_guard lock(m_externalForcesMutex); - - vec2* temp = m_continuousForcesRead; - m_continuousForcesRead = m_continuousForcesWrite; - m_continuousForcesWrite = temp; - - // Clear the new write buffer so it's ready for the next frame - for (size_t i = 0; i < m_allocated; i++) - { - m_continuousForcesWrite[i] = vec2(0.0f); - } + enqueueAction( + [this] + { + std::swap(m_continuousForcesRead, m_continuousForcesWrite); + std::fill_n(m_continuousForcesWrite, m_size, vec2(0.0f)); + }); } void Simulation2D::addSpring(SimulationID a, SimulationID b, float stiffness, float distance) { + if (!isPhysicsExecutionContext()) + { + enqueueAction([=, this] { addSpring(a, b, stiffness, distance); }); + return; + } + if (a == b) return; @@ -1257,6 +1351,12 @@ namespace WeirdEngine void Simulation2D::addPositionConstraint(SimulationID a, SimulationID b, float distance) { + if (!isPhysicsExecutionContext()) + { + enqueueAction([=, this] { addPositionConstraint(a, b, distance); }); + return; + } + if (a == b) return; @@ -1270,6 +1370,12 @@ namespace WeirdEngine void Simulation2D::addGravitationalConstraint(SimulationID a, SimulationID b, float gravity) { + if (!isPhysicsExecutionContext()) + { + enqueueAction([=, this] { addGravitationalConstraint(a, b, gravity); }); + return; + } + if (a == b) return; @@ -1279,6 +1385,13 @@ namespace WeirdEngine bool Simulation2D::setDistanceConstraintDistance(SimulationID a, SimulationID b, float distance) { + if (!isPhysicsExecutionContext()) + { + bool result = false; + executeSynchronous([&] { result = setDistanceConstraintDistance(a, b, distance); }); + return result; + } + if (a == b) return false; @@ -1299,6 +1412,13 @@ namespace WeirdEngine bool Simulation2D::removeDistanceConstraint(SimulationID a, SimulationID b) { + if (!isPhysicsExecutionContext()) + { + bool result = false; + executeSynchronous([&] { result = removeDistanceConstraint(a, b); }); + return result; + } + if (a == b) return false; @@ -1321,32 +1441,23 @@ namespace WeirdEngine void Simulation2D::fix(SimulationID id) { - if (std::this_thread::get_id() == m_physicsThreadId) - { - m_internalCommands.push_back({PhysicsCommandType::Fix, id}); - } - else - { - std::lock_guard lock(m_commandMutex); - m_pendingCommands.push_back({PhysicsCommandType::Fix, id}); - } + enqueueCommand({PhysicsCommandType::Fix, id}); } void Simulation2D::unFix(SimulationID id) { - if (std::this_thread::get_id() == m_physicsThreadId) - { - m_internalCommands.push_back({PhysicsCommandType::UnFix, id}); - } - else - { - std::lock_guard lock(m_commandMutex); - m_pendingCommands.push_back({PhysicsCommandType::UnFix, id}); - } + enqueueCommand({PhysicsCommandType::UnFix, id}); } bool Simulation2D::isFixed(SimulationID id) { + if (!isPhysicsExecutionContext()) + { + bool result = false; + executeSynchronous([&] { result = isFixed(id); }); + return result; + } + std::lock_guard lock(m_structuralMutex); return std::find(m_fixedObjects.begin(), m_fixedObjects.end(), id) != m_fixedObjects.end(); } @@ -1365,15 +1476,9 @@ namespace WeirdEngine WEIRD_ASSERT(id < m_allocated, "setPosition called with invalid simulation id"); WEIRD_ASSERT(!std::isnan(pos.x) && !std::isnan(pos.y), "setPosition called with NaN coordinates"); - if (std::this_thread::get_id() == m_physicsThreadId) - { - m_internalCommands.push_back({PhysicsCommandType::SetPosition, id, pos}); - } - else + enqueueCommand({PhysicsCommandType::SetPosition, id, pos}); + if (!isPhysicsExecutionContext()) { - std::lock_guard lock(m_commandMutex); - m_pendingCommands.push_back({PhysicsCommandType::SetPosition, id, pos}); - std::lock_guard readLock(m_readMutex); m_positionsRead[id] = pos; } @@ -1393,15 +1498,9 @@ namespace WeirdEngine WEIRD_ASSERT(id < m_allocated, "setVelocity called with invalid simulation id"); WEIRD_ASSERT(!std::isnan(vel.x) && !std::isnan(vel.y), "setVelocity called with NaN velocity"); - if (std::this_thread::get_id() == m_physicsThreadId) - { - m_internalCommands.push_back({PhysicsCommandType::SetVelocity, id, vel}); - } - else + enqueueCommand({PhysicsCommandType::SetVelocity, id, vel}); + if (!isPhysicsExecutionContext()) { - std::lock_guard lock(m_commandMutex); - m_pendingCommands.push_back({PhysicsCommandType::SetVelocity, id, vel}); - std::lock_guard readLock(m_readMutex); m_velocitiesRead[id] = vel; } @@ -1418,9 +1517,8 @@ namespace WeirdEngine std::lock_guard lock(m_readMutex); // Copy every allocated slot, not just the active bodies: bodies - // created this frame have ids in [m_size, m_allocated) until their - // ActivatePending command is processed, and the readback must be able - // to look up those ids. + // created this frame may still be pending activation, and readback + // must be able to look up their reserved IDs. size_t count = m_allocated; if (snapshot.positions.size() < count) snapshot.positions.resize(count); @@ -1462,15 +1560,7 @@ namespace WeirdEngine PhysicsCommand cmd = {PhysicsCommandType::SetMass, id}; cmd.floatData = mass; - if (std::this_thread::get_id() == m_physicsThreadId) - { - m_internalCommands.push_back(cmd); - } - else - { - std::lock_guard lock(m_commandMutex); - m_pendingCommands.push_back(cmd); - } + enqueueCommand(std::move(cmd)); } void Simulation2D::setSDFs(std::vector>& sdfs) @@ -1490,7 +1580,8 @@ namespace WeirdEngine WEIRD_ASSERT(!m_sdfs || shape.distanceFieldId < m_sdfs->size(), "CustomShape registered with unregistered distanceFieldId"); - DistanceFieldObject2D sdf(owner, shape.distanceFieldId, shape.combination, shape.groupIdx, shape.parameters); + DistanceFieldObject2D sdf(owner, shape.distanceFieldId, shape.combination, shape.groupIdx, shape.parameters, + shape.smoothFactor); // Check if the key exists auto it = m_entityToObjectsIdx.find(owner); @@ -1556,9 +1647,13 @@ namespace WeirdEngine SimulationID Simulation2D::raycast(vec2 pos) { + std::scoped_lock lock(m_readMutex, m_userDataMutex); + const vec2* positions = isPhysicsExecutionContext() ? m_positions : m_positionsRead; for (size_t i = 0; i < m_size; i++) { - vec2 ij = pos - m_positions[i]; + if (!m_bodyActive[i]) + continue; + vec2 ij = pos - positions[i]; float distanceSquared = (ij.x * ij.x) + (ij.y * ij.y); @@ -1607,15 +1702,14 @@ namespace WeirdEngine { while (m_simulating) { - if (m_simulationDelay >= m_fixedDeltaTime) - { - process(); - } - else - { - int delay = static_cast(std::ceil((m_fixedDeltaTime - m_simulationDelay) * 1000)); // ms - std::this_thread::sleep_for(std::chrono::milliseconds(delay)); - } + process(); + std::unique_lock lock(m_commandMutex); + m_commandReady.wait(lock, + [this] + { + return !m_simulating || !m_pendingCommands.empty() || !m_internalCommands.empty() || + m_simulationDelay >= m_fixedDeltaTime; + }); } } diff --git a/src/weird-renderer/core/SDLInitializer.cpp b/src/weird-renderer/core/SDLInitializer.cpp index 0448f46..e30214c 100644 --- a/src/weird-renderer/core/SDLInitializer.cpp +++ b/src/weird-renderer/core/SDLInitializer.cpp @@ -37,6 +37,7 @@ namespace WeirdEngine SDLInitializer::SDLInitializer(DisplaySettings& settings, SDL_Window*& window, WeirdAudio::AudioEngine& audioEngine) : m_window(window) + , m_audioEngine(audioEngine) { #ifdef WEIRD_USE_FBDEV_EGL // Video backend selection: fbdev EGL by default, SDL's own windowing @@ -211,8 +212,8 @@ namespace WeirdEngine desiredSpec.format = SDL_AUDIO_F32; desiredSpec.channels = audioEngine.getChannels(); - m_audioStream = - SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &desiredSpec, nullptr, nullptr); + m_audioStream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &desiredSpec, + WeirdAudio::AudioEngine::audioStreamCallback, &audioEngine); if (!m_audioStream) { // Audio is not critical: log and keep running silent. diff --git a/tools/sdf-node-editor/src/services/PreviewController.cpp b/tools/sdf-node-editor/src/services/PreviewController.cpp index 544fde8..09f8021 100644 --- a/tools/sdf-node-editor/src/services/PreviewController.cpp +++ b/tools/sdf-node-editor/src/services/PreviewController.cpp @@ -231,7 +231,7 @@ namespace WeirdEngine::Editor { lastSpawnTime = services.time().time(); - glm::vec2 mousePosForCam(mouseScreenX + std::sinf(services.time().time()) * 10.0f, + glm::vec2 mousePosForCam(mouseScreenX + std::sin(services.time().time()) * 10.0f, static_cast(winH) - gapMinY); glm::vec2 worldPos = ECS::Camera::screenPositionToWorldPosition2D(camTransform, mousePosForCam);