From 5ef600098ab9484299f279d202e3d42d6a4bd85e Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 2 Apr 2026 00:21:21 +0300 Subject: [PATCH 1/2] `chore(renderer): refactor renderer and add post-process + spell visuals systems` - Updated core render pipeline and renderer integration in CMakeLists.txt, renderer.cpp, renderer.hpp - Added post-process pipeline module: - post_process_pipeline.hpp - post_process_pipeline.cpp - Added spell visual system module: - spell_visual_system.hpp - spell_visual_system.cpp - Adjusted application/audio integration: - application.cpp - audio_coordinator.cpp --- CMakeLists.txt | 2 + include/rendering/post_process_pipeline.hpp | 282 +++ include/rendering/renderer.hpp | 270 +-- include/rendering/spell_visual_system.hpp | 62 + src/audio/audio_coordinator.cpp | 17 +- src/core/application.cpp | 11 + src/rendering/post_process_pipeline.cpp | 1879 ++++++++++++++ src/rendering/renderer.cpp | 2429 +++---------------- src/rendering/spell_visual_system.cpp | 231 ++ 9 files changed, 2803 insertions(+), 2380 deletions(-) create mode 100644 include/rendering/post_process_pipeline.hpp create mode 100644 include/rendering/spell_visual_system.hpp create mode 100644 src/rendering/post_process_pipeline.cpp create mode 100644 src/rendering/spell_visual_system.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index dbb31af1..fe288549 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -611,6 +611,8 @@ set(WOWEE_SOURCES src/rendering/mount_dust.cpp src/rendering/levelup_effect.cpp src/rendering/charge_effect.cpp + src/rendering/spell_visual_system.cpp + src/rendering/post_process_pipeline.cpp src/rendering/loading_screen.cpp # UI diff --git a/include/rendering/post_process_pipeline.hpp b/include/rendering/post_process_pipeline.hpp new file mode 100644 index 00000000..765dfff4 --- /dev/null +++ b/include/rendering/post_process_pipeline.hpp @@ -0,0 +1,282 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "rendering/vk_utils.hpp" +#if WOWEE_HAS_AMD_FSR2 +#include "ffx_fsr2.h" +#include "ffx_fsr2_vk.h" +#endif + +namespace wowee { +namespace rendering { + +class VkContext; +class Camera; +class AmdFsr3Runtime; + +/// Returned by setFSREnabled/setFSR2Enabled when they need the Renderer +/// to schedule an MSAA sample-count change (§4.3). +struct MsaaChangeRequest { + bool requested = false; + VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT; +}; + +/// PostProcessPipeline owns all FSR 1.0, FXAA, and FSR 2.2/3 state and +/// orchestrates post-processing passes between the scene render pass and +/// the final swapchain presentation (§4.3 extraction from Renderer). +class PostProcessPipeline { +public: + PostProcessPipeline(); + ~PostProcessPipeline(); + + void initialize(VkContext* ctx); + void shutdown(); + + // --- Frame-loop integration (called from Renderer::beginFrame) --- + + /// Lazy-create / lazy-destroy FSR/FXAA/FSR2 resources between frames. + void manageResources(); + + /// Recreate post-process resources after swapchain resize. + void handleSwapchainResize(); + + /// Apply FSR2 temporal jitter to the camera projection. + void applyJitter(Camera* camera); + + /// Returns the framebuffer the scene should render into. + /// If no post-processing is active, returns VK_NULL_HANDLE (use swapchain). + VkFramebuffer getSceneFramebuffer() const; + + /// Returns the render extent for the active post-process pipeline. + /// Falls back to swapchain extent if nothing is active. + VkExtent2D getSceneRenderExtent() const; + + /// True if any post-process pipeline is active (FSR/FXAA/FSR2). + bool hasActivePostProcess() const; + + /// True when FXAA alone (no FSR2) needs its own off-screen pass. + bool useFXAAPostPass() const { return fxaa_.enabled; } + + // --- Frame-loop integration (called from Renderer::endFrame) --- + + /// Execute all post-processing passes. Returns true if an INLINE + /// render pass was started (affects ImGui recording mode). + bool executePostProcessing(VkCommandBuffer cmd, uint32_t imageIndex, + Camera* camera, float deltaTime); + + // --- MSAA interop (called from Renderer::applyMsaaChange) --- + + /// Destroy FSR/FSR2/FXAA resources (they will be lazily recreated). + void destroyAllResources(); + + /// True when FSR2 is active and MSAA changes should be blocked. + bool isFsr2BlockingMsaa() const { return fsr2_.enabled; } + + // --- Public API (delegated from Renderer) --- + + // FXAA + void setFXAAEnabled(bool enabled); + bool isFXAAEnabled() const { return fxaa_.enabled; } + + // FSR 1.0 + MsaaChangeRequest setFSREnabled(bool enabled); + bool isFSREnabled() const { return fsr_.enabled; } + void setFSRQuality(float scaleFactor); + void setFSRSharpness(float sharpness); + float getFSRScaleFactor() const { return fsr_.scaleFactor; } + float getFSRSharpness() const { return fsr_.sharpness; } + + // FSR 2.2 + MsaaChangeRequest setFSR2Enabled(bool enabled, Camera* camera); + bool isFSR2Enabled() const { return fsr2_.enabled; } + void setFSR2DebugTuning(float jitterSign, float motionVecScaleX, float motionVecScaleY); + + // FSR3 Framegen + void setAmdFsr3FramegenEnabled(bool enabled); + bool isAmdFsr3FramegenEnabled() const { return fsr2_.amdFsr3FramegenEnabled; } + float getFSR2JitterSign() const { return fsr2_.jitterSign; } + float getFSR2MotionVecScaleX() const { return fsr2_.motionVecScaleX; } + float getFSR2MotionVecScaleY() const { return fsr2_.motionVecScaleY; } +#if WOWEE_HAS_AMD_FSR2 + bool isAmdFsr2SdkAvailable() const { return true; } +#else + bool isAmdFsr2SdkAvailable() const { return false; } +#endif +#if WOWEE_HAS_AMD_FSR3_FRAMEGEN + bool isAmdFsr3FramegenSdkAvailable() const { return true; } +#else + bool isAmdFsr3FramegenSdkAvailable() const { return false; } +#endif + bool isAmdFsr3FramegenRuntimeActive() const { return fsr2_.amdFsr3FramegenRuntimeActive; } + bool isAmdFsr3FramegenRuntimeReady() const { return fsr2_.amdFsr3FramegenRuntimeReady; } + const char* getAmdFsr3FramegenRuntimePath() const; + const std::string& getAmdFsr3FramegenRuntimeError() const { return fsr2_.amdFsr3RuntimeLastError; } + size_t getAmdFsr3UpscaleDispatchCount() const { return fsr2_.amdFsr3UpscaleDispatchCount; } + size_t getAmdFsr3FramegenDispatchCount() const { return fsr2_.amdFsr3FramegenDispatchCount; } + size_t getAmdFsr3FallbackCount() const { return fsr2_.amdFsr3FallbackCount; } + + // Brightness (1.0 = default, <1 darkens, >1 brightens) + void setBrightness(float b) { brightness_ = b; } + float getBrightness() const { return brightness_; } + +private: + VkContext* vkCtx_ = nullptr; + + // Per-frame state set during executePostProcessing + VkCommandBuffer currentCmd_ = VK_NULL_HANDLE; + Camera* camera_ = nullptr; + float lastDeltaTime_ = 0.0f; + + // Brightness + float brightness_ = 1.0f; + + // FSR 1.0 upscaling state + struct FSRState { + bool enabled = false; + bool needsRecreate = false; + float scaleFactor = 1.00f; // Native default + float sharpness = 1.6f; + uint32_t internalWidth = 0; + uint32_t internalHeight = 0; + + // Off-screen scene target (reduced resolution) + AllocatedImage sceneColor{}; // 1x color (non-MSAA render target / MSAA resolve target) + AllocatedImage sceneDepth{}; // Depth (matches current MSAA sample count) + AllocatedImage sceneMsaaColor{}; // MSAA color target (only when MSAA > 1x) + AllocatedImage sceneDepthResolve{}; // Depth resolve (only when MSAA + depth resolve) + VkFramebuffer sceneFramebuffer = VK_NULL_HANDLE; + VkSampler sceneSampler = VK_NULL_HANDLE; + + // Upscale pipeline + VkPipeline pipeline = VK_NULL_HANDLE; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkDescriptorSetLayout descSetLayout = VK_NULL_HANDLE; + VkDescriptorPool descPool = VK_NULL_HANDLE; + VkDescriptorSet descSet = VK_NULL_HANDLE; + }; + FSRState fsr_; + bool initFSRResources(); + void destroyFSRResources(); + void renderFSRUpscale(); + + // FXAA post-process state + struct FXAAState { + bool enabled = false; + bool needsRecreate = false; + + // Off-screen scene target (same resolution as swapchain — no scaling) + AllocatedImage sceneColor{}; // 1x resolved color target + AllocatedImage sceneDepth{}; // Depth (matches MSAA sample count) + AllocatedImage sceneMsaaColor{}; // MSAA color target (when MSAA > 1x) + AllocatedImage sceneDepthResolve{}; // Depth resolve (MSAA + depth resolve) + VkFramebuffer sceneFramebuffer = VK_NULL_HANDLE; + VkSampler sceneSampler = VK_NULL_HANDLE; + + // FXAA fullscreen pipeline + VkPipeline pipeline = VK_NULL_HANDLE; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkDescriptorSetLayout descSetLayout = VK_NULL_HANDLE; + VkDescriptorPool descPool = VK_NULL_HANDLE; + VkDescriptorSet descSet = VK_NULL_HANDLE; + }; + FXAAState fxaa_; + bool initFXAAResources(); + void destroyFXAAResources(); + void renderFXAAPass(); + + // FSR 2.2 temporal upscaling state + struct FSR2State { + bool enabled = false; + bool needsRecreate = false; + float scaleFactor = 0.77f; + float sharpness = 3.0f; // Very strong RCAS to counteract upscale softness + uint32_t internalWidth = 0; + uint32_t internalHeight = 0; + + // Off-screen scene targets (internal resolution, no MSAA — FSR2 replaces AA) + AllocatedImage sceneColor{}; + AllocatedImage sceneDepth{}; + VkFramebuffer sceneFramebuffer = VK_NULL_HANDLE; + + // Samplers + VkSampler linearSampler = VK_NULL_HANDLE; // For color + VkSampler nearestSampler = VK_NULL_HANDLE; // For depth / motion vectors + + // Motion vector buffer (internal resolution) + AllocatedImage motionVectors{}; + + // History buffers (display resolution, ping-pong) + AllocatedImage history[2]{}; + AllocatedImage framegenOutput{}; + bool framegenOutputValid = false; + uint32_t currentHistory = 0; // Output index (0 or 1) + + // Compute pipelines + VkPipeline motionVecPipeline = VK_NULL_HANDLE; + VkPipelineLayout motionVecPipelineLayout = VK_NULL_HANDLE; + VkDescriptorSetLayout motionVecDescSetLayout = VK_NULL_HANDLE; + VkDescriptorPool motionVecDescPool = VK_NULL_HANDLE; + VkDescriptorSet motionVecDescSet = VK_NULL_HANDLE; + + VkPipeline accumulatePipeline = VK_NULL_HANDLE; + VkPipelineLayout accumulatePipelineLayout = VK_NULL_HANDLE; + VkDescriptorSetLayout accumulateDescSetLayout = VK_NULL_HANDLE; + VkDescriptorPool accumulateDescPool = VK_NULL_HANDLE; + VkDescriptorSet accumulateDescSets[2] = {}; // Per ping-pong + + // RCAS sharpening pass (display resolution) + VkPipeline sharpenPipeline = VK_NULL_HANDLE; + VkPipelineLayout sharpenPipelineLayout = VK_NULL_HANDLE; + VkDescriptorSetLayout sharpenDescSetLayout = VK_NULL_HANDLE; + VkDescriptorPool sharpenDescPool = VK_NULL_HANDLE; + VkDescriptorSet sharpenDescSets[2] = {}; + + // Previous frame state for motion vector reprojection + glm::mat4 prevViewProjection = glm::mat4(1.0f); + glm::vec2 prevJitter = glm::vec2(0.0f); + uint32_t frameIndex = 0; + bool needsHistoryReset = true; + bool useAmdBackend = false; + bool amdFsr3FramegenEnabled = false; + bool amdFsr3FramegenRuntimeActive = false; + bool amdFsr3FramegenRuntimeReady = false; + std::string amdFsr3RuntimePath = "Path C"; + std::string amdFsr3RuntimeLastError{}; + size_t amdFsr3UpscaleDispatchCount = 0; + size_t amdFsr3FramegenDispatchCount = 0; + size_t amdFsr3FallbackCount = 0; + uint64_t amdFsr3InteropSyncValue = 1; + float jitterSign = 0.38f; + float motionVecScaleX = 1.0f; + float motionVecScaleY = 1.0f; +#if WOWEE_HAS_AMD_FSR2 + FfxFsr2Context amdContext{}; + FfxFsr2Interface amdInterface{}; + void* amdScratchBuffer = nullptr; + size_t amdScratchBufferSize = 0; +#endif + std::unique_ptr amdFsr3Runtime; + + // Convergent accumulation: jitter for N frames then freeze + int convergenceFrame = 0; + static constexpr int convergenceMaxFrames = 8; + glm::mat4 lastStableVP = glm::mat4(1.0f); + }; + FSR2State fsr2_; + bool initFSR2Resources(); + void destroyFSR2Resources(); + void dispatchMotionVectors(); + void dispatchTemporalAccumulate(); + void dispatchAmdFsr2(); + void dispatchAmdFsr3Framegen(); + void renderFSR2Sharpen(); + static float halton(uint32_t index, uint32_t base); +}; + +} // namespace rendering +} // namespace wowee diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 35943572..10867a7b 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -14,16 +14,12 @@ #include "rendering/vk_frame_data.hpp" #include "rendering/vk_utils.hpp" #include "rendering/sky_system.hpp" -#if WOWEE_HAS_AMD_FSR2 -#include "ffx_fsr2.h" -#include "ffx_fsr2_vk.h" -#endif namespace wowee { namespace core { class Window; } namespace rendering { class VkContext; } namespace game { class World; class ZoneManager; class GameHandler; } -namespace audio { class MusicManager; class FootstepManager; class ActivitySoundManager; class MountSoundManager; class NpcVoiceManager; class AmbientSoundManager; class UiSoundManager; class CombatSoundManager; class SpellSoundManager; class MovementSoundManager; enum class FootstepSurface : uint8_t; enum class VoiceType; } +namespace audio { class AudioCoordinator; class MusicManager; class FootstepManager; class ActivitySoundManager; class MountSoundManager; class NpcVoiceManager; class AmbientSoundManager; class UiSoundManager; class CombatSoundManager; class SpellSoundManager; class MovementSoundManager; enum class FootstepSurface : uint8_t; enum class VoiceType; } namespace pipeline { class AssetManager; } namespace rendering { @@ -54,6 +50,8 @@ class WorldMap; class QuestMarkerRenderer; class CharacterPreview; class AmdFsr3Runtime; +class SpellVisualSystem; +class PostProcessPipeline; class Renderer { public: @@ -157,9 +155,10 @@ public: bool captureScreenshot(const std::string& outputPath); // Spell visual effects (SMSG_PLAY_SPELL_VISUAL / SMSG_PLAY_SPELL_IMPACT) - // useImpactKit=false → CastKit path; useImpactKit=true → ImpactKit path + // Delegates to SpellVisualSystem (owned by Renderer) void playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition, bool useImpactKit = false); + SpellVisualSystem* getSpellVisualSystem() const { return spellVisualSystem_.get(); } bool isEmoteActive() const { return emoteActive; } static std::string getEmoteText(const std::string& emoteName, const std::string* targetName = nullptr); static uint32_t getEmoteDbcId(const std::string& emoteName); @@ -197,17 +196,21 @@ public: double getLastTerrainRenderMs() const { return lastTerrainRenderMs; } double getLastWMORenderMs() const { return lastWMORenderMs; } double getLastM2RenderMs() const { return lastM2RenderMs; } - audio::MusicManager* getMusicManager() { return musicManager.get(); } + // Audio accessors — delegate to AudioCoordinator (owned by Application). + // These pass-throughs remain until §4.2 moves animation audio out of Renderer. + void setAudioCoordinator(audio::AudioCoordinator* ac) { audioCoordinator_ = ac; } + audio::AudioCoordinator* getAudioCoordinator() { return audioCoordinator_; } + audio::MusicManager* getMusicManager(); + audio::FootstepManager* getFootstepManager(); + audio::ActivitySoundManager* getActivitySoundManager(); + audio::MountSoundManager* getMountSoundManager(); + audio::NpcVoiceManager* getNpcVoiceManager(); + audio::AmbientSoundManager* getAmbientSoundManager(); + audio::UiSoundManager* getUiSoundManager(); + audio::CombatSoundManager* getCombatSoundManager(); + audio::SpellSoundManager* getSpellSoundManager(); + audio::MovementSoundManager* getMovementSoundManager(); game::ZoneManager* getZoneManager() { return zoneManager.get(); } - audio::FootstepManager* getFootstepManager() { return footstepManager.get(); } - audio::ActivitySoundManager* getActivitySoundManager() { return activitySoundManager.get(); } - audio::MountSoundManager* getMountSoundManager() { return mountSoundManager.get(); } - audio::NpcVoiceManager* getNpcVoiceManager() { return npcVoiceManager.get(); } - audio::AmbientSoundManager* getAmbientSoundManager() { return ambientSoundManager.get(); } - audio::UiSoundManager* getUiSoundManager() { return uiSoundManager.get(); } - audio::CombatSoundManager* getCombatSoundManager() { return combatSoundManager.get(); } - audio::SpellSoundManager* getSpellSoundManager() { return spellSoundManager.get(); } - audio::MovementSoundManager* getMovementSoundManager() { return movementSoundManager.get(); } LightingManager* getLightingManager() { return lightingManager.get(); } private: @@ -239,16 +242,7 @@ private: std::unique_ptr minimap; std::unique_ptr worldMap; std::unique_ptr questMarkerRenderer; - std::unique_ptr musicManager; - std::unique_ptr footstepManager; - std::unique_ptr activitySoundManager; - std::unique_ptr mountSoundManager; - std::unique_ptr npcVoiceManager; - std::unique_ptr ambientSoundManager; - std::unique_ptr uiSoundManager; - std::unique_ptr combatSoundManager; - std::unique_ptr spellSoundManager; - std::unique_ptr movementSoundManager; + audio::AudioCoordinator* audioCoordinator_ = nullptr; // Owned by Application std::unique_ptr zoneManager; // Shadow mapping (Vulkan) static constexpr uint32_t SHADOW_MAP_SIZE = 4096; @@ -282,42 +276,35 @@ public: float getShadowDistance() const { return shadowDistance_; } void setMsaaSamples(VkSampleCountFlagBits samples); - // FXAA post-process anti-aliasing (combinable with MSAA) + // Post-process pipeline API — delegates to PostProcessPipeline (§4.3) + PostProcessPipeline* getPostProcessPipeline() const; void setFXAAEnabled(bool enabled); - bool isFXAAEnabled() const { return fxaa_.enabled; } - - // FSR (FidelityFX Super Resolution) upscaling + bool isFXAAEnabled() const; void setFSREnabled(bool enabled); - bool isFSREnabled() const { return fsr_.enabled; } - void setFSRQuality(float scaleFactor); // 0.59=Balanced, 0.67=Quality, 0.77=UltraQuality, 1.00=Native - void setFSRSharpness(float sharpness); // 0.0 - 2.0 - float getFSRScaleFactor() const { return fsr_.scaleFactor; } - float getFSRSharpness() const { return fsr_.sharpness; } + bool isFSREnabled() const; + void setFSRQuality(float scaleFactor); + void setFSRSharpness(float sharpness); + float getFSRScaleFactor() const; + float getFSRSharpness() const; void setFSR2Enabled(bool enabled); - bool isFSR2Enabled() const { return fsr2_.enabled; } + bool isFSR2Enabled() const; void setFSR2DebugTuning(float jitterSign, float motionVecScaleX, float motionVecScaleY); void setAmdFsr3FramegenEnabled(bool enabled); - bool isAmdFsr3FramegenEnabled() const { return fsr2_.amdFsr3FramegenEnabled; } - float getFSR2JitterSign() const { return fsr2_.jitterSign; } - float getFSR2MotionVecScaleX() const { return fsr2_.motionVecScaleX; } - float getFSR2MotionVecScaleY() const { return fsr2_.motionVecScaleY; } -#if WOWEE_HAS_AMD_FSR2 - bool isAmdFsr2SdkAvailable() const { return true; } -#else - bool isAmdFsr2SdkAvailable() const { return false; } -#endif -#if WOWEE_HAS_AMD_FSR3_FRAMEGEN - bool isAmdFsr3FramegenSdkAvailable() const { return true; } -#else - bool isAmdFsr3FramegenSdkAvailable() const { return false; } -#endif - bool isAmdFsr3FramegenRuntimeActive() const { return fsr2_.amdFsr3FramegenRuntimeActive; } - bool isAmdFsr3FramegenRuntimeReady() const { return fsr2_.amdFsr3FramegenRuntimeReady; } + bool isAmdFsr3FramegenEnabled() const; + float getFSR2JitterSign() const; + float getFSR2MotionVecScaleX() const; + float getFSR2MotionVecScaleY() const; + bool isAmdFsr2SdkAvailable() const; + bool isAmdFsr3FramegenSdkAvailable() const; + bool isAmdFsr3FramegenRuntimeActive() const; + bool isAmdFsr3FramegenRuntimeReady() const; const char* getAmdFsr3FramegenRuntimePath() const; - const std::string& getAmdFsr3FramegenRuntimeError() const { return fsr2_.amdFsr3RuntimeLastError; } - size_t getAmdFsr3UpscaleDispatchCount() const { return fsr2_.amdFsr3UpscaleDispatchCount; } - size_t getAmdFsr3FramegenDispatchCount() const { return fsr2_.amdFsr3FramegenDispatchCount; } - size_t getAmdFsr3FallbackCount() const { return fsr2_.amdFsr3FallbackCount; } + const std::string& getAmdFsr3FramegenRuntimeError() const; + size_t getAmdFsr3UpscaleDispatchCount() const; + size_t getAmdFsr3FramegenDispatchCount() const; + size_t getAmdFsr3FallbackCount() const; + void setBrightness(float b); + float getBrightness() const; void setWaterRefractionEnabled(bool enabled); bool isWaterRefractionEnabled() const; @@ -331,23 +318,11 @@ private: pipeline::AssetManager* cachedAssetManager = nullptr; - // Spell visual effects — transient M2 instances spawned by SMSG_PLAY_SPELL_VISUAL/IMPACT - struct SpellVisualInstance { - uint32_t instanceId; - float elapsed; - float duration; // per-instance lifetime in seconds (from M2 anim or default) - }; - std::vector activeSpellVisuals_; - std::unordered_map spellVisualCastPath_; // visualId → cast M2 path - std::unordered_map spellVisualImpactPath_; // visualId → impact M2 path - std::unordered_map spellVisualModelIds_; // M2 path → M2Renderer modelId - std::unordered_set spellVisualFailedModels_; // modelIds that failed to load (negative cache) - uint32_t nextSpellVisualModelId_ = 999000; // Reserved range 999000-999799 - bool spellVisualDbcLoaded_ = false; - void loadSpellVisualDbc(); - void updateSpellVisuals(float deltaTime); - static constexpr float SPELL_VISUAL_MAX_DURATION = 5.0f; - static constexpr float SPELL_VISUAL_DEFAULT_DURATION = 2.0f; + // Spell visual effects — owned SpellVisualSystem (extracted from Renderer §4.4) + std::unique_ptr spellVisualSystem_; + + // Post-process pipeline — owns all FSR/FXAA/FSR2 state (extracted §4.3) + std::unique_ptr postProcessPipeline_; uint32_t currentZoneId = 0; std::string currentZoneName; @@ -408,155 +383,6 @@ private: void initOverlayPipeline(); void renderOverlay(const glm::vec4& color, VkCommandBuffer overrideCmd = VK_NULL_HANDLE); - // Brightness (1.0 = default, <1 darkens, >1 brightens) - float brightness_ = 1.0f; -public: - void setBrightness(float b) { brightness_ = b; } - float getBrightness() const { return brightness_; } -private: - - // FSR 1.0 upscaling state - struct FSRState { - bool enabled = false; - bool needsRecreate = false; - float scaleFactor = 1.00f; // Native default - float sharpness = 1.6f; - uint32_t internalWidth = 0; - uint32_t internalHeight = 0; - - // Off-screen scene target (reduced resolution) - AllocatedImage sceneColor{}; // 1x color (non-MSAA render target / MSAA resolve target) - AllocatedImage sceneDepth{}; // Depth (matches current MSAA sample count) - AllocatedImage sceneMsaaColor{}; // MSAA color target (only when MSAA > 1x) - AllocatedImage sceneDepthResolve{}; // Depth resolve (only when MSAA + depth resolve) - VkFramebuffer sceneFramebuffer = VK_NULL_HANDLE; - VkSampler sceneSampler = VK_NULL_HANDLE; - - // Upscale pipeline - VkPipeline pipeline = VK_NULL_HANDLE; - VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; - VkDescriptorSetLayout descSetLayout = VK_NULL_HANDLE; - VkDescriptorPool descPool = VK_NULL_HANDLE; - VkDescriptorSet descSet = VK_NULL_HANDLE; - }; - FSRState fsr_; - bool initFSRResources(); - void destroyFSRResources(); - void renderFSRUpscale(); - - // FXAA post-process state - struct FXAAState { - bool enabled = false; - bool needsRecreate = false; - - // Off-screen scene target (same resolution as swapchain — no scaling) - AllocatedImage sceneColor{}; // 1x resolved color target - AllocatedImage sceneDepth{}; // Depth (matches MSAA sample count) - AllocatedImage sceneMsaaColor{}; // MSAA color target (when MSAA > 1x) - AllocatedImage sceneDepthResolve{}; // Depth resolve (MSAA + depth resolve) - VkFramebuffer sceneFramebuffer = VK_NULL_HANDLE; - VkSampler sceneSampler = VK_NULL_HANDLE; - - // FXAA fullscreen pipeline - VkPipeline pipeline = VK_NULL_HANDLE; - VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; - VkDescriptorSetLayout descSetLayout = VK_NULL_HANDLE; - VkDescriptorPool descPool = VK_NULL_HANDLE; - VkDescriptorSet descSet = VK_NULL_HANDLE; - }; - FXAAState fxaa_; - bool initFXAAResources(); - void destroyFXAAResources(); - void renderFXAAPass(); - - // FSR 2.2 temporal upscaling state - struct FSR2State { - bool enabled = false; - bool needsRecreate = false; - float scaleFactor = 0.77f; - float sharpness = 3.0f; // Very strong RCAS to counteract upscale softness - uint32_t internalWidth = 0; - uint32_t internalHeight = 0; - - // Off-screen scene targets (internal resolution, no MSAA — FSR2 replaces AA) - AllocatedImage sceneColor{}; - AllocatedImage sceneDepth{}; - VkFramebuffer sceneFramebuffer = VK_NULL_HANDLE; - - // Samplers - VkSampler linearSampler = VK_NULL_HANDLE; // For color - VkSampler nearestSampler = VK_NULL_HANDLE; // For depth / motion vectors - - // Motion vector buffer (internal resolution) - AllocatedImage motionVectors{}; - - // History buffers (display resolution, ping-pong) - AllocatedImage history[2]{}; - AllocatedImage framegenOutput{}; - bool framegenOutputValid = false; - uint32_t currentHistory = 0; // Output index (0 or 1) - - // Compute pipelines - VkPipeline motionVecPipeline = VK_NULL_HANDLE; - VkPipelineLayout motionVecPipelineLayout = VK_NULL_HANDLE; - VkDescriptorSetLayout motionVecDescSetLayout = VK_NULL_HANDLE; - VkDescriptorPool motionVecDescPool = VK_NULL_HANDLE; - VkDescriptorSet motionVecDescSet = VK_NULL_HANDLE; - - VkPipeline accumulatePipeline = VK_NULL_HANDLE; - VkPipelineLayout accumulatePipelineLayout = VK_NULL_HANDLE; - VkDescriptorSetLayout accumulateDescSetLayout = VK_NULL_HANDLE; - VkDescriptorPool accumulateDescPool = VK_NULL_HANDLE; - VkDescriptorSet accumulateDescSets[2] = {}; // Per ping-pong - - // RCAS sharpening pass (display resolution) - VkPipeline sharpenPipeline = VK_NULL_HANDLE; - VkPipelineLayout sharpenPipelineLayout = VK_NULL_HANDLE; - VkDescriptorSetLayout sharpenDescSetLayout = VK_NULL_HANDLE; - VkDescriptorPool sharpenDescPool = VK_NULL_HANDLE; - VkDescriptorSet sharpenDescSets[2] = {}; - - // Previous frame state for motion vector reprojection - glm::mat4 prevViewProjection = glm::mat4(1.0f); - glm::vec2 prevJitter = glm::vec2(0.0f); - uint32_t frameIndex = 0; - bool needsHistoryReset = true; - bool useAmdBackend = false; - bool amdFsr3FramegenEnabled = false; - bool amdFsr3FramegenRuntimeActive = false; - bool amdFsr3FramegenRuntimeReady = false; - std::string amdFsr3RuntimePath = "Path C"; - std::string amdFsr3RuntimeLastError{}; - size_t amdFsr3UpscaleDispatchCount = 0; - size_t amdFsr3FramegenDispatchCount = 0; - size_t amdFsr3FallbackCount = 0; - uint64_t amdFsr3InteropSyncValue = 1; - float jitterSign = 0.38f; - float motionVecScaleX = 1.0f; - float motionVecScaleY = 1.0f; -#if WOWEE_HAS_AMD_FSR2 - FfxFsr2Context amdContext{}; - FfxFsr2Interface amdInterface{}; - void* amdScratchBuffer = nullptr; - size_t amdScratchBufferSize = 0; -#endif - std::unique_ptr amdFsr3Runtime; - - // Convergent accumulation: jitter for N frames then freeze - int convergenceFrame = 0; - static constexpr int convergenceMaxFrames = 8; - glm::mat4 lastStableVP = glm::mat4(1.0f); - }; - FSR2State fsr2_; - bool initFSR2Resources(); - void destroyFSR2Resources(); - void dispatchMotionVectors(); - void dispatchTemporalAccumulate(); - void dispatchAmdFsr2(); - void dispatchAmdFsr3Framegen(); - void renderFSR2Sharpen(); - static float halton(uint32_t index, uint32_t base); - // Footstep event tracking (animation-driven) uint32_t footstepLastAnimationId = 0; float footstepLastNormTime = 0.0f; diff --git a/include/rendering/spell_visual_system.hpp b/include/rendering/spell_visual_system.hpp new file mode 100644 index 00000000..38d75c0d --- /dev/null +++ b/include/rendering/spell_visual_system.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace pipeline { class AssetManager; } +namespace rendering { + +class M2Renderer; + +class SpellVisualSystem { +public: + SpellVisualSystem() = default; + ~SpellVisualSystem() = default; + + // Initialize with references to the M2 renderer (for model loading/instance spawning) + void initialize(M2Renderer* m2Renderer); + void shutdown(); + + // Spawn a spell visual at a world position. + // useImpactKit=false → CastKit path; useImpactKit=true → ImpactKit path + void playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition, + bool useImpactKit = false); + + // Advance lifetime timers and remove expired instances. + void update(float deltaTime); + + // Remove all active spell visual instances and reset caches. + // Called on map change / combat reset. + void reset(); + +private: + // Spell visual effects — transient M2 instances spawned by SMSG_PLAY_SPELL_VISUAL/IMPACT + struct SpellVisualInstance { + uint32_t instanceId; + float elapsed; + float duration; // per-instance lifetime in seconds (from M2 anim or default) + }; + + void loadSpellVisualDbc(); + + M2Renderer* m2Renderer_ = nullptr; + pipeline::AssetManager* cachedAssetManager_ = nullptr; + + std::vector activeSpellVisuals_; + std::unordered_map spellVisualCastPath_; // visualId → cast M2 path + std::unordered_map spellVisualImpactPath_; // visualId → impact M2 path + std::unordered_map spellVisualModelIds_; // M2 path → M2Renderer modelId + std::unordered_set spellVisualFailedModels_; // modelIds that failed to load (negative cache) + uint32_t nextSpellVisualModelId_ = 999000; // Reserved range 999000-999799 + bool spellVisualDbcLoaded_ = false; + static constexpr float SPELL_VISUAL_MAX_DURATION = 5.0f; + static constexpr float SPELL_VISUAL_DEFAULT_DURATION = 2.0f; +}; + +} // namespace rendering +} // namespace wowee diff --git a/src/audio/audio_coordinator.cpp b/src/audio/audio_coordinator.cpp index 346dd3fe..deeb0861 100644 --- a/src/audio/audio_coordinator.cpp +++ b/src/audio/audio_coordinator.cpp @@ -50,13 +50,16 @@ bool AudioCoordinator::initialize() { void AudioCoordinator::initializeWithAssets(pipeline::AssetManager* assetManager) { if (!audioAvailable_ || !assetManager) return; - // MusicManager needs asset manager for zone music lookups - if (musicManager_) { - musicManager_->initialize(assetManager); - } - - // Other managers may need asset manager for sound bank loading - // (Add similar calls as needed for other managers) + if (musicManager_) musicManager_->initialize(assetManager); + if (footstepManager_) footstepManager_->initialize(assetManager); + if (activitySoundManager_) activitySoundManager_->initialize(assetManager); + if (mountSoundManager_) mountSoundManager_->initialize(assetManager); + if (npcVoiceManager_) npcVoiceManager_->initialize(assetManager); + if (ambientSoundManager_) ambientSoundManager_->initialize(assetManager); + if (uiSoundManager_) uiSoundManager_->initialize(assetManager); + if (combatSoundManager_) combatSoundManager_->initialize(assetManager); + if (spellSoundManager_) spellSoundManager_->initialize(assetManager); + if (movementSoundManager_) movementSoundManager_->initialize(assetManager); LOG_INFO("AudioCoordinator initialized with asset manager"); } diff --git a/src/core/application.cpp b/src/core/application.cpp index e597cb91..c5621daf 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -125,6 +125,11 @@ bool Application::initialize() { return false; } + // Create and initialize audio coordinator (owns all audio managers) + audioCoordinator_ = std::make_unique(); + audioCoordinator_->initialize(); + renderer->setAudioCoordinator(audioCoordinator_.get()); + // Create UI manager uiManager = std::make_unique(); if (!uiManager->initialize(window.get())) { @@ -845,6 +850,12 @@ void Application::shutdown() { LOG_WARNING("Renderer shutdown complete, resetting..."); renderer.reset(); + // Shutdown audio coordinator after renderer (renderer may reference audio during shutdown) + if (audioCoordinator_) { + audioCoordinator_->shutdown(); + } + audioCoordinator_.reset(); + LOG_WARNING("Resetting world..."); world.reset(); LOG_WARNING("Resetting gameHandler..."); diff --git a/src/rendering/post_process_pipeline.cpp b/src/rendering/post_process_pipeline.cpp new file mode 100644 index 00000000..fd0c49f0 --- /dev/null +++ b/src/rendering/post_process_pipeline.cpp @@ -0,0 +1,1879 @@ +// PostProcessPipeline — FSR 1.0, FXAA, FSR 2.2/3 state and passes (§4.3) +// Extracted from Renderer to isolate post-processing concerns. + +#include "rendering/post_process_pipeline.hpp" +#include "rendering/vk_context.hpp" +#include "rendering/vk_shader.hpp" +#include "rendering/vk_pipeline.hpp" +#include "rendering/camera.hpp" +#include "rendering/amd_fsr3_runtime.hpp" +#include "core/logger.hpp" +#include +#include +#include + +namespace wowee { +namespace rendering { + +PostProcessPipeline::PostProcessPipeline() = default; +PostProcessPipeline::~PostProcessPipeline() { shutdown(); } + +void PostProcessPipeline::initialize(VkContext* ctx) { + vkCtx_ = ctx; +} + +void PostProcessPipeline::shutdown() { + destroyFSRResources(); + destroyFSR2Resources(); + destroyFXAAResources(); + vkCtx_ = nullptr; +} + +// ========================= Frame-loop integration ========================= + +void PostProcessPipeline::manageResources() { + // FSR resource management (safe: between frames, no command buffer in flight) + if (fsr_.needsRecreate && fsr_.sceneFramebuffer) { + destroyFSRResources(); + fsr_.needsRecreate = false; + if (!fsr_.enabled) LOG_INFO("FSR: disabled"); + } + if (fsr_.enabled && !fsr2_.enabled && !fsr_.sceneFramebuffer) { + if (!initFSRResources()) { + LOG_ERROR("FSR: initialization failed, disabling"); + fsr_.enabled = false; + } + } + + // FSR 2.2 resource management + if (fsr2_.needsRecreate && fsr2_.sceneFramebuffer) { + destroyFSR2Resources(); + fsr2_.needsRecreate = false; + if (!fsr2_.enabled) LOG_INFO("FSR2: disabled"); + } + if (fsr2_.enabled && !fsr2_.sceneFramebuffer) { + if (!initFSR2Resources()) { + LOG_ERROR("FSR2: initialization failed, disabling"); + fsr2_.enabled = false; + } + } + + // FXAA resource management — FXAA can coexist with FSR1 and FSR3. + // When both FXAA and FSR3 are enabled, FXAA runs as a post-FSR3 pass. + // Do not force this pass for ghost mode; keep AA quality strictly user-controlled. + const bool useFXAA = fxaa_.enabled; + if ((fxaa_.needsRecreate || !useFXAA) && fxaa_.sceneFramebuffer) { + destroyFXAAResources(); + fxaa_.needsRecreate = false; + if (!useFXAA) LOG_INFO("FXAA: disabled"); + } + if (useFXAA && !fxaa_.sceneFramebuffer) { + if (!initFXAAResources()) { + LOG_ERROR("FXAA: initialization failed, disabling"); + fxaa_.enabled = false; + } + } +} + +void PostProcessPipeline::handleSwapchainResize() { + const bool useFXAA = fxaa_.enabled; + // Recreate FSR resources for new swapchain dimensions + if (fsr_.enabled && !fsr2_.enabled) { + destroyFSRResources(); + initFSRResources(); + } + if (fsr2_.enabled) { + destroyFSR2Resources(); + initFSR2Resources(); + } + // Recreate FXAA resources for new swapchain dimensions. + if (useFXAA) { + destroyFXAAResources(); + initFXAAResources(); + } +} + +void PostProcessPipeline::applyJitter(Camera* camera) { + if (!fsr2_.enabled || !fsr2_.sceneFramebuffer || !camera) return; + + if (!fsr2_.useAmdBackend) { + camera->setJitter(0.0f, 0.0f); + } else { +#if WOWEE_HAS_AMD_FSR2 + // AMD-recommended jitter sequence in pixel space, converted to NDC projection offset. + int32_t phaseCount = ffxFsr2GetJitterPhaseCount( + static_cast(fsr2_.internalWidth), + static_cast(vkCtx_->getSwapchainExtent().width)); + float jitterX = 0.0f; + float jitterY = 0.0f; + if (phaseCount > 0 && + ffxFsr2GetJitterOffset(&jitterX, &jitterY, static_cast(fsr2_.frameIndex % static_cast(phaseCount)), phaseCount) == FFX_OK) { + float ndcJx = (2.0f * jitterX) / static_cast(fsr2_.internalWidth); + float ndcJy = (2.0f * jitterY) / static_cast(fsr2_.internalHeight); + // Keep projection jitter and FSR dispatch jitter in sync. + camera->setJitter(fsr2_.jitterSign * ndcJx, fsr2_.jitterSign * ndcJy); + } else { + camera->setJitter(0.0f, 0.0f); + } +#else + const float jitterScale = 0.5f; + float jx = (halton(fsr2_.frameIndex + 1, 2) - 0.5f) * 2.0f * jitterScale / static_cast(fsr2_.internalWidth); + float jy = (halton(fsr2_.frameIndex + 1, 3) - 0.5f) * 2.0f * jitterScale / static_cast(fsr2_.internalHeight); + camera->setJitter(fsr2_.jitterSign * jx, fsr2_.jitterSign * jy); +#endif + } +} + +VkFramebuffer PostProcessPipeline::getSceneFramebuffer() const { + if (fsr2_.enabled && fsr2_.sceneFramebuffer) + return fsr2_.sceneFramebuffer; + if (fxaa_.enabled && fxaa_.sceneFramebuffer) + return fxaa_.sceneFramebuffer; + if (fsr_.enabled && fsr_.sceneFramebuffer) + return fsr_.sceneFramebuffer; + return VK_NULL_HANDLE; +} + +VkExtent2D PostProcessPipeline::getSceneRenderExtent() const { + if (fsr2_.enabled && fsr2_.sceneFramebuffer) + return { fsr2_.internalWidth, fsr2_.internalHeight }; + if (fxaa_.enabled && fxaa_.sceneFramebuffer) + return vkCtx_->getSwapchainExtent(); // native resolution — no downscaling + if (fsr_.enabled && fsr_.sceneFramebuffer) + return { fsr_.internalWidth, fsr_.internalHeight }; + return vkCtx_->getSwapchainExtent(); +} + +bool PostProcessPipeline::hasActivePostProcess() const { + return (fsr2_.enabled && fsr2_.sceneFramebuffer) + || (fxaa_.enabled && fxaa_.sceneFramebuffer) + || (fsr_.enabled && fsr_.sceneFramebuffer); +} + +bool PostProcessPipeline::executePostProcessing(VkCommandBuffer cmd, uint32_t imageIndex, + Camera* camera, float deltaTime) { + currentCmd_ = cmd; + camera_ = camera; + lastDeltaTime_ = deltaTime; + bool inlineMode = false; + + if (fsr2_.enabled && fsr2_.sceneFramebuffer) { + // End the off-screen scene render pass + vkCmdEndRenderPass(currentCmd_); + + if (fsr2_.useAmdBackend) { + // Compute passes: motion vectors -> temporal accumulation + dispatchMotionVectors(); + if (fsr2_.amdFsr3FramegenEnabled && fsr2_.amdFsr3FramegenRuntimeReady) { + dispatchAmdFsr3Framegen(); + if (!fsr2_.amdFsr3FramegenRuntimeActive) { + dispatchAmdFsr2(); + } + } else { + dispatchAmdFsr2(); + } + + // Transition post-FSR input for sharpen pass. + if (fsr2_.amdFsr3FramegenRuntimeActive && fsr2_.framegenOutput.image) { + transitionImageLayout(currentCmd_, fsr2_.framegenOutput.image, + VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + fsr2_.framegenOutputValid = true; + } else { + transitionImageLayout(currentCmd_, fsr2_.history[fsr2_.currentHistory].image, + VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + } + } else { + transitionImageLayout(currentCmd_, fsr2_.sceneColor.image, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + } + + // FSR3+FXAA combined: re-point FXAA's descriptor to the FSR3 temporal output + // so renderFXAAPass() applies spatial AA on the temporally-stabilized frame. + // This must happen outside the render pass (descriptor updates are CPU-side). + if (fxaa_.enabled && fxaa_.descSet && fxaa_.sceneSampler) { + VkImageView fsr3OutputView = VK_NULL_HANDLE; + if (fsr2_.useAmdBackend) { + if (fsr2_.amdFsr3FramegenRuntimeActive && fsr2_.framegenOutput.image) + fsr3OutputView = fsr2_.framegenOutput.imageView; + else if (fsr2_.history[fsr2_.currentHistory].image) + fsr3OutputView = fsr2_.history[fsr2_.currentHistory].imageView; + } else if (fsr2_.history[fsr2_.currentHistory].image) { + fsr3OutputView = fsr2_.history[fsr2_.currentHistory].imageView; + } + if (fsr3OutputView) { + VkDescriptorImageInfo imgInfo{}; + imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + imgInfo.imageView = fsr3OutputView; + imgInfo.sampler = fxaa_.sceneSampler; + VkWriteDescriptorSet write{}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstSet = fxaa_.descSet; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &imgInfo; + vkUpdateDescriptorSets(vkCtx_->getDevice(), 1, &write, 0, nullptr); + } + } + + // Begin swapchain render pass at full resolution for sharpening + ImGui + VkRenderPassBeginInfo rpInfo{}; + rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rpInfo.renderPass = vkCtx_->getImGuiRenderPass(); + rpInfo.framebuffer = vkCtx_->getSwapchainFramebuffers()[imageIndex]; + rpInfo.renderArea.offset = {0, 0}; + rpInfo.renderArea.extent = vkCtx_->getSwapchainExtent(); + + bool msaaOn = (vkCtx_->getMsaaSamples() > VK_SAMPLE_COUNT_1_BIT); + VkClearValue clearValues[4]{}; + clearValues[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}}; + clearValues[1].depthStencil = {1.0f, 0}; + clearValues[2].color = {{0.0f, 0.0f, 0.0f, 1.0f}}; + clearValues[3].depthStencil = {1.0f, 0}; + rpInfo.clearValueCount = msaaOn ? (vkCtx_->getDepthResolveImageView() ? 4u : 3u) : 2u; + rpInfo.pClearValues = clearValues; + + inlineMode = true; vkCmdBeginRenderPass(currentCmd_, &rpInfo, VK_SUBPASS_CONTENTS_INLINE); + + VkExtent2D ext = vkCtx_->getSwapchainExtent(); + VkViewport vp{}; + vp.width = static_cast(ext.width); + vp.height = static_cast(ext.height); + vp.maxDepth = 1.0f; + vkCmdSetViewport(currentCmd_, 0, 1, &vp); + VkRect2D sc{}; + sc.extent = ext; + vkCmdSetScissor(currentCmd_, 0, 1, &sc); + + // When FXAA is also enabled: apply FXAA on the FSR3 temporal output instead + // of RCAS sharpening. FXAA descriptor is temporarily pointed to the FSR3 + // history buffer (which is already in SHADER_READ_ONLY_OPTIMAL). This gives + // FSR3 temporal stability + FXAA spatial edge smoothing ("ultra quality native"). + if (fxaa_.enabled && fxaa_.pipeline && fxaa_.descSet) { + renderFXAAPass(); + } else { + // Draw RCAS sharpening from accumulated history buffer + renderFSR2Sharpen(); + } + + // Restore FXAA descriptor to its normal scene color source so standalone + // FXAA frames are not affected by the FSR3 history pointer set above. + if (fxaa_.enabled && fxaa_.descSet && fxaa_.sceneSampler && fxaa_.sceneColor.imageView) { + VkDescriptorImageInfo restoreInfo{}; + restoreInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + restoreInfo.imageView = fxaa_.sceneColor.imageView; + restoreInfo.sampler = fxaa_.sceneSampler; + VkWriteDescriptorSet restoreWrite{}; + restoreWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + restoreWrite.dstSet = fxaa_.descSet; + restoreWrite.dstBinding = 0; + restoreWrite.descriptorCount = 1; + restoreWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + restoreWrite.pImageInfo = &restoreInfo; + vkUpdateDescriptorSets(vkCtx_->getDevice(), 1, &restoreWrite, 0, nullptr); + } + + // Maintain frame bookkeeping + fsr2_.prevViewProjection = camera_->getViewProjectionMatrix(); + fsr2_.prevJitter = camera_->getJitter(); + camera_->clearJitter(); + if (fsr2_.useAmdBackend) { + fsr2_.currentHistory = 1 - fsr2_.currentHistory; + } + fsr2_.frameIndex = (fsr2_.frameIndex + 1) % 256; // Wrap to keep Halton values well-distributed + + } else if (fxaa_.enabled && fxaa_.sceneFramebuffer) { + // End the off-screen scene render pass + vkCmdEndRenderPass(currentCmd_); + + // Transition resolved scene color: PRESENT_SRC_KHR → SHADER_READ_ONLY + transitionImageLayout(currentCmd_, fxaa_.sceneColor.image, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + + // Begin swapchain render pass (1x — no MSAA on the output pass) + VkRenderPassBeginInfo rpInfo{}; + rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rpInfo.renderPass = vkCtx_->getImGuiRenderPass(); + rpInfo.framebuffer = vkCtx_->getSwapchainFramebuffers()[imageIndex]; + rpInfo.renderArea.offset = {0, 0}; + rpInfo.renderArea.extent = vkCtx_->getSwapchainExtent(); + // The swapchain render pass always has 2 attachments when MSAA is off; + // FXAA output goes to the non-MSAA swapchain directly. + VkClearValue fxaaClear[2]{}; + fxaaClear[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}}; + fxaaClear[1].depthStencil = {1.0f, 0}; + rpInfo.clearValueCount = 2; + rpInfo.pClearValues = fxaaClear; + + vkCmdBeginRenderPass(currentCmd_, &rpInfo, VK_SUBPASS_CONTENTS_INLINE); + + VkExtent2D ext = vkCtx_->getSwapchainExtent(); + VkViewport vp{}; + vp.width = static_cast(ext.width); + vp.height = static_cast(ext.height); + vp.maxDepth = 1.0f; + vkCmdSetViewport(currentCmd_, 0, 1, &vp); + VkRect2D sc{}; + sc.extent = ext; + vkCmdSetScissor(currentCmd_, 0, 1, &sc); + + // Draw FXAA pass + renderFXAAPass(); + + } else if (fsr_.enabled && fsr_.sceneFramebuffer) { + // FSR1 upscale path — only runs when FXAA is not active. + // When both FSR1 and FXAA are enabled, FXAA took priority above. + vkCmdEndRenderPass(currentCmd_); + + // Transition scene color (1x resolve/color target): PRESENT_SRC_KHR → SHADER_READ_ONLY + transitionImageLayout(currentCmd_, fsr_.sceneColor.image, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); + + // Begin swapchain render pass at full resolution + VkRenderPassBeginInfo fsrRpInfo{}; + fsrRpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + fsrRpInfo.renderPass = vkCtx_->getImGuiRenderPass(); + fsrRpInfo.framebuffer = vkCtx_->getSwapchainFramebuffers()[imageIndex]; + fsrRpInfo.renderArea.offset = {0, 0}; + fsrRpInfo.renderArea.extent = vkCtx_->getSwapchainExtent(); + + bool fsrMsaaOn = (vkCtx_->getMsaaSamples() > VK_SAMPLE_COUNT_1_BIT); + VkClearValue fsrClearValues[4]{}; + fsrClearValues[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}}; + fsrClearValues[1].depthStencil = {1.0f, 0}; + fsrClearValues[2].color = {{0.0f, 0.0f, 0.0f, 1.0f}}; + fsrClearValues[3].depthStencil = {1.0f, 0}; + if (fsrMsaaOn) { + bool depthRes = (vkCtx_->getDepthResolveImageView() != VK_NULL_HANDLE); + fsrRpInfo.clearValueCount = depthRes ? 4 : 3; + } else { + fsrRpInfo.clearValueCount = 2; + } + fsrRpInfo.pClearValues = fsrClearValues; + + vkCmdBeginRenderPass(currentCmd_, &fsrRpInfo, VK_SUBPASS_CONTENTS_INLINE); + + VkExtent2D fsrExt = vkCtx_->getSwapchainExtent(); + VkViewport fsrVp{}; + fsrVp.width = static_cast(fsrExt.width); + fsrVp.height = static_cast(fsrExt.height); + fsrVp.maxDepth = 1.0f; + vkCmdSetViewport(currentCmd_, 0, 1, &fsrVp); + VkRect2D fsrSc{}; + fsrSc.extent = fsrExt; + vkCmdSetScissor(currentCmd_, 0, 1, &fsrSc); + + renderFSRUpscale(); + } + + currentCmd_ = VK_NULL_HANDLE; + camera_ = nullptr; + return inlineMode; +} + +void PostProcessPipeline::destroyAllResources() { + if (fsr_.sceneFramebuffer) destroyFSRResources(); + if (fsr2_.sceneFramebuffer) destroyFSR2Resources(); + if (fxaa_.sceneFramebuffer) destroyFXAAResources(); +} + +// ========================= Public API ========================= + +void PostProcessPipeline::setFXAAEnabled(bool enabled) { + if (fxaa_.enabled == enabled) return; + // FXAA is a post-process AA pass intended to supplement FSR temporal output. + // It conflicts with MSAA (which resolves AA during the scene render pass), so + // refuse to enable FXAA when hardware MSAA is active. + if (enabled && vkCtx_ && vkCtx_->getMsaaSamples() > VK_SAMPLE_COUNT_1_BIT) { + LOG_INFO("FXAA: blocked while MSAA is active — disable MSAA first"); + return; + } + fxaa_.enabled = enabled; + if (!enabled) { + fxaa_.needsRecreate = true; // defer destruction to next beginFrame() + } +} + +MsaaChangeRequest PostProcessPipeline::setFSREnabled(bool enabled) { + MsaaChangeRequest req; + if (fsr_.enabled == enabled) return req; + fsr_.enabled = enabled; + + if (enabled) { + // FSR1 upscaling renders its own AA — disable MSAA to avoid redundant work + if (vkCtx_ && vkCtx_->getMsaaSamples() > VK_SAMPLE_COUNT_1_BIT) { + req.requested = true; + req.samples = VK_SAMPLE_COUNT_1_BIT; + } + } else { + // Defer destruction to next beginFrame() — can't destroy mid-render + fsr_.needsRecreate = true; + } + // Resources created/destroyed lazily in beginFrame() + return req; +} + +void PostProcessPipeline::setFSRQuality(float scaleFactor) { + scaleFactor = glm::clamp(scaleFactor, 0.5f, 1.0f); + fsr_.scaleFactor = scaleFactor; + fsr2_.scaleFactor = scaleFactor; + // Don't destroy/recreate mid-frame — mark for lazy recreation in next beginFrame() + if (fsr_.enabled && fsr_.sceneFramebuffer) { + fsr_.needsRecreate = true; + } + if (fsr2_.enabled && fsr2_.sceneFramebuffer) { + fsr2_.needsRecreate = true; + fsr2_.needsHistoryReset = true; + } +} + +void PostProcessPipeline::setFSRSharpness(float sharpness) { + fsr_.sharpness = glm::clamp(sharpness, 0.0f, 2.0f); + fsr2_.sharpness = glm::clamp(sharpness, 0.0f, 2.0f); +} + +MsaaChangeRequest PostProcessPipeline::setFSR2Enabled(bool enabled, Camera* camera) { + MsaaChangeRequest req; + if (fsr2_.enabled == enabled) return req; + fsr2_.enabled = enabled; + + if (enabled) { + static bool initFramegenToggleFromEnv = false; + if (!initFramegenToggleFromEnv) { + initFramegenToggleFromEnv = true; + if (std::getenv("WOWEE_ENABLE_AMD_FSR3_FRAMEGEN_RUNTIME") != nullptr) { + fsr2_.amdFsr3FramegenEnabled = true; + } + } + // FSR2 replaces both FSR1 and MSAA + if (fsr_.enabled) { + fsr_.enabled = false; + fsr_.needsRecreate = true; + } + // FSR2 requires non-MSAA render pass (its framebuffer has 2 attachments) + if (vkCtx_ && vkCtx_->getMsaaSamples() > VK_SAMPLE_COUNT_1_BIT) { + req.requested = true; + req.samples = VK_SAMPLE_COUNT_1_BIT; + } + // Use FSR1's scale factor and sharpness as defaults + fsr2_.scaleFactor = fsr_.scaleFactor; + fsr2_.sharpness = fsr_.sharpness; + fsr2_.needsHistoryReset = true; + } else { + fsr2_.needsRecreate = true; + if (camera) camera->clearJitter(); + } + return req; +} + +void PostProcessPipeline::setFSR2DebugTuning(float jitterSign, float motionVecScaleX, float motionVecScaleY) { + fsr2_.jitterSign = glm::clamp(jitterSign, -2.0f, 2.0f); + fsr2_.motionVecScaleX = glm::clamp(motionVecScaleX, -2.0f, 2.0f); + fsr2_.motionVecScaleY = glm::clamp(motionVecScaleY, -2.0f, 2.0f); +} + +void PostProcessPipeline::setAmdFsr3FramegenEnabled(bool enabled) { + if (fsr2_.amdFsr3FramegenEnabled == enabled) return; + fsr2_.amdFsr3FramegenEnabled = enabled; +#if WOWEE_HAS_AMD_FSR3_FRAMEGEN + if (enabled) { + fsr2_.amdFsr3FramegenRuntimeActive = false; + fsr2_.framegenOutputValid = false; + fsr2_.needsRecreate = true; + fsr2_.needsHistoryReset = true; + fsr2_.amdFsr3FramegenRuntimeReady = false; + fsr2_.amdFsr3RuntimePath = "Path C"; + fsr2_.amdFsr3RuntimeLastError.clear(); + LOG_INFO("FSR3 framegen requested; runtime will initialize on next FSR2 resource creation."); + } else { + fsr2_.amdFsr3FramegenRuntimeActive = false; + fsr2_.amdFsr3FramegenRuntimeReady = false; + fsr2_.framegenOutputValid = false; + fsr2_.needsHistoryReset = true; + fsr2_.needsRecreate = true; + fsr2_.amdFsr3RuntimePath = "Path C"; + fsr2_.amdFsr3RuntimeLastError = "disabled by user"; + if (fsr2_.amdFsr3Runtime) { + fsr2_.amdFsr3Runtime->shutdown(); + fsr2_.amdFsr3Runtime.reset(); + } + LOG_INFO("FSR3 framegen disabled; forcing FSR2-only path rebuild."); + } +#else + fsr2_.amdFsr3FramegenRuntimeActive = false; + fsr2_.amdFsr3FramegenRuntimeReady = false; + fsr2_.framegenOutputValid = false; + if (enabled) { + LOG_WARNING("FSR3 framegen requested, but AMD FSR3 framegen SDK headers are unavailable in this build."); + } +#endif +} + +const char* PostProcessPipeline::getAmdFsr3FramegenRuntimePath() const { + return fsr2_.amdFsr3RuntimePath.c_str(); +} + +// ========================= FSR 1.0 Upscaling ========================= + +bool PostProcessPipeline::initFSRResources() { + if (!vkCtx_) return false; + + VkDevice device = vkCtx_->getDevice(); + VmaAllocator alloc = vkCtx_->getAllocator(); + VkExtent2D swapExtent = vkCtx_->getSwapchainExtent(); + VkSampleCountFlagBits msaa = vkCtx_->getMsaaSamples(); + bool useMsaa = (msaa > VK_SAMPLE_COUNT_1_BIT); + bool useDepthResolve = (vkCtx_->getDepthResolveImageView() != VK_NULL_HANDLE); + + fsr_.internalWidth = static_cast(swapExtent.width * fsr_.scaleFactor); + fsr_.internalHeight = static_cast(swapExtent.height * fsr_.scaleFactor); + fsr_.internalWidth = (fsr_.internalWidth + 1) & ~1u; + fsr_.internalHeight = (fsr_.internalHeight + 1) & ~1u; + + LOG_INFO("FSR: initializing at ", fsr_.internalWidth, "x", fsr_.internalHeight, + " -> ", swapExtent.width, "x", swapExtent.height, + " (scale=", fsr_.scaleFactor, ", MSAA=", static_cast(msaa), "x)"); + + VkFormat colorFmt = vkCtx_->getSwapchainFormat(); + VkFormat depthFmt = vkCtx_->getDepthFormat(); + + // sceneColor: always 1x, always sampled — this is what FSR reads + // Non-MSAA: direct render target. MSAA: resolve target. + fsr_.sceneColor = createImage(device, alloc, fsr_.internalWidth, fsr_.internalHeight, + colorFmt, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); + if (!fsr_.sceneColor.image) { + LOG_ERROR("FSR: failed to create scene color image"); + return false; + } + + // sceneDepth: matches current MSAA sample count + fsr_.sceneDepth = createImage(device, alloc, fsr_.internalWidth, fsr_.internalHeight, + depthFmt, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, msaa); + if (!fsr_.sceneDepth.image) { + LOG_ERROR("FSR: failed to create scene depth image"); + destroyFSRResources(); + return false; + } + + if (useMsaa) { + // sceneMsaaColor: multisampled color target + fsr_.sceneMsaaColor = createImage(device, alloc, fsr_.internalWidth, fsr_.internalHeight, + colorFmt, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, msaa); + if (!fsr_.sceneMsaaColor.image) { + LOG_ERROR("FSR: failed to create MSAA color image"); + destroyFSRResources(); + return false; + } + + if (useDepthResolve) { + fsr_.sceneDepthResolve = createImage(device, alloc, fsr_.internalWidth, fsr_.internalHeight, + depthFmt, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT); + if (!fsr_.sceneDepthResolve.image) { + LOG_ERROR("FSR: failed to create depth resolve image"); + destroyFSRResources(); + return false; + } + } + } + + // Build framebuffer matching the main render pass attachment layout: + // Non-MSAA: [color, depth] + // MSAA (no depth res): [msaaColor, depth, resolve] + // MSAA (depth res): [msaaColor, depth, resolve, depthResolve] + VkImageView fbAttachments[4]{}; + uint32_t fbCount; + if (useMsaa) { + fbAttachments[0] = fsr_.sceneMsaaColor.imageView; + fbAttachments[1] = fsr_.sceneDepth.imageView; + fbAttachments[2] = fsr_.sceneColor.imageView; // resolve target + fbCount = 3; + if (useDepthResolve) { + fbAttachments[3] = fsr_.sceneDepthResolve.imageView; + fbCount = 4; + } + } else { + fbAttachments[0] = fsr_.sceneColor.imageView; + fbAttachments[1] = fsr_.sceneDepth.imageView; + fbCount = 2; + } + + VkFramebufferCreateInfo fbInfo{}; + fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fbInfo.renderPass = vkCtx_->getImGuiRenderPass(); + fbInfo.attachmentCount = fbCount; + fbInfo.pAttachments = fbAttachments; + fbInfo.width = fsr_.internalWidth; + fbInfo.height = fsr_.internalHeight; + fbInfo.layers = 1; + + if (vkCreateFramebuffer(device, &fbInfo, nullptr, &fsr_.sceneFramebuffer) != VK_SUCCESS) { + LOG_ERROR("FSR: failed to create scene framebuffer"); + destroyFSRResources(); + return false; + } + + // Sampler for the resolved scene color + VkSamplerCreateInfo samplerInfo{}; + samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + samplerInfo.minFilter = VK_FILTER_LINEAR; + samplerInfo.magFilter = VK_FILTER_LINEAR; + samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; + fsr_.sceneSampler = vkCtx_->getOrCreateSampler(samplerInfo); + if (fsr_.sceneSampler == VK_NULL_HANDLE) { + LOG_ERROR("FSR: failed to create sampler"); + destroyFSRResources(); + return false; + } + + // Descriptor set layout: binding 0 = combined image sampler + VkDescriptorSetLayoutBinding binding{}; + binding.binding = 0; + binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + binding.descriptorCount = 1; + binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + + VkDescriptorSetLayoutCreateInfo layoutInfo{}; + layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + layoutInfo.bindingCount = 1; + layoutInfo.pBindings = &binding; + vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &fsr_.descSetLayout); + + VkDescriptorPoolSize poolSize{}; + poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + poolSize.descriptorCount = 1; + VkDescriptorPoolCreateInfo poolInfo{}; + poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolInfo.maxSets = 1; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &poolSize; + vkCreateDescriptorPool(device, &poolInfo, nullptr, &fsr_.descPool); + + VkDescriptorSetAllocateInfo dsAllocInfo{}; + dsAllocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + dsAllocInfo.descriptorPool = fsr_.descPool; + dsAllocInfo.descriptorSetCount = 1; + dsAllocInfo.pSetLayouts = &fsr_.descSetLayout; + vkAllocateDescriptorSets(device, &dsAllocInfo, &fsr_.descSet); + + // Always bind the 1x sceneColor (FSR reads the resolved image) + VkDescriptorImageInfo imgInfo{}; + imgInfo.sampler = fsr_.sceneSampler; + imgInfo.imageView = fsr_.sceneColor.imageView; + imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + VkWriteDescriptorSet write{}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstSet = fsr_.descSet; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &imgInfo; + vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); + + // Pipeline layout + VkPushConstantRange pc{}; + pc.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + pc.offset = 0; + pc.size = 64; + VkPipelineLayoutCreateInfo plCI{}; + plCI.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + plCI.setLayoutCount = 1; + plCI.pSetLayouts = &fsr_.descSetLayout; + plCI.pushConstantRangeCount = 1; + plCI.pPushConstantRanges = &pc; + vkCreatePipelineLayout(device, &plCI, nullptr, &fsr_.pipelineLayout); + + // Load shaders + VkShaderModule vertMod, fragMod; + if (!vertMod.loadFromFile(device, "assets/shaders/postprocess.vert.spv") || + !fragMod.loadFromFile(device, "assets/shaders/fsr_easu.frag.spv")) { + LOG_ERROR("FSR: failed to load shaders"); + destroyFSRResources(); + return false; + } + + // FSR upscale pipeline renders into the swapchain pass at full resolution + // Must match swapchain pass MSAA setting + fsr_.pipeline = PipelineBuilder() + .setShaders(vertMod.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + fragMod.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({}, {}) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setNoDepthTest() + .setColorBlendAttachment(PipelineBuilder::blendDisabled()) + .setMultisample(msaa) + .setLayout(fsr_.pipelineLayout) + .setRenderPass(vkCtx_->getImGuiRenderPass()) + .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) + .build(device, vkCtx_->getPipelineCache()); + + vertMod.destroy(); + fragMod.destroy(); + + if (!fsr_.pipeline) { + LOG_ERROR("FSR: failed to create upscale pipeline"); + destroyFSRResources(); + return false; + } + + LOG_INFO("FSR: initialized successfully"); + return true; +} + +void PostProcessPipeline::destroyFSRResources() { + if (!vkCtx_) return; + + VkDevice device = vkCtx_->getDevice(); + VmaAllocator alloc = vkCtx_->getAllocator(); + vkDeviceWaitIdle(device); + + if (fsr_.pipeline) { vkDestroyPipeline(device, fsr_.pipeline, nullptr); fsr_.pipeline = VK_NULL_HANDLE; } + if (fsr_.pipelineLayout) { vkDestroyPipelineLayout(device, fsr_.pipelineLayout, nullptr); fsr_.pipelineLayout = VK_NULL_HANDLE; } + if (fsr_.descPool) { vkDestroyDescriptorPool(device, fsr_.descPool, nullptr); fsr_.descPool = VK_NULL_HANDLE; fsr_.descSet = VK_NULL_HANDLE; } + if (fsr_.descSetLayout) { vkDestroyDescriptorSetLayout(device, fsr_.descSetLayout, nullptr); fsr_.descSetLayout = VK_NULL_HANDLE; } + if (fsr_.sceneFramebuffer) { vkDestroyFramebuffer(device, fsr_.sceneFramebuffer, nullptr); fsr_.sceneFramebuffer = VK_NULL_HANDLE; } + fsr_.sceneSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache + destroyImage(device, alloc, fsr_.sceneDepthResolve); + destroyImage(device, alloc, fsr_.sceneMsaaColor); + destroyImage(device, alloc, fsr_.sceneDepth); + destroyImage(device, alloc, fsr_.sceneColor); + + fsr_.internalWidth = 0; + fsr_.internalHeight = 0; +} + +void PostProcessPipeline::renderFSRUpscale() { + if (!fsr_.pipeline || currentCmd_ == VK_NULL_HANDLE) return; + + VkExtent2D outExtent = vkCtx_->getSwapchainExtent(); + float inW = static_cast(fsr_.internalWidth); + float inH = static_cast(fsr_.internalHeight); + float outW = static_cast(outExtent.width); + float outH = static_cast(outExtent.height); + + // FSR push constants + struct { + glm::vec4 con0; // inputSize.xy, 1/inputSize.xy + glm::vec4 con1; // inputSize.xy / outputSize.xy, 0.5 * inputSize.xy / outputSize.xy + glm::vec4 con2; // outputSize.xy, 1/outputSize.xy + glm::vec4 con3; // sharpness, 0, 0, 0 + } fsrConst; + + fsrConst.con0 = glm::vec4(inW, inH, 1.0f / inW, 1.0f / inH); + fsrConst.con1 = glm::vec4(inW / outW, inH / outH, 0.5f * inW / outW, 0.5f * inH / outH); + fsrConst.con2 = glm::vec4(outW, outH, 1.0f / outW, 1.0f / outH); + fsrConst.con3 = glm::vec4(fsr_.sharpness, 0.0f, 0.0f, 0.0f); + + vkCmdBindPipeline(currentCmd_, VK_PIPELINE_BIND_POINT_GRAPHICS, fsr_.pipeline); + vkCmdBindDescriptorSets(currentCmd_, VK_PIPELINE_BIND_POINT_GRAPHICS, + fsr_.pipelineLayout, 0, 1, &fsr_.descSet, 0, nullptr); + vkCmdPushConstants(currentCmd_, fsr_.pipelineLayout, + VK_SHADER_STAGE_FRAGMENT_BIT, 0, 64, &fsrConst); + vkCmdDraw(currentCmd_, 3, 1, 0, 0); +} + +// ========================= FSR 2.2 Temporal Upscaling ========================= + +float PostProcessPipeline::halton(uint32_t index, uint32_t base) { + float f = 1.0f; + float r = 0.0f; + uint32_t current = index; + while (current > 0) { + f /= static_cast(base); + r += f * static_cast(current % base); + current /= base; + } + return r; +} + +bool PostProcessPipeline::initFSR2Resources() { + if (!vkCtx_) return false; + + VkDevice device = vkCtx_->getDevice(); + VmaAllocator alloc = vkCtx_->getAllocator(); + VkExtent2D swapExtent = vkCtx_->getSwapchainExtent(); + + // Temporary stability fallback: keep FSR2 path at native internal resolution + // until temporal reprojection is reworked. + fsr2_.internalWidth = static_cast(swapExtent.width * fsr2_.scaleFactor); + fsr2_.internalHeight = static_cast(swapExtent.height * fsr2_.scaleFactor); + fsr2_.internalWidth = (fsr2_.internalWidth + 1) & ~1u; + fsr2_.internalHeight = (fsr2_.internalHeight + 1) & ~1u; + + LOG_INFO("FSR2: initializing at ", fsr2_.internalWidth, "x", fsr2_.internalHeight, + " -> ", swapExtent.width, "x", swapExtent.height, + " (scale=", fsr2_.scaleFactor, ")"); + fsr2_.useAmdBackend = false; + fsr2_.amdFsr3FramegenRuntimeActive = false; + fsr2_.amdFsr3FramegenRuntimeReady = false; + fsr2_.framegenOutputValid = false; + fsr2_.amdFsr3RuntimePath = "Path C"; + fsr2_.amdFsr3RuntimeLastError.clear(); + fsr2_.amdFsr3UpscaleDispatchCount = 0; + fsr2_.amdFsr3FramegenDispatchCount = 0; + fsr2_.amdFsr3FallbackCount = 0; + fsr2_.amdFsr3InteropSyncValue = 1; +#if WOWEE_HAS_AMD_FSR2 + LOG_INFO("FSR2: AMD FidelityFX SDK detected at build time."); +#else + LOG_WARNING("FSR2: AMD FidelityFX SDK not detected; using internal fallback path."); +#endif + + VkFormat colorFmt = vkCtx_->getSwapchainFormat(); + VkFormat depthFmt = vkCtx_->getDepthFormat(); + + // Scene color (internal resolution, 1x — FSR2 replaces MSAA) + fsr2_.sceneColor = createImage(device, alloc, fsr2_.internalWidth, fsr2_.internalHeight, + colorFmt, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); + if (!fsr2_.sceneColor.image) { LOG_ERROR("FSR2: failed to create scene color"); return false; } + + // Scene depth (internal resolution, 1x, sampled for motion vectors) + fsr2_.sceneDepth = createImage(device, alloc, fsr2_.internalWidth, fsr2_.internalHeight, + depthFmt, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); + if (!fsr2_.sceneDepth.image) { LOG_ERROR("FSR2: failed to create scene depth"); destroyFSR2Resources(); return false; } + + // Motion vector buffer (internal resolution) + fsr2_.motionVectors = createImage(device, alloc, fsr2_.internalWidth, fsr2_.internalHeight, + VK_FORMAT_R16G16_SFLOAT, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); + if (!fsr2_.motionVectors.image) { LOG_ERROR("FSR2: failed to create motion vectors"); destroyFSR2Resources(); return false; } + + // History buffers (display resolution, ping-pong) + for (int i = 0; i < 2; i++) { + fsr2_.history[i] = createImage(device, alloc, swapExtent.width, swapExtent.height, + VK_FORMAT_R16G16B16A16_SFLOAT, + VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); + if (!fsr2_.history[i].image) { LOG_ERROR("FSR2: failed to create history buffer ", i); destroyFSR2Resources(); return false; } + } + fsr2_.framegenOutput = createImage(device, alloc, swapExtent.width, swapExtent.height, + VK_FORMAT_R16G16B16A16_SFLOAT, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); + if (!fsr2_.framegenOutput.image) { LOG_ERROR("FSR2: failed to create framegen output"); destroyFSR2Resources(); return false; } + + // Scene framebuffer (non-MSAA: [color, depth]) + // Must use the same render pass as the swapchain — which must be non-MSAA when FSR2 is active + VkImageView fbAttachments[2] = { fsr2_.sceneColor.imageView, fsr2_.sceneDepth.imageView }; + VkFramebufferCreateInfo fbInfo{}; + fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fbInfo.renderPass = vkCtx_->getImGuiRenderPass(); + fbInfo.attachmentCount = 2; + fbInfo.pAttachments = fbAttachments; + fbInfo.width = fsr2_.internalWidth; + fbInfo.height = fsr2_.internalHeight; + fbInfo.layers = 1; + if (vkCreateFramebuffer(device, &fbInfo, nullptr, &fsr2_.sceneFramebuffer) != VK_SUCCESS) { + LOG_ERROR("FSR2: failed to create scene framebuffer"); + destroyFSR2Resources(); + return false; + } + + // Samplers + VkSamplerCreateInfo samplerInfo{}; + samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + samplerInfo.minFilter = VK_FILTER_LINEAR; + samplerInfo.magFilter = VK_FILTER_LINEAR; + samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + fsr2_.linearSampler = vkCtx_->getOrCreateSampler(samplerInfo); + + samplerInfo.minFilter = VK_FILTER_NEAREST; + samplerInfo.magFilter = VK_FILTER_NEAREST; + fsr2_.nearestSampler = vkCtx_->getOrCreateSampler(samplerInfo); + +#if WOWEE_HAS_AMD_FSR2 + // Initialize AMD FSR2 context; fall back to internal path on any failure. + fsr2_.amdScratchBufferSize = ffxFsr2GetScratchMemorySizeVK(vkCtx_->getPhysicalDevice()); + if (fsr2_.amdScratchBufferSize > 0) { + fsr2_.amdScratchBuffer = std::malloc(fsr2_.amdScratchBufferSize); + } + if (!fsr2_.amdScratchBuffer) { + LOG_WARNING("FSR2 AMD: failed to allocate scratch buffer, using internal fallback."); + } else { + FfxErrorCode ifaceErr = ffxFsr2GetInterfaceVK( + &fsr2_.amdInterface, + fsr2_.amdScratchBuffer, + fsr2_.amdScratchBufferSize, + vkCtx_->getPhysicalDevice(), + vkGetDeviceProcAddr); + if (ifaceErr != FFX_OK) { + LOG_WARNING("FSR2 AMD: ffxFsr2GetInterfaceVK failed (", static_cast(ifaceErr), "), using internal fallback."); + std::free(fsr2_.amdScratchBuffer); + fsr2_.amdScratchBuffer = nullptr; + fsr2_.amdScratchBufferSize = 0; + } else { + FfxFsr2ContextDescription ctxDesc{}; + ctxDesc.flags = FFX_FSR2_ENABLE_AUTO_EXPOSURE | FFX_FSR2_ENABLE_MOTION_VECTORS_JITTER_CANCELLATION; + ctxDesc.maxRenderSize.width = fsr2_.internalWidth; + ctxDesc.maxRenderSize.height = fsr2_.internalHeight; + ctxDesc.displaySize.width = swapExtent.width; + ctxDesc.displaySize.height = swapExtent.height; + ctxDesc.callbacks = fsr2_.amdInterface; + ctxDesc.device = ffxGetDeviceVK(vkCtx_->getDevice()); + ctxDesc.fpMessage = nullptr; + + FfxErrorCode ctxErr = ffxFsr2ContextCreate(&fsr2_.amdContext, &ctxDesc); + if (ctxErr == FFX_OK) { + fsr2_.useAmdBackend = true; + LOG_INFO("FSR2 AMD: context created successfully."); +#if WOWEE_HAS_AMD_FSR3_FRAMEGEN + // FSR3 frame generation runtime uses AMD FidelityFX SDK which can + // corrupt Vulkan driver state on NVIDIA GPUs when context creation + // fails, causing subsequent vkCmdBeginRenderPass to crash. + // Skip FSR3 frame gen entirely on non-AMD GPUs. + if (fsr2_.amdFsr3FramegenEnabled && vkCtx_->isAmdGpu()) { + fsr2_.amdFsr3FramegenRuntimeActive = false; + if (!fsr2_.amdFsr3Runtime) fsr2_.amdFsr3Runtime = std::make_unique(); + AmdFsr3RuntimeInitDesc fgInit{}; + fgInit.physicalDevice = vkCtx_->getPhysicalDevice(); + fgInit.device = vkCtx_->getDevice(); + fgInit.getDeviceProcAddr = vkGetDeviceProcAddr; + fgInit.maxRenderWidth = fsr2_.internalWidth; + fgInit.maxRenderHeight = fsr2_.internalHeight; + fgInit.displayWidth = swapExtent.width; + fgInit.displayHeight = swapExtent.height; + fgInit.colorFormat = VK_FORMAT_R16G16B16A16_SFLOAT; + fgInit.hdrInput = false; + fgInit.depthInverted = false; + fgInit.enableFrameGeneration = true; + fsr2_.amdFsr3FramegenRuntimeReady = fsr2_.amdFsr3Runtime->initialize(fgInit); + if (fsr2_.amdFsr3FramegenRuntimeReady) { + fsr2_.amdFsr3RuntimeLastError.clear(); + fsr2_.amdFsr3RuntimePath = "Path A"; + LOG_INFO("FSR3 framegen runtime library loaded from ", fsr2_.amdFsr3Runtime->loadedLibraryPath(), + " (upscale+framegen dispatch enabled)"); + } else { + fsr2_.amdFsr3RuntimePath = "Path C"; + fsr2_.amdFsr3RuntimeLastError = fsr2_.amdFsr3Runtime->lastError(); + LOG_WARNING("FSR3 framegen toggle is enabled, but runtime initialization failed. ", + "path=", fsr2_.amdFsr3RuntimePath, + " error=", fsr2_.amdFsr3RuntimeLastError.empty() ? "(none)" : fsr2_.amdFsr3RuntimeLastError, + " runtimeLib=", fsr2_.amdFsr3Runtime->loadedLibraryPath().empty() ? "(not loaded)" : fsr2_.amdFsr3Runtime->loadedLibraryPath()); + } + } +#endif + } else { + LOG_WARNING("FSR2 AMD: context creation failed (", static_cast(ctxErr), "), using internal fallback."); + std::free(fsr2_.amdScratchBuffer); + fsr2_.amdScratchBuffer = nullptr; + fsr2_.amdScratchBufferSize = 0; + } + } + } +#endif + + // --- Motion Vector Compute Pipeline --- + { + // Descriptor set layout: binding 0 = depth (sampler), binding 1 = motion vectors (storage image) + VkDescriptorSetLayoutBinding bindings[2] = {}; + bindings[0].binding = 0; + bindings[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + bindings[0].descriptorCount = 1; + bindings[0].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + bindings[1].binding = 1; + bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + bindings[1].descriptorCount = 1; + bindings[1].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + + VkDescriptorSetLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + layoutInfo.bindingCount = 2; + layoutInfo.pBindings = bindings; + vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &fsr2_.motionVecDescSetLayout); + + VkPushConstantRange pc{}; + pc.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + pc.offset = 0; + pc.size = 2 * sizeof(glm::mat4); // 128 bytes + + VkPipelineLayoutCreateInfo plCI{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + plCI.setLayoutCount = 1; + plCI.pSetLayouts = &fsr2_.motionVecDescSetLayout; + plCI.pushConstantRangeCount = 1; + plCI.pPushConstantRanges = &pc; + vkCreatePipelineLayout(device, &plCI, nullptr, &fsr2_.motionVecPipelineLayout); + + VkShaderModule compMod; + if (!compMod.loadFromFile(device, "assets/shaders/fsr2_motion.comp.spv")) { + LOG_ERROR("FSR2: failed to load motion vector compute shader"); + destroyFSR2Resources(); + return false; + } + + VkComputePipelineCreateInfo cpCI{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + cpCI.stage = compMod.stageInfo(VK_SHADER_STAGE_COMPUTE_BIT); + cpCI.layout = fsr2_.motionVecPipelineLayout; + if (vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &cpCI, nullptr, &fsr2_.motionVecPipeline) != VK_SUCCESS) { + LOG_ERROR("FSR2: failed to create motion vector pipeline"); + compMod.destroy(); + destroyFSR2Resources(); + return false; + } + compMod.destroy(); + + // Descriptor pool + set + VkDescriptorPoolSize poolSizes[2] = {}; + poolSizes[0] = {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1}; + poolSizes[1] = {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1}; + VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + poolInfo.maxSets = 1; + poolInfo.poolSizeCount = 2; + poolInfo.pPoolSizes = poolSizes; + vkCreateDescriptorPool(device, &poolInfo, nullptr, &fsr2_.motionVecDescPool); + + VkDescriptorSetAllocateInfo dsAI{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + dsAI.descriptorPool = fsr2_.motionVecDescPool; + dsAI.descriptorSetCount = 1; + dsAI.pSetLayouts = &fsr2_.motionVecDescSetLayout; + vkAllocateDescriptorSets(device, &dsAI, &fsr2_.motionVecDescSet); + + // Write descriptors + VkDescriptorImageInfo depthImgInfo{}; + depthImgInfo.sampler = fsr2_.nearestSampler; + depthImgInfo.imageView = fsr2_.sceneDepth.imageView; + depthImgInfo.imageLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; + + VkDescriptorImageInfo mvImgInfo{}; + mvImgInfo.imageView = fsr2_.motionVectors.imageView; + mvImgInfo.imageLayout = VK_IMAGE_LAYOUT_GENERAL; + + VkWriteDescriptorSet writes[2] = {}; + writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[0].dstSet = fsr2_.motionVecDescSet; + writes[0].dstBinding = 0; + writes[0].descriptorCount = 1; + writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + writes[0].pImageInfo = &depthImgInfo; + + writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[1].dstSet = fsr2_.motionVecDescSet; + writes[1].dstBinding = 1; + writes[1].descriptorCount = 1; + writes[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + writes[1].pImageInfo = &mvImgInfo; + + vkUpdateDescriptorSets(device, 2, writes, 0, nullptr); + } + + // --- Temporal Accumulation Compute Pipeline --- + { + // bindings: 0=sceneColor, 1=depth, 2=motionVectors, 3=historyInput, 4=historyOutput + VkDescriptorSetLayoutBinding bindings[5] = {}; + for (int i = 0; i < 4; i++) { + bindings[i].binding = i; + bindings[i].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + bindings[i].descriptorCount = 1; + bindings[i].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + } + bindings[4].binding = 4; + bindings[4].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + bindings[4].descriptorCount = 1; + bindings[4].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + + VkDescriptorSetLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + layoutInfo.bindingCount = 5; + layoutInfo.pBindings = bindings; + vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &fsr2_.accumulateDescSetLayout); + + VkPushConstantRange pc{}; + pc.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + pc.offset = 0; + pc.size = 4 * sizeof(glm::vec4); // 64 bytes + + VkPipelineLayoutCreateInfo plCI{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + plCI.setLayoutCount = 1; + plCI.pSetLayouts = &fsr2_.accumulateDescSetLayout; + plCI.pushConstantRangeCount = 1; + plCI.pPushConstantRanges = &pc; + vkCreatePipelineLayout(device, &plCI, nullptr, &fsr2_.accumulatePipelineLayout); + + VkShaderModule compMod; + if (!compMod.loadFromFile(device, "assets/shaders/fsr2_accumulate.comp.spv")) { + LOG_ERROR("FSR2: failed to load accumulation compute shader"); + destroyFSR2Resources(); + return false; + } + + VkComputePipelineCreateInfo cpCI{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + cpCI.stage = compMod.stageInfo(VK_SHADER_STAGE_COMPUTE_BIT); + cpCI.layout = fsr2_.accumulatePipelineLayout; + if (vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &cpCI, nullptr, &fsr2_.accumulatePipeline) != VK_SUCCESS) { + LOG_ERROR("FSR2: failed to create accumulation pipeline"); + compMod.destroy(); + destroyFSR2Resources(); + return false; + } + compMod.destroy(); + + // Descriptor pool: 2 sets (ping-pong), each with 4 samplers + 1 storage image + VkDescriptorPoolSize poolSizes[2] = {}; + poolSizes[0] = {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 8}; + poolSizes[1] = {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 2}; + VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + poolInfo.maxSets = 2; + poolInfo.poolSizeCount = 2; + poolInfo.pPoolSizes = poolSizes; + vkCreateDescriptorPool(device, &poolInfo, nullptr, &fsr2_.accumulateDescPool); + + // Allocate 2 descriptor sets (one per ping-pong direction) + VkDescriptorSetLayout layouts[2] = { fsr2_.accumulateDescSetLayout, fsr2_.accumulateDescSetLayout }; + VkDescriptorSetAllocateInfo dsAI{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + dsAI.descriptorPool = fsr2_.accumulateDescPool; + dsAI.descriptorSetCount = 2; + dsAI.pSetLayouts = layouts; + vkAllocateDescriptorSets(device, &dsAI, fsr2_.accumulateDescSets); + + // Write descriptors for both ping-pong sets + for (int pp = 0; pp < 2; pp++) { + int inputHistory = 1 - pp; // Read from the other + int outputHistory = pp; // Write to this one + + // The accumulation shader already performs custom Lanczos reconstruction. + // Use nearest here to avoid double filtering (linear + Lanczos) softening. + VkDescriptorImageInfo colorInfo{fsr2_.nearestSampler, fsr2_.sceneColor.imageView, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; + VkDescriptorImageInfo depthInfo{fsr2_.nearestSampler, fsr2_.sceneDepth.imageView, VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL}; + VkDescriptorImageInfo mvInfo{fsr2_.nearestSampler, fsr2_.motionVectors.imageView, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; + VkDescriptorImageInfo histInInfo{fsr2_.linearSampler, fsr2_.history[inputHistory].imageView, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; + VkDescriptorImageInfo histOutInfo{VK_NULL_HANDLE, fsr2_.history[outputHistory].imageView, VK_IMAGE_LAYOUT_GENERAL}; + + VkWriteDescriptorSet writes[5] = {}; + for (int w = 0; w < 5; w++) { + writes[w].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[w].dstSet = fsr2_.accumulateDescSets[pp]; + writes[w].dstBinding = w; + writes[w].descriptorCount = 1; + } + writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[0].pImageInfo = &colorInfo; + writes[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[1].pImageInfo = &depthInfo; + writes[2].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[2].pImageInfo = &mvInfo; + writes[3].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[3].pImageInfo = &histInInfo; + writes[4].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; writes[4].pImageInfo = &histOutInfo; + + vkUpdateDescriptorSets(device, 5, writes, 0, nullptr); + } + } + + // --- RCAS Sharpening Pipeline (fragment shader, fullscreen pass) --- + { + VkDescriptorSetLayoutBinding binding{}; + binding.binding = 0; + binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + binding.descriptorCount = 1; + binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + + VkDescriptorSetLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + layoutInfo.bindingCount = 1; + layoutInfo.pBindings = &binding; + vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &fsr2_.sharpenDescSetLayout); + + VkPushConstantRange pc{}; + pc.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + pc.offset = 0; + pc.size = sizeof(glm::vec4); + + VkPipelineLayoutCreateInfo plCI{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + plCI.setLayoutCount = 1; + plCI.pSetLayouts = &fsr2_.sharpenDescSetLayout; + plCI.pushConstantRangeCount = 1; + plCI.pPushConstantRanges = &pc; + vkCreatePipelineLayout(device, &plCI, nullptr, &fsr2_.sharpenPipelineLayout); + + VkShaderModule vertMod, fragMod; + if (!vertMod.loadFromFile(device, "assets/shaders/postprocess.vert.spv") || + !fragMod.loadFromFile(device, "assets/shaders/fsr2_sharpen.frag.spv")) { + LOG_ERROR("FSR2: failed to load sharpen shaders"); + destroyFSR2Resources(); + return false; + } + + fsr2_.sharpenPipeline = PipelineBuilder() + .setShaders(vertMod.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + fragMod.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({}, {}) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setNoDepthTest() + .setColorBlendAttachment(PipelineBuilder::blendDisabled()) + .setMultisample(VK_SAMPLE_COUNT_1_BIT) + .setLayout(fsr2_.sharpenPipelineLayout) + .setRenderPass(vkCtx_->getImGuiRenderPass()) + .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) + .build(device, vkCtx_->getPipelineCache()); + + vertMod.destroy(); + fragMod.destroy(); + + if (!fsr2_.sharpenPipeline) { + LOG_ERROR("FSR2: failed to create sharpen pipeline"); + destroyFSR2Resources(); + return false; + } + + // Descriptor pool + sets for sharpen pass (double-buffered to avoid race condition) + VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 2}; + VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + poolInfo.maxSets = 2; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &poolSize; + vkCreateDescriptorPool(device, &poolInfo, nullptr, &fsr2_.sharpenDescPool); + + VkDescriptorSetLayout layouts[2] = {fsr2_.sharpenDescSetLayout, fsr2_.sharpenDescSetLayout}; + VkDescriptorSetAllocateInfo dsAI{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + dsAI.descriptorPool = fsr2_.sharpenDescPool; + dsAI.descriptorSetCount = 2; + dsAI.pSetLayouts = layouts; + vkAllocateDescriptorSets(device, &dsAI, fsr2_.sharpenDescSets); + // Descriptors updated dynamically each frame to point at the correct history buffer + } + + fsr2_.needsHistoryReset = true; + fsr2_.frameIndex = 0; + LOG_INFO("FSR2: initialized successfully"); + return true; +} + +void PostProcessPipeline::destroyFSR2Resources() { + if (!vkCtx_) return; + + VkDevice device = vkCtx_->getDevice(); + VmaAllocator alloc = vkCtx_->getAllocator(); + vkDeviceWaitIdle(device); + +#if WOWEE_HAS_AMD_FSR2 + if (fsr2_.useAmdBackend) { + ffxFsr2ContextDestroy(&fsr2_.amdContext); + fsr2_.useAmdBackend = false; + } + if (fsr2_.amdScratchBuffer) { + std::free(fsr2_.amdScratchBuffer); + fsr2_.amdScratchBuffer = nullptr; + } + fsr2_.amdScratchBufferSize = 0; +#endif + fsr2_.amdFsr3FramegenRuntimeActive = false; + fsr2_.amdFsr3FramegenRuntimeReady = false; + fsr2_.framegenOutputValid = false; + fsr2_.amdFsr3RuntimePath = "Path C"; + fsr2_.amdFsr3RuntimeLastError.clear(); + fsr2_.amdFsr3InteropSyncValue = 1; +#if WOWEE_HAS_AMD_FSR3_FRAMEGEN + if (fsr2_.amdFsr3Runtime) { + fsr2_.amdFsr3Runtime->shutdown(); + fsr2_.amdFsr3Runtime.reset(); + } +#endif + + if (fsr2_.sharpenPipeline) { vkDestroyPipeline(device, fsr2_.sharpenPipeline, nullptr); fsr2_.sharpenPipeline = VK_NULL_HANDLE; } + if (fsr2_.sharpenPipelineLayout) { vkDestroyPipelineLayout(device, fsr2_.sharpenPipelineLayout, nullptr); fsr2_.sharpenPipelineLayout = VK_NULL_HANDLE; } + if (fsr2_.sharpenDescPool) { vkDestroyDescriptorPool(device, fsr2_.sharpenDescPool, nullptr); fsr2_.sharpenDescPool = VK_NULL_HANDLE; fsr2_.sharpenDescSets[0] = fsr2_.sharpenDescSets[1] = VK_NULL_HANDLE; } + if (fsr2_.sharpenDescSetLayout) { vkDestroyDescriptorSetLayout(device, fsr2_.sharpenDescSetLayout, nullptr); fsr2_.sharpenDescSetLayout = VK_NULL_HANDLE; } + + if (fsr2_.accumulatePipeline) { vkDestroyPipeline(device, fsr2_.accumulatePipeline, nullptr); fsr2_.accumulatePipeline = VK_NULL_HANDLE; } + if (fsr2_.accumulatePipelineLayout) { vkDestroyPipelineLayout(device, fsr2_.accumulatePipelineLayout, nullptr); fsr2_.accumulatePipelineLayout = VK_NULL_HANDLE; } + if (fsr2_.accumulateDescPool) { vkDestroyDescriptorPool(device, fsr2_.accumulateDescPool, nullptr); fsr2_.accumulateDescPool = VK_NULL_HANDLE; fsr2_.accumulateDescSets[0] = fsr2_.accumulateDescSets[1] = VK_NULL_HANDLE; } + if (fsr2_.accumulateDescSetLayout) { vkDestroyDescriptorSetLayout(device, fsr2_.accumulateDescSetLayout, nullptr); fsr2_.accumulateDescSetLayout = VK_NULL_HANDLE; } + + if (fsr2_.motionVecPipeline) { vkDestroyPipeline(device, fsr2_.motionVecPipeline, nullptr); fsr2_.motionVecPipeline = VK_NULL_HANDLE; } + if (fsr2_.motionVecPipelineLayout) { vkDestroyPipelineLayout(device, fsr2_.motionVecPipelineLayout, nullptr); fsr2_.motionVecPipelineLayout = VK_NULL_HANDLE; } + if (fsr2_.motionVecDescPool) { vkDestroyDescriptorPool(device, fsr2_.motionVecDescPool, nullptr); fsr2_.motionVecDescPool = VK_NULL_HANDLE; fsr2_.motionVecDescSet = VK_NULL_HANDLE; } + if (fsr2_.motionVecDescSetLayout) { vkDestroyDescriptorSetLayout(device, fsr2_.motionVecDescSetLayout, nullptr); fsr2_.motionVecDescSetLayout = VK_NULL_HANDLE; } + + if (fsr2_.sceneFramebuffer) { vkDestroyFramebuffer(device, fsr2_.sceneFramebuffer, nullptr); fsr2_.sceneFramebuffer = VK_NULL_HANDLE; } + fsr2_.linearSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache + fsr2_.nearestSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache + + destroyImage(device, alloc, fsr2_.motionVectors); + for (int i = 0; i < 2; i++) destroyImage(device, alloc, fsr2_.history[i]); + destroyImage(device, alloc, fsr2_.framegenOutput); + destroyImage(device, alloc, fsr2_.sceneDepth); + destroyImage(device, alloc, fsr2_.sceneColor); + + fsr2_.internalWidth = 0; + fsr2_.internalHeight = 0; +} + +void PostProcessPipeline::dispatchMotionVectors() { + if (!fsr2_.motionVecPipeline || currentCmd_ == VK_NULL_HANDLE) return; + + // Transition depth: DEPTH_STENCIL_ATTACHMENT → DEPTH_STENCIL_READ_ONLY + transitionImageLayout(currentCmd_, fsr2_.sceneDepth.image, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + + // Transition motion vectors: UNDEFINED → GENERAL + transitionImageLayout(currentCmd_, fsr2_.motionVectors.image, + VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + + vkCmdBindPipeline(currentCmd_, VK_PIPELINE_BIND_POINT_COMPUTE, fsr2_.motionVecPipeline); + vkCmdBindDescriptorSets(currentCmd_, VK_PIPELINE_BIND_POINT_COMPUTE, + fsr2_.motionVecPipelineLayout, 0, 1, &fsr2_.motionVecDescSet, 0, nullptr); + + // Reprojection with jittered matrices: + // reconstruct world position from current depth, then project into previous clip. + struct { + glm::mat4 prevViewProjection; + glm::mat4 invCurrentViewProj; + } pc; + + glm::mat4 currentVP = camera_->getViewProjectionMatrix(); + pc.prevViewProjection = fsr2_.prevViewProjection; + pc.invCurrentViewProj = glm::inverse(currentVP); + + vkCmdPushConstants(currentCmd_, fsr2_.motionVecPipelineLayout, + VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(pc), &pc); + + uint32_t gx = (fsr2_.internalWidth + 7) / 8; + uint32_t gy = (fsr2_.internalHeight + 7) / 8; + vkCmdDispatch(currentCmd_, gx, gy, 1); + + // Transition motion vectors: GENERAL → SHADER_READ_ONLY for accumulation + transitionImageLayout(currentCmd_, fsr2_.motionVectors.image, + VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); +} + +void PostProcessPipeline::dispatchTemporalAccumulate() { + if (!fsr2_.accumulatePipeline || currentCmd_ == VK_NULL_HANDLE) return; + + VkExtent2D swapExtent = vkCtx_->getSwapchainExtent(); + uint32_t outputIdx = fsr2_.currentHistory; + uint32_t inputIdx = 1 - outputIdx; + + // Transition scene color: PRESENT_SRC_KHR → SHADER_READ_ONLY + transitionImageLayout(currentCmd_, fsr2_.sceneColor.image, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + + // History layout lifecycle: + // First frame: both in UNDEFINED + // Subsequent frames: both in SHADER_READ_ONLY (output was transitioned for sharpen, + // input was left in SHADER_READ_ONLY from its sharpen read) + VkImageLayout historyOldLayout = fsr2_.needsHistoryReset + ? VK_IMAGE_LAYOUT_UNDEFINED + : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + // Transition history input: SHADER_READ_ONLY → SHADER_READ_ONLY (barrier for sync) + transitionImageLayout(currentCmd_, fsr2_.history[inputIdx].image, + historyOldLayout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, // sharpen read in previous frame + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + + // Transition history output: SHADER_READ_ONLY → GENERAL (for compute write) + transitionImageLayout(currentCmd_, fsr2_.history[outputIdx].image, + historyOldLayout, VK_IMAGE_LAYOUT_GENERAL, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + + vkCmdBindPipeline(currentCmd_, VK_PIPELINE_BIND_POINT_COMPUTE, fsr2_.accumulatePipeline); + vkCmdBindDescriptorSets(currentCmd_, VK_PIPELINE_BIND_POINT_COMPUTE, + fsr2_.accumulatePipelineLayout, 0, 1, &fsr2_.accumulateDescSets[outputIdx], 0, nullptr); + + // Push constants + struct { + glm::vec4 internalSize; + glm::vec4 displaySize; + glm::vec4 jitterOffset; + glm::vec4 params; + } pc; + + pc.internalSize = glm::vec4( + static_cast(fsr2_.internalWidth), static_cast(fsr2_.internalHeight), + 1.0f / fsr2_.internalWidth, 1.0f / fsr2_.internalHeight); + pc.displaySize = glm::vec4( + static_cast(swapExtent.width), static_cast(swapExtent.height), + 1.0f / swapExtent.width, 1.0f / swapExtent.height); + glm::vec2 jitter = camera_->getJitter(); + pc.jitterOffset = glm::vec4(jitter.x, jitter.y, 0.0f, 0.0f); + pc.params = glm::vec4( + fsr2_.needsHistoryReset ? 1.0f : 0.0f, + fsr2_.sharpness, + static_cast(fsr2_.convergenceFrame), + 0.0f); + + vkCmdPushConstants(currentCmd_, fsr2_.accumulatePipelineLayout, + VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(pc), &pc); + + uint32_t gx = (swapExtent.width + 7) / 8; + uint32_t gy = (swapExtent.height + 7) / 8; + vkCmdDispatch(currentCmd_, gx, gy, 1); + + fsr2_.needsHistoryReset = false; +} + +void PostProcessPipeline::dispatchAmdFsr2() { + if (currentCmd_ == VK_NULL_HANDLE || !camera_) return; +#if WOWEE_HAS_AMD_FSR2 + if (!fsr2_.useAmdBackend) return; + + VkExtent2D swapExtent = vkCtx_->getSwapchainExtent(); + uint32_t outputIdx = fsr2_.currentHistory; + + transitionImageLayout(currentCmd_, fsr2_.sceneColor.image, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + transitionImageLayout(currentCmd_, fsr2_.motionVectors.image, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + transitionImageLayout(currentCmd_, fsr2_.sceneDepth.image, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + transitionImageLayout(currentCmd_, fsr2_.history[outputIdx].image, + fsr2_.needsHistoryReset ? VK_IMAGE_LAYOUT_UNDEFINED : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_GENERAL, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + + FfxFsr2DispatchDescription desc{}; + desc.commandList = ffxGetCommandListVK(currentCmd_); + desc.color = ffxGetTextureResourceVK(&fsr2_.amdContext, + fsr2_.sceneColor.image, fsr2_.sceneColor.imageView, + fsr2_.internalWidth, fsr2_.internalHeight, vkCtx_->getSwapchainFormat(), + L"FSR2_InputColor", FFX_RESOURCE_STATE_COMPUTE_READ); + desc.depth = ffxGetTextureResourceVK(&fsr2_.amdContext, + fsr2_.sceneDepth.image, fsr2_.sceneDepth.imageView, + fsr2_.internalWidth, fsr2_.internalHeight, vkCtx_->getDepthFormat(), + L"FSR2_InputDepth", FFX_RESOURCE_STATE_COMPUTE_READ); + desc.motionVectors = ffxGetTextureResourceVK(&fsr2_.amdContext, + fsr2_.motionVectors.image, fsr2_.motionVectors.imageView, + fsr2_.internalWidth, fsr2_.internalHeight, VK_FORMAT_R16G16_SFLOAT, + L"FSR2_InputMotionVectors", FFX_RESOURCE_STATE_COMPUTE_READ); + desc.output = ffxGetTextureResourceVK(&fsr2_.amdContext, + fsr2_.history[outputIdx].image, fsr2_.history[outputIdx].imageView, + swapExtent.width, swapExtent.height, VK_FORMAT_R16G16B16A16_SFLOAT, + L"FSR2_Output", FFX_RESOURCE_STATE_UNORDERED_ACCESS); + + // Camera jitter is stored as NDC projection offsets; convert to render-pixel offsets. + // Do not apply jitterSign again here: it is already baked into camera jitter. + glm::vec2 jitterNdc = camera_->getJitter(); + desc.jitterOffset.x = jitterNdc.x * 0.5f * static_cast(fsr2_.internalWidth); + desc.jitterOffset.y = jitterNdc.y * 0.5f * static_cast(fsr2_.internalHeight); + desc.motionVectorScale.x = static_cast(fsr2_.internalWidth) * fsr2_.motionVecScaleX; + desc.motionVectorScale.y = static_cast(fsr2_.internalHeight) * fsr2_.motionVecScaleY; + desc.renderSize.width = fsr2_.internalWidth; + desc.renderSize.height = fsr2_.internalHeight; + desc.enableSharpening = false; // Keep existing RCAS post pass. + desc.sharpness = 0.0f; + desc.frameTimeDelta = glm::max(0.001f, lastDeltaTime_ * 1000.0f); + desc.preExposure = 1.0f; + desc.reset = fsr2_.needsHistoryReset; + desc.cameraNear = camera_->getNearPlane(); + desc.cameraFar = camera_->getFarPlane(); + desc.cameraFovAngleVertical = glm::radians(camera_->getFovDegrees()); + desc.viewSpaceToMetersFactor = 1.0f; + desc.enableAutoReactive = false; + + FfxErrorCode dispatchErr = ffxFsr2ContextDispatch(&fsr2_.amdContext, &desc); + if (dispatchErr != FFX_OK) { + LOG_WARNING("FSR2 AMD: dispatch failed (", static_cast(dispatchErr), "), forcing history reset."); + fsr2_.needsHistoryReset = true; + } else { + fsr2_.needsHistoryReset = false; + } +#endif +} + +void PostProcessPipeline::dispatchAmdFsr3Framegen() { +#if WOWEE_HAS_AMD_FSR3_FRAMEGEN + if (!fsr2_.amdFsr3FramegenEnabled) { + fsr2_.amdFsr3FramegenRuntimeActive = false; + return; + } + if (!fsr2_.amdFsr3Runtime || !fsr2_.amdFsr3FramegenRuntimeReady) { + fsr2_.amdFsr3FramegenRuntimeActive = false; + return; + } + uint32_t outputIdx = fsr2_.currentHistory; + transitionImageLayout(currentCmd_, fsr2_.sceneColor.image, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + transitionImageLayout(currentCmd_, fsr2_.motionVectors.image, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + transitionImageLayout(currentCmd_, fsr2_.sceneDepth.image, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + transitionImageLayout(currentCmd_, fsr2_.history[outputIdx].image, + fsr2_.needsHistoryReset ? VK_IMAGE_LAYOUT_UNDEFINED : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_GENERAL, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + if (fsr2_.amdFsr3FramegenEnabled && fsr2_.framegenOutput.image) { + transitionImageLayout(currentCmd_, fsr2_.framegenOutput.image, + fsr2_.framegenOutputValid ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_GENERAL, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); + } + + AmdFsr3RuntimeDispatchDesc fgDispatch{}; + fgDispatch.commandBuffer = currentCmd_; + fgDispatch.colorImage = fsr2_.sceneColor.image; + fgDispatch.depthImage = fsr2_.sceneDepth.image; + fgDispatch.motionVectorImage = fsr2_.motionVectors.image; + fgDispatch.outputImage = fsr2_.history[fsr2_.currentHistory].image; + fgDispatch.renderWidth = fsr2_.internalWidth; + fgDispatch.renderHeight = fsr2_.internalHeight; + fgDispatch.outputWidth = vkCtx_->getSwapchainExtent().width; + fgDispatch.outputHeight = vkCtx_->getSwapchainExtent().height; + fgDispatch.colorFormat = VK_FORMAT_R16G16B16A16_SFLOAT; + fgDispatch.depthFormat = vkCtx_->getDepthFormat(); + fgDispatch.motionVectorFormat = VK_FORMAT_R16G16_SFLOAT; + fgDispatch.outputFormat = VK_FORMAT_R16G16B16A16_SFLOAT; + fgDispatch.frameGenOutputImage = fsr2_.framegenOutput.image; + glm::vec2 jitterNdc = camera_ ? camera_->getJitter() : glm::vec2(0.0f); + fgDispatch.jitterX = jitterNdc.x * 0.5f * static_cast(fsr2_.internalWidth); + fgDispatch.jitterY = jitterNdc.y * 0.5f * static_cast(fsr2_.internalHeight); + fgDispatch.motionScaleX = static_cast(fsr2_.internalWidth) * fsr2_.motionVecScaleX; + fgDispatch.motionScaleY = static_cast(fsr2_.internalHeight) * fsr2_.motionVecScaleY; + fgDispatch.frameTimeDeltaMs = glm::max(0.001f, lastDeltaTime_ * 1000.0f); + fgDispatch.cameraNear = camera_ ? camera_->getNearPlane() : 0.1f; + fgDispatch.cameraFar = camera_ ? camera_->getFarPlane() : 1000.0f; + fgDispatch.cameraFovYRadians = camera_ ? glm::radians(camera_->getFovDegrees()) : 1.0f; + fgDispatch.reset = fsr2_.needsHistoryReset; + + + if (!fsr2_.amdFsr3Runtime->dispatchUpscale(fgDispatch)) { + static bool warnedRuntimeDispatch = false; + if (!warnedRuntimeDispatch) { + warnedRuntimeDispatch = true; + LOG_WARNING("FSR3 runtime upscale dispatch failed; falling back to FSR2 dispatch output."); + } + fsr2_.amdFsr3RuntimeLastError = fsr2_.amdFsr3Runtime->lastError(); + fsr2_.amdFsr3FallbackCount++; + fsr2_.amdFsr3FramegenRuntimeActive = false; + return; + } + fsr2_.amdFsr3RuntimeLastError.clear(); + fsr2_.amdFsr3UpscaleDispatchCount++; + + if (!fsr2_.amdFsr3FramegenEnabled) { + fsr2_.amdFsr3FramegenRuntimeActive = false; + return; + } + if (!fsr2_.amdFsr3Runtime->isFrameGenerationReady()) { + fsr2_.amdFsr3FramegenRuntimeActive = false; + return; + } + if (!fsr2_.amdFsr3Runtime->dispatchFrameGeneration(fgDispatch)) { + static bool warnedFgDispatch = false; + if (!warnedFgDispatch) { + warnedFgDispatch = true; + LOG_WARNING("FSR3 runtime frame generation dispatch failed; using upscaled output only."); + } + fsr2_.amdFsr3RuntimeLastError = fsr2_.amdFsr3Runtime->lastError(); + fsr2_.amdFsr3FallbackCount++; + fsr2_.amdFsr3FramegenRuntimeActive = false; + return; + } + fsr2_.amdFsr3RuntimeLastError.clear(); + fsr2_.amdFsr3FramegenDispatchCount++; + fsr2_.framegenOutputValid = true; + fsr2_.amdFsr3FramegenRuntimeActive = true; +#else + fsr2_.amdFsr3FramegenRuntimeActive = false; +#endif +} + +void PostProcessPipeline::renderFSR2Sharpen() { + if (!fsr2_.sharpenPipeline || currentCmd_ == VK_NULL_HANDLE) return; + + VkExtent2D ext = vkCtx_->getSwapchainExtent(); + uint32_t outputIdx = fsr2_.currentHistory; + + // Use per-frame descriptor set to avoid race with in-flight command buffers + uint32_t frameIdx = vkCtx_->getCurrentFrame(); + VkDescriptorSet descSet = fsr2_.sharpenDescSets[frameIdx]; + + // Update sharpen descriptor to point at current history output + VkDescriptorImageInfo imgInfo{}; + imgInfo.sampler = fsr2_.linearSampler; + if (fsr2_.useAmdBackend) { + imgInfo.imageView = (fsr2_.amdFsr3FramegenEnabled && fsr2_.amdFsr3FramegenRuntimeActive && fsr2_.framegenOutput.imageView) + ? fsr2_.framegenOutput.imageView + : fsr2_.history[outputIdx].imageView; + } else { + imgInfo.imageView = fsr2_.sceneColor.imageView; + } + imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = descSet; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &imgInfo; + vkUpdateDescriptorSets(vkCtx_->getDevice(), 1, &write, 0, nullptr); + + vkCmdBindPipeline(currentCmd_, VK_PIPELINE_BIND_POINT_GRAPHICS, fsr2_.sharpenPipeline); + vkCmdBindDescriptorSets(currentCmd_, VK_PIPELINE_BIND_POINT_GRAPHICS, + fsr2_.sharpenPipelineLayout, 0, 1, &descSet, 0, nullptr); + + glm::vec4 params(1.0f / ext.width, 1.0f / ext.height, fsr2_.sharpness, 0.0f); + vkCmdPushConstants(currentCmd_, fsr2_.sharpenPipelineLayout, + VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(glm::vec4), ¶ms); + + vkCmdDraw(currentCmd_, 3, 1, 0, 0); +} + +// ========================= FXAA Post-Process ========================= + +bool PostProcessPipeline::initFXAAResources() { + if (!vkCtx_) return false; + + VkDevice device = vkCtx_->getDevice(); + VmaAllocator alloc = vkCtx_->getAllocator(); + VkExtent2D ext = vkCtx_->getSwapchainExtent(); + VkSampleCountFlagBits msaa = vkCtx_->getMsaaSamples(); + bool useMsaa = (msaa > VK_SAMPLE_COUNT_1_BIT); + bool useDepthResolve = (vkCtx_->getDepthResolveImageView() != VK_NULL_HANDLE); + + LOG_INFO("FXAA: initializing at ", ext.width, "x", ext.height, + " (MSAA=", static_cast(msaa), "x)"); + + VkFormat colorFmt = vkCtx_->getSwapchainFormat(); + VkFormat depthFmt = vkCtx_->getDepthFormat(); + + // sceneColor: 1x resolved color target — FXAA reads from here + fxaa_.sceneColor = createImage(device, alloc, ext.width, ext.height, + colorFmt, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); + if (!fxaa_.sceneColor.image) { + LOG_ERROR("FXAA: failed to create scene color image"); + return false; + } + + // sceneDepth: depth buffer at current MSAA sample count + fxaa_.sceneDepth = createImage(device, alloc, ext.width, ext.height, + depthFmt, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, msaa); + if (!fxaa_.sceneDepth.image) { + LOG_ERROR("FXAA: failed to create scene depth image"); + destroyFXAAResources(); + return false; + } + + if (useMsaa) { + fxaa_.sceneMsaaColor = createImage(device, alloc, ext.width, ext.height, + colorFmt, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, msaa); + if (!fxaa_.sceneMsaaColor.image) { + LOG_ERROR("FXAA: failed to create MSAA color image"); + destroyFXAAResources(); + return false; + } + if (useDepthResolve) { + fxaa_.sceneDepthResolve = createImage(device, alloc, ext.width, ext.height, + depthFmt, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT); + if (!fxaa_.sceneDepthResolve.image) { + LOG_ERROR("FXAA: failed to create depth resolve image"); + destroyFXAAResources(); + return false; + } + } + } + + // Framebuffer — same attachment layout as main render pass + VkImageView fbAttachments[4]{}; + uint32_t fbCount; + if (useMsaa) { + fbAttachments[0] = fxaa_.sceneMsaaColor.imageView; + fbAttachments[1] = fxaa_.sceneDepth.imageView; + fbAttachments[2] = fxaa_.sceneColor.imageView; // resolve target + fbCount = 3; + if (useDepthResolve) { + fbAttachments[3] = fxaa_.sceneDepthResolve.imageView; + fbCount = 4; + } + } else { + fbAttachments[0] = fxaa_.sceneColor.imageView; + fbAttachments[1] = fxaa_.sceneDepth.imageView; + fbCount = 2; + } + + VkFramebufferCreateInfo fbInfo{}; + fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fbInfo.renderPass = vkCtx_->getImGuiRenderPass(); + fbInfo.attachmentCount = fbCount; + fbInfo.pAttachments = fbAttachments; + fbInfo.width = ext.width; + fbInfo.height = ext.height; + fbInfo.layers = 1; + if (vkCreateFramebuffer(device, &fbInfo, nullptr, &fxaa_.sceneFramebuffer) != VK_SUCCESS) { + LOG_ERROR("FXAA: failed to create scene framebuffer"); + destroyFXAAResources(); + return false; + } + + // Sampler + VkSamplerCreateInfo samplerInfo{}; + samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + samplerInfo.minFilter = VK_FILTER_LINEAR; + samplerInfo.magFilter = VK_FILTER_LINEAR; + samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; + fxaa_.sceneSampler = vkCtx_->getOrCreateSampler(samplerInfo); + if (fxaa_.sceneSampler == VK_NULL_HANDLE) { + LOG_ERROR("FXAA: failed to create sampler"); + destroyFXAAResources(); + return false; + } + + // Descriptor set layout: binding 0 = combined image sampler + VkDescriptorSetLayoutBinding binding{}; + binding.binding = 0; + binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + binding.descriptorCount = 1; + binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + VkDescriptorSetLayoutCreateInfo layoutInfo{}; + layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + layoutInfo.bindingCount = 1; + layoutInfo.pBindings = &binding; + vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &fxaa_.descSetLayout); + + VkDescriptorPoolSize poolSize{}; + poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + poolSize.descriptorCount = 1; + VkDescriptorPoolCreateInfo poolInfo{}; + poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + poolInfo.maxSets = 1; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &poolSize; + vkCreateDescriptorPool(device, &poolInfo, nullptr, &fxaa_.descPool); + + VkDescriptorSetAllocateInfo dsAllocInfo{}; + dsAllocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + dsAllocInfo.descriptorPool = fxaa_.descPool; + dsAllocInfo.descriptorSetCount = 1; + dsAllocInfo.pSetLayouts = &fxaa_.descSetLayout; + vkAllocateDescriptorSets(device, &dsAllocInfo, &fxaa_.descSet); + + // Bind the resolved 1x sceneColor + VkDescriptorImageInfo imgInfo{}; + imgInfo.sampler = fxaa_.sceneSampler; + imgInfo.imageView = fxaa_.sceneColor.imageView; + imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + VkWriteDescriptorSet write{}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstSet = fxaa_.descSet; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &imgInfo; + vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); + + // Pipeline layout — push constant holds vec4(rcpFrame.xy, sharpness, pad) + VkPushConstantRange pc{}; + pc.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + pc.offset = 0; + pc.size = 16; // vec4 + VkPipelineLayoutCreateInfo plCI{}; + plCI.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + plCI.setLayoutCount = 1; + plCI.pSetLayouts = &fxaa_.descSetLayout; + plCI.pushConstantRangeCount = 1; + plCI.pPushConstantRanges = &pc; + vkCreatePipelineLayout(device, &plCI, nullptr, &fxaa_.pipelineLayout); + + // FXAA pipeline — fullscreen triangle into the swapchain render pass + // Uses VK_SAMPLE_COUNT_1_BIT: it always runs after MSAA resolve. + VkShaderModule vertMod, fragMod; + if (!vertMod.loadFromFile(device, "assets/shaders/postprocess.vert.spv") || + !fragMod.loadFromFile(device, "assets/shaders/fxaa.frag.spv")) { + LOG_ERROR("FXAA: failed to load shaders"); + destroyFXAAResources(); + return false; + } + + fxaa_.pipeline = PipelineBuilder() + .setShaders(vertMod.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), + fragMod.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) + .setVertexInput({}, {}) + .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) + .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) + .setNoDepthTest() + .setColorBlendAttachment(PipelineBuilder::blendDisabled()) + .setMultisample(VK_SAMPLE_COUNT_1_BIT) // swapchain pass is always 1x + .setLayout(fxaa_.pipelineLayout) + .setRenderPass(vkCtx_->getImGuiRenderPass()) + .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) + .build(device, vkCtx_->getPipelineCache()); + + vertMod.destroy(); + fragMod.destroy(); + + if (!fxaa_.pipeline) { + LOG_ERROR("FXAA: failed to create pipeline"); + destroyFXAAResources(); + return false; + } + + LOG_INFO("FXAA: initialized successfully"); + return true; +} + +void PostProcessPipeline::destroyFXAAResources() { + if (!vkCtx_) return; + VkDevice device = vkCtx_->getDevice(); + VmaAllocator alloc = vkCtx_->getAllocator(); + vkDeviceWaitIdle(device); + + if (fxaa_.pipeline) { vkDestroyPipeline(device, fxaa_.pipeline, nullptr); fxaa_.pipeline = VK_NULL_HANDLE; } + if (fxaa_.pipelineLayout) { vkDestroyPipelineLayout(device, fxaa_.pipelineLayout, nullptr); fxaa_.pipelineLayout = VK_NULL_HANDLE; } + if (fxaa_.descPool) { vkDestroyDescriptorPool(device, fxaa_.descPool, nullptr); fxaa_.descPool = VK_NULL_HANDLE; fxaa_.descSet = VK_NULL_HANDLE; } + if (fxaa_.descSetLayout) { vkDestroyDescriptorSetLayout(device, fxaa_.descSetLayout, nullptr); fxaa_.descSetLayout = VK_NULL_HANDLE; } + if (fxaa_.sceneFramebuffer) { vkDestroyFramebuffer(device, fxaa_.sceneFramebuffer, nullptr); fxaa_.sceneFramebuffer = VK_NULL_HANDLE; } + fxaa_.sceneSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache + destroyImage(device, alloc, fxaa_.sceneDepthResolve); + destroyImage(device, alloc, fxaa_.sceneMsaaColor); + destroyImage(device, alloc, fxaa_.sceneDepth); + destroyImage(device, alloc, fxaa_.sceneColor); +} + +void PostProcessPipeline::renderFXAAPass() { + if (!fxaa_.pipeline || currentCmd_ == VK_NULL_HANDLE) return; + VkExtent2D ext = vkCtx_->getSwapchainExtent(); + + vkCmdBindPipeline(currentCmd_, VK_PIPELINE_BIND_POINT_GRAPHICS, fxaa_.pipeline); + vkCmdBindDescriptorSets(currentCmd_, VK_PIPELINE_BIND_POINT_GRAPHICS, + fxaa_.pipelineLayout, 0, 1, &fxaa_.descSet, 0, nullptr); + + // Pass rcpFrame + sharpness + effect flag (vec4, 16 bytes). + // When FSR2/FSR3 is active alongside FXAA, forward FSR2's sharpness so the + // post-FXAA unsharp-mask step restores the crispness that FXAA's blur removes. + float sharpness = fsr2_.enabled ? fsr2_.sharpness : 0.0f; + float pc[4] = { + 1.0f / static_cast(ext.width), + 1.0f / static_cast(ext.height), + sharpness, + 0.0f + }; + vkCmdPushConstants(currentCmd_, fxaa_.pipelineLayout, + VK_SHADER_STAGE_FRAGMENT_BIT, 0, 16, pc); + + vkCmdDraw(currentCmd_, 3, 1, 0, 0); // fullscreen triangle +} + +} // namespace rendering +} // namespace wowee diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index e4735b0b..5ce45f0f 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -39,6 +39,7 @@ #include "core/logger.hpp" #include "game/world.hpp" #include "game/zone_manager.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/audio_engine.hpp" #include "audio/music_manager.hpp" #include "audio/footstep_manager.hpp" @@ -56,6 +57,8 @@ #include "rendering/vk_pipeline.hpp" #include "rendering/vk_utils.hpp" #include "rendering/amd_fsr3_runtime.hpp" +#include "rendering/spell_visual_system.hpp" +#include "rendering/post_process_pipeline.hpp" #include #include #include @@ -83,6 +86,19 @@ namespace wowee { namespace rendering { +// Audio accessor pass-throughs — delegate to AudioCoordinator (owned by Application). +// These remain until §4.2 (AnimationController) removes Renderer's last audio usage. +audio::MusicManager* Renderer::getMusicManager() { return audioCoordinator_ ? audioCoordinator_->getMusicManager() : nullptr; } +audio::FootstepManager* Renderer::getFootstepManager() { return audioCoordinator_ ? audioCoordinator_->getFootstepManager() : nullptr; } +audio::ActivitySoundManager* Renderer::getActivitySoundManager() { return audioCoordinator_ ? audioCoordinator_->getActivitySoundManager() : nullptr; } +audio::MountSoundManager* Renderer::getMountSoundManager() { return audioCoordinator_ ? audioCoordinator_->getMountSoundManager() : nullptr; } +audio::NpcVoiceManager* Renderer::getNpcVoiceManager() { return audioCoordinator_ ? audioCoordinator_->getNpcVoiceManager() : nullptr; } +audio::AmbientSoundManager* Renderer::getAmbientSoundManager() { return audioCoordinator_ ? audioCoordinator_->getAmbientSoundManager() : nullptr; } +audio::UiSoundManager* Renderer::getUiSoundManager() { return audioCoordinator_ ? audioCoordinator_->getUiSoundManager() : nullptr; } +audio::CombatSoundManager* Renderer::getCombatSoundManager() { return audioCoordinator_ ? audioCoordinator_->getCombatSoundManager() : nullptr; } +audio::SpellSoundManager* Renderer::getSpellSoundManager() { return audioCoordinator_ ? audioCoordinator_->getSpellSoundManager() : nullptr; } +audio::MovementSoundManager* Renderer::getMovementSoundManager() { return audioCoordinator_ ? audioCoordinator_->getMovementSoundManager() : nullptr; } + struct EmoteInfo { uint32_t animId = 0; uint32_t dbcId = 0; // EmotesText.dbc record ID (for CMSG_TEXT_EMOTE) @@ -728,28 +744,18 @@ bool Renderer::initialize(core::Window* win) { zoneManager->enrichFromDBC(assetManager); } - // Initialize AudioEngine (singleton) - if (!audio::AudioEngine::instance().initialize()) { - LOG_WARNING("Failed to initialize AudioEngine - audio will be disabled"); - } - - // Create music manager (initialized later with asset manager) - musicManager = std::make_unique(); - footstepManager = std::make_unique(); - activitySoundManager = std::make_unique(); - mountSoundManager = std::make_unique(); - npcVoiceManager = std::make_unique(); - ambientSoundManager = std::make_unique(); - uiSoundManager = std::make_unique(); - combatSoundManager = std::make_unique(); - spellSoundManager = std::make_unique(); - movementSoundManager = std::make_unique(); + // Audio is now owned by AudioCoordinator (created by Application). + // Renderer receives AudioCoordinator* via setAudioCoordinator(). // Create secondary command buffer resources for multithreaded rendering if (!createSecondaryCommandResources()) { LOG_WARNING("Failed to create secondary command buffers — falling back to single-threaded rendering"); } + // Create PostProcessPipeline (§4.3 — owns FSR/FXAA/FSR2/FSR3/brightness) + postProcessPipeline_ = std::make_unique(); + postProcessPipeline_->initialize(vkCtx); + LOG_INFO("Renderer initialized"); return true; } @@ -827,31 +833,20 @@ void Renderer::shutdown() { wmoRenderer.reset(); } + // Shutdown SpellVisualSystem before M2Renderer (it holds M2Renderer pointer) (§4.4) + if (spellVisualSystem_) { + spellVisualSystem_->shutdown(); + spellVisualSystem_.reset(); + } + LOG_WARNING("Renderer::shutdown - m2Renderer..."); if (m2Renderer) { m2Renderer->shutdown(); m2Renderer.reset(); } - LOG_WARNING("Renderer::shutdown - musicManager..."); - if (musicManager) { - musicManager->shutdown(); - musicManager.reset(); - } - LOG_WARNING("Renderer::shutdown - footstepManager..."); - if (footstepManager) { - footstepManager->shutdown(); - footstepManager.reset(); - } - LOG_WARNING("Renderer::shutdown - activitySoundManager..."); - if (activitySoundManager) { - activitySoundManager->shutdown(); - activitySoundManager.reset(); - } - - LOG_WARNING("Renderer::shutdown - AudioEngine..."); - // Shutdown AudioEngine singleton - audio::AudioEngine::instance().shutdown(); + // Audio shutdown is handled by AudioCoordinator (owned by Application). + audioCoordinator_ = nullptr; // Cleanup Vulkan selection circle resources if (vkCtx) { @@ -864,9 +859,11 @@ void Renderer::shutdown() { if (overlayPipelineLayout) { vkDestroyPipelineLayout(device, overlayPipelineLayout, nullptr); overlayPipelineLayout = VK_NULL_HANDLE; } } - destroyFSRResources(); - destroyFSR2Resources(); - destroyFXAAResources(); + // Shutdown post-process pipeline (FSR/FXAA/FSR2 resources) (§4.3) + if (postProcessPipeline_) { + postProcessPipeline_->shutdown(); + postProcessPipeline_.reset(); + } destroyPerFrameResources(); zoneManager.reset(); @@ -905,7 +902,7 @@ void Renderer::setMsaaSamples(VkSampleCountFlagBits samples) { if (!vkCtx) return; // FSR2 requires non-MSAA render pass — block MSAA changes while FSR2 is active - if (fsr2_.enabled && samples > VK_SAMPLE_COUNT_1_BIT) return; + if (postProcessPipeline_ && postProcessPipeline_->isFsr2BlockingMsaa() && samples > VK_SAMPLE_COUNT_1_BIT) return; // Clamp to device maximum VkSampleCountFlagBits maxSamples = vkCtx->getMaxUsableSampleCount(); @@ -969,9 +966,7 @@ void Renderer::applyMsaaChange() { VkDevice device = vkCtx->getDevice(); if (selCirclePipeline) { vkDestroyPipeline(device, selCirclePipeline, nullptr); selCirclePipeline = VK_NULL_HANDLE; } if (overlayPipeline) { vkDestroyPipeline(device, overlayPipeline, nullptr); overlayPipeline = VK_NULL_HANDLE; } - if (fsr_.sceneFramebuffer) destroyFSRResources(); // Will be lazily recreated in beginFrame() - if (fsr2_.sceneFramebuffer) destroyFSR2Resources(); - if (fxaa_.sceneFramebuffer) destroyFXAAResources(); // Will be lazily recreated in beginFrame() + if (postProcessPipeline_) postProcessPipeline_->destroyAllResources(); // Will be lazily recreated in beginFrame() // Reinitialize ImGui Vulkan backend with new MSAA sample count ImGui_ImplVulkan_Shutdown(); @@ -1001,47 +996,8 @@ void Renderer::beginFrame() { applyMsaaChange(); } - // FSR resource management (safe: between frames, no command buffer in flight) - if (fsr_.needsRecreate && fsr_.sceneFramebuffer) { - destroyFSRResources(); - fsr_.needsRecreate = false; - if (!fsr_.enabled) LOG_INFO("FSR: disabled"); - } - if (fsr_.enabled && !fsr2_.enabled && !fsr_.sceneFramebuffer) { - if (!initFSRResources()) { - LOG_ERROR("FSR: initialization failed, disabling"); - fsr_.enabled = false; - } - } - - // FSR 2.2 resource management - if (fsr2_.needsRecreate && fsr2_.sceneFramebuffer) { - destroyFSR2Resources(); - fsr2_.needsRecreate = false; - if (!fsr2_.enabled) LOG_INFO("FSR2: disabled"); - } - if (fsr2_.enabled && !fsr2_.sceneFramebuffer) { - if (!initFSR2Resources()) { - LOG_ERROR("FSR2: initialization failed, disabling"); - fsr2_.enabled = false; - } - } - - // FXAA resource management — FXAA can coexist with FSR1 and FSR3. - // When both FXAA and FSR3 are enabled, FXAA runs as a post-FSR3 pass. - // Do not force this pass for ghost mode; keep AA quality strictly user-controlled. - const bool useFXAAPostPass = fxaa_.enabled; - if ((fxaa_.needsRecreate || !useFXAAPostPass) && fxaa_.sceneFramebuffer) { - destroyFXAAResources(); - fxaa_.needsRecreate = false; - if (!useFXAAPostPass) LOG_INFO("FXAA: disabled"); - } - if (useFXAAPostPass && !fxaa_.sceneFramebuffer) { - if (!initFXAAResources()) { - LOG_ERROR("FXAA: initialization failed, disabling"); - fxaa_.enabled = false; - } - } + // Post-process resource management (§4.3 — delegates to PostProcessPipeline) + if (postProcessPipeline_) postProcessPipeline_->manageResources(); // Handle swapchain recreation if needed if (vkCtx->isSwapchainDirty()) { @@ -1050,20 +1006,8 @@ void Renderer::beginFrame() { if (waterRenderer) { waterRenderer->recreatePipelines(); } - // Recreate FSR resources for new swapchain dimensions - if (fsr_.enabled && !fsr2_.enabled) { - destroyFSRResources(); - initFSRResources(); - } - if (fsr2_.enabled) { - destroyFSR2Resources(); - initFSR2Resources(); - } - // Recreate FXAA resources for new swapchain dimensions. - if (useFXAAPostPass) { - destroyFXAAResources(); - initFXAAResources(); - } + // Recreate post-process resources for new swapchain dimensions + if (postProcessPipeline_) postProcessPipeline_->handleSwapchainResize(); } // Acquire swapchain image and begin command buffer @@ -1073,35 +1017,8 @@ void Renderer::beginFrame() { return; } - // FSR2 jitter pattern. - if (fsr2_.enabled && fsr2_.sceneFramebuffer && camera) { - if (!fsr2_.useAmdBackend) { - camera->setJitter(0.0f, 0.0f); - } else { -#if WOWEE_HAS_AMD_FSR2 - // AMD-recommended jitter sequence in pixel space, converted to NDC projection offset. - int32_t phaseCount = ffxFsr2GetJitterPhaseCount( - static_cast(fsr2_.internalWidth), - static_cast(vkCtx->getSwapchainExtent().width)); - float jitterX = 0.0f; - float jitterY = 0.0f; - if (phaseCount > 0 && - ffxFsr2GetJitterOffset(&jitterX, &jitterY, static_cast(fsr2_.frameIndex % static_cast(phaseCount)), phaseCount) == FFX_OK) { - float ndcJx = (2.0f * jitterX) / static_cast(fsr2_.internalWidth); - float ndcJy = (2.0f * jitterY) / static_cast(fsr2_.internalHeight); - // Keep projection jitter and FSR dispatch jitter in sync. - camera->setJitter(fsr2_.jitterSign * ndcJx, fsr2_.jitterSign * ndcJy); - } else { - camera->setJitter(0.0f, 0.0f); - } -#else - const float jitterScale = 0.5f; - float jx = (halton(fsr2_.frameIndex + 1, 2) - 0.5f) * 2.0f * jitterScale / static_cast(fsr2_.internalWidth); - float jy = (halton(fsr2_.frameIndex + 1, 3) - 0.5f) * 2.0f * jitterScale / static_cast(fsr2_.internalHeight); - camera->setJitter(fsr2_.jitterSign * jx, fsr2_.jitterSign * jy); -#endif - } - } + // FSR2 jitter pattern (§4.3 — delegates to PostProcessPipeline) + if (postProcessPipeline_ && camera) postProcessPipeline_->applyJitter(camera.get()); // Update per-frame UBO with current camera/lighting state updatePerFrameUBO(); @@ -1140,24 +1057,16 @@ void Renderer::beginFrame() { } // !skipPrePasses // --- Begin render pass --- - // If FSR is enabled, render scene to off-screen target at reduced resolution. - // Otherwise, render directly to swapchain. + // Select framebuffer: PP off-screen target or swapchain (§4.3 — PostProcessPipeline) VkRenderPassBeginInfo rpInfo{}; rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; rpInfo.renderPass = vkCtx->getImGuiRenderPass(); VkExtent2D renderExtent; - if (fsr2_.enabled && fsr2_.sceneFramebuffer) { - rpInfo.framebuffer = fsr2_.sceneFramebuffer; - renderExtent = { fsr2_.internalWidth, fsr2_.internalHeight }; - } else if (useFXAAPostPass && fxaa_.sceneFramebuffer) { - // FXAA takes priority over FSR1: renders at native res with AA post-process. - // When both FSR1 and FXAA are enabled, FXAA wins (native res, no downscale). - rpInfo.framebuffer = fxaa_.sceneFramebuffer; - renderExtent = vkCtx->getSwapchainExtent(); // native resolution — no downscaling - } else if (fsr_.enabled && fsr_.sceneFramebuffer) { - rpInfo.framebuffer = fsr_.sceneFramebuffer; - renderExtent = { fsr_.internalWidth, fsr_.internalHeight }; + VkFramebuffer ppFB = postProcessPipeline_ ? postProcessPipeline_->getSceneFramebuffer() : VK_NULL_HANDLE; + if (ppFB != VK_NULL_HANDLE) { + rpInfo.framebuffer = ppFB; + renderExtent = postProcessPipeline_->getSceneRenderExtent(); } else { rpInfo.framebuffer = vkCtx->getSwapchainFramebuffers()[currentImageIndex]; renderExtent = vkCtx->getSwapchainExtent(); @@ -1213,223 +1122,10 @@ void Renderer::endFrame() { // post-proc paths end it and begin a new INLINE pass for the swapchain output. endFrameInlineMode_ = false; - if (fsr2_.enabled && fsr2_.sceneFramebuffer) { - // End the off-screen scene render pass - vkCmdEndRenderPass(currentCmd); - - if (fsr2_.useAmdBackend) { - // Compute passes: motion vectors -> temporal accumulation - dispatchMotionVectors(); - if (fsr2_.amdFsr3FramegenEnabled && fsr2_.amdFsr3FramegenRuntimeReady) { - dispatchAmdFsr3Framegen(); - if (!fsr2_.amdFsr3FramegenRuntimeActive) { - dispatchAmdFsr2(); - } - } else { - dispatchAmdFsr2(); - } - - // Transition post-FSR input for sharpen pass. - if (fsr2_.amdFsr3FramegenRuntimeActive && fsr2_.framegenOutput.image) { - transitionImageLayout(currentCmd, fsr2_.framegenOutput.image, - VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); - fsr2_.framegenOutputValid = true; - } else { - transitionImageLayout(currentCmd, fsr2_.history[fsr2_.currentHistory].image, - VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); - } - } else { - transitionImageLayout(currentCmd, fsr2_.sceneColor.image, - VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); - } - - // FSR3+FXAA combined: re-point FXAA's descriptor to the FSR3 temporal output - // so renderFXAAPass() applies spatial AA on the temporally-stabilized frame. - // This must happen outside the render pass (descriptor updates are CPU-side). - if (fxaa_.enabled && fxaa_.descSet && fxaa_.sceneSampler) { - VkImageView fsr3OutputView = VK_NULL_HANDLE; - if (fsr2_.useAmdBackend) { - if (fsr2_.amdFsr3FramegenRuntimeActive && fsr2_.framegenOutput.image) - fsr3OutputView = fsr2_.framegenOutput.imageView; - else if (fsr2_.history[fsr2_.currentHistory].image) - fsr3OutputView = fsr2_.history[fsr2_.currentHistory].imageView; - } else if (fsr2_.history[fsr2_.currentHistory].image) { - fsr3OutputView = fsr2_.history[fsr2_.currentHistory].imageView; - } - if (fsr3OutputView) { - VkDescriptorImageInfo imgInfo{}; - imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - imgInfo.imageView = fsr3OutputView; - imgInfo.sampler = fxaa_.sceneSampler; - VkWriteDescriptorSet write{}; - write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - write.dstSet = fxaa_.descSet; - write.dstBinding = 0; - write.descriptorCount = 1; - write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - write.pImageInfo = &imgInfo; - vkUpdateDescriptorSets(vkCtx->getDevice(), 1, &write, 0, nullptr); - } - } - - // Begin swapchain render pass at full resolution for sharpening + ImGui - VkRenderPassBeginInfo rpInfo{}; - rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; - rpInfo.renderPass = vkCtx->getImGuiRenderPass(); - rpInfo.framebuffer = vkCtx->getSwapchainFramebuffers()[currentImageIndex]; - rpInfo.renderArea.offset = {0, 0}; - rpInfo.renderArea.extent = vkCtx->getSwapchainExtent(); - - bool msaaOn = (vkCtx->getMsaaSamples() > VK_SAMPLE_COUNT_1_BIT); - VkClearValue clearValues[4]{}; - clearValues[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}}; - clearValues[1].depthStencil = {1.0f, 0}; - clearValues[2].color = {{0.0f, 0.0f, 0.0f, 1.0f}}; - clearValues[3].depthStencil = {1.0f, 0}; - rpInfo.clearValueCount = msaaOn ? (vkCtx->getDepthResolveImageView() ? 4u : 3u) : 2u; - rpInfo.pClearValues = clearValues; - - endFrameInlineMode_ = true; vkCmdBeginRenderPass(currentCmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE); - - VkExtent2D ext = vkCtx->getSwapchainExtent(); - VkViewport vp{}; - vp.width = static_cast(ext.width); - vp.height = static_cast(ext.height); - vp.maxDepth = 1.0f; - vkCmdSetViewport(currentCmd, 0, 1, &vp); - VkRect2D sc{}; - sc.extent = ext; - vkCmdSetScissor(currentCmd, 0, 1, &sc); - - // When FXAA is also enabled: apply FXAA on the FSR3 temporal output instead - // of RCAS sharpening. FXAA descriptor is temporarily pointed to the FSR3 - // history buffer (which is already in SHADER_READ_ONLY_OPTIMAL). This gives - // FSR3 temporal stability + FXAA spatial edge smoothing ("ultra quality native"). - if (fxaa_.enabled && fxaa_.pipeline && fxaa_.descSet) { - renderFXAAPass(); - } else { - // Draw RCAS sharpening from accumulated history buffer - renderFSR2Sharpen(); - } - - // Restore FXAA descriptor to its normal scene color source so standalone - // FXAA frames are not affected by the FSR3 history pointer set above. - if (fxaa_.enabled && fxaa_.descSet && fxaa_.sceneSampler && fxaa_.sceneColor.imageView) { - VkDescriptorImageInfo restoreInfo{}; - restoreInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - restoreInfo.imageView = fxaa_.sceneColor.imageView; - restoreInfo.sampler = fxaa_.sceneSampler; - VkWriteDescriptorSet restoreWrite{}; - restoreWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - restoreWrite.dstSet = fxaa_.descSet; - restoreWrite.dstBinding = 0; - restoreWrite.descriptorCount = 1; - restoreWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - restoreWrite.pImageInfo = &restoreInfo; - vkUpdateDescriptorSets(vkCtx->getDevice(), 1, &restoreWrite, 0, nullptr); - } - - // Maintain frame bookkeeping - fsr2_.prevViewProjection = camera->getViewProjectionMatrix(); - fsr2_.prevJitter = camera->getJitter(); - camera->clearJitter(); - if (fsr2_.useAmdBackend) { - fsr2_.currentHistory = 1 - fsr2_.currentHistory; - } - fsr2_.frameIndex = (fsr2_.frameIndex + 1) % 256; // Wrap to keep Halton values well-distributed - - } else if (fxaa_.enabled && fxaa_.sceneFramebuffer) { - // End the off-screen scene render pass - vkCmdEndRenderPass(currentCmd); - - // Transition resolved scene color: PRESENT_SRC_KHR → SHADER_READ_ONLY - transitionImageLayout(currentCmd, fxaa_.sceneColor.image, - VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); - - // Begin swapchain render pass (1x — no MSAA on the output pass) - VkRenderPassBeginInfo rpInfo{}; - rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; - rpInfo.renderPass = vkCtx->getImGuiRenderPass(); - rpInfo.framebuffer = vkCtx->getSwapchainFramebuffers()[currentImageIndex]; - rpInfo.renderArea.offset = {0, 0}; - rpInfo.renderArea.extent = vkCtx->getSwapchainExtent(); - // The swapchain render pass always has 2 attachments when MSAA is off; - // FXAA output goes to the non-MSAA swapchain directly. - VkClearValue fxaaClear[2]{}; - fxaaClear[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}}; - fxaaClear[1].depthStencil = {1.0f, 0}; - rpInfo.clearValueCount = 2; - rpInfo.pClearValues = fxaaClear; - - vkCmdBeginRenderPass(currentCmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE); - - VkExtent2D ext = vkCtx->getSwapchainExtent(); - VkViewport vp{}; - vp.width = static_cast(ext.width); - vp.height = static_cast(ext.height); - vp.maxDepth = 1.0f; - vkCmdSetViewport(currentCmd, 0, 1, &vp); - VkRect2D sc{}; - sc.extent = ext; - vkCmdSetScissor(currentCmd, 0, 1, &sc); - - // Draw FXAA pass - renderFXAAPass(); - - } else if (fsr_.enabled && fsr_.sceneFramebuffer) { - // FSR1 upscale path — only runs when FXAA is not active. - // When both FSR1 and FXAA are enabled, FXAA took priority above. - vkCmdEndRenderPass(currentCmd); - - // Transition scene color (1x resolve/color target): PRESENT_SRC_KHR → SHADER_READ_ONLY - transitionImageLayout(currentCmd, fsr_.sceneColor.image, - VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT); - - // Begin swapchain render pass at full resolution - VkRenderPassBeginInfo fsrRpInfo{}; - fsrRpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; - fsrRpInfo.renderPass = vkCtx->getImGuiRenderPass(); - fsrRpInfo.framebuffer = vkCtx->getSwapchainFramebuffers()[currentImageIndex]; - fsrRpInfo.renderArea.offset = {0, 0}; - fsrRpInfo.renderArea.extent = vkCtx->getSwapchainExtent(); - - bool fsrMsaaOn = (vkCtx->getMsaaSamples() > VK_SAMPLE_COUNT_1_BIT); - VkClearValue fsrClearValues[4]{}; - fsrClearValues[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}}; - fsrClearValues[1].depthStencil = {1.0f, 0}; - fsrClearValues[2].color = {{0.0f, 0.0f, 0.0f, 1.0f}}; - fsrClearValues[3].depthStencil = {1.0f, 0}; - if (fsrMsaaOn) { - bool depthRes = (vkCtx->getDepthResolveImageView() != VK_NULL_HANDLE); - fsrRpInfo.clearValueCount = depthRes ? 4 : 3; - } else { - fsrRpInfo.clearValueCount = 2; - } - fsrRpInfo.pClearValues = fsrClearValues; - - vkCmdBeginRenderPass(currentCmd, &fsrRpInfo, VK_SUBPASS_CONTENTS_INLINE); - - VkExtent2D fsrExt = vkCtx->getSwapchainExtent(); - VkViewport fsrVp{}; - fsrVp.width = static_cast(fsrExt.width); - fsrVp.height = static_cast(fsrExt.height); - fsrVp.maxDepth = 1.0f; - vkCmdSetViewport(currentCmd, 0, 1, &fsrVp); - VkRect2D fsrSc{}; - fsrSc.extent = fsrExt; - vkCmdSetScissor(currentCmd, 0, 1, &fsrSc); - - renderFSRUpscale(); + // Post-process execution (§4.3 — delegates to PostProcessPipeline) + if (postProcessPipeline_) { + endFrameInlineMode_ = postProcessPipeline_->executePostProcessing( + currentCmd, currentImageIndex, camera.get(), lastDeltaTime_); } // ImGui rendering — must respect the subpass contents mode of the @@ -1714,9 +1410,9 @@ void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float h " fidgets=", mountAnims_.fidgets.size()); // Notify mount sound manager - if (mountSoundManager) { + if (getMountSoundManager()) { bool isFlying = taxiFlight_; // Taxi flights are flying mounts - mountSoundManager->onMount(mountDisplayId, isFlying, modelPath); + getMountSoundManager()->onMount(mountDisplayId, isFlying, modelPath); } } @@ -1737,8 +1433,8 @@ void Renderer::clearMount() { } // Notify mount sound manager - if (mountSoundManager) { - mountSoundManager->onDismount(); + if (getMountSoundManager()) { + getMountSoundManager()->onDismount(); } } @@ -1999,8 +1695,8 @@ void Renderer::updateCharacterAnimation() { mountAction_ = MountAction::Jump; mountActionPhase_ = 1; // Start in airborne phase mountAnimId = mountAnims_.jumpLoop; - if (mountSoundManager) { - mountSoundManager->playJumpSound(); + if (getMountSoundManager()) { + getMountSoundManager()->playJumpSound(); } if (cameraController) { cameraController->triggerMountJump(); @@ -2013,8 +1709,8 @@ void Renderer::updateCharacterAnimation() { mountActionPhase_ = 0; mountAnimId = mountAnims_.rearUp; // Trigger semantic rear-up sound - if (mountSoundManager) { - mountSoundManager->playRearUpSound(); + if (getMountSoundManager()) { + getMountSoundManager()->playRearUpSound(); } } } @@ -2043,8 +1739,8 @@ void Renderer::updateCharacterAnimation() { mountActionPhase_ = 2; mountAnimId = mountAnims_.jumpEnd; // Trigger semantic landing sound - if (mountSoundManager) { - mountSoundManager->playLandSound(); + if (getMountSoundManager()) { + getMountSoundManager()->playLandSound(); } } else if (mountActionPhase_ == 1 && grounded && mountAnims_.jumpEnd == 0) { // No JumpEnd animation, return directly to movement after landing @@ -2133,13 +1829,13 @@ void Renderer::updateCharacterAnimation() { } // Idle ambient sounds: snorts and whinnies only, infrequent - if (!moving && mountSoundManager) { + if (!moving && getMountSoundManager()) { mountIdleSoundTimer_ += lastDeltaTime_; static std::mt19937 soundRng(std::random_device{}()); static float nextIdleSoundTime = std::uniform_real_distribution(45.0f, 90.0f)(soundRng); if (mountIdleSoundTimer_ >= nextIdleSoundTime) { - mountSoundManager->playIdleSound(); + getMountSoundManager()->playIdleSound(); mountIdleSoundTimer_ = 0.0f; nextIdleSoundTime = std::uniform_real_distribution(45.0f, 90.0f)(soundRng); } @@ -2730,199 +2426,11 @@ void Renderer::stopChargeEffect() { } } -// ─── Spell Visual Effects ──────────────────────────────────────────────────── - -void Renderer::loadSpellVisualDbc() { - if (spellVisualDbcLoaded_) return; - spellVisualDbcLoaded_ = true; // Set early to prevent re-entry on failure - - if (!cachedAssetManager) { - cachedAssetManager = core::Application::getInstance().getAssetManager(); - } - if (!cachedAssetManager) return; - - auto* layout = pipeline::getActiveDBCLayout(); - const pipeline::DBCFieldMap* svLayout = layout ? layout->getLayout("SpellVisual") : nullptr; - const pipeline::DBCFieldMap* kitLayout = layout ? layout->getLayout("SpellVisualKit") : nullptr; - const pipeline::DBCFieldMap* fxLayout = layout ? layout->getLayout("SpellVisualEffectName") : nullptr; - - uint32_t svCastKitField = svLayout ? (*svLayout)["CastKit"] : 2; - uint32_t svImpactKitField = svLayout ? (*svLayout)["ImpactKit"] : 3; - uint32_t svMissileField = svLayout ? (*svLayout)["MissileModel"] : 8; - uint32_t kitSpecial0Field = kitLayout ? (*kitLayout)["SpecialEffect0"] : 11; - uint32_t kitBaseField = kitLayout ? (*kitLayout)["BaseEffect"] : 5; - uint32_t fxFilePathField = fxLayout ? (*fxLayout)["FilePath"] : 2; - - // Helper to look up effectName path from a kit ID - // Load SpellVisualEffectName.dbc — ID → M2 path - auto fxDbc = cachedAssetManager->loadDBC("SpellVisualEffectName.dbc"); - if (!fxDbc || !fxDbc->isLoaded() || fxDbc->getFieldCount() <= fxFilePathField) { - LOG_DEBUG("SpellVisual: SpellVisualEffectName.dbc unavailable (fc=", - fxDbc ? fxDbc->getFieldCount() : 0, ")"); - return; - } - std::unordered_map effectPaths; // effectNameId → path - for (uint32_t i = 0; i < fxDbc->getRecordCount(); ++i) { - uint32_t id = fxDbc->getUInt32(i, 0); - std::string p = fxDbc->getString(i, fxFilePathField); - if (id && !p.empty()) effectPaths[id] = p; - } - - // Load SpellVisualKit.dbc — kitId → best SpellVisualEffectName ID - auto kitDbc = cachedAssetManager->loadDBC("SpellVisualKit.dbc"); - std::unordered_map kitToEffectName; // kitId → effectNameId - if (kitDbc && kitDbc->isLoaded()) { - uint32_t fc = kitDbc->getFieldCount(); - for (uint32_t i = 0; i < kitDbc->getRecordCount(); ++i) { - uint32_t kitId = kitDbc->getUInt32(i, 0); - if (!kitId) continue; - // Prefer SpecialEffect0, fall back to BaseEffect - uint32_t eff = 0; - if (kitSpecial0Field < fc) eff = kitDbc->getUInt32(i, kitSpecial0Field); - if (!eff && kitBaseField < fc) eff = kitDbc->getUInt32(i, kitBaseField); - if (eff) kitToEffectName[kitId] = eff; - } - } - - // Helper: resolve path for a given kit ID - auto kitPath = [&](uint32_t kitId) -> std::string { - if (!kitId) return {}; - auto kitIt = kitToEffectName.find(kitId); - if (kitIt == kitToEffectName.end()) return {}; - auto fxIt = effectPaths.find(kitIt->second); - return (fxIt != effectPaths.end()) ? fxIt->second : std::string{}; - }; - auto missilePath = [&](uint32_t effId) -> std::string { - if (!effId) return {}; - auto fxIt = effectPaths.find(effId); - return (fxIt != effectPaths.end()) ? fxIt->second : std::string{}; - }; - - // Load SpellVisual.dbc — visualId → cast/impact M2 paths via kit chain - auto svDbc = cachedAssetManager->loadDBC("SpellVisual.dbc"); - if (!svDbc || !svDbc->isLoaded()) { - LOG_DEBUG("SpellVisual: SpellVisual.dbc unavailable"); - return; - } - uint32_t svFc = svDbc->getFieldCount(); - uint32_t loadedCast = 0, loadedImpact = 0; - for (uint32_t i = 0; i < svDbc->getRecordCount(); ++i) { - uint32_t vid = svDbc->getUInt32(i, 0); - if (!vid) continue; - - // Cast path: CastKit → SpecialEffect0/BaseEffect, fallback to MissileModel - { - std::string path; - if (svCastKitField < svFc) - path = kitPath(svDbc->getUInt32(i, svCastKitField)); - if (path.empty() && svMissileField < svFc) - path = missilePath(svDbc->getUInt32(i, svMissileField)); - if (!path.empty()) { spellVisualCastPath_[vid] = path; ++loadedCast; } - } - // Impact path: ImpactKit → SpecialEffect0/BaseEffect, fallback to MissileModel - { - std::string path; - if (svImpactKitField < svFc) - path = kitPath(svDbc->getUInt32(i, svImpactKitField)); - if (path.empty() && svMissileField < svFc) - path = missilePath(svDbc->getUInt32(i, svMissileField)); - if (!path.empty()) { spellVisualImpactPath_[vid] = path; ++loadedImpact; } - } - } - LOG_INFO("SpellVisual: loaded cast=", loadedCast, " impact=", loadedImpact, - " visual→M2 mappings (of ", svDbc->getRecordCount(), " records)"); -} +// ─── Spell Visual Effects — delegated to SpellVisualSystem (§4.4) ──────────── void Renderer::playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition, bool useImpactKit) { - if (!m2Renderer || visualId == 0) return; - - if (!cachedAssetManager) - cachedAssetManager = core::Application::getInstance().getAssetManager(); - if (!cachedAssetManager) return; - - if (!spellVisualDbcLoaded_) loadSpellVisualDbc(); - - // Select cast or impact path map - auto& pathMap = useImpactKit ? spellVisualImpactPath_ : spellVisualCastPath_; - auto pathIt = pathMap.find(visualId); - if (pathIt == pathMap.end()) return; // No model for this visual - - const std::string& modelPath = pathIt->second; - - // Get or assign a model ID for this path - auto midIt = spellVisualModelIds_.find(modelPath); - uint32_t modelId = 0; - if (midIt != spellVisualModelIds_.end()) { - modelId = midIt->second; - } else { - if (nextSpellVisualModelId_ >= 999800) { - LOG_WARNING("SpellVisual: model ID pool exhausted"); - return; - } - modelId = nextSpellVisualModelId_++; - spellVisualModelIds_[modelPath] = modelId; - } - - // Skip models that have previously failed to load (avoid repeated I/O) - if (spellVisualFailedModels_.count(modelId)) return; - - // Load the M2 model if not already loaded - if (!m2Renderer->hasModel(modelId)) { - auto m2Data = cachedAssetManager->readFile(modelPath); - if (m2Data.empty()) { - LOG_DEBUG("SpellVisual: could not read model: ", modelPath); - spellVisualFailedModels_.insert(modelId); - return; - } - pipeline::M2Model model = pipeline::M2Loader::load(m2Data); - if (model.vertices.empty() && model.particleEmitters.empty()) { - LOG_DEBUG("SpellVisual: empty model: ", modelPath); - spellVisualFailedModels_.insert(modelId); - return; - } - // Load skin file for WotLK-format M2s - if (model.version >= 264) { - std::string skinPath = modelPath.substr(0, modelPath.rfind('.')) + "00.skin"; - auto skinData = cachedAssetManager->readFile(skinPath); - if (!skinData.empty()) pipeline::M2Loader::loadSkin(skinData, model); - } - if (!m2Renderer->loadModel(model, modelId)) { - LOG_WARNING("SpellVisual: failed to load model to GPU: ", modelPath); - spellVisualFailedModels_.insert(modelId); - return; - } - LOG_DEBUG("SpellVisual: loaded model id=", modelId, " path=", modelPath); - } - - // Spawn instance at world position - uint32_t instanceId = m2Renderer->createInstance(modelId, worldPosition, - glm::vec3(0.0f), 1.0f); - if (instanceId == 0) { - LOG_WARNING("SpellVisual: failed to create instance for visualId=", visualId); - return; - } - // Determine lifetime from M2 animation duration (clamp to reasonable range) - float animDurMs = m2Renderer->getInstanceAnimDuration(instanceId); - float duration = (animDurMs > 100.0f) - ? std::clamp(animDurMs / 1000.0f, 0.5f, SPELL_VISUAL_MAX_DURATION) - : SPELL_VISUAL_DEFAULT_DURATION; - activeSpellVisuals_.push_back({instanceId, 0.0f, duration}); - LOG_DEBUG("SpellVisual: spawned visualId=", visualId, " instanceId=", instanceId, - " duration=", duration, "s model=", modelPath); -} - -void Renderer::updateSpellVisuals(float deltaTime) { - if (activeSpellVisuals_.empty() || !m2Renderer) return; - for (auto it = activeSpellVisuals_.begin(); it != activeSpellVisuals_.end(); ) { - it->elapsed += deltaTime; - if (it->elapsed >= it->duration) { - m2Renderer->removeInstance(it->instanceId); - it = activeSpellVisuals_.erase(it); - } else { - ++it; - } - } + if (spellVisualSystem_) spellVisualSystem_->playSpellVisual(visualId, worldPosition, useImpactKit); } void Renderer::triggerMeleeSwing() { @@ -2937,8 +2445,8 @@ void Renderer::triggerMeleeSwing() { if (durationSec < 0.25f) durationSec = 0.25f; if (durationSec > 1.0f) durationSec = 1.0f; meleeSwingTimer = durationSec; - if (activitySoundManager) { - activitySoundManager->playMeleeSwing(); + if (getActivitySoundManager()) { + getActivitySoundManager()->playMeleeSwing(); } } @@ -3022,13 +2530,7 @@ void Renderer::resetCombatVisualState() { meleeSwingTimer = 0.0f; meleeSwingCooldown = 0.0f; // Clear lingering spell visual instances from the previous map/combat session. - // Without this, old effects could remain visible after teleport or map change. - for (auto& sv : activeSpellVisuals_) { - if (m2Renderer) m2Renderer->removeInstance(sv.instanceId); - } - activeSpellVisuals_.clear(); - // Reset the negative cache so models that failed during asset loading can retry. - spellVisualFailedModels_.clear(); + if (spellVisualSystem_) spellVisualSystem_->reset(); } bool Renderer::isMoving() const { @@ -3318,8 +2820,8 @@ void Renderer::update(float deltaTime) { if (chargeEffect) { chargeEffect->update(deltaTime); } - // Update transient spell visual instances - updateSpellVisuals(deltaTime); + // Update transient spell visual instances (delegated to SpellVisualSystem §4.4) + if (spellVisualSystem_) spellVisualSystem_->update(deltaTime); // Launch M2 doodad animation on background thread (overlaps with character animation + audio) @@ -3345,8 +2847,8 @@ void Renderer::update(float deltaTime) { audio::AudioEngine::instance().update(deltaTime); // Footsteps: animation-event driven + surface query at event time. - if (footstepManager) { - footstepManager->update(deltaTime); + if (getFootstepManager()) { + getFootstepManager()->update(deltaTime); cachedFootstepUpdateTimer += deltaTime; // Update surface cache timer bool canPlayFootsteps = characterRenderer && characterInstanceId > 0 && cameraController && cameraController->isThirdPerson() && @@ -3382,7 +2884,7 @@ void Renderer::update(float deltaTime) { return mountFootstepLastNormTime < eventNorm || eventNorm <= norm; }; if (crossed(0.25f) || crossed(0.75f)) { - footstepManager->playFootstep(resolveFootstepSurface(), true); + getFootstepManager()->playFootstep(resolveFootstepSurface(), true); } mountFootstepLastNormTime = norm; } @@ -3397,11 +2899,11 @@ void Renderer::update(float deltaTime) { if (characterRenderer->getAnimationState(characterInstanceId, animId, animTimeMs, animDurationMs) && shouldTriggerFootstepEvent(animId, animTimeMs, animDurationMs)) { auto surface = resolveFootstepSurface(); - footstepManager->playFootstep(surface, cameraController->isSprinting()); + getFootstepManager()->playFootstep(surface, cameraController->isSprinting()); // Play additional splash sound and spawn foot splash particles when wading if (surface == audio::FootstepSurface::WATER) { - if (movementSoundManager) { - movementSoundManager->playWaterFootstep(audio::MovementSoundManager::CharacterSize::MEDIUM); + if (getMovementSoundManager()) { + getMovementSoundManager()->playWaterFootstep(audio::MovementSoundManager::CharacterSize::MEDIUM); } if (swimEffects && waterRenderer) { auto wh = waterRenderer->getWaterHeightAt(characterPosition.x, characterPosition.y); @@ -3419,8 +2921,8 @@ void Renderer::update(float deltaTime) { } // Activity SFX: animation/state-driven jump, landing, and swim loops/splashes. - if (activitySoundManager) { - activitySoundManager->update(deltaTime); + if (getActivitySoundManager()) { + getActivitySoundManager()->update(deltaTime); if (cameraController && cameraController->isThirdPerson()) { bool grounded = cameraController->isGrounded(); bool jumping = cameraController->isJumping(); @@ -3437,25 +2939,25 @@ void Renderer::update(float deltaTime) { } if (jumping && !sfxPrevJumping && !swimming) { - activitySoundManager->playJump(); + getActivitySoundManager()->playJump(); } if (grounded && !sfxPrevGrounded) { bool hardLanding = sfxPrevFalling; - activitySoundManager->playLanding(resolveFootstepSurface(), hardLanding); + getActivitySoundManager()->playLanding(resolveFootstepSurface(), hardLanding); } if (swimming && !sfxPrevSwimming) { - activitySoundManager->playWaterEnter(); + getActivitySoundManager()->playWaterEnter(); } else if (!swimming && sfxPrevSwimming) { - activitySoundManager->playWaterExit(); + getActivitySoundManager()->playWaterExit(); } - activitySoundManager->setSwimmingState(swimming, moving); + getActivitySoundManager()->setSwimmingState(swimming, moving); // Fade music underwater - if (musicManager) { - musicManager->setUnderwaterMode(swimming); + if (getMusicManager()) { + getMusicManager()->setUnderwaterMode(swimming); } sfxPrevGrounded = grounded; @@ -3463,23 +2965,23 @@ void Renderer::update(float deltaTime) { sfxPrevFalling = falling; sfxPrevSwimming = swimming; } else { - activitySoundManager->setSwimmingState(false, false); + getActivitySoundManager()->setSwimmingState(false, false); // Restore music volume when activity sounds disabled - if (musicManager) { - musicManager->setUnderwaterMode(false); + if (getMusicManager()) { + getMusicManager()->setUnderwaterMode(false); } sfxStateInitialized = false; } } // Mount ambient sounds: wing flaps, breathing, etc. - if (mountSoundManager) { - mountSoundManager->update(deltaTime); + if (getMountSoundManager()) { + getMountSoundManager()->update(deltaTime); if (cameraController && isMounted()) { bool moving = cameraController->isMoving(); bool flying = taxiFlight_ || !cameraController->isGrounded(); // Flying if taxi or airborne - mountSoundManager->setMoving(moving); - mountSoundManager->setFlying(flying); + getMountSoundManager()->setMoving(moving); + getMountSoundManager()->setFlying(flying); } } @@ -3491,7 +2993,7 @@ void Renderer::update(float deltaTime) { playerIndoors_ = insideWmo; // Ambient environmental sounds: fireplaces, water, birds, etc. - if (ambientSoundManager && camera && wmoRenderer && cameraController) { + if (getAmbientSoundManager() && camera && wmoRenderer && cameraController) { bool isIndoor = insideWmo; bool isSwimming = cameraController->isSwimming(); @@ -3525,10 +3027,10 @@ void Renderer::update(float deltaTime) { } } - ambientSoundManager->setWeather(audioWeatherType); + getAmbientSoundManager()->setWeather(audioWeatherType); } - ambientSoundManager->update(deltaTime, camPos, isIndoor, isSwimming, isBlacksmith); + getAmbientSoundManager()->update(deltaTime, camPos, isIndoor, isSwimming, isBlacksmith); } // Wait for M2 doodad animation to finish (was launched earlier in parallel with character anim) @@ -3541,14 +3043,14 @@ void Renderer::update(float deltaTime) { auto playZoneMusic = [&](const std::string& music) { if (music.empty()) return; if (music.rfind("file:", 0) == 0) { - musicManager->crossfadeToFile(music.substr(5)); + getMusicManager()->crossfadeToFile(music.substr(5)); } else { - musicManager->crossfadeTo(music); + getMusicManager()->crossfadeTo(music); } }; // Update zone detection and music - if (zoneManager && musicManager && terrainManager && camera) { + if (zoneManager && getMusicManager() && terrainManager && camera) { // Prefer server-authoritative zone ID (from SMSG_INIT_WORLD_STATES); // fall back to tile-based lookup for single-player / offline mode. const auto* gh = core::Application::getInstance().getGameHandler(); @@ -3613,7 +3115,7 @@ void Renderer::update(float deltaTime) { if (!inTavern_ && !tavernMusic.empty()) { inTavern_ = true; LOG_INFO("Entered tavern"); - musicManager->playMusic(tavernMusic, true); // Immediate playback, looping + getMusicManager()->playMusic(tavernMusic, true); // Immediate playback, looping musicSwitchCooldown_ = 6.0f; } } else if (inTavern_) { @@ -3635,7 +3137,7 @@ void Renderer::update(float deltaTime) { if (!inBlacksmith_) { inBlacksmith_ = true; LOG_INFO("Entered blacksmith - stopping music"); - musicManager->stopMusic(); + getMusicManager()->stopMusic(); } } else if (inBlacksmith_) { // Exited blacksmith - restore zone music with crossfade @@ -3667,15 +3169,15 @@ void Renderer::update(float deltaTime) { } } // Update ambient sound manager zone type - if (ambientSoundManager) { - ambientSoundManager->setZoneId(zoneId); + if (getAmbientSoundManager()) { + getAmbientSoundManager()->setZoneId(zoneId); } } - musicManager->update(deltaTime); + getMusicManager()->update(deltaTime); // When a track finishes, pick a new random track from the current zone - if (!musicManager->isPlaying() && !inTavern_ && !inBlacksmith_ && + if (!getMusicManager()->isPlaying() && !inTavern_ && !inBlacksmith_ && currentZoneId != 0 && musicSwitchCooldown_ <= 0.0f) { std::string music = zoneManager->getRandomMusic(currentZoneId); if (!music.empty()) { @@ -3716,24 +3218,24 @@ void Renderer::runDeferredWorldInitStep(float deltaTime) { switch (deferredWorldInitStage_) { case 0: - if (ambientSoundManager) { - ambientSoundManager->initialize(cachedAssetManager); + if (getAmbientSoundManager()) { + getAmbientSoundManager()->initialize(cachedAssetManager); } - if (terrainManager && ambientSoundManager) { - terrainManager->setAmbientSoundManager(ambientSoundManager.get()); + if (terrainManager && getAmbientSoundManager()) { + terrainManager->setAmbientSoundManager(getAmbientSoundManager()); } break; case 1: - if (uiSoundManager) uiSoundManager->initialize(cachedAssetManager); + if (getUiSoundManager()) getUiSoundManager()->initialize(cachedAssetManager); break; case 2: - if (combatSoundManager) combatSoundManager->initialize(cachedAssetManager); + if (getCombatSoundManager()) getCombatSoundManager()->initialize(cachedAssetManager); break; case 3: - if (spellSoundManager) spellSoundManager->initialize(cachedAssetManager); + if (getSpellSoundManager()) getSpellSoundManager()->initialize(cachedAssetManager); break; case 4: - if (movementSoundManager) movementSoundManager->initialize(cachedAssetManager); + if (getMovementSoundManager()) getMovementSoundManager()->initialize(cachedAssetManager); break; case 5: if (questMarkerRenderer) questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, cachedAssetManager); @@ -3958,1489 +3460,107 @@ void Renderer::renderOverlay(const glm::vec4& color, VkCommandBuffer overrideCmd vkCmdDraw(cmd, 3, 1, 0, 0); // fullscreen triangle } -// ========================= FSR 1.0 Upscaling ========================= +// ========================= PostProcessPipeline delegation stubs (§4.3) ========================= -bool Renderer::initFSRResources() { - if (!vkCtx) return false; - - VkDevice device = vkCtx->getDevice(); - VmaAllocator alloc = vkCtx->getAllocator(); - VkExtent2D swapExtent = vkCtx->getSwapchainExtent(); - VkSampleCountFlagBits msaa = vkCtx->getMsaaSamples(); - bool useMsaa = (msaa > VK_SAMPLE_COUNT_1_BIT); - bool useDepthResolve = (vkCtx->getDepthResolveImageView() != VK_NULL_HANDLE); - - fsr_.internalWidth = static_cast(swapExtent.width * fsr_.scaleFactor); - fsr_.internalHeight = static_cast(swapExtent.height * fsr_.scaleFactor); - fsr_.internalWidth = (fsr_.internalWidth + 1) & ~1u; - fsr_.internalHeight = (fsr_.internalHeight + 1) & ~1u; - - LOG_INFO("FSR: initializing at ", fsr_.internalWidth, "x", fsr_.internalHeight, - " -> ", swapExtent.width, "x", swapExtent.height, - " (scale=", fsr_.scaleFactor, ", MSAA=", static_cast(msaa), "x)"); - - VkFormat colorFmt = vkCtx->getSwapchainFormat(); - VkFormat depthFmt = vkCtx->getDepthFormat(); - - // sceneColor: always 1x, always sampled — this is what FSR reads - // Non-MSAA: direct render target. MSAA: resolve target. - fsr_.sceneColor = createImage(device, alloc, fsr_.internalWidth, fsr_.internalHeight, - colorFmt, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); - if (!fsr_.sceneColor.image) { - LOG_ERROR("FSR: failed to create scene color image"); - return false; - } - - // sceneDepth: matches current MSAA sample count - fsr_.sceneDepth = createImage(device, alloc, fsr_.internalWidth, fsr_.internalHeight, - depthFmt, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, msaa); - if (!fsr_.sceneDepth.image) { - LOG_ERROR("FSR: failed to create scene depth image"); - destroyFSRResources(); - return false; - } - - if (useMsaa) { - // sceneMsaaColor: multisampled color target - fsr_.sceneMsaaColor = createImage(device, alloc, fsr_.internalWidth, fsr_.internalHeight, - colorFmt, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, msaa); - if (!fsr_.sceneMsaaColor.image) { - LOG_ERROR("FSR: failed to create MSAA color image"); - destroyFSRResources(); - return false; - } - - if (useDepthResolve) { - fsr_.sceneDepthResolve = createImage(device, alloc, fsr_.internalWidth, fsr_.internalHeight, - depthFmt, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT); - if (!fsr_.sceneDepthResolve.image) { - LOG_ERROR("FSR: failed to create depth resolve image"); - destroyFSRResources(); - return false; - } - } - } - - // Build framebuffer matching the main render pass attachment layout: - // Non-MSAA: [color, depth] - // MSAA (no depth res): [msaaColor, depth, resolve] - // MSAA (depth res): [msaaColor, depth, resolve, depthResolve] - VkImageView fbAttachments[4]{}; - uint32_t fbCount; - if (useMsaa) { - fbAttachments[0] = fsr_.sceneMsaaColor.imageView; - fbAttachments[1] = fsr_.sceneDepth.imageView; - fbAttachments[2] = fsr_.sceneColor.imageView; // resolve target - fbCount = 3; - if (useDepthResolve) { - fbAttachments[3] = fsr_.sceneDepthResolve.imageView; - fbCount = 4; - } - } else { - fbAttachments[0] = fsr_.sceneColor.imageView; - fbAttachments[1] = fsr_.sceneDepth.imageView; - fbCount = 2; - } - - VkFramebufferCreateInfo fbInfo{}; - fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; - fbInfo.renderPass = vkCtx->getImGuiRenderPass(); - fbInfo.attachmentCount = fbCount; - fbInfo.pAttachments = fbAttachments; - fbInfo.width = fsr_.internalWidth; - fbInfo.height = fsr_.internalHeight; - fbInfo.layers = 1; - - if (vkCreateFramebuffer(device, &fbInfo, nullptr, &fsr_.sceneFramebuffer) != VK_SUCCESS) { - LOG_ERROR("FSR: failed to create scene framebuffer"); - destroyFSRResources(); - return false; - } - - // Sampler for the resolved scene color - VkSamplerCreateInfo samplerInfo{}; - samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; - samplerInfo.minFilter = VK_FILTER_LINEAR; - samplerInfo.magFilter = VK_FILTER_LINEAR; - samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; - fsr_.sceneSampler = vkCtx->getOrCreateSampler(samplerInfo); - if (fsr_.sceneSampler == VK_NULL_HANDLE) { - LOG_ERROR("FSR: failed to create sampler"); - destroyFSRResources(); - return false; - } - - // Descriptor set layout: binding 0 = combined image sampler - VkDescriptorSetLayoutBinding binding{}; - binding.binding = 0; - binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - binding.descriptorCount = 1; - binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - - VkDescriptorSetLayoutCreateInfo layoutInfo{}; - layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; - layoutInfo.bindingCount = 1; - layoutInfo.pBindings = &binding; - vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &fsr_.descSetLayout); - - VkDescriptorPoolSize poolSize{}; - poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - poolSize.descriptorCount = 1; - VkDescriptorPoolCreateInfo poolInfo{}; - poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; - poolInfo.maxSets = 1; - poolInfo.poolSizeCount = 1; - poolInfo.pPoolSizes = &poolSize; - vkCreateDescriptorPool(device, &poolInfo, nullptr, &fsr_.descPool); - - VkDescriptorSetAllocateInfo dsAllocInfo{}; - dsAllocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; - dsAllocInfo.descriptorPool = fsr_.descPool; - dsAllocInfo.descriptorSetCount = 1; - dsAllocInfo.pSetLayouts = &fsr_.descSetLayout; - vkAllocateDescriptorSets(device, &dsAllocInfo, &fsr_.descSet); - - // Always bind the 1x sceneColor (FSR reads the resolved image) - VkDescriptorImageInfo imgInfo{}; - imgInfo.sampler = fsr_.sceneSampler; - imgInfo.imageView = fsr_.sceneColor.imageView; - imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - - VkWriteDescriptorSet write{}; - write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - write.dstSet = fsr_.descSet; - write.dstBinding = 0; - write.descriptorCount = 1; - write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - write.pImageInfo = &imgInfo; - vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); - - // Pipeline layout - VkPushConstantRange pc{}; - pc.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - pc.offset = 0; - pc.size = 64; - VkPipelineLayoutCreateInfo plCI{}; - plCI.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; - plCI.setLayoutCount = 1; - plCI.pSetLayouts = &fsr_.descSetLayout; - plCI.pushConstantRangeCount = 1; - plCI.pPushConstantRanges = &pc; - vkCreatePipelineLayout(device, &plCI, nullptr, &fsr_.pipelineLayout); - - // Load shaders - VkShaderModule vertMod, fragMod; - if (!vertMod.loadFromFile(device, "assets/shaders/postprocess.vert.spv") || - !fragMod.loadFromFile(device, "assets/shaders/fsr_easu.frag.spv")) { - LOG_ERROR("FSR: failed to load shaders"); - destroyFSRResources(); - return false; - } - - // FSR upscale pipeline renders into the swapchain pass at full resolution - // Must match swapchain pass MSAA setting - fsr_.pipeline = PipelineBuilder() - .setShaders(vertMod.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), - fragMod.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) - .setVertexInput({}, {}) - .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) - .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) - .setNoDepthTest() - .setColorBlendAttachment(PipelineBuilder::blendDisabled()) - .setMultisample(msaa) - .setLayout(fsr_.pipelineLayout) - .setRenderPass(vkCtx->getImGuiRenderPass()) - .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device, vkCtx->getPipelineCache()); - - vertMod.destroy(); - fragMod.destroy(); - - if (!fsr_.pipeline) { - LOG_ERROR("FSR: failed to create upscale pipeline"); - destroyFSRResources(); - return false; - } - - LOG_INFO("FSR: initialized successfully"); - return true; -} - -void Renderer::destroyFSRResources() { - if (!vkCtx) return; - - VkDevice device = vkCtx->getDevice(); - VmaAllocator alloc = vkCtx->getAllocator(); - vkDeviceWaitIdle(device); - - if (fsr_.pipeline) { vkDestroyPipeline(device, fsr_.pipeline, nullptr); fsr_.pipeline = VK_NULL_HANDLE; } - if (fsr_.pipelineLayout) { vkDestroyPipelineLayout(device, fsr_.pipelineLayout, nullptr); fsr_.pipelineLayout = VK_NULL_HANDLE; } - if (fsr_.descPool) { vkDestroyDescriptorPool(device, fsr_.descPool, nullptr); fsr_.descPool = VK_NULL_HANDLE; fsr_.descSet = VK_NULL_HANDLE; } - if (fsr_.descSetLayout) { vkDestroyDescriptorSetLayout(device, fsr_.descSetLayout, nullptr); fsr_.descSetLayout = VK_NULL_HANDLE; } - if (fsr_.sceneFramebuffer) { vkDestroyFramebuffer(device, fsr_.sceneFramebuffer, nullptr); fsr_.sceneFramebuffer = VK_NULL_HANDLE; } - fsr_.sceneSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache - destroyImage(device, alloc, fsr_.sceneDepthResolve); - destroyImage(device, alloc, fsr_.sceneMsaaColor); - destroyImage(device, alloc, fsr_.sceneDepth); - destroyImage(device, alloc, fsr_.sceneColor); - - fsr_.internalWidth = 0; - fsr_.internalHeight = 0; -} - -void Renderer::renderFSRUpscale() { - if (!fsr_.pipeline || currentCmd == VK_NULL_HANDLE) return; - - VkExtent2D outExtent = vkCtx->getSwapchainExtent(); - float inW = static_cast(fsr_.internalWidth); - float inH = static_cast(fsr_.internalHeight); - float outW = static_cast(outExtent.width); - float outH = static_cast(outExtent.height); - - // FSR push constants - struct { - glm::vec4 con0; // inputSize.xy, 1/inputSize.xy - glm::vec4 con1; // inputSize.xy / outputSize.xy, 0.5 * inputSize.xy / outputSize.xy - glm::vec4 con2; // outputSize.xy, 1/outputSize.xy - glm::vec4 con3; // sharpness, 0, 0, 0 - } fsrConst; - - fsrConst.con0 = glm::vec4(inW, inH, 1.0f / inW, 1.0f / inH); - fsrConst.con1 = glm::vec4(inW / outW, inH / outH, 0.5f * inW / outW, 0.5f * inH / outH); - fsrConst.con2 = glm::vec4(outW, outH, 1.0f / outW, 1.0f / outH); - fsrConst.con3 = glm::vec4(fsr_.sharpness, 0.0f, 0.0f, 0.0f); - - vkCmdBindPipeline(currentCmd, VK_PIPELINE_BIND_POINT_GRAPHICS, fsr_.pipeline); - vkCmdBindDescriptorSets(currentCmd, VK_PIPELINE_BIND_POINT_GRAPHICS, - fsr_.pipelineLayout, 0, 1, &fsr_.descSet, 0, nullptr); - vkCmdPushConstants(currentCmd, fsr_.pipelineLayout, - VK_SHADER_STAGE_FRAGMENT_BIT, 0, 64, &fsrConst); - vkCmdDraw(currentCmd, 3, 1, 0, 0); -} - -void Renderer::setFSREnabled(bool enabled) { - if (fsr_.enabled == enabled) return; - fsr_.enabled = enabled; - - if (enabled) { - // FSR1 upscaling renders its own AA — disable MSAA to avoid redundant work - if (vkCtx && vkCtx->getMsaaSamples() > VK_SAMPLE_COUNT_1_BIT) { - pendingMsaaSamples_ = VK_SAMPLE_COUNT_1_BIT; - msaaChangePending_ = true; - } - } else { - // Defer destruction to next beginFrame() — can't destroy mid-render - fsr_.needsRecreate = true; - } - // Resources created/destroyed lazily in beginFrame() -} - -void Renderer::setFSRQuality(float scaleFactor) { - scaleFactor = glm::clamp(scaleFactor, 0.5f, 1.0f); - fsr_.scaleFactor = scaleFactor; - fsr2_.scaleFactor = scaleFactor; - // Don't destroy/recreate mid-frame — mark for lazy recreation in next beginFrame() - if (fsr_.enabled && fsr_.sceneFramebuffer) { - fsr_.needsRecreate = true; - } - if (fsr2_.enabled && fsr2_.sceneFramebuffer) { - fsr2_.needsRecreate = true; - fsr2_.needsHistoryReset = true; - } -} - -void Renderer::setFSRSharpness(float sharpness) { - fsr_.sharpness = glm::clamp(sharpness, 0.0f, 2.0f); - fsr2_.sharpness = glm::clamp(sharpness, 0.0f, 2.0f); -} - -// ========================= End FSR 1.0 ========================= - -// ========================= FSR 2.2 Temporal Upscaling ========================= - -float Renderer::halton(uint32_t index, uint32_t base) { - float f = 1.0f; - float r = 0.0f; - uint32_t current = index; - while (current > 0) { - f /= static_cast(base); - r += f * static_cast(current % base); - current /= base; - } - return r; -} - -bool Renderer::initFSR2Resources() { - if (!vkCtx) return false; - - VkDevice device = vkCtx->getDevice(); - VmaAllocator alloc = vkCtx->getAllocator(); - VkExtent2D swapExtent = vkCtx->getSwapchainExtent(); - - // Temporary stability fallback: keep FSR2 path at native internal resolution - // until temporal reprojection is reworked. - fsr2_.internalWidth = static_cast(swapExtent.width * fsr2_.scaleFactor); - fsr2_.internalHeight = static_cast(swapExtent.height * fsr2_.scaleFactor); - fsr2_.internalWidth = (fsr2_.internalWidth + 1) & ~1u; - fsr2_.internalHeight = (fsr2_.internalHeight + 1) & ~1u; - - LOG_INFO("FSR2: initializing at ", fsr2_.internalWidth, "x", fsr2_.internalHeight, - " -> ", swapExtent.width, "x", swapExtent.height, - " (scale=", fsr2_.scaleFactor, ")"); - fsr2_.useAmdBackend = false; - fsr2_.amdFsr3FramegenRuntimeActive = false; - fsr2_.amdFsr3FramegenRuntimeReady = false; - fsr2_.framegenOutputValid = false; - fsr2_.amdFsr3RuntimePath = "Path C"; - fsr2_.amdFsr3RuntimeLastError.clear(); - fsr2_.amdFsr3UpscaleDispatchCount = 0; - fsr2_.amdFsr3FramegenDispatchCount = 0; - fsr2_.amdFsr3FallbackCount = 0; - fsr2_.amdFsr3InteropSyncValue = 1; -#if WOWEE_HAS_AMD_FSR2 - LOG_INFO("FSR2: AMD FidelityFX SDK detected at build time."); -#else - LOG_WARNING("FSR2: AMD FidelityFX SDK not detected; using internal fallback path."); -#endif - - VkFormat colorFmt = vkCtx->getSwapchainFormat(); - VkFormat depthFmt = vkCtx->getDepthFormat(); - - // Scene color (internal resolution, 1x — FSR2 replaces MSAA) - fsr2_.sceneColor = createImage(device, alloc, fsr2_.internalWidth, fsr2_.internalHeight, - colorFmt, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); - if (!fsr2_.sceneColor.image) { LOG_ERROR("FSR2: failed to create scene color"); return false; } - - // Scene depth (internal resolution, 1x, sampled for motion vectors) - fsr2_.sceneDepth = createImage(device, alloc, fsr2_.internalWidth, fsr2_.internalHeight, - depthFmt, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); - if (!fsr2_.sceneDepth.image) { LOG_ERROR("FSR2: failed to create scene depth"); destroyFSR2Resources(); return false; } - - // Motion vector buffer (internal resolution) - fsr2_.motionVectors = createImage(device, alloc, fsr2_.internalWidth, fsr2_.internalHeight, - VK_FORMAT_R16G16_SFLOAT, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); - if (!fsr2_.motionVectors.image) { LOG_ERROR("FSR2: failed to create motion vectors"); destroyFSR2Resources(); return false; } - - // History buffers (display resolution, ping-pong) - for (int i = 0; i < 2; i++) { - fsr2_.history[i] = createImage(device, alloc, swapExtent.width, swapExtent.height, - VK_FORMAT_R16G16B16A16_SFLOAT, - VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); - if (!fsr2_.history[i].image) { LOG_ERROR("FSR2: failed to create history buffer ", i); destroyFSR2Resources(); return false; } - } - fsr2_.framegenOutput = createImage(device, alloc, swapExtent.width, swapExtent.height, - VK_FORMAT_R16G16B16A16_SFLOAT, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); - if (!fsr2_.framegenOutput.image) { LOG_ERROR("FSR2: failed to create framegen output"); destroyFSR2Resources(); return false; } - - // Scene framebuffer (non-MSAA: [color, depth]) - // Must use the same render pass as the swapchain — which must be non-MSAA when FSR2 is active - VkImageView fbAttachments[2] = { fsr2_.sceneColor.imageView, fsr2_.sceneDepth.imageView }; - VkFramebufferCreateInfo fbInfo{}; - fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; - fbInfo.renderPass = vkCtx->getImGuiRenderPass(); - fbInfo.attachmentCount = 2; - fbInfo.pAttachments = fbAttachments; - fbInfo.width = fsr2_.internalWidth; - fbInfo.height = fsr2_.internalHeight; - fbInfo.layers = 1; - if (vkCreateFramebuffer(device, &fbInfo, nullptr, &fsr2_.sceneFramebuffer) != VK_SUCCESS) { - LOG_ERROR("FSR2: failed to create scene framebuffer"); - destroyFSR2Resources(); - return false; - } - - // Samplers - VkSamplerCreateInfo samplerInfo{}; - samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; - samplerInfo.minFilter = VK_FILTER_LINEAR; - samplerInfo.magFilter = VK_FILTER_LINEAR; - samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - fsr2_.linearSampler = vkCtx->getOrCreateSampler(samplerInfo); - - samplerInfo.minFilter = VK_FILTER_NEAREST; - samplerInfo.magFilter = VK_FILTER_NEAREST; - fsr2_.nearestSampler = vkCtx->getOrCreateSampler(samplerInfo); - -#if WOWEE_HAS_AMD_FSR2 - // Initialize AMD FSR2 context; fall back to internal path on any failure. - fsr2_.amdScratchBufferSize = ffxFsr2GetScratchMemorySizeVK(vkCtx->getPhysicalDevice()); - if (fsr2_.amdScratchBufferSize > 0) { - fsr2_.amdScratchBuffer = std::malloc(fsr2_.amdScratchBufferSize); - } - if (!fsr2_.amdScratchBuffer) { - LOG_WARNING("FSR2 AMD: failed to allocate scratch buffer, using internal fallback."); - } else { - FfxErrorCode ifaceErr = ffxFsr2GetInterfaceVK( - &fsr2_.amdInterface, - fsr2_.amdScratchBuffer, - fsr2_.amdScratchBufferSize, - vkCtx->getPhysicalDevice(), - vkGetDeviceProcAddr); - if (ifaceErr != FFX_OK) { - LOG_WARNING("FSR2 AMD: ffxFsr2GetInterfaceVK failed (", static_cast(ifaceErr), "), using internal fallback."); - std::free(fsr2_.amdScratchBuffer); - fsr2_.amdScratchBuffer = nullptr; - fsr2_.amdScratchBufferSize = 0; - } else { - FfxFsr2ContextDescription ctxDesc{}; - ctxDesc.flags = FFX_FSR2_ENABLE_AUTO_EXPOSURE | FFX_FSR2_ENABLE_MOTION_VECTORS_JITTER_CANCELLATION; - ctxDesc.maxRenderSize.width = fsr2_.internalWidth; - ctxDesc.maxRenderSize.height = fsr2_.internalHeight; - ctxDesc.displaySize.width = swapExtent.width; - ctxDesc.displaySize.height = swapExtent.height; - ctxDesc.callbacks = fsr2_.amdInterface; - ctxDesc.device = ffxGetDeviceVK(vkCtx->getDevice()); - ctxDesc.fpMessage = nullptr; - - FfxErrorCode ctxErr = ffxFsr2ContextCreate(&fsr2_.amdContext, &ctxDesc); - if (ctxErr == FFX_OK) { - fsr2_.useAmdBackend = true; - LOG_INFO("FSR2 AMD: context created successfully."); -#if WOWEE_HAS_AMD_FSR3_FRAMEGEN - // FSR3 frame generation runtime uses AMD FidelityFX SDK which can - // corrupt Vulkan driver state on NVIDIA GPUs when context creation - // fails, causing subsequent vkCmdBeginRenderPass to crash. - // Skip FSR3 frame gen entirely on non-AMD GPUs. - if (fsr2_.amdFsr3FramegenEnabled && vkCtx->isAmdGpu()) { - fsr2_.amdFsr3FramegenRuntimeActive = false; - if (!fsr2_.amdFsr3Runtime) fsr2_.amdFsr3Runtime = std::make_unique(); - AmdFsr3RuntimeInitDesc fgInit{}; - fgInit.physicalDevice = vkCtx->getPhysicalDevice(); - fgInit.device = vkCtx->getDevice(); - fgInit.getDeviceProcAddr = vkGetDeviceProcAddr; - fgInit.maxRenderWidth = fsr2_.internalWidth; - fgInit.maxRenderHeight = fsr2_.internalHeight; - fgInit.displayWidth = swapExtent.width; - fgInit.displayHeight = swapExtent.height; - fgInit.colorFormat = VK_FORMAT_R16G16B16A16_SFLOAT; - fgInit.hdrInput = false; - fgInit.depthInverted = false; - fgInit.enableFrameGeneration = true; - fsr2_.amdFsr3FramegenRuntimeReady = fsr2_.amdFsr3Runtime->initialize(fgInit); - if (fsr2_.amdFsr3FramegenRuntimeReady) { - fsr2_.amdFsr3RuntimeLastError.clear(); - fsr2_.amdFsr3RuntimePath = "Path A"; - LOG_INFO("FSR3 framegen runtime library loaded from ", fsr2_.amdFsr3Runtime->loadedLibraryPath(), - " (upscale+framegen dispatch enabled)"); - } else { - fsr2_.amdFsr3RuntimePath = "Path C"; - fsr2_.amdFsr3RuntimeLastError = fsr2_.amdFsr3Runtime->lastError(); - LOG_WARNING("FSR3 framegen toggle is enabled, but runtime initialization failed. ", - "path=", fsr2_.amdFsr3RuntimePath, - " error=", fsr2_.amdFsr3RuntimeLastError.empty() ? "(none)" : fsr2_.amdFsr3RuntimeLastError, - " runtimeLib=", fsr2_.amdFsr3Runtime->loadedLibraryPath().empty() ? "(not loaded)" : fsr2_.amdFsr3Runtime->loadedLibraryPath()); - } - } -#endif - } else { - LOG_WARNING("FSR2 AMD: context creation failed (", static_cast(ctxErr), "), using internal fallback."); - std::free(fsr2_.amdScratchBuffer); - fsr2_.amdScratchBuffer = nullptr; - fsr2_.amdScratchBufferSize = 0; - } - } - } -#endif - - // --- Motion Vector Compute Pipeline --- - { - // Descriptor set layout: binding 0 = depth (sampler), binding 1 = motion vectors (storage image) - VkDescriptorSetLayoutBinding bindings[2] = {}; - bindings[0].binding = 0; - bindings[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - bindings[0].descriptorCount = 1; - bindings[0].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - bindings[1].binding = 1; - bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; - bindings[1].descriptorCount = 1; - bindings[1].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - - VkDescriptorSetLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; - layoutInfo.bindingCount = 2; - layoutInfo.pBindings = bindings; - vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &fsr2_.motionVecDescSetLayout); - - VkPushConstantRange pc{}; - pc.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - pc.offset = 0; - pc.size = 2 * sizeof(glm::mat4); // 128 bytes - - VkPipelineLayoutCreateInfo plCI{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; - plCI.setLayoutCount = 1; - plCI.pSetLayouts = &fsr2_.motionVecDescSetLayout; - plCI.pushConstantRangeCount = 1; - plCI.pPushConstantRanges = &pc; - vkCreatePipelineLayout(device, &plCI, nullptr, &fsr2_.motionVecPipelineLayout); - - VkShaderModule compMod; - if (!compMod.loadFromFile(device, "assets/shaders/fsr2_motion.comp.spv")) { - LOG_ERROR("FSR2: failed to load motion vector compute shader"); - destroyFSR2Resources(); - return false; - } - - VkComputePipelineCreateInfo cpCI{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; - cpCI.stage = compMod.stageInfo(VK_SHADER_STAGE_COMPUTE_BIT); - cpCI.layout = fsr2_.motionVecPipelineLayout; - if (vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &cpCI, nullptr, &fsr2_.motionVecPipeline) != VK_SUCCESS) { - LOG_ERROR("FSR2: failed to create motion vector pipeline"); - compMod.destroy(); - destroyFSR2Resources(); - return false; - } - compMod.destroy(); - - // Descriptor pool + set - VkDescriptorPoolSize poolSizes[2] = {}; - poolSizes[0] = {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1}; - poolSizes[1] = {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1}; - VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; - poolInfo.maxSets = 1; - poolInfo.poolSizeCount = 2; - poolInfo.pPoolSizes = poolSizes; - vkCreateDescriptorPool(device, &poolInfo, nullptr, &fsr2_.motionVecDescPool); - - VkDescriptorSetAllocateInfo dsAI{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; - dsAI.descriptorPool = fsr2_.motionVecDescPool; - dsAI.descriptorSetCount = 1; - dsAI.pSetLayouts = &fsr2_.motionVecDescSetLayout; - vkAllocateDescriptorSets(device, &dsAI, &fsr2_.motionVecDescSet); - - // Write descriptors - VkDescriptorImageInfo depthImgInfo{}; - depthImgInfo.sampler = fsr2_.nearestSampler; - depthImgInfo.imageView = fsr2_.sceneDepth.imageView; - depthImgInfo.imageLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; - - VkDescriptorImageInfo mvImgInfo{}; - mvImgInfo.imageView = fsr2_.motionVectors.imageView; - mvImgInfo.imageLayout = VK_IMAGE_LAYOUT_GENERAL; - - VkWriteDescriptorSet writes[2] = {}; - writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[0].dstSet = fsr2_.motionVecDescSet; - writes[0].dstBinding = 0; - writes[0].descriptorCount = 1; - writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - writes[0].pImageInfo = &depthImgInfo; - - writes[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[1].dstSet = fsr2_.motionVecDescSet; - writes[1].dstBinding = 1; - writes[1].descriptorCount = 1; - writes[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; - writes[1].pImageInfo = &mvImgInfo; - - vkUpdateDescriptorSets(device, 2, writes, 0, nullptr); - } - - // --- Temporal Accumulation Compute Pipeline --- - { - // bindings: 0=sceneColor, 1=depth, 2=motionVectors, 3=historyInput, 4=historyOutput - VkDescriptorSetLayoutBinding bindings[5] = {}; - for (int i = 0; i < 4; i++) { - bindings[i].binding = i; - bindings[i].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - bindings[i].descriptorCount = 1; - bindings[i].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - } - bindings[4].binding = 4; - bindings[4].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; - bindings[4].descriptorCount = 1; - bindings[4].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - - VkDescriptorSetLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; - layoutInfo.bindingCount = 5; - layoutInfo.pBindings = bindings; - vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &fsr2_.accumulateDescSetLayout); - - VkPushConstantRange pc{}; - pc.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - pc.offset = 0; - pc.size = 4 * sizeof(glm::vec4); // 64 bytes - - VkPipelineLayoutCreateInfo plCI{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; - plCI.setLayoutCount = 1; - plCI.pSetLayouts = &fsr2_.accumulateDescSetLayout; - plCI.pushConstantRangeCount = 1; - plCI.pPushConstantRanges = &pc; - vkCreatePipelineLayout(device, &plCI, nullptr, &fsr2_.accumulatePipelineLayout); - - VkShaderModule compMod; - if (!compMod.loadFromFile(device, "assets/shaders/fsr2_accumulate.comp.spv")) { - LOG_ERROR("FSR2: failed to load accumulation compute shader"); - destroyFSR2Resources(); - return false; - } - - VkComputePipelineCreateInfo cpCI{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; - cpCI.stage = compMod.stageInfo(VK_SHADER_STAGE_COMPUTE_BIT); - cpCI.layout = fsr2_.accumulatePipelineLayout; - if (vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &cpCI, nullptr, &fsr2_.accumulatePipeline) != VK_SUCCESS) { - LOG_ERROR("FSR2: failed to create accumulation pipeline"); - compMod.destroy(); - destroyFSR2Resources(); - return false; - } - compMod.destroy(); - - // Descriptor pool: 2 sets (ping-pong), each with 4 samplers + 1 storage image - VkDescriptorPoolSize poolSizes[2] = {}; - poolSizes[0] = {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 8}; - poolSizes[1] = {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 2}; - VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; - poolInfo.maxSets = 2; - poolInfo.poolSizeCount = 2; - poolInfo.pPoolSizes = poolSizes; - vkCreateDescriptorPool(device, &poolInfo, nullptr, &fsr2_.accumulateDescPool); - - // Allocate 2 descriptor sets (one per ping-pong direction) - VkDescriptorSetLayout layouts[2] = { fsr2_.accumulateDescSetLayout, fsr2_.accumulateDescSetLayout }; - VkDescriptorSetAllocateInfo dsAI{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; - dsAI.descriptorPool = fsr2_.accumulateDescPool; - dsAI.descriptorSetCount = 2; - dsAI.pSetLayouts = layouts; - vkAllocateDescriptorSets(device, &dsAI, fsr2_.accumulateDescSets); - - // Write descriptors for both ping-pong sets - for (int pp = 0; pp < 2; pp++) { - int inputHistory = 1 - pp; // Read from the other - int outputHistory = pp; // Write to this one - - // The accumulation shader already performs custom Lanczos reconstruction. - // Use nearest here to avoid double filtering (linear + Lanczos) softening. - VkDescriptorImageInfo colorInfo{fsr2_.nearestSampler, fsr2_.sceneColor.imageView, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; - VkDescriptorImageInfo depthInfo{fsr2_.nearestSampler, fsr2_.sceneDepth.imageView, VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL}; - VkDescriptorImageInfo mvInfo{fsr2_.nearestSampler, fsr2_.motionVectors.imageView, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; - VkDescriptorImageInfo histInInfo{fsr2_.linearSampler, fsr2_.history[inputHistory].imageView, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; - VkDescriptorImageInfo histOutInfo{VK_NULL_HANDLE, fsr2_.history[outputHistory].imageView, VK_IMAGE_LAYOUT_GENERAL}; - - VkWriteDescriptorSet writes[5] = {}; - for (int w = 0; w < 5; w++) { - writes[w].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[w].dstSet = fsr2_.accumulateDescSets[pp]; - writes[w].dstBinding = w; - writes[w].descriptorCount = 1; - } - writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[0].pImageInfo = &colorInfo; - writes[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[1].pImageInfo = &depthInfo; - writes[2].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[2].pImageInfo = &mvInfo; - writes[3].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[3].pImageInfo = &histInInfo; - writes[4].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; writes[4].pImageInfo = &histOutInfo; - - vkUpdateDescriptorSets(device, 5, writes, 0, nullptr); - } - } - - // --- RCAS Sharpening Pipeline (fragment shader, fullscreen pass) --- - { - VkDescriptorSetLayoutBinding binding{}; - binding.binding = 0; - binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - binding.descriptorCount = 1; - binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - - VkDescriptorSetLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; - layoutInfo.bindingCount = 1; - layoutInfo.pBindings = &binding; - vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &fsr2_.sharpenDescSetLayout); - - VkPushConstantRange pc{}; - pc.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - pc.offset = 0; - pc.size = sizeof(glm::vec4); - - VkPipelineLayoutCreateInfo plCI{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; - plCI.setLayoutCount = 1; - plCI.pSetLayouts = &fsr2_.sharpenDescSetLayout; - plCI.pushConstantRangeCount = 1; - plCI.pPushConstantRanges = &pc; - vkCreatePipelineLayout(device, &plCI, nullptr, &fsr2_.sharpenPipelineLayout); - - VkShaderModule vertMod, fragMod; - if (!vertMod.loadFromFile(device, "assets/shaders/postprocess.vert.spv") || - !fragMod.loadFromFile(device, "assets/shaders/fsr2_sharpen.frag.spv")) { - LOG_ERROR("FSR2: failed to load sharpen shaders"); - destroyFSR2Resources(); - return false; - } - - fsr2_.sharpenPipeline = PipelineBuilder() - .setShaders(vertMod.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), - fragMod.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) - .setVertexInput({}, {}) - .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) - .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) - .setNoDepthTest() - .setColorBlendAttachment(PipelineBuilder::blendDisabled()) - .setMultisample(VK_SAMPLE_COUNT_1_BIT) - .setLayout(fsr2_.sharpenPipelineLayout) - .setRenderPass(vkCtx->getImGuiRenderPass()) - .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device, vkCtx->getPipelineCache()); - - vertMod.destroy(); - fragMod.destroy(); - - if (!fsr2_.sharpenPipeline) { - LOG_ERROR("FSR2: failed to create sharpen pipeline"); - destroyFSR2Resources(); - return false; - } - - // Descriptor pool + sets for sharpen pass (double-buffered to avoid race condition) - VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 2}; - VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; - poolInfo.maxSets = 2; - poolInfo.poolSizeCount = 1; - poolInfo.pPoolSizes = &poolSize; - vkCreateDescriptorPool(device, &poolInfo, nullptr, &fsr2_.sharpenDescPool); - - VkDescriptorSetLayout layouts[2] = {fsr2_.sharpenDescSetLayout, fsr2_.sharpenDescSetLayout}; - VkDescriptorSetAllocateInfo dsAI{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; - dsAI.descriptorPool = fsr2_.sharpenDescPool; - dsAI.descriptorSetCount = 2; - dsAI.pSetLayouts = layouts; - vkAllocateDescriptorSets(device, &dsAI, fsr2_.sharpenDescSets); - // Descriptors updated dynamically each frame to point at the correct history buffer - } - - fsr2_.needsHistoryReset = true; - fsr2_.frameIndex = 0; - LOG_INFO("FSR2: initialized successfully"); - return true; -} - -void Renderer::destroyFSR2Resources() { - if (!vkCtx) return; - - VkDevice device = vkCtx->getDevice(); - VmaAllocator alloc = vkCtx->getAllocator(); - vkDeviceWaitIdle(device); - -#if WOWEE_HAS_AMD_FSR2 - if (fsr2_.useAmdBackend) { - ffxFsr2ContextDestroy(&fsr2_.amdContext); - fsr2_.useAmdBackend = false; - } - if (fsr2_.amdScratchBuffer) { - std::free(fsr2_.amdScratchBuffer); - fsr2_.amdScratchBuffer = nullptr; - } - fsr2_.amdScratchBufferSize = 0; -#endif - fsr2_.amdFsr3FramegenRuntimeActive = false; - fsr2_.amdFsr3FramegenRuntimeReady = false; - fsr2_.framegenOutputValid = false; - fsr2_.amdFsr3RuntimePath = "Path C"; - fsr2_.amdFsr3RuntimeLastError.clear(); - fsr2_.amdFsr3InteropSyncValue = 1; -#if WOWEE_HAS_AMD_FSR3_FRAMEGEN - if (fsr2_.amdFsr3Runtime) { - fsr2_.amdFsr3Runtime->shutdown(); - fsr2_.amdFsr3Runtime.reset(); - } -#endif - - if (fsr2_.sharpenPipeline) { vkDestroyPipeline(device, fsr2_.sharpenPipeline, nullptr); fsr2_.sharpenPipeline = VK_NULL_HANDLE; } - if (fsr2_.sharpenPipelineLayout) { vkDestroyPipelineLayout(device, fsr2_.sharpenPipelineLayout, nullptr); fsr2_.sharpenPipelineLayout = VK_NULL_HANDLE; } - if (fsr2_.sharpenDescPool) { vkDestroyDescriptorPool(device, fsr2_.sharpenDescPool, nullptr); fsr2_.sharpenDescPool = VK_NULL_HANDLE; fsr2_.sharpenDescSets[0] = fsr2_.sharpenDescSets[1] = VK_NULL_HANDLE; } - if (fsr2_.sharpenDescSetLayout) { vkDestroyDescriptorSetLayout(device, fsr2_.sharpenDescSetLayout, nullptr); fsr2_.sharpenDescSetLayout = VK_NULL_HANDLE; } - - if (fsr2_.accumulatePipeline) { vkDestroyPipeline(device, fsr2_.accumulatePipeline, nullptr); fsr2_.accumulatePipeline = VK_NULL_HANDLE; } - if (fsr2_.accumulatePipelineLayout) { vkDestroyPipelineLayout(device, fsr2_.accumulatePipelineLayout, nullptr); fsr2_.accumulatePipelineLayout = VK_NULL_HANDLE; } - if (fsr2_.accumulateDescPool) { vkDestroyDescriptorPool(device, fsr2_.accumulateDescPool, nullptr); fsr2_.accumulateDescPool = VK_NULL_HANDLE; fsr2_.accumulateDescSets[0] = fsr2_.accumulateDescSets[1] = VK_NULL_HANDLE; } - if (fsr2_.accumulateDescSetLayout) { vkDestroyDescriptorSetLayout(device, fsr2_.accumulateDescSetLayout, nullptr); fsr2_.accumulateDescSetLayout = VK_NULL_HANDLE; } - - if (fsr2_.motionVecPipeline) { vkDestroyPipeline(device, fsr2_.motionVecPipeline, nullptr); fsr2_.motionVecPipeline = VK_NULL_HANDLE; } - if (fsr2_.motionVecPipelineLayout) { vkDestroyPipelineLayout(device, fsr2_.motionVecPipelineLayout, nullptr); fsr2_.motionVecPipelineLayout = VK_NULL_HANDLE; } - if (fsr2_.motionVecDescPool) { vkDestroyDescriptorPool(device, fsr2_.motionVecDescPool, nullptr); fsr2_.motionVecDescPool = VK_NULL_HANDLE; fsr2_.motionVecDescSet = VK_NULL_HANDLE; } - if (fsr2_.motionVecDescSetLayout) { vkDestroyDescriptorSetLayout(device, fsr2_.motionVecDescSetLayout, nullptr); fsr2_.motionVecDescSetLayout = VK_NULL_HANDLE; } - - if (fsr2_.sceneFramebuffer) { vkDestroyFramebuffer(device, fsr2_.sceneFramebuffer, nullptr); fsr2_.sceneFramebuffer = VK_NULL_HANDLE; } - fsr2_.linearSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache - fsr2_.nearestSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache - - destroyImage(device, alloc, fsr2_.motionVectors); - for (int i = 0; i < 2; i++) destroyImage(device, alloc, fsr2_.history[i]); - destroyImage(device, alloc, fsr2_.framegenOutput); - destroyImage(device, alloc, fsr2_.sceneDepth); - destroyImage(device, alloc, fsr2_.sceneColor); - - fsr2_.internalWidth = 0; - fsr2_.internalHeight = 0; -} - -void Renderer::dispatchMotionVectors() { - if (!fsr2_.motionVecPipeline || currentCmd == VK_NULL_HANDLE) return; - - // Transition depth: DEPTH_STENCIL_ATTACHMENT → DEPTH_STENCIL_READ_ONLY - transitionImageLayout(currentCmd, fsr2_.sceneDepth.image, - VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, - VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL, - VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); - - // Transition motion vectors: UNDEFINED → GENERAL - transitionImageLayout(currentCmd, fsr2_.motionVectors.image, - VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, - VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); - - vkCmdBindPipeline(currentCmd, VK_PIPELINE_BIND_POINT_COMPUTE, fsr2_.motionVecPipeline); - vkCmdBindDescriptorSets(currentCmd, VK_PIPELINE_BIND_POINT_COMPUTE, - fsr2_.motionVecPipelineLayout, 0, 1, &fsr2_.motionVecDescSet, 0, nullptr); - - // Reprojection with jittered matrices: - // reconstruct world position from current depth, then project into previous clip. - struct { - glm::mat4 prevViewProjection; - glm::mat4 invCurrentViewProj; - } pc; - - glm::mat4 currentVP = camera->getViewProjectionMatrix(); - pc.prevViewProjection = fsr2_.prevViewProjection; - pc.invCurrentViewProj = glm::inverse(currentVP); - - vkCmdPushConstants(currentCmd, fsr2_.motionVecPipelineLayout, - VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(pc), &pc); - - uint32_t gx = (fsr2_.internalWidth + 7) / 8; - uint32_t gy = (fsr2_.internalHeight + 7) / 8; - vkCmdDispatch(currentCmd, gx, gy, 1); - - // Transition motion vectors: GENERAL → SHADER_READ_ONLY for accumulation - transitionImageLayout(currentCmd, fsr2_.motionVectors.image, - VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); -} - -void Renderer::dispatchTemporalAccumulate() { - if (!fsr2_.accumulatePipeline || currentCmd == VK_NULL_HANDLE) return; - - VkExtent2D swapExtent = vkCtx->getSwapchainExtent(); - uint32_t outputIdx = fsr2_.currentHistory; - uint32_t inputIdx = 1 - outputIdx; - - // Transition scene color: PRESENT_SRC_KHR → SHADER_READ_ONLY - transitionImageLayout(currentCmd, fsr2_.sceneColor.image, - VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); - - // History layout lifecycle: - // First frame: both in UNDEFINED - // Subsequent frames: both in SHADER_READ_ONLY (output was transitioned for sharpen, - // input was left in SHADER_READ_ONLY from its sharpen read) - VkImageLayout historyOldLayout = fsr2_.needsHistoryReset - ? VK_IMAGE_LAYOUT_UNDEFINED - : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - - // Transition history input: SHADER_READ_ONLY → SHADER_READ_ONLY (barrier for sync) - transitionImageLayout(currentCmd, fsr2_.history[inputIdx].image, - historyOldLayout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, // sharpen read in previous frame - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); - - // Transition history output: SHADER_READ_ONLY → GENERAL (for compute write) - transitionImageLayout(currentCmd, fsr2_.history[outputIdx].image, - historyOldLayout, VK_IMAGE_LAYOUT_GENERAL, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); - - vkCmdBindPipeline(currentCmd, VK_PIPELINE_BIND_POINT_COMPUTE, fsr2_.accumulatePipeline); - vkCmdBindDescriptorSets(currentCmd, VK_PIPELINE_BIND_POINT_COMPUTE, - fsr2_.accumulatePipelineLayout, 0, 1, &fsr2_.accumulateDescSets[outputIdx], 0, nullptr); - - // Push constants - struct { - glm::vec4 internalSize; - glm::vec4 displaySize; - glm::vec4 jitterOffset; - glm::vec4 params; - } pc; - - pc.internalSize = glm::vec4( - static_cast(fsr2_.internalWidth), static_cast(fsr2_.internalHeight), - 1.0f / fsr2_.internalWidth, 1.0f / fsr2_.internalHeight); - pc.displaySize = glm::vec4( - static_cast(swapExtent.width), static_cast(swapExtent.height), - 1.0f / swapExtent.width, 1.0f / swapExtent.height); - glm::vec2 jitter = camera->getJitter(); - pc.jitterOffset = glm::vec4(jitter.x, jitter.y, 0.0f, 0.0f); - pc.params = glm::vec4( - fsr2_.needsHistoryReset ? 1.0f : 0.0f, - fsr2_.sharpness, - static_cast(fsr2_.convergenceFrame), - 0.0f); - - vkCmdPushConstants(currentCmd, fsr2_.accumulatePipelineLayout, - VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(pc), &pc); - - uint32_t gx = (swapExtent.width + 7) / 8; - uint32_t gy = (swapExtent.height + 7) / 8; - vkCmdDispatch(currentCmd, gx, gy, 1); - - fsr2_.needsHistoryReset = false; -} - -void Renderer::dispatchAmdFsr2() { - if (currentCmd == VK_NULL_HANDLE || !camera) return; -#if WOWEE_HAS_AMD_FSR2 - if (!fsr2_.useAmdBackend) return; - - VkExtent2D swapExtent = vkCtx->getSwapchainExtent(); - uint32_t outputIdx = fsr2_.currentHistory; - - transitionImageLayout(currentCmd, fsr2_.sceneColor.image, - VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); - transitionImageLayout(currentCmd, fsr2_.motionVectors.image, - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); - transitionImageLayout(currentCmd, fsr2_.sceneDepth.image, - VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); - transitionImageLayout(currentCmd, fsr2_.history[outputIdx].image, - fsr2_.needsHistoryReset ? VK_IMAGE_LAYOUT_UNDEFINED : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_IMAGE_LAYOUT_GENERAL, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); - - FfxFsr2DispatchDescription desc{}; - desc.commandList = ffxGetCommandListVK(currentCmd); - desc.color = ffxGetTextureResourceVK(&fsr2_.amdContext, - fsr2_.sceneColor.image, fsr2_.sceneColor.imageView, - fsr2_.internalWidth, fsr2_.internalHeight, vkCtx->getSwapchainFormat(), - L"FSR2_InputColor", FFX_RESOURCE_STATE_COMPUTE_READ); - desc.depth = ffxGetTextureResourceVK(&fsr2_.amdContext, - fsr2_.sceneDepth.image, fsr2_.sceneDepth.imageView, - fsr2_.internalWidth, fsr2_.internalHeight, vkCtx->getDepthFormat(), - L"FSR2_InputDepth", FFX_RESOURCE_STATE_COMPUTE_READ); - desc.motionVectors = ffxGetTextureResourceVK(&fsr2_.amdContext, - fsr2_.motionVectors.image, fsr2_.motionVectors.imageView, - fsr2_.internalWidth, fsr2_.internalHeight, VK_FORMAT_R16G16_SFLOAT, - L"FSR2_InputMotionVectors", FFX_RESOURCE_STATE_COMPUTE_READ); - desc.output = ffxGetTextureResourceVK(&fsr2_.amdContext, - fsr2_.history[outputIdx].image, fsr2_.history[outputIdx].imageView, - swapExtent.width, swapExtent.height, VK_FORMAT_R16G16B16A16_SFLOAT, - L"FSR2_Output", FFX_RESOURCE_STATE_UNORDERED_ACCESS); - - // Camera jitter is stored as NDC projection offsets; convert to render-pixel offsets. - // Do not apply jitterSign again here: it is already baked into camera jitter. - glm::vec2 jitterNdc = camera->getJitter(); - desc.jitterOffset.x = jitterNdc.x * 0.5f * static_cast(fsr2_.internalWidth); - desc.jitterOffset.y = jitterNdc.y * 0.5f * static_cast(fsr2_.internalHeight); - desc.motionVectorScale.x = static_cast(fsr2_.internalWidth) * fsr2_.motionVecScaleX; - desc.motionVectorScale.y = static_cast(fsr2_.internalHeight) * fsr2_.motionVecScaleY; - desc.renderSize.width = fsr2_.internalWidth; - desc.renderSize.height = fsr2_.internalHeight; - desc.enableSharpening = false; // Keep existing RCAS post pass. - desc.sharpness = 0.0f; - desc.frameTimeDelta = glm::max(0.001f, lastDeltaTime_ * 1000.0f); - desc.preExposure = 1.0f; - desc.reset = fsr2_.needsHistoryReset; - desc.cameraNear = camera->getNearPlane(); - desc.cameraFar = camera->getFarPlane(); - desc.cameraFovAngleVertical = glm::radians(camera->getFovDegrees()); - desc.viewSpaceToMetersFactor = 1.0f; - desc.enableAutoReactive = false; - - FfxErrorCode dispatchErr = ffxFsr2ContextDispatch(&fsr2_.amdContext, &desc); - if (dispatchErr != FFX_OK) { - LOG_WARNING("FSR2 AMD: dispatch failed (", static_cast(dispatchErr), "), forcing history reset."); - fsr2_.needsHistoryReset = true; - } else { - fsr2_.needsHistoryReset = false; - } -#endif -} - -void Renderer::dispatchAmdFsr3Framegen() { -#if WOWEE_HAS_AMD_FSR3_FRAMEGEN - if (!fsr2_.amdFsr3FramegenEnabled) { - fsr2_.amdFsr3FramegenRuntimeActive = false; - return; - } - if (!fsr2_.amdFsr3Runtime || !fsr2_.amdFsr3FramegenRuntimeReady) { - fsr2_.amdFsr3FramegenRuntimeActive = false; - return; - } - uint32_t outputIdx = fsr2_.currentHistory; - transitionImageLayout(currentCmd, fsr2_.sceneColor.image, - VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); - transitionImageLayout(currentCmd, fsr2_.motionVectors.image, - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); - transitionImageLayout(currentCmd, fsr2_.sceneDepth.image, - VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); - transitionImageLayout(currentCmd, fsr2_.history[outputIdx].image, - fsr2_.needsHistoryReset ? VK_IMAGE_LAYOUT_UNDEFINED : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_IMAGE_LAYOUT_GENERAL, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); - if (fsr2_.amdFsr3FramegenEnabled && fsr2_.framegenOutput.image) { - transitionImageLayout(currentCmd, fsr2_.framegenOutput.image, - fsr2_.framegenOutputValid ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED, - VK_IMAGE_LAYOUT_GENERAL, - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); - } - - AmdFsr3RuntimeDispatchDesc fgDispatch{}; - fgDispatch.commandBuffer = currentCmd; - fgDispatch.colorImage = fsr2_.sceneColor.image; - fgDispatch.depthImage = fsr2_.sceneDepth.image; - fgDispatch.motionVectorImage = fsr2_.motionVectors.image; - fgDispatch.outputImage = fsr2_.history[fsr2_.currentHistory].image; - fgDispatch.renderWidth = fsr2_.internalWidth; - fgDispatch.renderHeight = fsr2_.internalHeight; - fgDispatch.outputWidth = vkCtx->getSwapchainExtent().width; - fgDispatch.outputHeight = vkCtx->getSwapchainExtent().height; - fgDispatch.colorFormat = VK_FORMAT_R16G16B16A16_SFLOAT; - fgDispatch.depthFormat = vkCtx->getDepthFormat(); - fgDispatch.motionVectorFormat = VK_FORMAT_R16G16_SFLOAT; - fgDispatch.outputFormat = VK_FORMAT_R16G16B16A16_SFLOAT; - fgDispatch.frameGenOutputImage = fsr2_.framegenOutput.image; - glm::vec2 jitterNdc = camera ? camera->getJitter() : glm::vec2(0.0f); - fgDispatch.jitterX = jitterNdc.x * 0.5f * static_cast(fsr2_.internalWidth); - fgDispatch.jitterY = jitterNdc.y * 0.5f * static_cast(fsr2_.internalHeight); - fgDispatch.motionScaleX = static_cast(fsr2_.internalWidth) * fsr2_.motionVecScaleX; - fgDispatch.motionScaleY = static_cast(fsr2_.internalHeight) * fsr2_.motionVecScaleY; - fgDispatch.frameTimeDeltaMs = glm::max(0.001f, lastDeltaTime_ * 1000.0f); - fgDispatch.cameraNear = camera ? camera->getNearPlane() : 0.1f; - fgDispatch.cameraFar = camera ? camera->getFarPlane() : 1000.0f; - fgDispatch.cameraFovYRadians = camera ? glm::radians(camera->getFovDegrees()) : 1.0f; - fgDispatch.reset = fsr2_.needsHistoryReset; - - - if (!fsr2_.amdFsr3Runtime->dispatchUpscale(fgDispatch)) { - static bool warnedRuntimeDispatch = false; - if (!warnedRuntimeDispatch) { - warnedRuntimeDispatch = true; - LOG_WARNING("FSR3 runtime upscale dispatch failed; falling back to FSR2 dispatch output."); - } - fsr2_.amdFsr3RuntimeLastError = fsr2_.amdFsr3Runtime->lastError(); - fsr2_.amdFsr3FallbackCount++; - fsr2_.amdFsr3FramegenRuntimeActive = false; - return; - } - fsr2_.amdFsr3RuntimeLastError.clear(); - fsr2_.amdFsr3UpscaleDispatchCount++; - - if (!fsr2_.amdFsr3FramegenEnabled) { - fsr2_.amdFsr3FramegenRuntimeActive = false; - return; - } - if (!fsr2_.amdFsr3Runtime->isFrameGenerationReady()) { - fsr2_.amdFsr3FramegenRuntimeActive = false; - return; - } - if (!fsr2_.amdFsr3Runtime->dispatchFrameGeneration(fgDispatch)) { - static bool warnedFgDispatch = false; - if (!warnedFgDispatch) { - warnedFgDispatch = true; - LOG_WARNING("FSR3 runtime frame generation dispatch failed; using upscaled output only."); - } - fsr2_.amdFsr3RuntimeLastError = fsr2_.amdFsr3Runtime->lastError(); - fsr2_.amdFsr3FallbackCount++; - fsr2_.amdFsr3FramegenRuntimeActive = false; - return; - } - fsr2_.amdFsr3RuntimeLastError.clear(); - fsr2_.amdFsr3FramegenDispatchCount++; - fsr2_.framegenOutputValid = true; - fsr2_.amdFsr3FramegenRuntimeActive = true; -#else - fsr2_.amdFsr3FramegenRuntimeActive = false; -#endif -} - -void Renderer::renderFSR2Sharpen() { - if (!fsr2_.sharpenPipeline || currentCmd == VK_NULL_HANDLE) return; - - VkExtent2D ext = vkCtx->getSwapchainExtent(); - uint32_t outputIdx = fsr2_.currentHistory; - - // Use per-frame descriptor set to avoid race with in-flight command buffers - uint32_t frameIdx = vkCtx->getCurrentFrame(); - VkDescriptorSet descSet = fsr2_.sharpenDescSets[frameIdx]; - - // Update sharpen descriptor to point at current history output - VkDescriptorImageInfo imgInfo{}; - imgInfo.sampler = fsr2_.linearSampler; - if (fsr2_.useAmdBackend) { - imgInfo.imageView = (fsr2_.amdFsr3FramegenEnabled && fsr2_.amdFsr3FramegenRuntimeActive && fsr2_.framegenOutput.imageView) - ? fsr2_.framegenOutput.imageView - : fsr2_.history[outputIdx].imageView; - } else { - imgInfo.imageView = fsr2_.sceneColor.imageView; - } - imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - - VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; - write.dstSet = descSet; - write.dstBinding = 0; - write.descriptorCount = 1; - write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - write.pImageInfo = &imgInfo; - vkUpdateDescriptorSets(vkCtx->getDevice(), 1, &write, 0, nullptr); - - vkCmdBindPipeline(currentCmd, VK_PIPELINE_BIND_POINT_GRAPHICS, fsr2_.sharpenPipeline); - vkCmdBindDescriptorSets(currentCmd, VK_PIPELINE_BIND_POINT_GRAPHICS, - fsr2_.sharpenPipelineLayout, 0, 1, &descSet, 0, nullptr); - - glm::vec4 params(1.0f / ext.width, 1.0f / ext.height, fsr2_.sharpness, 0.0f); - vkCmdPushConstants(currentCmd, fsr2_.sharpenPipelineLayout, - VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(glm::vec4), ¶ms); - - vkCmdDraw(currentCmd, 3, 1, 0, 0); -} - -void Renderer::setFSR2Enabled(bool enabled) { - if (fsr2_.enabled == enabled) return; - fsr2_.enabled = enabled; - - if (enabled) { - static bool initFramegenToggleFromEnv = false; - if (!initFramegenToggleFromEnv) { - initFramegenToggleFromEnv = true; - if (std::getenv("WOWEE_ENABLE_AMD_FSR3_FRAMEGEN_RUNTIME") != nullptr) { - fsr2_.amdFsr3FramegenEnabled = true; - } - } - // FSR2 replaces both FSR1 and MSAA - if (fsr_.enabled) { - fsr_.enabled = false; - fsr_.needsRecreate = true; - } - // FSR2 requires non-MSAA render pass (its framebuffer has 2 attachments) - if (vkCtx && vkCtx->getMsaaSamples() > VK_SAMPLE_COUNT_1_BIT) { - pendingMsaaSamples_ = VK_SAMPLE_COUNT_1_BIT; - msaaChangePending_ = true; - } - // Use FSR1's scale factor and sharpness as defaults - fsr2_.scaleFactor = fsr_.scaleFactor; - fsr2_.sharpness = fsr_.sharpness; - fsr2_.needsHistoryReset = true; - } else { - fsr2_.needsRecreate = true; - if (camera) camera->clearJitter(); - } -} - -void Renderer::setFSR2DebugTuning(float jitterSign, float motionVecScaleX, float motionVecScaleY) { - fsr2_.jitterSign = glm::clamp(jitterSign, -2.0f, 2.0f); - fsr2_.motionVecScaleX = glm::clamp(motionVecScaleX, -2.0f, 2.0f); - fsr2_.motionVecScaleY = glm::clamp(motionVecScaleY, -2.0f, 2.0f); -} - -void Renderer::setAmdFsr3FramegenEnabled(bool enabled) { - if (fsr2_.amdFsr3FramegenEnabled == enabled) return; - fsr2_.amdFsr3FramegenEnabled = enabled; -#if WOWEE_HAS_AMD_FSR3_FRAMEGEN - if (enabled) { - fsr2_.amdFsr3FramegenRuntimeActive = false; - fsr2_.framegenOutputValid = false; - fsr2_.needsRecreate = true; - fsr2_.needsHistoryReset = true; - fsr2_.amdFsr3FramegenRuntimeReady = false; - fsr2_.amdFsr3RuntimePath = "Path C"; - fsr2_.amdFsr3RuntimeLastError.clear(); - LOG_INFO("FSR3 framegen requested; runtime will initialize on next FSR2 resource creation."); - } else { - fsr2_.amdFsr3FramegenRuntimeActive = false; - fsr2_.amdFsr3FramegenRuntimeReady = false; - fsr2_.framegenOutputValid = false; - fsr2_.needsHistoryReset = true; - fsr2_.needsRecreate = true; - fsr2_.amdFsr3RuntimePath = "Path C"; - fsr2_.amdFsr3RuntimeLastError = "disabled by user"; - if (fsr2_.amdFsr3Runtime) { - fsr2_.amdFsr3Runtime->shutdown(); - fsr2_.amdFsr3Runtime.reset(); - } - LOG_INFO("FSR3 framegen disabled; forcing FSR2-only path rebuild."); - } -#else - fsr2_.amdFsr3FramegenRuntimeActive = false; - fsr2_.amdFsr3FramegenRuntimeReady = false; - fsr2_.framegenOutputValid = false; - if (enabled) { - LOG_WARNING("FSR3 framegen requested, but AMD FSR3 framegen SDK headers are unavailable in this build."); - } -#endif -} - -// ========================= End FSR 2.2 ========================= - -// ========================= FXAA Post-Process ========================= - -bool Renderer::initFXAAResources() { - if (!vkCtx) return false; - - VkDevice device = vkCtx->getDevice(); - VmaAllocator alloc = vkCtx->getAllocator(); - VkExtent2D ext = vkCtx->getSwapchainExtent(); - VkSampleCountFlagBits msaa = vkCtx->getMsaaSamples(); - bool useMsaa = (msaa > VK_SAMPLE_COUNT_1_BIT); - bool useDepthResolve = (vkCtx->getDepthResolveImageView() != VK_NULL_HANDLE); - - LOG_INFO("FXAA: initializing at ", ext.width, "x", ext.height, - " (MSAA=", static_cast(msaa), "x)"); - - VkFormat colorFmt = vkCtx->getSwapchainFormat(); - VkFormat depthFmt = vkCtx->getDepthFormat(); - - // sceneColor: 1x resolved color target — FXAA reads from here - fxaa_.sceneColor = createImage(device, alloc, ext.width, ext.height, - colorFmt, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT); - if (!fxaa_.sceneColor.image) { - LOG_ERROR("FXAA: failed to create scene color image"); - return false; - } - - // sceneDepth: depth buffer at current MSAA sample count - fxaa_.sceneDepth = createImage(device, alloc, ext.width, ext.height, - depthFmt, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, msaa); - if (!fxaa_.sceneDepth.image) { - LOG_ERROR("FXAA: failed to create scene depth image"); - destroyFXAAResources(); - return false; - } - - if (useMsaa) { - fxaa_.sceneMsaaColor = createImage(device, alloc, ext.width, ext.height, - colorFmt, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, msaa); - if (!fxaa_.sceneMsaaColor.image) { - LOG_ERROR("FXAA: failed to create MSAA color image"); - destroyFXAAResources(); - return false; - } - if (useDepthResolve) { - fxaa_.sceneDepthResolve = createImage(device, alloc, ext.width, ext.height, - depthFmt, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT); - if (!fxaa_.sceneDepthResolve.image) { - LOG_ERROR("FXAA: failed to create depth resolve image"); - destroyFXAAResources(); - return false; - } - } - } - - // Framebuffer — same attachment layout as main render pass - VkImageView fbAttachments[4]{}; - uint32_t fbCount; - if (useMsaa) { - fbAttachments[0] = fxaa_.sceneMsaaColor.imageView; - fbAttachments[1] = fxaa_.sceneDepth.imageView; - fbAttachments[2] = fxaa_.sceneColor.imageView; // resolve target - fbCount = 3; - if (useDepthResolve) { - fbAttachments[3] = fxaa_.sceneDepthResolve.imageView; - fbCount = 4; - } - } else { - fbAttachments[0] = fxaa_.sceneColor.imageView; - fbAttachments[1] = fxaa_.sceneDepth.imageView; - fbCount = 2; - } - - VkFramebufferCreateInfo fbInfo{}; - fbInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; - fbInfo.renderPass = vkCtx->getImGuiRenderPass(); - fbInfo.attachmentCount = fbCount; - fbInfo.pAttachments = fbAttachments; - fbInfo.width = ext.width; - fbInfo.height = ext.height; - fbInfo.layers = 1; - if (vkCreateFramebuffer(device, &fbInfo, nullptr, &fxaa_.sceneFramebuffer) != VK_SUCCESS) { - LOG_ERROR("FXAA: failed to create scene framebuffer"); - destroyFXAAResources(); - return false; - } - - // Sampler - VkSamplerCreateInfo samplerInfo{}; - samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; - samplerInfo.minFilter = VK_FILTER_LINEAR; - samplerInfo.magFilter = VK_FILTER_LINEAR; - samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; - samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; - fxaa_.sceneSampler = vkCtx->getOrCreateSampler(samplerInfo); - if (fxaa_.sceneSampler == VK_NULL_HANDLE) { - LOG_ERROR("FXAA: failed to create sampler"); - destroyFXAAResources(); - return false; - } - - // Descriptor set layout: binding 0 = combined image sampler - VkDescriptorSetLayoutBinding binding{}; - binding.binding = 0; - binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - binding.descriptorCount = 1; - binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - VkDescriptorSetLayoutCreateInfo layoutInfo{}; - layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; - layoutInfo.bindingCount = 1; - layoutInfo.pBindings = &binding; - vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &fxaa_.descSetLayout); - - VkDescriptorPoolSize poolSize{}; - poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - poolSize.descriptorCount = 1; - VkDescriptorPoolCreateInfo poolInfo{}; - poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; - poolInfo.maxSets = 1; - poolInfo.poolSizeCount = 1; - poolInfo.pPoolSizes = &poolSize; - vkCreateDescriptorPool(device, &poolInfo, nullptr, &fxaa_.descPool); - - VkDescriptorSetAllocateInfo dsAllocInfo{}; - dsAllocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; - dsAllocInfo.descriptorPool = fxaa_.descPool; - dsAllocInfo.descriptorSetCount = 1; - dsAllocInfo.pSetLayouts = &fxaa_.descSetLayout; - vkAllocateDescriptorSets(device, &dsAllocInfo, &fxaa_.descSet); - - // Bind the resolved 1x sceneColor - VkDescriptorImageInfo imgInfo{}; - imgInfo.sampler = fxaa_.sceneSampler; - imgInfo.imageView = fxaa_.sceneColor.imageView; - imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - VkWriteDescriptorSet write{}; - write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - write.dstSet = fxaa_.descSet; - write.dstBinding = 0; - write.descriptorCount = 1; - write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - write.pImageInfo = &imgInfo; - vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); - - // Pipeline layout — push constant holds vec4(rcpFrame.xy, sharpness, pad) - VkPushConstantRange pc{}; - pc.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - pc.offset = 0; - pc.size = 16; // vec4 - VkPipelineLayoutCreateInfo plCI{}; - plCI.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; - plCI.setLayoutCount = 1; - plCI.pSetLayouts = &fxaa_.descSetLayout; - plCI.pushConstantRangeCount = 1; - plCI.pPushConstantRanges = &pc; - vkCreatePipelineLayout(device, &plCI, nullptr, &fxaa_.pipelineLayout); - - // FXAA pipeline — fullscreen triangle into the swapchain render pass - // Uses VK_SAMPLE_COUNT_1_BIT: it always runs after MSAA resolve. - VkShaderModule vertMod, fragMod; - if (!vertMod.loadFromFile(device, "assets/shaders/postprocess.vert.spv") || - !fragMod.loadFromFile(device, "assets/shaders/fxaa.frag.spv")) { - LOG_ERROR("FXAA: failed to load shaders"); - destroyFXAAResources(); - return false; - } - - fxaa_.pipeline = PipelineBuilder() - .setShaders(vertMod.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), - fragMod.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) - .setVertexInput({}, {}) - .setTopology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) - .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) - .setNoDepthTest() - .setColorBlendAttachment(PipelineBuilder::blendDisabled()) - .setMultisample(VK_SAMPLE_COUNT_1_BIT) // swapchain pass is always 1x - .setLayout(fxaa_.pipelineLayout) - .setRenderPass(vkCtx->getImGuiRenderPass()) - .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) - .build(device, vkCtx->getPipelineCache()); - - vertMod.destroy(); - fragMod.destroy(); - - if (!fxaa_.pipeline) { - LOG_ERROR("FXAA: failed to create pipeline"); - destroyFXAAResources(); - return false; - } - - LOG_INFO("FXAA: initialized successfully"); - return true; -} - -void Renderer::destroyFXAAResources() { - if (!vkCtx) return; - VkDevice device = vkCtx->getDevice(); - VmaAllocator alloc = vkCtx->getAllocator(); - vkDeviceWaitIdle(device); - - if (fxaa_.pipeline) { vkDestroyPipeline(device, fxaa_.pipeline, nullptr); fxaa_.pipeline = VK_NULL_HANDLE; } - if (fxaa_.pipelineLayout) { vkDestroyPipelineLayout(device, fxaa_.pipelineLayout, nullptr); fxaa_.pipelineLayout = VK_NULL_HANDLE; } - if (fxaa_.descPool) { vkDestroyDescriptorPool(device, fxaa_.descPool, nullptr); fxaa_.descPool = VK_NULL_HANDLE; fxaa_.descSet = VK_NULL_HANDLE; } - if (fxaa_.descSetLayout) { vkDestroyDescriptorSetLayout(device, fxaa_.descSetLayout, nullptr); fxaa_.descSetLayout = VK_NULL_HANDLE; } - if (fxaa_.sceneFramebuffer) { vkDestroyFramebuffer(device, fxaa_.sceneFramebuffer, nullptr); fxaa_.sceneFramebuffer = VK_NULL_HANDLE; } - fxaa_.sceneSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache - destroyImage(device, alloc, fxaa_.sceneDepthResolve); - destroyImage(device, alloc, fxaa_.sceneMsaaColor); - destroyImage(device, alloc, fxaa_.sceneDepth); - destroyImage(device, alloc, fxaa_.sceneColor); -} - -void Renderer::renderFXAAPass() { - if (!fxaa_.pipeline || currentCmd == VK_NULL_HANDLE) return; - VkExtent2D ext = vkCtx->getSwapchainExtent(); - - vkCmdBindPipeline(currentCmd, VK_PIPELINE_BIND_POINT_GRAPHICS, fxaa_.pipeline); - vkCmdBindDescriptorSets(currentCmd, VK_PIPELINE_BIND_POINT_GRAPHICS, - fxaa_.pipelineLayout, 0, 1, &fxaa_.descSet, 0, nullptr); - - // Pass rcpFrame + sharpness + effect flag (vec4, 16 bytes). - // When FSR2/FSR3 is active alongside FXAA, forward FSR2's sharpness so the - // post-FXAA unsharp-mask step restores the crispness that FXAA's blur removes. - float sharpness = fsr2_.enabled ? fsr2_.sharpness : 0.0f; - float pc[4] = { - 1.0f / static_cast(ext.width), - 1.0f / static_cast(ext.height), - sharpness, - 0.0f - }; - vkCmdPushConstants(currentCmd, fxaa_.pipelineLayout, - VK_SHADER_STAGE_FRAGMENT_BIT, 0, 16, pc); - - vkCmdDraw(currentCmd, 3, 1, 0, 0); // fullscreen triangle +PostProcessPipeline* Renderer::getPostProcessPipeline() const { + return postProcessPipeline_.get(); } void Renderer::setFXAAEnabled(bool enabled) { - if (fxaa_.enabled == enabled) return; - // FXAA is a post-process AA pass intended to supplement FSR temporal output. - // It conflicts with MSAA (which resolves AA during the scene render pass), so - // refuse to enable FXAA when hardware MSAA is active. - if (enabled && vkCtx && vkCtx->getMsaaSamples() > VK_SAMPLE_COUNT_1_BIT) { - LOG_INFO("FXAA: blocked while MSAA is active — disable MSAA first"); - return; - } - fxaa_.enabled = enabled; - if (!enabled) { - fxaa_.needsRecreate = true; // defer destruction to next beginFrame() - } + if (postProcessPipeline_) postProcessPipeline_->setFXAAEnabled(enabled); +} +bool Renderer::isFXAAEnabled() const { + return postProcessPipeline_ && postProcessPipeline_->isFXAAEnabled(); } -// ========================= End FXAA ========================= +void Renderer::setFSREnabled(bool enabled) { + if (!postProcessPipeline_) return; + auto req = postProcessPipeline_->setFSREnabled(enabled); + if (req.requested) { + pendingMsaaSamples_ = req.samples; + msaaChangePending_ = true; + } +} +bool Renderer::isFSREnabled() const { + return postProcessPipeline_ && postProcessPipeline_->isFSREnabled(); +} +void Renderer::setFSRQuality(float scaleFactor) { + if (postProcessPipeline_) postProcessPipeline_->setFSRQuality(scaleFactor); +} +void Renderer::setFSRSharpness(float sharpness) { + if (postProcessPipeline_) postProcessPipeline_->setFSRSharpness(sharpness); +} +float Renderer::getFSRScaleFactor() const { + return postProcessPipeline_ ? postProcessPipeline_->getFSRScaleFactor() : 1.0f; +} +float Renderer::getFSRSharpness() const { + return postProcessPipeline_ ? postProcessPipeline_->getFSRSharpness() : 0.0f; +} + +void Renderer::setFSR2Enabled(bool enabled) { + if (!postProcessPipeline_) return; + auto req = postProcessPipeline_->setFSR2Enabled(enabled, camera.get()); + if (req.requested) { + pendingMsaaSamples_ = req.samples; + msaaChangePending_ = true; + } +} +bool Renderer::isFSR2Enabled() const { + return postProcessPipeline_ && postProcessPipeline_->isFSR2Enabled(); +} +void Renderer::setFSR2DebugTuning(float jitterSign, float motionVecScaleX, float motionVecScaleY) { + if (postProcessPipeline_) postProcessPipeline_->setFSR2DebugTuning(jitterSign, motionVecScaleX, motionVecScaleY); +} + +void Renderer::setAmdFsr3FramegenEnabled(bool enabled) { + if (postProcessPipeline_) postProcessPipeline_->setAmdFsr3FramegenEnabled(enabled); +} +bool Renderer::isAmdFsr3FramegenEnabled() const { + return postProcessPipeline_ && postProcessPipeline_->isAmdFsr3FramegenEnabled(); +} +float Renderer::getFSR2JitterSign() const { + return postProcessPipeline_ ? postProcessPipeline_->getFSR2JitterSign() : 1.0f; +} +float Renderer::getFSR2MotionVecScaleX() const { + return postProcessPipeline_ ? postProcessPipeline_->getFSR2MotionVecScaleX() : 1.0f; +} +float Renderer::getFSR2MotionVecScaleY() const { + return postProcessPipeline_ ? postProcessPipeline_->getFSR2MotionVecScaleY() : 1.0f; +} +bool Renderer::isAmdFsr2SdkAvailable() const { + return postProcessPipeline_ && postProcessPipeline_->isAmdFsr2SdkAvailable(); +} +bool Renderer::isAmdFsr3FramegenSdkAvailable() const { + return postProcessPipeline_ && postProcessPipeline_->isAmdFsr3FramegenSdkAvailable(); +} +bool Renderer::isAmdFsr3FramegenRuntimeActive() const { + return postProcessPipeline_ && postProcessPipeline_->isAmdFsr3FramegenRuntimeActive(); +} +bool Renderer::isAmdFsr3FramegenRuntimeReady() const { + return postProcessPipeline_ && postProcessPipeline_->isAmdFsr3FramegenRuntimeReady(); +} +const char* Renderer::getAmdFsr3FramegenRuntimePath() const { + return postProcessPipeline_ ? postProcessPipeline_->getAmdFsr3FramegenRuntimePath() : ""; +} +const std::string& Renderer::getAmdFsr3FramegenRuntimeError() const { + static const std::string empty; + return postProcessPipeline_ ? postProcessPipeline_->getAmdFsr3FramegenRuntimeError() : empty; +} +size_t Renderer::getAmdFsr3UpscaleDispatchCount() const { + return postProcessPipeline_ ? postProcessPipeline_->getAmdFsr3UpscaleDispatchCount() : 0; +} +size_t Renderer::getAmdFsr3FramegenDispatchCount() const { + return postProcessPipeline_ ? postProcessPipeline_->getAmdFsr3FramegenDispatchCount() : 0; +} +size_t Renderer::getAmdFsr3FallbackCount() const { + return postProcessPipeline_ ? postProcessPipeline_->getAmdFsr3FallbackCount() : 0; +} +void Renderer::setBrightness(float b) { + if (postProcessPipeline_) postProcessPipeline_->setBrightness(b); +} +float Renderer::getBrightness() const { + return postProcessPipeline_ ? postProcessPipeline_->getBrightness() : 1.0f; +} void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { (void)world; @@ -5619,11 +3739,14 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { renderOverlay(glm::vec4(0.30f, 0.35f, 0.42f, 0.45f), cmd); } // Brightness overlay (applied before minimap so it doesn't affect UI) - if (brightness_ < 0.99f) { - renderOverlay(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f - brightness_), cmd); - } else if (brightness_ > 1.01f) { - float alpha = (brightness_ - 1.0f) / 1.0f; // maps 1.0-2.0 → 0.0-1.0 - renderOverlay(glm::vec4(1.0f, 1.0f, 1.0f, alpha), cmd); + { + float br = postProcessPipeline_ ? postProcessPipeline_->getBrightness() : 1.0f; + if (br < 0.99f) { + renderOverlay(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f - br), cmd); + } else if (br > 1.01f) { + float alpha = (br - 1.0f) / 1.0f; + renderOverlay(glm::vec4(1.0f, 1.0f, 1.0f, alpha), cmd); + } } if (minimap && minimap->isEnabled() && camera && window) { glm::vec3 minimapCenter = camera->getPosition(); @@ -5764,11 +3887,14 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { renderOverlay(glm::vec4(0.30f, 0.35f, 0.42f, 0.45f)); } // Brightness overlay (applied before minimap so it doesn't affect UI) - if (brightness_ < 0.99f) { - renderOverlay(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f - brightness_)); - } else if (brightness_ > 1.01f) { - float alpha = (brightness_ - 1.0f) / 1.0f; - renderOverlay(glm::vec4(1.0f, 1.0f, 1.0f, alpha)); + { + float br = postProcessPipeline_ ? postProcessPipeline_->getBrightness() : 1.0f; + if (br < 0.99f) { + renderOverlay(glm::vec4(0.0f, 0.0f, 0.0f, 1.0f - br)); + } else if (br > 1.01f) { + float alpha = (br - 1.0f) / 1.0f; + renderOverlay(glm::vec4(1.0f, 1.0f, 1.0f, alpha)); + } } if (minimap && minimap->isEnabled() && camera && window) { glm::vec3 minimapCenter = camera->getPosition(); @@ -5859,6 +3985,11 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s if (swimEffects) { swimEffects->setM2Renderer(m2Renderer.get()); } + // Initialize SpellVisualSystem once M2Renderer is available (§4.4) + if (!spellVisualSystem_) { + spellVisualSystem_ = std::make_unique(); + spellVisualSystem_->initialize(m2Renderer.get()); + } } if (!wmoRenderer) { wmoRenderer = std::make_unique(); @@ -5901,8 +4032,8 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s terrainManager->setWMORenderer(wmoRenderer.get()); } // Set ambient sound manager for environmental audio emitters - if (ambientSoundManager) { - terrainManager->setAmbientSoundManager(ambientSoundManager.get()); + if (getAmbientSoundManager()) { + terrainManager->setAmbientSoundManager(getAmbientSoundManager()); } // Pass asset manager to character renderer for texture loading if (characterRenderer) { @@ -5933,36 +4064,36 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s if (worldMap) worldMap->setMapName(mapName); // Initialize audio managers - if (musicManager && assetManager && !cachedAssetManager) { + if (getMusicManager() && assetManager && !cachedAssetManager) { audio::AudioEngine::instance().setAssetManager(assetManager); - musicManager->initialize(assetManager); - if (footstepManager) { - footstepManager->initialize(assetManager); + getMusicManager()->initialize(assetManager); + if (getFootstepManager()) { + getFootstepManager()->initialize(assetManager); } - if (activitySoundManager) { - activitySoundManager->initialize(assetManager); + if (getActivitySoundManager()) { + getActivitySoundManager()->initialize(assetManager); } - if (mountSoundManager) { - mountSoundManager->initialize(assetManager); + if (getMountSoundManager()) { + getMountSoundManager()->initialize(assetManager); } - if (npcVoiceManager) { - npcVoiceManager->initialize(assetManager); + if (getNpcVoiceManager()) { + getNpcVoiceManager()->initialize(assetManager); } if (!deferredWorldInitEnabled_) { - if (ambientSoundManager) { - ambientSoundManager->initialize(assetManager); + if (getAmbientSoundManager()) { + getAmbientSoundManager()->initialize(assetManager); } - if (uiSoundManager) { - uiSoundManager->initialize(assetManager); + if (getUiSoundManager()) { + getUiSoundManager()->initialize(assetManager); } - if (combatSoundManager) { - combatSoundManager->initialize(assetManager); + if (getCombatSoundManager()) { + getCombatSoundManager()->initialize(assetManager); } - if (spellSoundManager) { - spellSoundManager->initialize(assetManager); + if (getSpellSoundManager()) { + getSpellSoundManager()->initialize(assetManager); } - if (movementSoundManager) { - movementSoundManager->initialize(assetManager); + if (getMovementSoundManager()) { + getMovementSoundManager()->initialize(assetManager); } if (questMarkerRenderer) { questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, assetManager); @@ -5971,7 +4102,7 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s if (envFlagEnabled("WOWEE_PREWARM_ZONE_MUSIC", false)) { if (zoneManager) { for (const auto& musicPath : zoneManager->getAllMusicPaths()) { - musicManager->preloadMusic(musicPath); + getMusicManager()->preloadMusic(musicPath); } } static const std::vector tavernTracks = { @@ -5981,7 +4112,7 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s "Sound\\Music\\ZoneMusic\\TavernHuman\\RA_HumanTavern2A.mp3", }; for (const auto& musicPath : tavernTracks) { - musicManager->preloadMusic(musicPath); + getMusicManager()->preloadMusic(musicPath); } } } else { @@ -6120,42 +4251,42 @@ bool Renderer::loadTerrainArea(const std::string& mapName, int centerX, int cent } // Initialize music manager with asset manager - if (musicManager && cachedAssetManager) { - if (!musicManager->isInitialized()) { - musicManager->initialize(cachedAssetManager); + if (getMusicManager() && cachedAssetManager) { + if (!getMusicManager()->isInitialized()) { + getMusicManager()->initialize(cachedAssetManager); } } - if (footstepManager && cachedAssetManager) { - if (!footstepManager->isInitialized()) { - footstepManager->initialize(cachedAssetManager); + if (getFootstepManager() && cachedAssetManager) { + if (!getFootstepManager()->isInitialized()) { + getFootstepManager()->initialize(cachedAssetManager); } } - if (activitySoundManager && cachedAssetManager) { - if (!activitySoundManager->isInitialized()) { - activitySoundManager->initialize(cachedAssetManager); + if (getActivitySoundManager() && cachedAssetManager) { + if (!getActivitySoundManager()->isInitialized()) { + getActivitySoundManager()->initialize(cachedAssetManager); } } - if (mountSoundManager && cachedAssetManager) { - mountSoundManager->initialize(cachedAssetManager); + if (getMountSoundManager() && cachedAssetManager) { + getMountSoundManager()->initialize(cachedAssetManager); } - if (npcVoiceManager && cachedAssetManager) { - npcVoiceManager->initialize(cachedAssetManager); + if (getNpcVoiceManager() && cachedAssetManager) { + getNpcVoiceManager()->initialize(cachedAssetManager); } if (!deferredWorldInitEnabled_) { - if (ambientSoundManager && cachedAssetManager) { - ambientSoundManager->initialize(cachedAssetManager); + if (getAmbientSoundManager() && cachedAssetManager) { + getAmbientSoundManager()->initialize(cachedAssetManager); } - if (uiSoundManager && cachedAssetManager) { - uiSoundManager->initialize(cachedAssetManager); + if (getUiSoundManager() && cachedAssetManager) { + getUiSoundManager()->initialize(cachedAssetManager); } - if (combatSoundManager && cachedAssetManager) { - combatSoundManager->initialize(cachedAssetManager); + if (getCombatSoundManager() && cachedAssetManager) { + getCombatSoundManager()->initialize(cachedAssetManager); } - if (spellSoundManager && cachedAssetManager) { - spellSoundManager->initialize(cachedAssetManager); + if (getSpellSoundManager() && cachedAssetManager) { + getSpellSoundManager()->initialize(cachedAssetManager); } - if (movementSoundManager && cachedAssetManager) { - movementSoundManager->initialize(cachedAssetManager); + if (getMovementSoundManager() && cachedAssetManager) { + getMovementSoundManager()->initialize(cachedAssetManager); } if (questMarkerRenderer && cachedAssetManager) { questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, cachedAssetManager); @@ -6167,8 +4298,8 @@ bool Renderer::loadTerrainArea(const std::string& mapName, int centerX, int cent } // Wire ambient sound manager to terrain manager for emitter registration - if (terrainManager && ambientSoundManager) { - terrainManager->setAmbientSoundManager(ambientSoundManager.get()); + if (terrainManager && getAmbientSoundManager()) { + terrainManager->setAmbientSoundManager(getAmbientSoundManager()); } // Wire WMO, M2, and water renderer to camera controller @@ -6199,10 +4330,6 @@ void Renderer::setTerrainStreaming(bool enabled) { } } -const char* Renderer::getAmdFsr3FramegenRuntimePath() const { - return fsr2_.amdFsr3RuntimePath.c_str(); -} - void Renderer::renderHUD() { if (currentCmd == VK_NULL_HANDLE) return; if (performanceHUD && camera) { diff --git a/src/rendering/spell_visual_system.cpp b/src/rendering/spell_visual_system.cpp new file mode 100644 index 00000000..a60a3870 --- /dev/null +++ b/src/rendering/spell_visual_system.cpp @@ -0,0 +1,231 @@ +#include "rendering/spell_visual_system.hpp" +#include "rendering/m2_renderer.hpp" +#include "pipeline/asset_manager.hpp" +#include "pipeline/dbc_loader.hpp" +#include "pipeline/dbc_layout.hpp" +#include "pipeline/m2_loader.hpp" +#include "core/application.hpp" +#include "core/logger.hpp" +#include + +namespace wowee { +namespace rendering { + +void SpellVisualSystem::initialize(M2Renderer* m2Renderer) { + m2Renderer_ = m2Renderer; +} + +void SpellVisualSystem::shutdown() { + reset(); + m2Renderer_ = nullptr; + cachedAssetManager_ = nullptr; +} + +// Load SpellVisual DBC chain: SpellVisualEffectName → SpellVisualKit → SpellVisual +// to build cast/impact M2 path lookup maps. +void SpellVisualSystem::loadSpellVisualDbc() { + if (spellVisualDbcLoaded_) return; + spellVisualDbcLoaded_ = true; // Set early to prevent re-entry on failure + + if (!cachedAssetManager_) { + cachedAssetManager_ = core::Application::getInstance().getAssetManager(); + } + if (!cachedAssetManager_) return; + + auto* layout = pipeline::getActiveDBCLayout(); + const pipeline::DBCFieldMap* svLayout = layout ? layout->getLayout("SpellVisual") : nullptr; + const pipeline::DBCFieldMap* kitLayout = layout ? layout->getLayout("SpellVisualKit") : nullptr; + const pipeline::DBCFieldMap* fxLayout = layout ? layout->getLayout("SpellVisualEffectName") : nullptr; + + uint32_t svCastKitField = svLayout ? (*svLayout)["CastKit"] : 2; + uint32_t svImpactKitField = svLayout ? (*svLayout)["ImpactKit"] : 3; + uint32_t svMissileField = svLayout ? (*svLayout)["MissileModel"] : 8; + uint32_t kitSpecial0Field = kitLayout ? (*kitLayout)["SpecialEffect0"] : 11; + uint32_t kitBaseField = kitLayout ? (*kitLayout)["BaseEffect"] : 5; + uint32_t fxFilePathField = fxLayout ? (*fxLayout)["FilePath"] : 2; + + // Helper to look up effectName path from a kit ID + // Load SpellVisualEffectName.dbc — ID → M2 path + auto fxDbc = cachedAssetManager_->loadDBC("SpellVisualEffectName.dbc"); + if (!fxDbc || !fxDbc->isLoaded() || fxDbc->getFieldCount() <= fxFilePathField) { + LOG_DEBUG("SpellVisual: SpellVisualEffectName.dbc unavailable (fc=", + fxDbc ? fxDbc->getFieldCount() : 0, ")"); + return; + } + std::unordered_map effectPaths; // effectNameId → path + for (uint32_t i = 0; i < fxDbc->getRecordCount(); ++i) { + uint32_t id = fxDbc->getUInt32(i, 0); + std::string p = fxDbc->getString(i, fxFilePathField); + if (id && !p.empty()) effectPaths[id] = p; + } + + // Load SpellVisualKit.dbc — kitId → best SpellVisualEffectName ID + auto kitDbc = cachedAssetManager_->loadDBC("SpellVisualKit.dbc"); + std::unordered_map kitToEffectName; // kitId → effectNameId + if (kitDbc && kitDbc->isLoaded()) { + uint32_t fc = kitDbc->getFieldCount(); + for (uint32_t i = 0; i < kitDbc->getRecordCount(); ++i) { + uint32_t kitId = kitDbc->getUInt32(i, 0); + if (!kitId) continue; + // Prefer SpecialEffect0, fall back to BaseEffect + uint32_t eff = 0; + if (kitSpecial0Field < fc) eff = kitDbc->getUInt32(i, kitSpecial0Field); + if (!eff && kitBaseField < fc) eff = kitDbc->getUInt32(i, kitBaseField); + if (eff) kitToEffectName[kitId] = eff; + } + } + + // Helper: resolve path for a given kit ID + auto kitPath = [&](uint32_t kitId) -> std::string { + if (!kitId) return {}; + auto kitIt = kitToEffectName.find(kitId); + if (kitIt == kitToEffectName.end()) return {}; + auto fxIt = effectPaths.find(kitIt->second); + return (fxIt != effectPaths.end()) ? fxIt->second : std::string{}; + }; + auto missilePath = [&](uint32_t effId) -> std::string { + if (!effId) return {}; + auto fxIt = effectPaths.find(effId); + return (fxIt != effectPaths.end()) ? fxIt->second : std::string{}; + }; + + // Load SpellVisual.dbc — visualId → cast/impact M2 paths via kit chain + auto svDbc = cachedAssetManager_->loadDBC("SpellVisual.dbc"); + if (!svDbc || !svDbc->isLoaded()) { + LOG_DEBUG("SpellVisual: SpellVisual.dbc unavailable"); + return; + } + uint32_t svFc = svDbc->getFieldCount(); + uint32_t loadedCast = 0, loadedImpact = 0; + for (uint32_t i = 0; i < svDbc->getRecordCount(); ++i) { + uint32_t vid = svDbc->getUInt32(i, 0); + if (!vid) continue; + + // Cast path: CastKit → SpecialEffect0/BaseEffect, fallback to MissileModel + { + std::string path; + if (svCastKitField < svFc) + path = kitPath(svDbc->getUInt32(i, svCastKitField)); + if (path.empty() && svMissileField < svFc) + path = missilePath(svDbc->getUInt32(i, svMissileField)); + if (!path.empty()) { spellVisualCastPath_[vid] = path; ++loadedCast; } + } + // Impact path: ImpactKit → SpecialEffect0/BaseEffect, fallback to MissileModel + { + std::string path; + if (svImpactKitField < svFc) + path = kitPath(svDbc->getUInt32(i, svImpactKitField)); + if (path.empty() && svMissileField < svFc) + path = missilePath(svDbc->getUInt32(i, svMissileField)); + if (!path.empty()) { spellVisualImpactPath_[vid] = path; ++loadedImpact; } + } + } + LOG_INFO("SpellVisual: loaded cast=", loadedCast, " impact=", loadedImpact, + " visual→M2 mappings (of ", svDbc->getRecordCount(), " records)"); +} + +void SpellVisualSystem::playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition, + bool useImpactKit) { + if (!m2Renderer_ || visualId == 0) return; + + if (!cachedAssetManager_) + cachedAssetManager_ = core::Application::getInstance().getAssetManager(); + if (!cachedAssetManager_) return; + + if (!spellVisualDbcLoaded_) loadSpellVisualDbc(); + + // Select cast or impact path map + auto& pathMap = useImpactKit ? spellVisualImpactPath_ : spellVisualCastPath_; + auto pathIt = pathMap.find(visualId); + if (pathIt == pathMap.end()) return; // No model for this visual + + const std::string& modelPath = pathIt->second; + + // Get or assign a model ID for this path + auto midIt = spellVisualModelIds_.find(modelPath); + uint32_t modelId = 0; + if (midIt != spellVisualModelIds_.end()) { + modelId = midIt->second; + } else { + if (nextSpellVisualModelId_ >= 999800) { + LOG_WARNING("SpellVisual: model ID pool exhausted"); + return; + } + modelId = nextSpellVisualModelId_++; + spellVisualModelIds_[modelPath] = modelId; + } + + // Skip models that have previously failed to load (avoid repeated I/O) + if (spellVisualFailedModels_.count(modelId)) return; + + // Load the M2 model if not already loaded + if (!m2Renderer_->hasModel(modelId)) { + auto m2Data = cachedAssetManager_->readFile(modelPath); + if (m2Data.empty()) { + LOG_DEBUG("SpellVisual: could not read model: ", modelPath); + spellVisualFailedModels_.insert(modelId); + return; + } + pipeline::M2Model model = pipeline::M2Loader::load(m2Data); + if (model.vertices.empty() && model.particleEmitters.empty()) { + LOG_DEBUG("SpellVisual: empty model: ", modelPath); + spellVisualFailedModels_.insert(modelId); + return; + } + // Load skin file for WotLK-format M2s + if (model.version >= 264) { + std::string skinPath = modelPath.substr(0, modelPath.rfind('.')) + "00.skin"; + auto skinData = cachedAssetManager_->readFile(skinPath); + if (!skinData.empty()) pipeline::M2Loader::loadSkin(skinData, model); + } + if (!m2Renderer_->loadModel(model, modelId)) { + LOG_WARNING("SpellVisual: failed to load model to GPU: ", modelPath); + spellVisualFailedModels_.insert(modelId); + return; + } + LOG_DEBUG("SpellVisual: loaded model id=", modelId, " path=", modelPath); + } + + // Spawn instance at world position + uint32_t instanceId = m2Renderer_->createInstance(modelId, worldPosition, + glm::vec3(0.0f), 1.0f); + if (instanceId == 0) { + LOG_WARNING("SpellVisual: failed to create instance for visualId=", visualId); + return; + } + // Determine lifetime from M2 animation duration (clamp to reasonable range) + float animDurMs = m2Renderer_->getInstanceAnimDuration(instanceId); + float duration = (animDurMs > 100.0f) + ? std::clamp(animDurMs / 1000.0f, 0.5f, SPELL_VISUAL_MAX_DURATION) + : SPELL_VISUAL_DEFAULT_DURATION; + activeSpellVisuals_.push_back({instanceId, 0.0f, duration}); + LOG_DEBUG("SpellVisual: spawned visualId=", visualId, " instanceId=", instanceId, + " duration=", duration, "s model=", modelPath); +} + +void SpellVisualSystem::update(float deltaTime) { + if (activeSpellVisuals_.empty() || !m2Renderer_) return; + for (auto it = activeSpellVisuals_.begin(); it != activeSpellVisuals_.end(); ) { + it->elapsed += deltaTime; + if (it->elapsed >= it->duration) { + m2Renderer_->removeInstance(it->instanceId); + it = activeSpellVisuals_.erase(it); + } else { + ++it; + } + } +} + +void SpellVisualSystem::reset() { + // Clear lingering spell visual instances from the previous map/combat session. + // Without this, old effects could remain visible after teleport or map change. + for (auto& sv : activeSpellVisuals_) { + if (m2Renderer_) m2Renderer_->removeInstance(sv.instanceId); + } + activeSpellVisuals_.clear(); + // Reset the negative cache so models that failed during asset loading can retry. + spellVisualFailedModels_.clear(); +} + +} // namespace rendering +} // namespace wowee From 5af9f7aa4bfdaec2d0abf3d2db06d321e359324a Mon Sep 17 00:00:00 2001 From: Paul Date: Thu, 2 Apr 2026 13:06:31 +0300 Subject: [PATCH 2/2] chore(renderer): extract AnimationController and remove audio pass-throughs Extract ~1,500 lines of character animation state from Renderer into a dedicated AnimationController class, and complete the AudioCoordinator migration by removing all 10 audio pass-through getters from Renderer. AnimationController: - New: include/rendering/animation_controller.hpp (182 lines) - New: src/rendering/animation_controller.cpp (1,703 lines) - Moves: locomotion state machine (50+ members), mount animation (40+ members), emote system, footstep triggering, surface detection, melee combat animations - Renderer holds std::unique_ptr and delegates completely - AnimationController accesses audio via renderer_->getAudioCoordinator() Audio caller migration: - Migrate ~60 external callers from renderer->getXManager() to AudioCoordinator directly, grouped by access pattern: - UIServices: settings_panel, game_screen, toast_manager, chat_panel, combat_ui, window_manager - GameServices: game_handler, spell_handler, inventory_handler, quest_handler, social_handler, combat_handler - Application singleton: application.cpp, auth_screen.cpp, lua_engine.cpp - Remove 10 pass-through getter definitions from renderer.cpp - Remove 10 pass-through getter declarations from renderer.hpp - Remove individual audio manager forward declarations from renderer.hpp - Redirect 69 internal renderer.cpp audio calls to audioCoordinator_ directly - game_handler.cpp: withSoundManager template uses services_.audioCoordinator; MFP changed from &Renderer::getUiSoundManager to &AudioCoordinator::getUiSoundManager - GameServices struct: add AudioCoordinator* audioCoordinator member - settings_panel: applyAudioVolumes(Renderer*) -> applyAudioVolumes(AudioCoordinator*) --- CMakeLists.txt | 1 + include/game/game_services.hpp | 2 + include/rendering/animation_controller.hpp | 182 ++ include/rendering/renderer.hpp | 126 +- include/ui/settings_panel.hpp | 5 +- src/addons/lua_engine.cpp | 7 +- src/core/application.cpp | 23 +- src/game/combat_handler.cpp | 7 +- src/game/game_handler.cpp | 17 +- src/game/inventory_handler.cpp | 47 +- src/game/quest_handler.cpp | 9 +- src/game/social_handler.cpp | 9 +- src/game/spell_handler.cpp | 37 +- src/rendering/animation_controller.cpp | 1703 ++++++++++++++++++ src/rendering/renderer.cpp | 1876 ++------------------ src/ui/auth_screen.cpp | 17 +- src/ui/chat_panel.cpp | 5 +- src/ui/combat_ui.cpp | 9 +- src/ui/game_screen.cpp | 35 +- src/ui/settings_panel.cpp | 27 +- src/ui/toast_manager.cpp | 15 +- src/ui/window_manager.cpp | 7 +- 22 files changed, 2208 insertions(+), 1958 deletions(-) create mode 100644 include/rendering/animation_controller.hpp create mode 100644 src/rendering/animation_controller.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index fe288549..e2a95590 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -613,6 +613,7 @@ set(WOWEE_SOURCES src/rendering/charge_effect.cpp src/rendering/spell_visual_system.cpp src/rendering/post_process_pipeline.cpp + src/rendering/animation_controller.cpp src/rendering/loading_screen.cpp # UI diff --git a/include/game/game_services.hpp b/include/game/game_services.hpp index e01f4487..21a14251 100644 --- a/include/game/game_services.hpp +++ b/include/game/game_services.hpp @@ -4,6 +4,7 @@ namespace wowee { namespace rendering { class Renderer; } namespace pipeline { class AssetManager; } +namespace audio { class AudioCoordinator; } namespace game { class ExpansionRegistry; } namespace game { @@ -13,6 +14,7 @@ namespace game { // Replaces hidden Application::getInstance() singleton access. struct GameServices { rendering::Renderer* renderer = nullptr; + audio::AudioCoordinator* audioCoordinator = nullptr; pipeline::AssetManager* assetManager = nullptr; ExpansionRegistry* expansionRegistry = nullptr; uint32_t gryphonDisplayId = 0; diff --git a/include/rendering/animation_controller.hpp b/include/rendering/animation_controller.hpp new file mode 100644 index 00000000..81166d12 --- /dev/null +++ b/include/rendering/animation_controller.hpp @@ -0,0 +1,182 @@ +#pragma once + +#include +#include +#include +#include + +namespace wowee { +namespace audio { enum class FootstepSurface : uint8_t; } +namespace rendering { + +class Renderer; + +// ============================================================================ +// AnimationController — extracted from Renderer (§4.2) +// +// Owns the character locomotion state machine, mount animation state, +// emote system, footstep triggering, surface detection, melee combat +// animation, and activity SFX transition tracking. +// ============================================================================ +class AnimationController { +public: + AnimationController(); + ~AnimationController(); + + void initialize(Renderer* renderer); + + // ── Per-frame update hooks (called from Renderer::update) ────────────── + // Runs the character animation state machine (mounted + unmounted). + void updateCharacterAnimation(); + // Processes animation-driven footstep events (player + mount). + void updateFootsteps(float deltaTime); + // Tracks state transitions for activity SFX (jump, landing, swim) and + // mount ambient sounds. + void updateSfxState(float deltaTime); + // Decrements melee swing timer / cooldown. + void updateMeleeTimers(float deltaTime); + // Store per-frame delta time (used inside animation state machine). + void setDeltaTime(float dt) { lastDeltaTime_ = dt; } + + // ── Character follow ─────────────────────────────────────────────────── + void onCharacterFollow(uint32_t instanceId); + + // ── Emote support ────────────────────────────────────────────────────── + void playEmote(const std::string& emoteName); + void cancelEmote(); + bool isEmoteActive() const { return emoteActive_; } + static std::string getEmoteText(const std::string& emoteName, + const std::string* targetName = nullptr); + static uint32_t getEmoteDbcId(const std::string& emoteName); + static std::string getEmoteTextByDbcId(uint32_t dbcId, + const std::string& senderName, + const std::string* targetName = nullptr); + static uint32_t getEmoteAnimByDbcId(uint32_t dbcId); + + // ── Targeting / combat ───────────────────────────────────────────────── + void setTargetPosition(const glm::vec3* pos); + void setInCombat(bool combat) { inCombat_ = combat; } + bool isInCombat() const { return inCombat_; } + const glm::vec3* getTargetPosition() const { return targetPosition_; } + void resetCombatVisualState(); + bool isMoving() const; + + // ── Melee combat ─────────────────────────────────────────────────────── + void triggerMeleeSwing(); + void setEquippedWeaponType(uint32_t inventoryType) { equippedWeaponInvType_ = inventoryType; meleeAnimId_ = 0; } + void setCharging(bool charging) { charging_ = charging; } + bool isCharging() const { return charging_; } + + // ── Effect triggers ──────────────────────────────────────────────────── + void triggerLevelUpEffect(const glm::vec3& position); + void startChargeEffect(const glm::vec3& position, const glm::vec3& direction); + void emitChargeEffect(const glm::vec3& position, const glm::vec3& direction); + void stopChargeEffect(); + + // ── Mount ────────────────────────────────────────────────────────────── + void setMounted(uint32_t mountInstId, uint32_t mountDisplayId, + float heightOffset, const std::string& modelPath = ""); + void setTaxiFlight(bool onTaxi) { taxiFlight_ = onTaxi; } + void setMountPitchRoll(float pitch, float roll) { mountPitch_ = pitch; mountRoll_ = roll; } + void clearMount(); + bool isMounted() const { return mountInstanceId_ != 0; } + uint32_t getMountInstanceId() const { return mountInstanceId_; } + + // ── Query helpers (used by Renderer) ─────────────────────────────────── + bool isFootstepAnimationState() const; + float getMeleeSwingTimer() const { return meleeSwingTimer_; } + float getMountHeightOffset() const { return mountHeightOffset_; } + bool isTaxiFlight() const { return taxiFlight_; } + +private: + Renderer* renderer_ = nullptr; + + // Character animation state machine + enum class CharAnimState { + IDLE, WALK, RUN, JUMP_START, JUMP_MID, JUMP_END, SIT_DOWN, SITTING, + EMOTE, SWIM_IDLE, SWIM, MELEE_SWING, MOUNT, CHARGE, COMBAT_IDLE + }; + CharAnimState charAnimState_ = CharAnimState::IDLE; + float locomotionStopGraceTimer_ = 0.0f; + bool locomotionWasSprinting_ = false; + uint32_t lastPlayerAnimRequest_ = UINT32_MAX; + bool lastPlayerAnimLoopRequest_ = true; + + // Emote state + bool emoteActive_ = false; + uint32_t emoteAnimId_ = 0; + bool emoteLoop_ = false; + + // Target facing + const glm::vec3* targetPosition_ = nullptr; + bool inCombat_ = false; + + // Footstep event tracking (animation-driven) + uint32_t footstepLastAnimationId_ = 0; + float footstepLastNormTime_ = 0.0f; + bool footstepNormInitialized_ = false; + + // Footstep surface cache (avoid expensive queries every step) + mutable audio::FootstepSurface cachedFootstepSurface_{}; + mutable glm::vec3 cachedFootstepPosition_{0.0f, 0.0f, 0.0f}; + mutable float cachedFootstepUpdateTimer_{999.0f}; + + // Mount footstep tracking (separate from player's) + uint32_t mountFootstepLastAnimId_ = 0; + float mountFootstepLastNormTime_ = 0.0f; + bool mountFootstepNormInitialized_ = false; + + // SFX transition state + bool sfxStateInitialized_ = false; + bool sfxPrevGrounded_ = true; + bool sfxPrevJumping_ = false; + bool sfxPrevFalling_ = false; + bool sfxPrevSwimming_ = false; + + // Melee combat + bool charging_ = false; + float meleeSwingTimer_ = 0.0f; + float meleeSwingCooldown_ = 0.0f; + float meleeAnimDurationMs_ = 0.0f; + uint32_t meleeAnimId_ = 0; + uint32_t equippedWeaponInvType_ = 0; + + // Mount animation capabilities (discovered at mount time, varies per model) + struct MountAnimSet { + uint32_t jumpStart = 0; // Jump start animation + uint32_t jumpLoop = 0; // Jump airborne loop + uint32_t jumpEnd = 0; // Jump landing + uint32_t rearUp = 0; // Rear-up / special flourish + uint32_t run = 0; // Run animation (discovered, don't assume) + uint32_t stand = 0; // Stand animation (discovered) + std::vector fidgets; // Idle fidget animations (head turn, tail swish, etc.) + }; + + enum class MountAction { None, Jump, RearUp }; + + uint32_t mountInstanceId_ = 0; + float mountHeightOffset_ = 0.0f; + float mountPitch_ = 0.0f; // Up/down tilt (radians) + float mountRoll_ = 0.0f; // Left/right banking (radians) + int mountSeatAttachmentId_ = -1; // -1 unknown, -2 unavailable + glm::vec3 smoothedMountSeatPos_ = glm::vec3(0.0f); + bool mountSeatSmoothingInit_ = false; + float prevMountYaw_ = 0.0f; // Previous yaw for turn rate calculation (procedural lean) + float lastDeltaTime_ = 0.0f; // Cached for use in updateCharacterAnimation() + MountAction mountAction_ = MountAction::None; // Current mount action (jump/rear-up) + uint32_t mountActionPhase_ = 0; // 0=start, 1=loop, 2=end (for jump chaining) + MountAnimSet mountAnims_; // Cached animation IDs for current mount + float mountIdleFidgetTimer_ = 0.0f; // Timer for random idle fidgets + float mountIdleSoundTimer_ = 0.0f; // Timer for ambient idle sounds + uint32_t mountActiveFidget_ = 0; // Currently playing fidget animation ID (0 = none) + bool taxiFlight_ = false; + bool taxiAnimsLogged_ = false; + + // Private animation helpers + bool shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs); + audio::FootstepSurface resolveFootstepSurface() const; + uint32_t resolveMeleeAnimId(); +}; + +} // namespace rendering +} // namespace wowee diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index 10867a7b..54372da9 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -19,7 +19,7 @@ namespace wowee { namespace core { class Window; } namespace rendering { class VkContext; } namespace game { class World; class ZoneManager; class GameHandler; } -namespace audio { class AudioCoordinator; class MusicManager; class FootstepManager; class ActivitySoundManager; class MountSoundManager; class NpcVoiceManager; class AmbientSoundManager; class UiSoundManager; class CombatSoundManager; class SpellSoundManager; class MovementSoundManager; enum class FootstepSurface : uint8_t; enum class VoiceType; } +namespace audio { class AudioCoordinator; } namespace pipeline { class AssetManager; } namespace rendering { @@ -52,6 +52,10 @@ class CharacterPreview; class AmdFsr3Runtime; class SpellVisualSystem; class PostProcessPipeline; +class AnimationController; +class LevelUpEffect; +class ChargeEffect; +class SwimEffects; class Renderer { public: @@ -146,7 +150,7 @@ public: float getCharacterYaw() const { return characterYaw; } void setCharacterYaw(float yawDeg) { characterYaw = yawDeg; } - // Emote support + // Emote support — delegates to AnimationController (§4.2) void playEmote(const std::string& emoteName); void triggerLevelUpEffect(const glm::vec3& position); void cancelEmote(); @@ -159,31 +163,37 @@ public: void playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition, bool useImpactKit = false); SpellVisualSystem* getSpellVisualSystem() const { return spellVisualSystem_.get(); } - bool isEmoteActive() const { return emoteActive; } + bool isEmoteActive() const; static std::string getEmoteText(const std::string& emoteName, const std::string* targetName = nullptr); static uint32_t getEmoteDbcId(const std::string& emoteName); static std::string getEmoteTextByDbcId(uint32_t dbcId, const std::string& senderName, const std::string* targetName = nullptr); static uint32_t getEmoteAnimByDbcId(uint32_t dbcId); - // Targeting support + // Targeting support — delegates to AnimationController (§4.2) void setTargetPosition(const glm::vec3* pos); - void setInCombat(bool combat) { inCombat_ = combat; } + void setInCombat(bool combat); void resetCombatVisualState(); bool isMoving() const; void triggerMeleeSwing(); - void setEquippedWeaponType(uint32_t inventoryType) { equippedWeaponInvType_ = inventoryType; meleeAnimId = 0; } - void setCharging(bool charging) { charging_ = charging; } - bool isCharging() const { return charging_; } + void setEquippedWeaponType(uint32_t inventoryType); + void setCharging(bool charging); + bool isCharging() const; void startChargeEffect(const glm::vec3& position, const glm::vec3& direction); void emitChargeEffect(const glm::vec3& position, const glm::vec3& direction); void stopChargeEffect(); - // Mount rendering + // Mount rendering — delegates to AnimationController (§4.2) void setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset, const std::string& modelPath = ""); - void setTaxiFlight(bool onTaxi) { taxiFlight_ = onTaxi; } - void setMountPitchRoll(float pitch, float roll) { mountPitch_ = pitch; mountRoll_ = roll; } + void setTaxiFlight(bool onTaxi); + void setMountPitchRoll(float pitch, float roll); void clearMount(); - bool isMounted() const { return mountInstanceId_ != 0; } + bool isMounted() const; + + // AnimationController access (§4.2) + AnimationController* getAnimationController() const { return animationController_.get(); } + LevelUpEffect* getLevelUpEffect() const { return levelUpEffect.get(); } + ChargeEffect* getChargeEffect() const { return chargeEffect.get(); } + SwimEffects* getSwimEffects() const { return swimEffects.get(); } // Selection circle for targeted entity void setSelectionCircle(const glm::vec3& pos, float radius, const glm::vec3& color); @@ -196,20 +206,9 @@ public: double getLastTerrainRenderMs() const { return lastTerrainRenderMs; } double getLastWMORenderMs() const { return lastWMORenderMs; } double getLastM2RenderMs() const { return lastM2RenderMs; } - // Audio accessors — delegate to AudioCoordinator (owned by Application). - // These pass-throughs remain until §4.2 moves animation audio out of Renderer. + // Audio coordinator — owned by Application, set via setAudioCoordinator(). void setAudioCoordinator(audio::AudioCoordinator* ac) { audioCoordinator_ = ac; } audio::AudioCoordinator* getAudioCoordinator() { return audioCoordinator_; } - audio::MusicManager* getMusicManager(); - audio::FootstepManager* getFootstepManager(); - audio::ActivitySoundManager* getActivitySoundManager(); - audio::MountSoundManager* getMountSoundManager(); - audio::NpcVoiceManager* getNpcVoiceManager(); - audio::AmbientSoundManager* getAmbientSoundManager(); - audio::UiSoundManager* getUiSoundManager(); - audio::CombatSoundManager* getCombatSoundManager(); - audio::SpellSoundManager* getSpellSoundManager(); - audio::MovementSoundManager* getMovementSoundManager(); game::ZoneManager* getZoneManager() { return zoneManager.get(); } LightingManager* getLightingManager() { return lightingManager.get(); } @@ -243,6 +242,7 @@ private: std::unique_ptr worldMap; std::unique_ptr questMarkerRenderer; audio::AudioCoordinator* audioCoordinator_ = nullptr; // Owned by Application + std::unique_ptr animationController_; // §4.2 std::unique_ptr zoneManager; // Shadow mapping (Vulkan) static constexpr uint32_t SHADOW_MAP_SIZE = 4096; @@ -340,27 +340,7 @@ private: uint32_t characterInstanceId = 0; float characterYaw = 0.0f; - // Character animation state - enum class CharAnimState { IDLE, WALK, RUN, JUMP_START, JUMP_MID, JUMP_END, SIT_DOWN, SITTING, EMOTE, SWIM_IDLE, SWIM, MELEE_SWING, MOUNT, CHARGE, COMBAT_IDLE }; - CharAnimState charAnimState = CharAnimState::IDLE; - float locomotionStopGraceTimer_ = 0.0f; - bool locomotionWasSprinting_ = false; - uint32_t lastPlayerAnimRequest_ = UINT32_MAX; - bool lastPlayerAnimLoopRequest_ = true; - void updateCharacterAnimation(); - bool isFootstepAnimationState() const; - bool shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs); - audio::FootstepSurface resolveFootstepSurface() const; - uint32_t resolveMeleeAnimId(); - // Emote state - bool emoteActive = false; - uint32_t emoteAnimId = 0; - bool emoteLoop = false; - - // Target facing - const glm::vec3* targetPosition = nullptr; - bool inCombat_ = false; // Selection circle rendering (Vulkan) VkPipeline selCirclePipeline = VK_NULL_HANDLE; @@ -383,64 +363,7 @@ private: void initOverlayPipeline(); void renderOverlay(const glm::vec4& color, VkCommandBuffer overrideCmd = VK_NULL_HANDLE); - // Footstep event tracking (animation-driven) - uint32_t footstepLastAnimationId = 0; - float footstepLastNormTime = 0.0f; - bool footstepNormInitialized = false; - // Footstep surface cache (avoid expensive queries every step) - mutable audio::FootstepSurface cachedFootstepSurface{}; - mutable glm::vec3 cachedFootstepPosition{0.0f, 0.0f, 0.0f}; - mutable float cachedFootstepUpdateTimer{999.0f}; // Force initial query - - // Mount footstep tracking (separate from player's) - uint32_t mountFootstepLastAnimId = 0; - float mountFootstepLastNormTime = 0.0f; - bool mountFootstepNormInitialized = false; - bool sfxStateInitialized = false; - bool sfxPrevGrounded = true; - bool sfxPrevJumping = false; - bool sfxPrevFalling = false; - bool sfxPrevSwimming = false; - - bool charging_ = false; - float meleeSwingTimer = 0.0f; - float meleeSwingCooldown = 0.0f; - float meleeAnimDurationMs = 0.0f; - uint32_t meleeAnimId = 0; - uint32_t equippedWeaponInvType_ = 0; - - // Mount state - // Mount animation capabilities (discovered at mount time, varies per model) - struct MountAnimSet { - uint32_t jumpStart = 0; // Jump start animation - uint32_t jumpLoop = 0; // Jump airborne loop - uint32_t jumpEnd = 0; // Jump landing - uint32_t rearUp = 0; // Rear-up / special flourish - uint32_t run = 0; // Run animation (discovered, don't assume) - uint32_t stand = 0; // Stand animation (discovered) - std::vector fidgets; // Idle fidget animations (head turn, tail swish, etc.) - }; - - enum class MountAction { None, Jump, RearUp }; - - uint32_t mountInstanceId_ = 0; - float mountHeightOffset_ = 0.0f; - float mountPitch_ = 0.0f; // Up/down tilt (radians) - float mountRoll_ = 0.0f; // Left/right banking (radians) - int mountSeatAttachmentId_ = -1; // -1 unknown, -2 unavailable - glm::vec3 smoothedMountSeatPos_ = glm::vec3(0.0f); - bool mountSeatSmoothingInit_ = false; - float prevMountYaw_ = 0.0f; // Previous yaw for turn rate calculation (procedural lean) - float lastDeltaTime_ = 0.0f; // Cached for use in updateCharacterAnimation() - MountAction mountAction_ = MountAction::None; // Current mount action (jump/rear-up) - uint32_t mountActionPhase_ = 0; // 0=start, 1=loop, 2=end (for jump chaining) - MountAnimSet mountAnims_; // Cached animation IDs for current mount - float mountIdleFidgetTimer_ = 0.0f; // Timer for random idle fidgets - float mountIdleSoundTimer_ = 0.0f; // Timer for ambient idle sounds - uint32_t mountActiveFidget_ = 0; // Currently playing fidget animation ID (0 = none) - bool taxiFlight_ = false; - bool taxiAnimsLogged_ = false; // Vulkan frame state VkContext* vkCtx = nullptr; @@ -491,6 +414,7 @@ private: bool parallelRecordingEnabled_ = false; // set true after pools/buffers created bool endFrameInlineMode_ = false; // true when endFrame switched to INLINE render pass + float lastDeltaTime_ = 0.0f; // cached for post-process pipeline bool createSecondaryCommandResources(); void destroySecondaryCommandResources(); VkCommandBuffer beginSecondary(uint32_t secondaryIndex); diff --git a/include/ui/settings_panel.hpp b/include/ui/settings_panel.hpp index 5dd58136..a024ffad 100644 --- a/include/ui/settings_panel.hpp +++ b/include/ui/settings_panel.hpp @@ -8,6 +8,7 @@ namespace wowee { namespace rendering { class Renderer; } +namespace audio { class AudioCoordinator; } namespace ui { class InventoryScreen; @@ -144,8 +145,8 @@ public: void renderSettingsWindow(InventoryScreen& inventoryScreen, ChatPanel& chatPanel, std::function saveCallback); - /// Apply audio volume levels to all renderer sound managers - void applyAudioVolumes(rendering::Renderer* renderer); + /// Apply audio volume levels to all audio coordinator sound managers + void applyAudioVolumes(audio::AudioCoordinator* ac); /// Return the platform-specific settings file path static std::string getSettingsPath(); diff --git a/src/addons/lua_engine.cpp b/src/addons/lua_engine.cpp index 625054c2..bbba10b9 100644 --- a/src/addons/lua_engine.cpp +++ b/src/addons/lua_engine.cpp @@ -6,6 +6,7 @@ #include "core/logger.hpp" #include "core/application.hpp" #include "rendering/renderer.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/ui_sound_manager.hpp" #include "game/expansion_profile.hpp" #include @@ -1003,9 +1004,9 @@ static int lua_IsInRaid(lua_State* L) { // PlaySound(soundId) — play a WoW UI sound by ID or name static int lua_PlaySound(lua_State* L) { - auto* renderer = core::Application::getInstance().getRenderer(); - if (!renderer) return 0; - auto* sfx = renderer->getUiSoundManager(); + auto* ac = core::Application::getInstance().getAudioCoordinator(); + if (!ac) return 0; + auto* sfx = ac->getUiSoundManager(); if (!sfx) return 0; // Accept numeric sound ID or string name diff --git a/src/core/application.cpp b/src/core/application.cpp index c5621daf..a68bf754 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -152,6 +152,7 @@ bool Application::initialize() { // Populate game services — all subsystems now available gameServices_.renderer = renderer.get(); + gameServices_.audioCoordinator = audioCoordinator_.get(); gameServices_.assetManager = assetManager.get(); gameServices_.expansionRegistry = expansionRegistry_.get(); @@ -1091,7 +1092,7 @@ void Application::logoutToLogin() { } renderer->clearMount(); renderer->setCharacterFollow(0); - if (auto* music = renderer->getMusicManager()) { + if (auto* music = audioCoordinator_ ? audioCoordinator_->getMusicManager() : nullptr) { music->stopMusic(0.0f); } } @@ -2828,7 +2829,7 @@ void Application::setupUICallbacks() { // Resolves soundId → SoundEntries.dbc → MPQ path → MusicManager. gameHandler->setPlayMusicCallback([this](uint32_t soundId) { if (!assetManager || !renderer) return; - auto* music = renderer->getMusicManager(); + auto* music = audioCoordinator_ ? audioCoordinator_->getMusicManager() : nullptr; if (!music) return; auto dbc = assetManager->loadDBC("SoundEntries.dbc"); @@ -3418,7 +3419,7 @@ void Application::setupUICallbacks() { // NPC greeting callback - play voice line gameHandler->setNpcGreetingCallback([this](uint64_t guid, const glm::vec3& position) { - if (renderer && renderer->getNpcVoiceManager()) { + if (audioCoordinator_ && audioCoordinator_->getNpcVoiceManager()) { // Convert canonical to render coords for 3D audio glm::vec3 renderPos = core::coords::canonicalToRender(position); @@ -3431,13 +3432,13 @@ void Application::setupUICallbacks() { voiceType = entitySpawner_->detectVoiceTypeFromDisplayId(displayId); } - renderer->getNpcVoiceManager()->playGreeting(guid, voiceType, renderPos); + audioCoordinator_->getNpcVoiceManager()->playGreeting(guid, voiceType, renderPos); } }); // NPC farewell callback - play farewell voice line gameHandler->setNpcFarewellCallback([this](uint64_t guid, const glm::vec3& position) { - if (renderer && renderer->getNpcVoiceManager()) { + if (audioCoordinator_ && audioCoordinator_->getNpcVoiceManager()) { glm::vec3 renderPos = core::coords::canonicalToRender(position); audio::VoiceType voiceType = audio::VoiceType::GENERIC; @@ -3448,13 +3449,13 @@ void Application::setupUICallbacks() { voiceType = entitySpawner_->detectVoiceTypeFromDisplayId(displayId); } - renderer->getNpcVoiceManager()->playFarewell(guid, voiceType, renderPos); + audioCoordinator_->getNpcVoiceManager()->playFarewell(guid, voiceType, renderPos); } }); // NPC vendor callback - play vendor voice line gameHandler->setNpcVendorCallback([this](uint64_t guid, const glm::vec3& position) { - if (renderer && renderer->getNpcVoiceManager()) { + if (audioCoordinator_ && audioCoordinator_->getNpcVoiceManager()) { glm::vec3 renderPos = core::coords::canonicalToRender(position); audio::VoiceType voiceType = audio::VoiceType::GENERIC; @@ -3465,13 +3466,13 @@ void Application::setupUICallbacks() { voiceType = entitySpawner_->detectVoiceTypeFromDisplayId(displayId); } - renderer->getNpcVoiceManager()->playVendor(guid, voiceType, renderPos); + audioCoordinator_->getNpcVoiceManager()->playVendor(guid, voiceType, renderPos); } }); // NPC aggro callback - play combat start voice line gameHandler->setNpcAggroCallback([this](uint64_t guid, const glm::vec3& position) { - if (renderer && renderer->getNpcVoiceManager()) { + if (audioCoordinator_ && audioCoordinator_->getNpcVoiceManager()) { glm::vec3 renderPos = core::coords::canonicalToRender(position); audio::VoiceType voiceType = audio::VoiceType::GENERIC; @@ -3482,7 +3483,7 @@ void Application::setupUICallbacks() { voiceType = entitySpawner_->detectVoiceTypeFromDisplayId(displayId); } - renderer->getNpcVoiceManager()->playAggro(guid, voiceType, renderPos); + audioCoordinator_->getNpcVoiceManager()->playAggro(guid, voiceType, renderPos); } }); @@ -3718,7 +3719,7 @@ void Application::spawnPlayerCharacter() { playerCharacterSpawned = true; // Set voice profile to match character race/gender - if (auto* asm_ = renderer->getActivitySoundManager()) { + if (auto* asm_ = audioCoordinator_ ? audioCoordinator_->getActivitySoundManager() : nullptr) { const char* raceFolder = "Human"; const char* raceBase = "Human"; switch (playerRace_) { diff --git a/src/game/combat_handler.cpp b/src/game/combat_handler.cpp index 32f120b7..a32f1d47 100644 --- a/src/game/combat_handler.cpp +++ b/src/game/combat_handler.cpp @@ -6,6 +6,7 @@ #include "game/update_field_table.hpp" #include "game/opcode_table.hpp" #include "rendering/renderer.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/combat_sound_manager.hpp" #include "audio/activity_sound_manager.hpp" #include "core/application.hpp" @@ -451,8 +452,8 @@ void CombatHandler::handleAttackerStateUpdate(network::Packet& packet) { } // Play combat sounds via CombatSoundManager + character vocalizations - if (auto* renderer = owner_.services().renderer) { - if (auto* csm = renderer->getCombatSoundManager()) { + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* csm = ac->getCombatSoundManager()) { auto weaponSize = audio::CombatSoundManager::WeaponSize::MEDIUM; if (data.isMiss()) { csm->playWeaponMiss(false); @@ -466,7 +467,7 @@ void CombatHandler::handleAttackerStateUpdate(network::Packet& packet) { } } // Character vocalizations - if (auto* asm_ = renderer->getActivitySoundManager()) { + if (auto* asm_ = ac->getActivitySoundManager()) { if (isPlayerAttacker && !data.isMiss() && data.victimState != 1 && data.victimState != 2) { asm_->playAttackGrunt(); } diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 6a8f37a4..ed8087b0 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -17,6 +17,7 @@ #include "game/update_field_table.hpp" #include "game/expansion_profile.hpp" #include "rendering/renderer.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/activity_sound_manager.hpp" #include "audio/combat_sound_manager.hpp" #include "audio/spell_sound_manager.hpp" @@ -599,8 +600,8 @@ static QuestQueryRewards tryParseQuestRewards(const std::vector& data, template void GameHandler::withSoundManager(ManagerGetter getter, Callback cb) { - if (auto* renderer = services_.renderer) { - if (auto* mgr = (renderer->*getter)()) cb(mgr); + if (auto* ac = services_.audioCoordinator) { + if (auto* mgr = (ac->*getter)()) cb(mgr); } } @@ -1198,9 +1199,9 @@ void GameHandler::updateTimers(float deltaTime) { } if (!alreadyAnnounced && pendingLootMoneyAmount_ > 0) { addSystemChatMessage("Looted: " + formatCopperAmount(pendingLootMoneyAmount_)); - auto* renderer = services_.renderer; - if (renderer) { - if (auto* sfx = renderer->getUiSoundManager()) { + auto* ac = services_.audioCoordinator; + if (ac) { + if (auto* sfx = ac->getUiSoundManager()) { if (pendingLootMoneyAmount_ >= 10000) { sfx->playLootCoinLarge(); } else { @@ -1974,7 +1975,7 @@ void GameHandler::registerOpcodeHandlers() { ping.age = 0.0f; minimapPings_.push_back(ping); if (senderGuid != playerGuid) { - withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playMinimapPing(); }); + withSoundManager(&audio::AudioCoordinator::getUiSoundManager, [](auto* sfx) { sfx->playMinimapPing(); }); } }; dispatchTable_[Opcode::SMSG_ZONE_UNDER_ATTACK] = [this](network::Packet& packet) { @@ -2124,7 +2125,7 @@ void GameHandler::registerOpcodeHandlers() { if (info && info->type == 17) { addUIError("A fish is on your line!"); addSystemChatMessage("A fish is on your line!"); - withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playQuestUpdate(); }); + withSoundManager(&audio::AudioCoordinator::getUiSoundManager, [](auto* sfx) { sfx->playQuestUpdate(); }); } } } @@ -2350,7 +2351,7 @@ void GameHandler::registerOpcodeHandlers() { } if (newLevel > oldLevel) { addSystemChatMessage("You have reached level " + std::to_string(newLevel) + "!"); - withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playLevelUp(); }); + withSoundManager(&audio::AudioCoordinator::getUiSoundManager, [](auto* sfx) { sfx->playLevelUp(); }); if (levelUpCallback_) levelUpCallback_(newLevel); fireAddonEvent("PLAYER_LEVEL_UP", {std::to_string(newLevel)}); } diff --git a/src/game/inventory_handler.cpp b/src/game/inventory_handler.cpp index 385a5850..1a4c1ab4 100644 --- a/src/game/inventory_handler.cpp +++ b/src/game/inventory_handler.cpp @@ -4,6 +4,7 @@ #include "game/entity.hpp" #include "game/packet_parsers.hpp" #include "rendering/renderer.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/ui_sound_manager.hpp" #include "core/application.hpp" #include "core/logger.hpp" @@ -70,9 +71,9 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) { } if (!alreadyAnnounced) { owner_.addSystemChatMessage("Looted: " + formatCopperAmount(amount)); - auto* renderer = owner_.services().renderer; - if (renderer) { - if (auto* sfx = renderer->getUiSoundManager()) { + auto* ac = owner_.services().audioCoordinator; + if (ac) { + if (auto* sfx = ac->getUiSoundManager()) { if (amount >= 10000) sfx->playLootCoinLarge(); else sfx->playLootCoinSmall(); } @@ -222,8 +223,8 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) { std::string msg = "Received item: " + link; if (count > 1) msg += " x" + std::to_string(count); owner_.addSystemChatMessage(msg); - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playLootItem(); } if (owner_.addonEventCallback_) { @@ -253,8 +254,8 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) { " result=", static_cast(result)); if (result == 0) { pendingSellToBuyback_.erase(itemGuid); - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playDropOnGround(); } if (owner_.addonEventCallback_) { @@ -295,8 +296,8 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) { const char* msg = (result < 7) ? sellErrors[result] : "Unknown sell error"; owner_.addUIError(std::string("Sell failed: ") + msg); owner_.addSystemChatMessage(std::string("Sell failed: ") + msg); - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playError(); } LOG_WARNING("SMSG_SELL_ITEM error: ", (int)result, " (", msg, ")"); @@ -392,8 +393,8 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) { std::string msg = errMsg ? errMsg : "Inventory error (" + std::to_string(error) + ")."; owner_.addUIError(msg); owner_.addSystemChatMessage(msg); - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playError(); } } @@ -450,8 +451,8 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) { } owner_.addUIError(msg); owner_.addSystemChatMessage(msg); - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playError(); } } @@ -474,8 +475,8 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) { std::string msg = "Purchased: " + buildItemLink(pendingBuyItemId_, buyQuality, itemLabel); if (itemCount > 1) msg += " x" + std::to_string(itemCount); owner_.addSystemChatMessage(msg); - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playPickupBag(); } } @@ -766,8 +767,8 @@ void InventoryHandler::handleLootRemoved(network::Packet& packet) { std::string msgStr = "Looted: " + link; if (it->count > 1) msgStr += " x" + std::to_string(it->count); owner_.addSystemChatMessage(msgStr); - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playLootItem(); } currentLoot_.items.erase(it); @@ -2382,8 +2383,8 @@ void InventoryHandler::handleItemQueryResponse(network::Packet& packet) { std::string msg = "Received: " + link; if (it->count > 1) msg += " x" + std::to_string(it->count); owner_.addSystemChatMessage(msg); - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) sfx->playLootItem(); + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playLootItem(); } if (owner_.itemLootCallback_) owner_.itemLootCallback_(data.entry, it->count, data.quality, itemName); it = owner_.pendingItemPushNotifs_.erase(it); @@ -3149,8 +3150,8 @@ void InventoryHandler::handleTrainerBuySucceeded(network::Packet& packet) { owner_.addSystemChatMessage("You have learned " + name + "."); else owner_.addSystemChatMessage("Spell learned."); - if (auto* renderer = owner_.services().renderer) - if (auto* sfx = renderer->getUiSoundManager()) sfx->playQuestActivate(); + if (auto* ac = owner_.services().audioCoordinator) + if (auto* sfx = ac->getUiSoundManager()) sfx->playQuestActivate(); owner_.fireAddonEvent("TRAINER_UPDATE", {}); owner_.fireAddonEvent("SPELLS_CHANGED", {}); } @@ -3171,8 +3172,8 @@ void InventoryHandler::handleTrainerBuyFailed(network::Packet& packet) { else if (errorCode != 0) msg += " (error " + std::to_string(errorCode) + ")"; owner_.addUIError(msg); owner_.addSystemChatMessage(msg); - if (auto* renderer = owner_.services().renderer) - if (auto* sfx = renderer->getUiSoundManager()) sfx->playError(); + if (auto* ac = owner_.services().audioCoordinator) + if (auto* sfx = ac->getUiSoundManager()) sfx->playError(); } // ============================================================ diff --git a/src/game/quest_handler.cpp b/src/game/quest_handler.cpp index a10bc83e..b4a1aad6 100644 --- a/src/game/quest_handler.cpp +++ b/src/game/quest_handler.cpp @@ -6,6 +6,7 @@ #include "game/packet_parsers.hpp" #include "network/world_socket.hpp" #include "rendering/renderer.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/ui_sound_manager.hpp" #include "core/application.hpp" #include "core/logger.hpp" @@ -469,8 +470,8 @@ void QuestHandler::registerOpcodes(DispatchTable& table) { owner_.questCompleteCallback_(questId, it->title); } // Play quest-complete sound - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playQuestComplete(); } questLog_.erase(it); @@ -1095,8 +1096,8 @@ void QuestHandler::acceptQuest() { pendingQuestAcceptNpcGuids_[questId] = npcGuid; // Play quest-accept sound - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playQuestActivate(); } diff --git a/src/game/social_handler.cpp b/src/game/social_handler.cpp index 2e2e15d3..ae9b2716 100644 --- a/src/game/social_handler.cpp +++ b/src/game/social_handler.cpp @@ -5,6 +5,7 @@ #include "game/packet_parsers.hpp" #include "game/update_field_table.hpp" #include "game/opcode_table.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/ui_sound_manager.hpp" #include "network/world_socket.hpp" #include "rendering/renderer.hpp" @@ -1053,8 +1054,8 @@ void SocialHandler::handleDuelRequested(network::Packet& packet) { } pendingDuelRequest_ = true; owner_.addSystemChatMessage(duelChallengerName_ + " challenges you to a duel!"); - if (auto* renderer = owner_.services().renderer) - if (auto* sfx = renderer->getUiSoundManager()) sfx->playTargetSelect(); + if (auto* ac = owner_.services().audioCoordinator) + if (auto* sfx = ac->getUiSoundManager()) sfx->playTargetSelect(); if (owner_.addonEventCallback_) owner_.addonEventCallback_("DUEL_REQUESTED", {duelChallengerName_}); } @@ -1219,8 +1220,8 @@ void SocialHandler::handleGroupInvite(network::Packet& packet) { pendingInviterName = data.inviterName; if (!data.inviterName.empty()) owner_.addSystemChatMessage(data.inviterName + " has invited you to a group."); - if (auto* renderer = owner_.services().renderer) - if (auto* sfx = renderer->getUiSoundManager()) sfx->playTargetSelect(); + if (auto* ac = owner_.services().audioCoordinator) + if (auto* sfx = ac->getUiSoundManager()) sfx->playTargetSelect(); if (owner_.addonEventCallback_) owner_.addonEventCallback_("PARTY_INVITE_REQUEST", {data.inviterName}); } diff --git a/src/game/spell_handler.cpp b/src/game/spell_handler.cpp index 3fc626de..59e0d29f 100644 --- a/src/game/spell_handler.cpp +++ b/src/game/spell_handler.cpp @@ -4,6 +4,7 @@ #include "game/packet_parsers.hpp" #include "game/entity.hpp" #include "rendering/renderer.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/spell_sound_manager.hpp" #include "audio/combat_sound_manager.hpp" #include "core/application.hpp" @@ -795,8 +796,8 @@ void SpellHandler::handleCastFailed(network::Packet& packet) { queuedSpellTarget_ = 0; // Stop precast sound - if (auto* renderer = owner_.services().renderer) { - if (auto* ssm = renderer->getSpellSoundManager()) { + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* ssm = ac->getSpellSoundManager()) { ssm->stopPrecast(); } } @@ -817,8 +818,8 @@ void SpellHandler::handleCastFailed(network::Packet& packet) { msg.message = errMsg; owner_.addLocalChatMessage(msg); - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playError(); } @@ -869,8 +870,8 @@ void SpellHandler::handleSpellStart(network::Packet& packet) { // Play precast sound — skip profession/tradeskill spells if (!owner_.isProfessionSpell(data.spellId)) { - if (auto* renderer = owner_.services().renderer) { - if (auto* ssm = renderer->getSpellSoundManager()) { + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* ssm = ac->getSpellSoundManager()) { owner_.loadSpellNameCache(); auto it = owner_.spellNameCache_.find(data.spellId); auto school = (it != owner_.spellNameCache_.end() && it->second.schoolMask) @@ -907,8 +908,8 @@ void SpellHandler::handleSpellGo(network::Packet& packet) { if (data.casterUnit == owner_.playerGuid) { // Play cast-complete sound if (!owner_.isProfessionSpell(data.spellId)) { - if (auto* renderer = owner_.services().renderer) { - if (auto* ssm = renderer->getSpellSoundManager()) { + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* ssm = ac->getSpellSoundManager()) { owner_.loadSpellNameCache(); auto it = owner_.spellNameCache_.find(data.spellId); auto school = (it != owner_.spellNameCache_.end() && it->second.schoolMask) @@ -931,8 +932,8 @@ void SpellHandler::handleSpellGo(network::Packet& packet) { } if (isMeleeAbility) { if (owner_.meleeSwingCallback_) owner_.meleeSwingCallback_(); - if (auto* renderer = owner_.services().renderer) { - if (auto* csm = renderer->getCombatSoundManager()) { + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* csm = ac->getCombatSoundManager()) { csm->playWeaponSwing(audio::CombatSoundManager::WeaponSize::MEDIUM, false); csm->playImpact(audio::CombatSoundManager::WeaponSize::MEDIUM, audio::CombatSoundManager::ImpactType::FLESH, false); @@ -990,8 +991,8 @@ void SpellHandler::handleSpellGo(network::Packet& packet) { if (tgt == owner_.playerGuid) { targetsPlayer = true; break; } } if (targetsPlayer) { - if (auto* renderer = owner_.services().renderer) { - if (auto* ssm = renderer->getSpellSoundManager()) { + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* ssm = ac->getSpellSoundManager()) { owner_.loadSpellNameCache(); auto it = owner_.spellNameCache_.find(data.spellId); auto school = (it != owner_.spellNameCache_.end() && it->second.schoolMask) @@ -1036,8 +1037,8 @@ void SpellHandler::handleSpellGo(network::Packet& packet) { } if (playerIsHit || playerHitEnemy) { - if (auto* renderer = owner_.services().renderer) { - if (auto* ssm = renderer->getSpellSoundManager()) { + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* ssm = ac->getSpellSoundManager()) { owner_.loadSpellNameCache(); auto it = owner_.spellNameCache_.find(data.spellId); auto school = (it != owner_.spellNameCache_.end() && it->second.schoolMask) @@ -1396,8 +1397,8 @@ void SpellHandler::handleAchievementEarned(network::Packet& packet) { owner_.earnedAchievements_.insert(achievementId); owner_.achievementDates_[achievementId] = earnDate; - if (auto* renderer = owner_.services().renderer) { - if (auto* sfx = renderer->getUiSoundManager()) + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playAchievementAlert(); } if (owner_.achievementEarnedCallback_) { @@ -2350,8 +2351,8 @@ void SpellHandler::handleSpellFailure(network::Packet& packet) { craftQueueRemaining_ = 0; queuedSpellId_ = 0; queuedSpellTarget_ = 0; - if (auto* renderer = owner_.services().renderer) { - if (auto* ssm = renderer->getSpellSoundManager()) { + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* ssm = ac->getSpellSoundManager()) { ssm->stopPrecast(); } } diff --git a/src/rendering/animation_controller.cpp b/src/rendering/animation_controller.cpp new file mode 100644 index 00000000..7b46614e --- /dev/null +++ b/src/rendering/animation_controller.cpp @@ -0,0 +1,1703 @@ +#include "rendering/animation_controller.hpp" +#include "rendering/renderer.hpp" +#include "rendering/camera.hpp" +#include "rendering/camera_controller.hpp" +#include "rendering/character_renderer.hpp" +#include "rendering/terrain_manager.hpp" +#include "rendering/wmo_renderer.hpp" +#include "rendering/water_renderer.hpp" +#include "rendering/m2_renderer.hpp" +#include "rendering/levelup_effect.hpp" +#include "rendering/charge_effect.hpp" +#include "rendering/spell_visual_system.hpp" +#include "pipeline/m2_loader.hpp" +#include "pipeline/asset_manager.hpp" +#include "pipeline/dbc_loader.hpp" +#include "pipeline/dbc_layout.hpp" +#include "core/application.hpp" +#include "core/logger.hpp" +#include "audio/audio_coordinator.hpp" +#include "audio/audio_engine.hpp" +#include "audio/footstep_manager.hpp" +#include "audio/activity_sound_manager.hpp" +#include "audio/mount_sound_manager.hpp" +#include "audio/music_manager.hpp" +#include "audio/movement_sound_manager.hpp" +#include "rendering/swim_effects.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace rendering { + +// ── Static emote data (shared across all AnimationController instances) ────── + +struct EmoteInfo { + uint32_t animId = 0; + uint32_t dbcId = 0; + bool loop = false; + std::string textNoTarget; + std::string textTarget; + std::string othersNoTarget; + std::string othersTarget; + std::string command; +}; + +static std::unordered_map EMOTE_TABLE; +static std::unordered_map EMOTE_BY_DBCID; +static bool emoteTableLoaded = false; + +static std::vector parseEmoteCommands(const std::string& raw) { + std::vector out; + std::string cur; + for (char c : raw) { + if (std::isalnum(static_cast(c)) || c == '_') { + cur.push_back(static_cast(std::tolower(static_cast(c)))); + } else if (!cur.empty()) { + out.push_back(cur); + cur.clear(); + } + } + if (!cur.empty()) out.push_back(cur); + return out; +} + +static bool isLoopingEmote(const std::string& command) { + static const std::unordered_set kLooping = { + "dance", + "train", + }; + return kLooping.find(command) != kLooping.end(); +} + +static void loadFallbackEmotes() { + if (!EMOTE_TABLE.empty()) return; + EMOTE_TABLE = { + {"wave", {67, 0, false, "You wave.", "You wave at %s.", "%s waves.", "%s waves at %s.", "wave"}}, + {"bow", {66, 0, false, "You bow down graciously.", "You bow down before %s.", "%s bows down graciously.", "%s bows down before %s.", "bow"}}, + {"laugh", {70, 0, false, "You laugh.", "You laugh at %s.", "%s laughs.", "%s laughs at %s.", "laugh"}}, + {"point", {84, 0, false, "You point over yonder.", "You point at %s.", "%s points over yonder.", "%s points at %s.", "point"}}, + {"cheer", {68, 0, false, "You cheer!", "You cheer at %s.", "%s cheers!", "%s cheers at %s.", "cheer"}}, + {"dance", {69, 0, true, "You burst into dance.", "You dance with %s.", "%s bursts into dance.", "%s dances with %s.", "dance"}}, + {"kneel", {75, 0, false, "You kneel down.", "You kneel before %s.", "%s kneels down.", "%s kneels before %s.", "kneel"}}, + {"applaud", {80, 0, false, "You applaud. Bravo!", "You applaud at %s. Bravo!", "%s applauds. Bravo!", "%s applauds at %s. Bravo!", "applaud"}}, + {"shout", {81, 0, false, "You shout.", "You shout at %s.", "%s shouts.", "%s shouts at %s.", "shout"}}, + {"chicken", {78, 0, false, "With arms flapping, you strut around. Cluck, Cluck, Chicken!", + "With arms flapping, you strut around %s. Cluck, Cluck, Chicken!", + "%s struts around. Cluck, Cluck, Chicken!", "%s struts around %s. Cluck, Cluck, Chicken!", "chicken"}}, + {"cry", {77, 0, false, "You cry.", "You cry on %s's shoulder.", "%s cries.", "%s cries on %s's shoulder.", "cry"}}, + {"kiss", {76, 0, false, "You blow a kiss into the wind.", "You blow a kiss to %s.", "%s blows a kiss into the wind.", "%s blows a kiss to %s.", "kiss"}}, + {"roar", {74, 0, false, "You roar with bestial vigor. So fierce!", "You roar with bestial vigor at %s. So fierce!", "%s roars with bestial vigor. So fierce!", "%s roars with bestial vigor at %s. So fierce!", "roar"}}, + {"salute", {113, 0, false, "You salute.", "You salute %s with respect.", "%s salutes.", "%s salutes %s with respect.", "salute"}}, + {"rude", {73, 0, false, "You make a rude gesture.", "You make a rude gesture at %s.", "%s makes a rude gesture.", "%s makes a rude gesture at %s.", "rude"}}, + {"flex", {82, 0, false, "You flex your muscles. Oooooh so strong!", "You flex at %s. Oooooh so strong!", "%s flexes. Oooooh so strong!", "%s flexes at %s. Oooooh so strong!", "flex"}}, + {"shy", {83, 0, false, "You smile shyly.", "You smile shyly at %s.", "%s smiles shyly.", "%s smiles shyly at %s.", "shy"}}, + {"beg", {79, 0, false, "You beg everyone around you. How pathetic.", "You beg %s. How pathetic.", "%s begs everyone around. How pathetic.", "%s begs %s. How pathetic.", "beg"}}, + {"eat", {61, 0, false, "You begin to eat.", "You begin to eat in front of %s.", "%s begins to eat.", "%s begins to eat in front of %s.", "eat"}}, + }; +} + +static std::string replacePlaceholders(const std::string& text, const std::string* targetName) { + if (text.empty()) return text; + std::string out; + out.reserve(text.size() + 16); + for (size_t i = 0; i < text.size(); ++i) { + if (text[i] == '%' && i + 1 < text.size() && text[i + 1] == 's') { + if (targetName && !targetName->empty()) out += *targetName; + i++; + } else { + out.push_back(text[i]); + } + } + return out; +} + +static void loadEmotesFromDbc() { + if (emoteTableLoaded) return; + emoteTableLoaded = true; + + auto* assetManager = core::Application::getInstance().getAssetManager(); + if (!assetManager) { + LOG_WARNING("Emotes: no AssetManager"); + loadFallbackEmotes(); + return; + } + + auto emotesTextDbc = assetManager->loadDBC("EmotesText.dbc"); + auto emotesTextDataDbc = assetManager->loadDBC("EmotesTextData.dbc"); + if (!emotesTextDbc || !emotesTextDataDbc || !emotesTextDbc->isLoaded() || !emotesTextDataDbc->isLoaded()) { + LOG_WARNING("Emotes: DBCs not available (EmotesText/EmotesTextData)"); + loadFallbackEmotes(); + return; + } + + const auto* activeLayout = pipeline::getActiveDBCLayout(); + const auto* etdL = activeLayout ? activeLayout->getLayout("EmotesTextData") : nullptr; + const auto* emL = activeLayout ? activeLayout->getLayout("Emotes") : nullptr; + const auto* etL = activeLayout ? activeLayout->getLayout("EmotesText") : nullptr; + + std::unordered_map textData; + textData.reserve(emotesTextDataDbc->getRecordCount()); + for (uint32_t r = 0; r < emotesTextDataDbc->getRecordCount(); ++r) { + uint32_t id = emotesTextDataDbc->getUInt32(r, etdL ? (*etdL)["ID"] : 0); + std::string text = emotesTextDataDbc->getString(r, etdL ? (*etdL)["Text"] : 1); + if (!text.empty()) textData.emplace(id, std::move(text)); + } + + std::unordered_map emoteIdToAnim; + if (auto emotesDbc = assetManager->loadDBC("Emotes.dbc"); emotesDbc && emotesDbc->isLoaded()) { + emoteIdToAnim.reserve(emotesDbc->getRecordCount()); + for (uint32_t r = 0; r < emotesDbc->getRecordCount(); ++r) { + uint32_t emoteId = emotesDbc->getUInt32(r, emL ? (*emL)["ID"] : 0); + uint32_t animId = emotesDbc->getUInt32(r, emL ? (*emL)["AnimID"] : 2); + if (animId != 0) emoteIdToAnim[emoteId] = animId; + } + } + + EMOTE_TABLE.clear(); + EMOTE_TABLE.reserve(emotesTextDbc->getRecordCount()); + for (uint32_t r = 0; r < emotesTextDbc->getRecordCount(); ++r) { + uint32_t recordId = emotesTextDbc->getUInt32(r, etL ? (*etL)["ID"] : 0); + std::string cmdRaw = emotesTextDbc->getString(r, etL ? (*etL)["Command"] : 1); + if (cmdRaw.empty()) continue; + + uint32_t emoteRef = emotesTextDbc->getUInt32(r, etL ? (*etL)["EmoteRef"] : 2); + uint32_t animId = 0; + auto animIt = emoteIdToAnim.find(emoteRef); + if (animIt != emoteIdToAnim.end()) { + animId = animIt->second; + } else { + animId = emoteRef; + } + + uint32_t senderTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderTargetTextID"] : 5); + uint32_t senderNoTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderNoTargetTextID"] : 9); + uint32_t othersTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["OthersTargetTextID"] : 3); + uint32_t othersNoTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["OthersNoTargetTextID"] : 7); + + std::string textTarget, textNoTarget, oTarget, oNoTarget; + if (auto it = textData.find(senderTargetTextId); it != textData.end()) textTarget = it->second; + if (auto it = textData.find(senderNoTargetTextId); it != textData.end()) textNoTarget = it->second; + if (auto it = textData.find(othersTargetTextId); it != textData.end()) oTarget = it->second; + if (auto it = textData.find(othersNoTargetTextId); it != textData.end()) oNoTarget = it->second; + + for (const std::string& cmd : parseEmoteCommands(cmdRaw)) { + if (cmd.empty()) continue; + EmoteInfo info; + info.animId = animId; + info.dbcId = recordId; + info.loop = isLoopingEmote(cmd); + info.textNoTarget = textNoTarget; + info.textTarget = textTarget; + info.othersNoTarget = oNoTarget; + info.othersTarget = oTarget; + info.command = cmd; + EMOTE_TABLE.emplace(cmd, std::move(info)); + } + } + + if (EMOTE_TABLE.empty()) { + LOG_WARNING("Emotes: DBC loaded but no commands parsed, using fallback list"); + loadFallbackEmotes(); + } else { + LOG_INFO("Emotes: loaded ", EMOTE_TABLE.size(), " commands from DBC"); + } + + EMOTE_BY_DBCID.clear(); + for (auto& [cmd, info] : EMOTE_TABLE) { + if (info.dbcId != 0) { + EMOTE_BY_DBCID.emplace(info.dbcId, &info); + } + } +} + +// ── AnimationController implementation ─────────────────────────────────────── + +AnimationController::AnimationController() = default; +AnimationController::~AnimationController() = default; + +void AnimationController::initialize(Renderer* renderer) { + renderer_ = renderer; +} + +void AnimationController::onCharacterFollow(uint32_t /*instanceId*/) { + // Reset animation state when follow target changes +} + +// ── Emote support ──────────────────────────────────────────────────────────── + +void AnimationController::playEmote(const std::string& emoteName) { + loadEmotesFromDbc(); + auto it = EMOTE_TABLE.find(emoteName); + if (it == EMOTE_TABLE.end()) return; + + const auto& info = it->second; + if (info.animId == 0) return; + emoteActive_ = true; + emoteAnimId_ = info.animId; + emoteLoop_ = info.loop; + charAnimState_ = CharAnimState::EMOTE; + + auto* characterRenderer = renderer_->getCharacterRenderer(); + uint32_t characterInstanceId = renderer_->getCharacterInstanceId(); + if (characterRenderer && characterInstanceId > 0) { + characterRenderer->playAnimation(characterInstanceId, emoteAnimId_, emoteLoop_); + } +} + +void AnimationController::cancelEmote() { + emoteActive_ = false; + emoteAnimId_ = 0; + emoteLoop_ = false; +} + +std::string AnimationController::getEmoteText(const std::string& emoteName, const std::string* targetName) { + loadEmotesFromDbc(); + auto it = EMOTE_TABLE.find(emoteName); + if (it != EMOTE_TABLE.end()) { + const auto& info = it->second; + const std::string& base = (targetName ? info.textTarget : info.textNoTarget); + if (!base.empty()) { + return replacePlaceholders(base, targetName); + } + if (targetName && !targetName->empty()) { + return "You " + info.command + " at " + *targetName + "."; + } + return "You " + info.command + "."; + } + return ""; +} + +uint32_t AnimationController::getEmoteDbcId(const std::string& emoteName) { + loadEmotesFromDbc(); + auto it = EMOTE_TABLE.find(emoteName); + if (it != EMOTE_TABLE.end()) { + return it->second.dbcId; + } + return 0; +} + +std::string AnimationController::getEmoteTextByDbcId(uint32_t dbcId, const std::string& senderName, + const std::string* targetName) { + loadEmotesFromDbc(); + auto it = EMOTE_BY_DBCID.find(dbcId); + if (it == EMOTE_BY_DBCID.end()) return ""; + + const EmoteInfo& info = *it->second; + + if (targetName && !targetName->empty()) { + if (!info.othersTarget.empty()) { + std::string out; + out.reserve(info.othersTarget.size() + senderName.size() + targetName->size()); + bool firstReplaced = false; + for (size_t i = 0; i < info.othersTarget.size(); ++i) { + if (info.othersTarget[i] == '%' && i + 1 < info.othersTarget.size() && info.othersTarget[i + 1] == 's') { + out += firstReplaced ? *targetName : senderName; + firstReplaced = true; + ++i; + } else { + out.push_back(info.othersTarget[i]); + } + } + return out; + } + return senderName + " " + info.command + "s at " + *targetName + "."; + } else { + if (!info.othersNoTarget.empty()) { + return replacePlaceholders(info.othersNoTarget, &senderName); + } + return senderName + " " + info.command + "s."; + } +} + +uint32_t AnimationController::getEmoteAnimByDbcId(uint32_t dbcId) { + loadEmotesFromDbc(); + auto it = EMOTE_BY_DBCID.find(dbcId); + if (it != EMOTE_BY_DBCID.end()) { + return it->second->animId; + } + return 0; +} + +// ── Targeting / combat ─────────────────────────────────────────────────────── + +void AnimationController::setTargetPosition(const glm::vec3* pos) { + targetPosition_ = pos; +} + +void AnimationController::resetCombatVisualState() { + inCombat_ = false; + targetPosition_ = nullptr; + meleeSwingTimer_ = 0.0f; + meleeSwingCooldown_ = 0.0f; + if (auto* svs = renderer_->getSpellVisualSystem()) svs->reset(); +} + +bool AnimationController::isMoving() const { + auto* cameraController = renderer_->getCameraController(); + return cameraController && cameraController->isMoving(); +} + +// ── Melee combat ───────────────────────────────────────────────────────────── + +void AnimationController::triggerMeleeSwing() { + auto* characterRenderer = renderer_->getCharacterRenderer(); + uint32_t characterInstanceId = renderer_->getCharacterInstanceId(); + if (!characterRenderer || characterInstanceId == 0) return; + if (meleeSwingCooldown_ > 0.0f) return; + if (emoteActive_) { + cancelEmote(); + } + resolveMeleeAnimId(); + meleeSwingCooldown_ = 0.1f; + float durationSec = meleeAnimDurationMs_ > 0.0f ? meleeAnimDurationMs_ / 1000.0f : 0.6f; + if (durationSec < 0.25f) durationSec = 0.25f; + if (durationSec > 1.0f) durationSec = 1.0f; + meleeSwingTimer_ = durationSec; + if (renderer_->getAudioCoordinator()->getActivitySoundManager()) { + renderer_->getAudioCoordinator()->getActivitySoundManager()->playMeleeSwing(); + } +} + +uint32_t AnimationController::resolveMeleeAnimId() { + auto* characterRenderer = renderer_->getCharacterRenderer(); + uint32_t characterInstanceId = renderer_->getCharacterInstanceId(); + if (!characterRenderer || characterInstanceId == 0) { + meleeAnimId_ = 0; + meleeAnimDurationMs_ = 0.0f; + return 0; + } + + if (meleeAnimId_ != 0 && characterRenderer->hasAnimation(characterInstanceId, meleeAnimId_)) { + return meleeAnimId_; + } + + std::vector sequences; + if (!characterRenderer->getAnimationSequences(characterInstanceId, sequences)) { + meleeAnimId_ = 0; + meleeAnimDurationMs_ = 0.0f; + return 0; + } + + auto findDuration = [&](uint32_t id) -> float { + for (const auto& seq : sequences) { + if (seq.id == id && seq.duration > 0) { + return static_cast(seq.duration); + } + } + return 0.0f; + }; + + const uint32_t* attackCandidates; + size_t candidateCount; + static const uint32_t candidates2H[] = {18, 17, 16, 19, 20, 21}; + static const uint32_t candidates1H[] = {17, 18, 16, 19, 20, 21}; + static const uint32_t candidatesUnarmed[] = {16, 17, 18, 19, 20, 21}; + if (equippedWeaponInvType_ == 17) { + attackCandidates = candidates2H; + candidateCount = 6; + } else if (equippedWeaponInvType_ == 0) { + attackCandidates = candidatesUnarmed; + candidateCount = 6; + } else { + attackCandidates = candidates1H; + candidateCount = 6; + } + for (size_t ci = 0; ci < candidateCount; ci++) { + uint32_t id = attackCandidates[ci]; + if (characterRenderer->hasAnimation(characterInstanceId, id)) { + meleeAnimId_ = id; + meleeAnimDurationMs_ = findDuration(id); + return meleeAnimId_; + } + } + + const uint32_t avoidIds[] = {0, 1, 4, 5, 11, 12, 13, 37, 38, 39, 41, 42, 97}; + auto isAvoid = [&](uint32_t id) -> bool { + for (uint32_t avoid : avoidIds) { + if (id == avoid) return true; + } + return false; + }; + + uint32_t bestId = 0; + uint32_t bestDuration = 0; + for (const auto& seq : sequences) { + if (seq.duration == 0) continue; + if (isAvoid(seq.id)) continue; + if (seq.movingSpeed > 0.1f) continue; + if (seq.duration < 150 || seq.duration > 2000) continue; + if (bestId == 0 || seq.duration < bestDuration) { + bestId = seq.id; + bestDuration = seq.duration; + } + } + + if (bestId == 0) { + for (const auto& seq : sequences) { + if (seq.duration == 0) continue; + if (isAvoid(seq.id)) continue; + if (bestId == 0 || seq.duration < bestDuration) { + bestId = seq.id; + bestDuration = seq.duration; + } + } + } + + meleeAnimId_ = bestId; + meleeAnimDurationMs_ = static_cast(bestDuration); + return meleeAnimId_; +} + +// ── Effect triggers ────────────────────────────────────────────────────────── + +void AnimationController::triggerLevelUpEffect(const glm::vec3& position) { + auto* levelUpEffect = renderer_->getLevelUpEffect(); + if (!levelUpEffect) return; + + if (!levelUpEffect->isModelLoaded()) { + auto* m2Renderer = renderer_->getM2Renderer(); + if (m2Renderer) { + auto* assetManager = core::Application::getInstance().getAssetManager(); + if (!assetManager) { + LOG_WARNING("LevelUpEffect: no asset manager available"); + } else { + auto m2Data = assetManager->readFile("Spells\\LevelUp\\LevelUp.m2"); + auto skinData = assetManager->readFile("Spells\\LevelUp\\LevelUp00.skin"); + LOG_INFO("LevelUpEffect: m2Data=", m2Data.size(), " skinData=", skinData.size()); + if (!m2Data.empty()) { + levelUpEffect->loadModel(m2Renderer, m2Data, skinData); + } else { + LOG_WARNING("LevelUpEffect: failed to read Spell\\LevelUp\\LevelUp.m2"); + } + } + } + } + + levelUpEffect->trigger(position); +} + +void AnimationController::startChargeEffect(const glm::vec3& position, const glm::vec3& direction) { + auto* chargeEffect = renderer_->getChargeEffect(); + if (!chargeEffect) return; + + if (!chargeEffect->isActive()) { + auto* m2Renderer = renderer_->getM2Renderer(); + if (m2Renderer) { + auto* assetManager = core::Application::getInstance().getAssetManager(); + if (assetManager) { + chargeEffect->tryLoadM2Models(m2Renderer, assetManager); + } + } + } + + chargeEffect->start(position, direction); +} + +void AnimationController::emitChargeEffect(const glm::vec3& position, const glm::vec3& direction) { + if (auto* chargeEffect = renderer_->getChargeEffect()) { + chargeEffect->emit(position, direction); + } +} + +void AnimationController::stopChargeEffect() { + if (auto* chargeEffect = renderer_->getChargeEffect()) { + chargeEffect->stop(); + } +} + +// ── Mount ──────────────────────────────────────────────────────────────────── + +void AnimationController::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset, const std::string& modelPath) { + auto* characterRenderer = renderer_->getCharacterRenderer(); + auto* cameraController = renderer_->getCameraController(); + + mountInstanceId_ = mountInstId; + mountHeightOffset_ = heightOffset; + mountSeatAttachmentId_ = -1; + smoothedMountSeatPos_ = renderer_->getCharacterPosition(); + mountSeatSmoothingInit_ = false; + mountAction_ = MountAction::None; + mountActionPhase_ = 0; + charAnimState_ = CharAnimState::MOUNT; + if (cameraController) { + cameraController->setMounted(true); + cameraController->setMountHeightOffset(heightOffset); + } + + if (characterRenderer && mountInstId > 0) { + characterRenderer->dumpAnimations(mountInstId); + } + + // Discover mount animation capabilities (property-based, not hardcoded IDs) + LOG_DEBUG("=== Mount Animation Dump (Display ID ", mountDisplayId, ") ==="); + if (characterRenderer) characterRenderer->dumpAnimations(mountInstId); + + std::vector sequences; + if (!characterRenderer || !characterRenderer->getAnimationSequences(mountInstId, sequences)) { + LOG_WARNING("Failed to get animation sequences for mount, using fallback IDs"); + sequences.clear(); + } + + auto findFirst = [&](std::initializer_list candidates) -> uint32_t { + for (uint32_t id : candidates) { + if (characterRenderer && characterRenderer->hasAnimation(mountInstId, id)) { + return id; + } + } + return 0; + }; + + // Property-based jump animation discovery with chain-based scoring + auto discoverJumpSet = [&]() { + LOG_DEBUG("=== Full sequence table for mount ==="); + for (const auto& seq : sequences) { + LOG_DEBUG("SEQ id=", seq.id, + " dur=", seq.duration, + " flags=0x", std::hex, seq.flags, std::dec, + " moveSpd=", seq.movingSpeed, + " blend=", seq.blendTime, + " next=", seq.nextAnimation, + " alias=", seq.aliasNext); + } + LOG_DEBUG("=== End sequence table ==="); + + std::set forbiddenIds = {53, 54, 16}; + + auto scoreNear = [](int a, int b) -> int { + int d = std::abs(a - b); + return (d <= 8) ? (20 - d) : 0; + }; + + auto isForbidden = [&](uint32_t id) { + return forbiddenIds.count(id) != 0; + }; + + auto findSeqById = [&](uint32_t id) -> const pipeline::M2Sequence* { + for (const auto& s : sequences) { + if (s.id == id) return &s; + } + return nullptr; + }; + + uint32_t runId = findFirst({5, 4}); + uint32_t standId = findFirst({0}); + + std::vector loops; + for (const auto& seq : sequences) { + if (isForbidden(seq.id)) continue; + bool isLoop = (seq.flags & 0x01) == 0; + if (isLoop && seq.duration >= 350 && seq.duration <= 1000 && + seq.id != runId && seq.id != standId) { + loops.push_back(seq.id); + } + } + + uint32_t loop = 0; + if (!loops.empty()) { + uint32_t best = loops[0]; + int bestScore = -999; + for (uint32_t id : loops) { + int sc = 0; + sc += scoreNear(static_cast(id), 38); + const auto* s = findSeqById(id); + if (s) sc += (s->duration >= 500 && s->duration <= 800) ? 5 : 0; + if (sc > bestScore) { + bestScore = sc; + best = id; + } + } + loop = best; + } + + uint32_t start = 0, end = 0; + int bestStart = -999, bestEnd = -999; + + for (const auto& seq : sequences) { + if (isForbidden(seq.id)) continue; + bool isLoop = (seq.flags & 0x01) == 0; + if (isLoop) continue; + + if (seq.duration >= 450 && seq.duration <= 1100) { + int sc = 0; + if (loop) sc += scoreNear(static_cast(seq.id), static_cast(loop)); + if (loop && (seq.nextAnimation == static_cast(loop) || seq.aliasNext == loop)) sc += 30; + if (loop && scoreNear(seq.nextAnimation, static_cast(loop)) > 0) sc += 10; + if (seq.blendTime > 400) sc -= 5; + + if (sc > bestStart) { + bestStart = sc; + start = seq.id; + } + } + + if (seq.duration >= 650 && seq.duration <= 1600) { + int sc = 0; + if (loop) sc += scoreNear(static_cast(seq.id), static_cast(loop)); + if (seq.nextAnimation == static_cast(runId) || seq.nextAnimation == static_cast(standId)) sc += 10; + if (seq.nextAnimation < 0) sc += 5; + if (sc > bestEnd) { + bestEnd = sc; + end = seq.id; + } + } + } + + LOG_DEBUG("Property-based jump discovery: start=", start, " loop=", loop, " end=", end, + " scores: start=", bestStart, " end=", bestEnd); + return std::make_tuple(start, loop, end); + }; + + auto [discoveredStart, discoveredLoop, discoveredEnd] = discoverJumpSet(); + + mountAnims_.jumpStart = discoveredStart > 0 ? discoveredStart : findFirst({40, 37}); + mountAnims_.jumpLoop = discoveredLoop > 0 ? discoveredLoop : findFirst({38}); + mountAnims_.jumpEnd = discoveredEnd > 0 ? discoveredEnd : findFirst({39}); + mountAnims_.rearUp = findFirst({94, 92, 40}); + mountAnims_.run = findFirst({5, 4}); + mountAnims_.stand = findFirst({0}); + + // Discover idle fidget animations using proper WoW M2 metadata + mountAnims_.fidgets.clear(); + core::Logger::getInstance().debug("Scanning for fidget animations in ", sequences.size(), " sequences"); + + core::Logger::getInstance().debug("=== ALL potential fidgets (no metadata filter) ==="); + for (const auto& seq : sequences) { + bool isLoop = (seq.flags & 0x01) == 0; + bool isStationary = std::abs(seq.movingSpeed) < 0.05f; + bool reasonableDuration = seq.duration >= 400 && seq.duration <= 2500; + + if (!isLoop && reasonableDuration && isStationary) { + core::Logger::getInstance().debug(" ALL: id=", seq.id, + " dur=", seq.duration, "ms", + " freq=", seq.frequency, + " replay=", seq.replayMin, "-", seq.replayMax, + " flags=0x", std::hex, seq.flags, std::dec, + " next=", seq.nextAnimation); + } + } + + for (const auto& seq : sequences) { + bool isLoop = (seq.flags & 0x01) == 0; + bool hasFrequency = seq.frequency > 0; + bool hasReplay = seq.replayMax > 0; + bool isStationary = std::abs(seq.movingSpeed) < 0.05f; + bool reasonableDuration = seq.duration >= 400 && seq.duration <= 2500; + + if (!isLoop && reasonableDuration && isStationary && (hasFrequency || hasReplay)) { + core::Logger::getInstance().debug(" Candidate: id=", seq.id, + " dur=", seq.duration, "ms", + " freq=", seq.frequency, + " replay=", seq.replayMin, "-", seq.replayMax, + " next=", seq.nextAnimation, + " speed=", seq.movingSpeed); + } + + bool isDeathOrWound = (seq.id >= 5 && seq.id <= 9); + bool isAttackOrCombat = (seq.id >= 11 && seq.id <= 21); + bool isSpecial = (seq.id == 2 || seq.id == 3); + + if (!isLoop && (hasFrequency || hasReplay) && isStationary && reasonableDuration && + !isDeathOrWound && !isAttackOrCombat && !isSpecial) { + bool chainsToStand = (seq.nextAnimation == static_cast(mountAnims_.stand)) || + (seq.aliasNext == mountAnims_.stand) || + (seq.nextAnimation == -1); + + mountAnims_.fidgets.push_back(seq.id); + core::Logger::getInstance().debug(" >> Selected fidget: id=", seq.id, + (chainsToStand ? " (chains to stand)" : "")); + } + } + + if (mountAnims_.run == 0) mountAnims_.run = mountAnims_.stand; + + core::Logger::getInstance().debug("Mount animation set: jumpStart=", mountAnims_.jumpStart, + " jumpLoop=", mountAnims_.jumpLoop, + " jumpEnd=", mountAnims_.jumpEnd, + " rearUp=", mountAnims_.rearUp, + " run=", mountAnims_.run, + " stand=", mountAnims_.stand, + " fidgets=", mountAnims_.fidgets.size()); + + if (renderer_->getAudioCoordinator()->getMountSoundManager()) { + bool isFlying = taxiFlight_; + renderer_->getAudioCoordinator()->getMountSoundManager()->onMount(mountDisplayId, isFlying, modelPath); + } +} + +void AnimationController::clearMount() { + mountInstanceId_ = 0; + mountHeightOffset_ = 0.0f; + mountPitch_ = 0.0f; + mountRoll_ = 0.0f; + mountSeatAttachmentId_ = -1; + smoothedMountSeatPos_ = glm::vec3(0.0f); + mountSeatSmoothingInit_ = false; + mountAction_ = MountAction::None; + mountActionPhase_ = 0; + charAnimState_ = CharAnimState::IDLE; + if (auto* cameraController = renderer_->getCameraController()) { + cameraController->setMounted(false); + cameraController->setMountHeightOffset(0.0f); + } + + if (renderer_->getAudioCoordinator()->getMountSoundManager()) { + renderer_->getAudioCoordinator()->getMountSoundManager()->onDismount(); + } +} + +// ── Query helpers ──────────────────────────────────────────────────────────── + +bool AnimationController::isFootstepAnimationState() const { + return charAnimState_ == CharAnimState::WALK || charAnimState_ == CharAnimState::RUN; +} + +// ── Melee timers ───────────────────────────────────────────────────────────── + +void AnimationController::updateMeleeTimers(float deltaTime) { + if (meleeSwingCooldown_ > 0.0f) { + meleeSwingCooldown_ = std::max(0.0f, meleeSwingCooldown_ - deltaTime); + } + if (meleeSwingTimer_ > 0.0f) { + meleeSwingTimer_ = std::max(0.0f, meleeSwingTimer_ - deltaTime); + } +} + +// ── Character animation state machine ──────────────────────────────────────── + +void AnimationController::updateCharacterAnimation() { + auto* characterRenderer = renderer_->getCharacterRenderer(); + auto* cameraController = renderer_->getCameraController(); + uint32_t characterInstanceId = renderer_->getCharacterInstanceId(); + + // WoW WotLK AnimationData.dbc IDs + constexpr uint32_t ANIM_STAND = 0; + constexpr uint32_t ANIM_WALK = 4; + constexpr uint32_t ANIM_RUN = 5; + constexpr uint32_t ANIM_STRAFE_RUN_RIGHT = 92; + constexpr uint32_t ANIM_STRAFE_RUN_LEFT = 93; + constexpr uint32_t ANIM_STRAFE_WALK_LEFT = 11; + constexpr uint32_t ANIM_STRAFE_WALK_RIGHT = 12; + constexpr uint32_t ANIM_BACKPEDAL = 13; + constexpr uint32_t ANIM_JUMP_START = 37; + constexpr uint32_t ANIM_JUMP_MID = 38; + constexpr uint32_t ANIM_JUMP_END = 39; + constexpr uint32_t ANIM_SIT_DOWN = 97; + constexpr uint32_t ANIM_SITTING = 97; + constexpr uint32_t ANIM_SWIM_IDLE = 41; + constexpr uint32_t ANIM_SWIM = 42; + constexpr uint32_t ANIM_MOUNT = 91; + constexpr uint32_t ANIM_READY_UNARMED = 22; + constexpr uint32_t ANIM_READY_1H = 23; + constexpr uint32_t ANIM_READY_2H = 24; + constexpr uint32_t ANIM_READY_2H_L = 25; + constexpr uint32_t ANIM_FLY_IDLE = 158; + constexpr uint32_t ANIM_FLY_FORWARD = 159; + + CharAnimState newState = charAnimState_; + + const bool rawMoving = cameraController->isMoving(); + const bool rawSprinting = cameraController->isSprinting(); + constexpr float kLocomotionStopGraceSec = 0.12f; + if (rawMoving) { + locomotionStopGraceTimer_ = kLocomotionStopGraceSec; + locomotionWasSprinting_ = rawSprinting; + } else { + locomotionStopGraceTimer_ = std::max(0.0f, locomotionStopGraceTimer_ - lastDeltaTime_); + } + bool moving = rawMoving || locomotionStopGraceTimer_ > 0.0f; + bool movingForward = cameraController->isMovingForward(); + bool movingBackward = cameraController->isMovingBackward(); + bool autoRunning = cameraController->isAutoRunning(); + bool strafeLeft = cameraController->isStrafingLeft(); + bool strafeRight = cameraController->isStrafingRight(); + bool pureStrafe = !movingForward && !movingBackward && !autoRunning; + bool anyStrafeLeft = strafeLeft && !strafeRight && pureStrafe; + bool anyStrafeRight = strafeRight && !strafeLeft && pureStrafe; + bool grounded = cameraController->isGrounded(); + bool jumping = cameraController->isJumping(); + bool sprinting = rawSprinting || (!rawMoving && moving && locomotionWasSprinting_); + bool sitting = cameraController->isSitting(); + bool swim = cameraController->isSwimming(); + bool forceMelee = meleeSwingTimer_ > 0.0f && grounded && !swim; + + const glm::vec3& characterPosition = renderer_->getCharacterPosition(); + float characterYaw = renderer_->getCharacterYaw(); + + // When mounted, force MOUNT state and skip normal transitions + if (isMounted()) { + newState = CharAnimState::MOUNT; + charAnimState_ = newState; + + uint32_t currentAnimId = 0; + float currentAnimTimeMs = 0.0f, currentAnimDurationMs = 0.0f; + bool haveState = characterRenderer->getAnimationState(characterInstanceId, currentAnimId, currentAnimTimeMs, currentAnimDurationMs); + if (!haveState || currentAnimId != ANIM_MOUNT) { + characterRenderer->playAnimation(characterInstanceId, ANIM_MOUNT, true); + } + + float mountBob = 0.0f; + float mountYawRad = glm::radians(characterYaw); + if (mountInstanceId_ > 0) { + characterRenderer->setInstancePosition(mountInstanceId_, characterPosition); + + if (!taxiFlight_ && moving && lastDeltaTime_ > 0.0f) { + float currentYawDeg = characterYaw; + float turnRate = (currentYawDeg - prevMountYaw_) / lastDeltaTime_; + while (turnRate > 180.0f) turnRate -= 360.0f; + while (turnRate < -180.0f) turnRate += 360.0f; + + float targetLean = glm::clamp(turnRate * 0.15f, -0.25f, 0.25f); + mountRoll_ = glm::mix(mountRoll_, targetLean, lastDeltaTime_ * 6.0f); + prevMountYaw_ = currentYawDeg; + } else { + mountRoll_ = glm::mix(mountRoll_, 0.0f, lastDeltaTime_ * 8.0f); + } + + characterRenderer->setInstanceRotation(mountInstanceId_, glm::vec3(mountPitch_, mountRoll_, mountYawRad)); + + auto pickMountAnim = [&](std::initializer_list candidates, uint32_t fallback) -> uint32_t { + for (uint32_t id : candidates) { + if (characterRenderer->hasAnimation(mountInstanceId_, id)) { + return id; + } + } + return fallback; + }; + + uint32_t mountAnimId = ANIM_STAND; + + uint32_t curMountAnim = 0; + float curMountTime = 0, curMountDur = 0; + bool haveMountState = characterRenderer->getAnimationState(mountInstanceId_, curMountAnim, curMountTime, curMountDur); + + if (taxiFlight_) { + if (!taxiAnimsLogged_) { + taxiAnimsLogged_ = true; + LOG_INFO("Taxi flight active: mountInstanceId_=", mountInstanceId_, + " curMountAnim=", curMountAnim, " haveMountState=", haveMountState); + std::vector seqs; + if (characterRenderer->getAnimationSequences(mountInstanceId_, seqs)) { + std::string animList; + for (const auto& s : seqs) { + if (!animList.empty()) animList += ", "; + animList += std::to_string(s.id); + } + LOG_INFO("Taxi mount available animations: [", animList, "]"); + } + } + + uint32_t flyAnims[] = {ANIM_FLY_FORWARD, ANIM_FLY_IDLE, 234, 229, 233, 141, 369, 6, ANIM_RUN}; + mountAnimId = ANIM_STAND; + for (uint32_t fa : flyAnims) { + if (characterRenderer->hasAnimation(mountInstanceId_, fa)) { + mountAnimId = fa; + break; + } + } + + if (!haveMountState || curMountAnim != mountAnimId) { + LOG_INFO("Taxi mount: playing animation ", mountAnimId); + characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); + } + + goto taxi_mount_done; + } else { + taxiAnimsLogged_ = false; + } + + // Check for jump trigger + if (cameraController->isJumpKeyPressed() && grounded && mountAction_ == MountAction::None) { + if (moving && mountAnims_.jumpLoop > 0) { + LOG_DEBUG("Mount jump triggered while moving: using jumpLoop anim ", mountAnims_.jumpLoop); + characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpLoop, true); + mountAction_ = MountAction::Jump; + mountActionPhase_ = 1; + mountAnimId = mountAnims_.jumpLoop; + if (renderer_->getAudioCoordinator()->getMountSoundManager()) { + renderer_->getAudioCoordinator()->getMountSoundManager()->playJumpSound(); + } + if (cameraController) { + cameraController->triggerMountJump(); + } + } else if (!moving && mountAnims_.rearUp > 0) { + LOG_DEBUG("Mount rear-up triggered: playing rearUp anim ", mountAnims_.rearUp); + characterRenderer->playAnimation(mountInstanceId_, mountAnims_.rearUp, false); + mountAction_ = MountAction::RearUp; + mountActionPhase_ = 0; + mountAnimId = mountAnims_.rearUp; + if (renderer_->getAudioCoordinator()->getMountSoundManager()) { + renderer_->getAudioCoordinator()->getMountSoundManager()->playRearUpSound(); + } + } + } + + // Handle active mount actions (jump chaining or rear-up) + if (mountAction_ != MountAction::None) { + bool animFinished = haveMountState && curMountDur > 0.1f && + (curMountTime >= curMountDur - 0.05f); + + if (mountAction_ == MountAction::Jump) { + if (mountActionPhase_ == 0 && animFinished && mountAnims_.jumpLoop > 0) { + LOG_DEBUG("Mount jump: phase 0→1 (JumpStart→JumpLoop anim ", mountAnims_.jumpLoop, ")"); + characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpLoop, true); + mountActionPhase_ = 1; + mountAnimId = mountAnims_.jumpLoop; + } else if (mountActionPhase_ == 0 && animFinished && mountAnims_.jumpLoop == 0) { + LOG_DEBUG("Mount jump: phase 0→1 (no JumpLoop, holding JumpStart)"); + mountActionPhase_ = 1; + } else if (mountActionPhase_ == 1 && grounded && mountAnims_.jumpEnd > 0) { + LOG_DEBUG("Mount jump: phase 1→2 (landed, JumpEnd anim ", mountAnims_.jumpEnd, ")"); + characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpEnd, false); + mountActionPhase_ = 2; + mountAnimId = mountAnims_.jumpEnd; + if (renderer_->getAudioCoordinator()->getMountSoundManager()) { + renderer_->getAudioCoordinator()->getMountSoundManager()->playLandSound(); + } + } else if (mountActionPhase_ == 1 && grounded && mountAnims_.jumpEnd == 0) { + LOG_DEBUG("Mount jump: phase 1→done (landed, no JumpEnd, returning to ", + moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand), ")"); + mountAction_ = MountAction::None; + mountAnimId = moving ? mountAnims_.run : mountAnims_.stand; + characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); + } else if (mountActionPhase_ == 2 && animFinished) { + LOG_DEBUG("Mount jump: phase 2→done (JumpEnd finished, returning to ", + moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand), ")"); + mountAction_ = MountAction::None; + mountAnimId = moving ? mountAnims_.run : mountAnims_.stand; + characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); + } else { + mountAnimId = curMountAnim; + } + } else if (mountAction_ == MountAction::RearUp) { + if (animFinished) { + LOG_DEBUG("Mount rear-up: finished, returning to ", + moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand)); + mountAction_ = MountAction::None; + mountAnimId = moving ? mountAnims_.run : mountAnims_.stand; + characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); + } else { + mountAnimId = curMountAnim; + } + } + } else if (moving) { + if (anyStrafeLeft) { + mountAnimId = pickMountAnim({ANIM_STRAFE_RUN_LEFT, ANIM_STRAFE_WALK_LEFT, ANIM_RUN}, ANIM_RUN); + } else if (anyStrafeRight) { + mountAnimId = pickMountAnim({ANIM_STRAFE_RUN_RIGHT, ANIM_STRAFE_WALK_RIGHT, ANIM_RUN}, ANIM_RUN); + } else if (movingBackward) { + mountAnimId = pickMountAnim({ANIM_BACKPEDAL}, ANIM_RUN); + } else { + mountAnimId = ANIM_RUN; + } + } + + // Cancel active fidget immediately if movement starts + if (moving && mountActiveFidget_ != 0) { + mountActiveFidget_ = 0; + characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); + } + + // Check if active fidget has completed + if (!moving && mountActiveFidget_ != 0) { + uint32_t curAnim = 0; + float curTime = 0.0f, curDur = 0.0f; + if (characterRenderer->getAnimationState(mountInstanceId_, curAnim, curTime, curDur)) { + if (curAnim != mountActiveFidget_ || curTime >= curDur * 0.95f) { + mountActiveFidget_ = 0; + LOG_DEBUG("Mount fidget completed"); + } + } + } + + // Idle fidgets + if (!moving && mountAction_ == MountAction::None && mountActiveFidget_ == 0 && !mountAnims_.fidgets.empty()) { + mountIdleFidgetTimer_ += lastDeltaTime_; + static std::mt19937 idleRng(std::random_device{}()); + static float nextFidgetTime = std::uniform_real_distribution(6.0f, 12.0f)(idleRng); + + if (mountIdleFidgetTimer_ >= nextFidgetTime) { + std::uniform_int_distribution dist(0, mountAnims_.fidgets.size() - 1); + uint32_t fidgetAnim = mountAnims_.fidgets[dist(idleRng)]; + + characterRenderer->playAnimation(mountInstanceId_, fidgetAnim, false); + mountActiveFidget_ = fidgetAnim; + mountIdleFidgetTimer_ = 0.0f; + nextFidgetTime = std::uniform_real_distribution(6.0f, 12.0f)(idleRng); + + LOG_DEBUG("Mount idle fidget: playing anim ", fidgetAnim); + } + } + if (moving) { + mountIdleFidgetTimer_ = 0.0f; + } + + // Idle ambient sounds + if (!moving && renderer_->getAudioCoordinator()->getMountSoundManager()) { + mountIdleSoundTimer_ += lastDeltaTime_; + static std::mt19937 soundRng(std::random_device{}()); + static float nextIdleSoundTime = std::uniform_real_distribution(45.0f, 90.0f)(soundRng); + + if (mountIdleSoundTimer_ >= nextIdleSoundTime) { + renderer_->getAudioCoordinator()->getMountSoundManager()->playIdleSound(); + mountIdleSoundTimer_ = 0.0f; + nextIdleSoundTime = std::uniform_real_distribution(45.0f, 90.0f)(soundRng); + } + } else if (moving) { + mountIdleSoundTimer_ = 0.0f; + } + + // Only update animation if changed and not in action or fidget + if (mountAction_ == MountAction::None && mountActiveFidget_ == 0 && (!haveMountState || curMountAnim != mountAnimId)) { + bool loop = true; + characterRenderer->playAnimation(mountInstanceId_, mountAnimId, loop); + } + + taxi_mount_done: + mountBob = 0.0f; + if (moving && haveMountState && curMountDur > 1.0f) { + float wrappedTime = curMountTime; + while (wrappedTime >= curMountDur) { + wrappedTime -= curMountDur; + } + float norm = wrappedTime / curMountDur; + float bobSpeed = taxiFlight_ ? 2.0f : 1.0f; + mountBob = std::sin(norm * 2.0f * 3.14159f * bobSpeed) * 0.12f; + } + } + + // Use mount's attachment point for proper bone-driven rider positioning. + if (taxiFlight_) { + glm::mat4 mountSeatTransform(1.0f); + bool haveSeat = false; + static constexpr uint32_t kTaxiSeatAttachmentId = 0; + if (mountSeatAttachmentId_ == -1) { + mountSeatAttachmentId_ = static_cast(kTaxiSeatAttachmentId); + } + if (mountSeatAttachmentId_ >= 0) { + haveSeat = characterRenderer->getAttachmentTransform( + mountInstanceId_, static_cast(mountSeatAttachmentId_), mountSeatTransform); + } + if (!haveSeat) { + mountSeatAttachmentId_ = -2; + } + + if (haveSeat) { + glm::vec3 targetRiderPos = glm::vec3(mountSeatTransform[3]) + glm::vec3(0.0f, 0.0f, 0.02f); + mountSeatSmoothingInit_ = false; + smoothedMountSeatPos_ = targetRiderPos; + characterRenderer->setInstancePosition(characterInstanceId, targetRiderPos); + } else { + mountSeatSmoothingInit_ = false; + glm::vec3 playerPos = characterPosition + glm::vec3(0.0f, 0.0f, mountHeightOffset_ + 0.10f); + characterRenderer->setInstancePosition(characterInstanceId, playerPos); + } + + float riderPitch = mountPitch_ * 0.35f; + float riderRoll = mountRoll_ * 0.35f; + float mountYawRadVal = glm::radians(characterYaw); + characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(riderPitch, riderRoll, mountYawRadVal)); + return; + } + + // Ground mounts: try a seat attachment first. + glm::mat4 mountSeatTransform; + bool haveSeat = false; + if (mountSeatAttachmentId_ >= 0) { + haveSeat = characterRenderer->getAttachmentTransform( + mountInstanceId_, static_cast(mountSeatAttachmentId_), mountSeatTransform); + } else if (mountSeatAttachmentId_ == -1) { + static constexpr uint32_t kSeatAttachments[] = {0, 5, 6, 7, 8}; + for (uint32_t attId : kSeatAttachments) { + if (characterRenderer->getAttachmentTransform(mountInstanceId_, attId, mountSeatTransform)) { + mountSeatAttachmentId_ = static_cast(attId); + haveSeat = true; + break; + } + } + if (!haveSeat) { + mountSeatAttachmentId_ = -2; + } + } + + if (haveSeat) { + glm::vec3 mountSeatPos = glm::vec3(mountSeatTransform[3]); + glm::vec3 seatOffset = glm::vec3(0.0f, 0.0f, taxiFlight_ ? 0.04f : 0.08f); + glm::vec3 targetRiderPos = mountSeatPos + seatOffset; + if (moving) { + mountSeatSmoothingInit_ = false; + smoothedMountSeatPos_ = targetRiderPos; + } else if (!mountSeatSmoothingInit_) { + smoothedMountSeatPos_ = targetRiderPos; + mountSeatSmoothingInit_ = true; + } else { + float smoothHz = taxiFlight_ ? 10.0f : 14.0f; + float alpha = 1.0f - std::exp(-smoothHz * std::max(lastDeltaTime_, 0.001f)); + smoothedMountSeatPos_ = glm::mix(smoothedMountSeatPos_, targetRiderPos, alpha); + } + + characterRenderer->setInstancePosition(characterInstanceId, smoothedMountSeatPos_); + + float yawRad = glm::radians(characterYaw); + float riderPitch = mountPitch_ * 0.35f; + float riderRoll = mountRoll_ * 0.35f; + characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(riderPitch, riderRoll, yawRad)); + } else { + mountSeatSmoothingInit_ = false; + float yawRad = glm::radians(characterYaw); + glm::mat4 mountRotation = glm::mat4(1.0f); + mountRotation = glm::rotate(mountRotation, yawRad, glm::vec3(0.0f, 0.0f, 1.0f)); + mountRotation = glm::rotate(mountRotation, mountRoll_, glm::vec3(1.0f, 0.0f, 0.0f)); + mountRotation = glm::rotate(mountRotation, mountPitch_, glm::vec3(0.0f, 1.0f, 0.0f)); + glm::vec3 localOffset(0.0f, 0.0f, mountHeightOffset_ + mountBob); + glm::vec3 worldOffset = glm::vec3(mountRotation * glm::vec4(localOffset, 0.0f)); + glm::vec3 playerPos = characterPosition + worldOffset; + characterRenderer->setInstancePosition(characterInstanceId, playerPos); + characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(mountPitch_, mountRoll_, yawRad)); + } + return; + } + + if (!forceMelee) switch (charAnimState_) { + case CharAnimState::IDLE: + if (swim) { + newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; + } else if (sitting && grounded) { + newState = CharAnimState::SIT_DOWN; + } else if (!grounded && jumping) { + newState = CharAnimState::JUMP_START; + } else if (!grounded) { + newState = CharAnimState::JUMP_MID; + } else if (moving && sprinting) { + newState = CharAnimState::RUN; + } else if (moving) { + newState = CharAnimState::WALK; + } else if (inCombat_ && grounded) { + newState = CharAnimState::COMBAT_IDLE; + } + break; + + case CharAnimState::WALK: + if (swim) { + newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; + } else if (!grounded && jumping) { + newState = CharAnimState::JUMP_START; + } else if (!grounded) { + newState = CharAnimState::JUMP_MID; + } else if (!moving) { + newState = CharAnimState::IDLE; + } else if (sprinting) { + newState = CharAnimState::RUN; + } + break; + + case CharAnimState::RUN: + if (swim) { + newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; + } else if (!grounded && jumping) { + newState = CharAnimState::JUMP_START; + } else if (!grounded) { + newState = CharAnimState::JUMP_MID; + } else if (!moving) { + newState = CharAnimState::IDLE; + } else if (!sprinting) { + newState = CharAnimState::WALK; + } + break; + + case CharAnimState::JUMP_START: + if (swim) { + newState = CharAnimState::SWIM_IDLE; + } else if (grounded) { + newState = CharAnimState::JUMP_END; + } else { + newState = CharAnimState::JUMP_MID; + } + break; + + case CharAnimState::JUMP_MID: + if (swim) { + newState = CharAnimState::SWIM_IDLE; + } else if (grounded) { + newState = CharAnimState::JUMP_END; + } + break; + + case CharAnimState::JUMP_END: + if (swim) { + newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; + } else if (moving && sprinting) { + newState = CharAnimState::RUN; + } else if (moving) { + newState = CharAnimState::WALK; + } else { + newState = CharAnimState::IDLE; + } + break; + + case CharAnimState::SIT_DOWN: + if (swim) { + newState = CharAnimState::SWIM_IDLE; + } else if (!sitting) { + newState = CharAnimState::IDLE; + } + break; + + case CharAnimState::SITTING: + if (swim) { + newState = CharAnimState::SWIM_IDLE; + } else if (!sitting) { + newState = CharAnimState::IDLE; + } + break; + + case CharAnimState::EMOTE: + if (swim) { + cancelEmote(); + newState = CharAnimState::SWIM_IDLE; + } else if (jumping || !grounded) { + cancelEmote(); + newState = CharAnimState::JUMP_START; + } else if (moving) { + cancelEmote(); + newState = sprinting ? CharAnimState::RUN : CharAnimState::WALK; + } else if (sitting) { + cancelEmote(); + newState = CharAnimState::SIT_DOWN; + } else if (!emoteLoop_ && characterRenderer && characterInstanceId > 0) { + uint32_t curId = 0; float curT = 0.0f, curDur = 0.0f; + if (characterRenderer->getAnimationState(characterInstanceId, curId, curT, curDur) + && curDur > 0.1f && curT >= curDur - 0.05f) { + cancelEmote(); + newState = CharAnimState::IDLE; + } + } + break; + + case CharAnimState::SWIM_IDLE: + if (!swim) { + newState = moving ? CharAnimState::WALK : CharAnimState::IDLE; + } else if (moving) { + newState = CharAnimState::SWIM; + } + break; + + case CharAnimState::SWIM: + if (!swim) { + newState = moving ? CharAnimState::WALK : CharAnimState::IDLE; + } else if (!moving) { + newState = CharAnimState::SWIM_IDLE; + } + break; + + case CharAnimState::MELEE_SWING: + if (swim) { + newState = CharAnimState::SWIM_IDLE; + } else if (!grounded && jumping) { + newState = CharAnimState::JUMP_START; + } else if (!grounded) { + newState = CharAnimState::JUMP_MID; + } else if (moving && sprinting) { + newState = CharAnimState::RUN; + } else if (moving) { + newState = CharAnimState::WALK; + } else if (sitting) { + newState = CharAnimState::SIT_DOWN; + } else if (inCombat_) { + newState = CharAnimState::COMBAT_IDLE; + } else { + newState = CharAnimState::IDLE; + } + break; + + case CharAnimState::MOUNT: + if (swim) { + newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; + } else if (sitting && grounded) { + newState = CharAnimState::SIT_DOWN; + } else if (!grounded && jumping) { + newState = CharAnimState::JUMP_START; + } else if (!grounded) { + newState = CharAnimState::JUMP_MID; + } else if (moving && sprinting) { + newState = CharAnimState::RUN; + } else if (moving) { + newState = CharAnimState::WALK; + } else { + newState = CharAnimState::IDLE; + } + break; + + case CharAnimState::COMBAT_IDLE: + if (swim) { + newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; + } else if (!grounded && jumping) { + newState = CharAnimState::JUMP_START; + } else if (!grounded) { + newState = CharAnimState::JUMP_MID; + } else if (moving && sprinting) { + newState = CharAnimState::RUN; + } else if (moving) { + newState = CharAnimState::WALK; + } else if (!inCombat_) { + newState = CharAnimState::IDLE; + } + break; + + case CharAnimState::CHARGE: + break; + } + + if (forceMelee) { + newState = CharAnimState::MELEE_SWING; + } + + if (charging_) { + newState = CharAnimState::CHARGE; + } + + if (newState != charAnimState_) { + charAnimState_ = newState; + } + + auto pickFirstAvailable = [&](std::initializer_list candidates, uint32_t fallback) -> uint32_t { + for (uint32_t id : candidates) { + if (characterRenderer->hasAnimation(characterInstanceId, id)) { + return id; + } + } + return fallback; + }; + + uint32_t animId = ANIM_STAND; + bool loop = true; + + switch (charAnimState_) { + case CharAnimState::IDLE: animId = ANIM_STAND; loop = true; break; + case CharAnimState::WALK: + if (movingBackward) { + animId = pickFirstAvailable({ANIM_BACKPEDAL}, ANIM_WALK); + } else if (anyStrafeLeft) { + animId = pickFirstAvailable({ANIM_STRAFE_WALK_LEFT, ANIM_STRAFE_RUN_LEFT}, ANIM_WALK); + } else if (anyStrafeRight) { + animId = pickFirstAvailable({ANIM_STRAFE_WALK_RIGHT, ANIM_STRAFE_RUN_RIGHT}, ANIM_WALK); + } else { + animId = pickFirstAvailable({ANIM_WALK, ANIM_RUN}, ANIM_STAND); + } + loop = true; + break; + case CharAnimState::RUN: + if (movingBackward) { + animId = pickFirstAvailable({ANIM_BACKPEDAL}, ANIM_WALK); + } else if (anyStrafeLeft) { + animId = pickFirstAvailable({ANIM_STRAFE_RUN_LEFT}, ANIM_RUN); + } else if (anyStrafeRight) { + animId = pickFirstAvailable({ANIM_STRAFE_RUN_RIGHT}, ANIM_RUN); + } else { + animId = pickFirstAvailable({ANIM_RUN, ANIM_WALK}, ANIM_STAND); + } + loop = true; + break; + case CharAnimState::JUMP_START: animId = ANIM_JUMP_START; loop = false; break; + case CharAnimState::JUMP_MID: animId = ANIM_JUMP_MID; loop = false; break; + case CharAnimState::JUMP_END: animId = ANIM_JUMP_END; loop = false; break; + case CharAnimState::SIT_DOWN: animId = ANIM_SIT_DOWN; loop = false; break; + case CharAnimState::SITTING: animId = ANIM_SITTING; loop = true; break; + case CharAnimState::EMOTE: animId = emoteAnimId_; loop = emoteLoop_; break; + case CharAnimState::SWIM_IDLE: animId = ANIM_SWIM_IDLE; loop = true; break; + case CharAnimState::SWIM: animId = ANIM_SWIM; loop = true; break; + case CharAnimState::MELEE_SWING: + animId = resolveMeleeAnimId(); + if (animId == 0) { + animId = ANIM_STAND; + } + loop = false; + break; + case CharAnimState::MOUNT: animId = ANIM_MOUNT; loop = true; break; + case CharAnimState::COMBAT_IDLE: + animId = pickFirstAvailable( + {ANIM_READY_1H, ANIM_READY_2H, ANIM_READY_2H_L, ANIM_READY_UNARMED}, + ANIM_STAND); + loop = true; + break; + case CharAnimState::CHARGE: + animId = ANIM_RUN; + loop = true; + break; + } + + uint32_t currentAnimId = 0; + float currentAnimTimeMs = 0.0f; + float currentAnimDurationMs = 0.0f; + bool haveState = characterRenderer->getAnimationState(characterInstanceId, currentAnimId, currentAnimTimeMs, currentAnimDurationMs); + const bool requestChanged = (lastPlayerAnimRequest_ != animId) || (lastPlayerAnimLoopRequest_ != loop); + const bool shouldPlay = (haveState && currentAnimId != animId) || (!haveState && requestChanged); + if (shouldPlay) { + characterRenderer->playAnimation(characterInstanceId, animId, loop); + lastPlayerAnimRequest_ = animId; + lastPlayerAnimLoopRequest_ = loop; + } +} + +// ── Footstep event detection ───────────────────────────────────────────────── + +bool AnimationController::shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs) { + if (animationDurationMs <= 1.0f) { + footstepNormInitialized_ = false; + return false; + } + + float wrappedTime = animationTimeMs; + while (wrappedTime >= animationDurationMs) { + wrappedTime -= animationDurationMs; + } + if (wrappedTime < 0.0f) wrappedTime += animationDurationMs; + float norm = wrappedTime / animationDurationMs; + + if (animationId != footstepLastAnimationId_) { + footstepLastAnimationId_ = animationId; + footstepLastNormTime_ = norm; + footstepNormInitialized_ = true; + return false; + } + + if (!footstepNormInitialized_) { + footstepNormInitialized_ = true; + footstepLastNormTime_ = norm; + return false; + } + + auto crossed = [&](float eventNorm) { + if (footstepLastNormTime_ <= norm) { + return footstepLastNormTime_ < eventNorm && eventNorm <= norm; + } + return footstepLastNormTime_ < eventNorm || eventNorm <= norm; + }; + + bool trigger = crossed(0.22f) || crossed(0.72f); + footstepLastNormTime_ = norm; + return trigger; +} + +audio::FootstepSurface AnimationController::resolveFootstepSurface() const { + auto* cameraController = renderer_->getCameraController(); + if (!cameraController || !cameraController->isThirdPerson()) { + return audio::FootstepSurface::STONE; + } + + const glm::vec3& p = renderer_->getCharacterPosition(); + + float distSq = glm::dot(p - cachedFootstepPosition_, p - cachedFootstepPosition_); + if (distSq < 2.25f && cachedFootstepUpdateTimer_ < 0.5f) { + return cachedFootstepSurface_; + } + + cachedFootstepPosition_ = p; + cachedFootstepUpdateTimer_ = 0.0f; + + if (cameraController->isSwimming()) { + cachedFootstepSurface_ = audio::FootstepSurface::WATER; + return audio::FootstepSurface::WATER; + } + + auto* waterRenderer = renderer_->getWaterRenderer(); + if (waterRenderer) { + auto waterH = waterRenderer->getWaterHeightAt(p.x, p.y); + if (waterH && p.z < (*waterH + 0.25f)) { + cachedFootstepSurface_ = audio::FootstepSurface::WATER; + return audio::FootstepSurface::WATER; + } + } + + auto* wmoRenderer = renderer_->getWMORenderer(); + auto* terrainManager = renderer_->getTerrainManager(); + if (wmoRenderer) { + auto wmoFloor = wmoRenderer->getFloorHeight(p.x, p.y, p.z + 1.5f); + auto terrainFloor = terrainManager ? terrainManager->getHeightAt(p.x, p.y) : std::nullopt; + if (wmoFloor && (!terrainFloor || *wmoFloor >= *terrainFloor - 0.1f)) { + cachedFootstepSurface_ = audio::FootstepSurface::STONE; + return audio::FootstepSurface::STONE; + } + } + + audio::FootstepSurface surface = audio::FootstepSurface::STONE; + + if (terrainManager) { + auto texture = terrainManager->getDominantTextureAt(p.x, p.y); + if (texture) { + std::string t = *texture; + for (char& c : t) c = static_cast(std::tolower(static_cast(c))); + if (t.find("snow") != std::string::npos || t.find("ice") != std::string::npos) surface = audio::FootstepSurface::SNOW; + else if (t.find("grass") != std::string::npos || t.find("moss") != std::string::npos || t.find("leaf") != std::string::npos) surface = audio::FootstepSurface::GRASS; + else if (t.find("sand") != std::string::npos || t.find("dirt") != std::string::npos || t.find("mud") != std::string::npos) surface = audio::FootstepSurface::DIRT; + else if (t.find("wood") != std::string::npos || t.find("timber") != std::string::npos) surface = audio::FootstepSurface::WOOD; + else if (t.find("metal") != std::string::npos || t.find("iron") != std::string::npos) surface = audio::FootstepSurface::METAL; + else if (t.find("stone") != std::string::npos || t.find("rock") != std::string::npos || t.find("cobble") != std::string::npos || t.find("brick") != std::string::npos) surface = audio::FootstepSurface::STONE; + } + } + + cachedFootstepSurface_ = surface; + return surface; +} + +// ── Footstep update (called from Renderer::update) ────────────────────────── + +void AnimationController::updateFootsteps(float deltaTime) { + auto* footstepManager = renderer_->getAudioCoordinator()->getFootstepManager(); + if (!footstepManager) return; + + auto* characterRenderer = renderer_->getCharacterRenderer(); + auto* cameraController = renderer_->getCameraController(); + uint32_t characterInstanceId = renderer_->getCharacterInstanceId(); + + footstepManager->update(deltaTime); + cachedFootstepUpdateTimer_ += deltaTime; + + bool canPlayFootsteps = characterRenderer && characterInstanceId > 0 && + cameraController && cameraController->isThirdPerson() && + cameraController->isGrounded() && !cameraController->isSwimming(); + + if (canPlayFootsteps && isMounted() && mountInstanceId_ > 0 && !taxiFlight_) { + // Mount footsteps: use mount's animation for timing + uint32_t animId = 0; + float animTimeMs = 0.0f, animDurationMs = 0.0f; + if (characterRenderer->getAnimationState(mountInstanceId_, animId, animTimeMs, animDurationMs) && + animDurationMs > 1.0f && cameraController->isMoving()) { + float wrappedTime = animTimeMs; + while (wrappedTime >= animDurationMs) { + wrappedTime -= animDurationMs; + } + if (wrappedTime < 0.0f) wrappedTime += animDurationMs; + float norm = wrappedTime / animDurationMs; + + if (animId != mountFootstepLastAnimId_) { + mountFootstepLastAnimId_ = animId; + mountFootstepLastNormTime_ = norm; + mountFootstepNormInitialized_ = true; + } else if (!mountFootstepNormInitialized_) { + mountFootstepNormInitialized_ = true; + mountFootstepLastNormTime_ = norm; + } else { + auto crossed = [&](float eventNorm) { + if (mountFootstepLastNormTime_ <= norm) { + return mountFootstepLastNormTime_ < eventNorm && eventNorm <= norm; + } + return mountFootstepLastNormTime_ < eventNorm || eventNorm <= norm; + }; + if (crossed(0.25f) || crossed(0.75f)) { + footstepManager->playFootstep(resolveFootstepSurface(), true); + } + mountFootstepLastNormTime_ = norm; + } + } else { + mountFootstepNormInitialized_ = false; + } + footstepNormInitialized_ = false; + } else if (canPlayFootsteps && isFootstepAnimationState()) { + uint32_t animId = 0; + float animTimeMs = 0.0f; + float animDurationMs = 0.0f; + if (characterRenderer->getAnimationState(characterInstanceId, animId, animTimeMs, animDurationMs) && + shouldTriggerFootstepEvent(animId, animTimeMs, animDurationMs)) { + auto surface = resolveFootstepSurface(); + footstepManager->playFootstep(surface, cameraController->isSprinting()); + if (surface == audio::FootstepSurface::WATER) { + if (renderer_->getAudioCoordinator()->getMovementSoundManager()) { + renderer_->getAudioCoordinator()->getMovementSoundManager()->playWaterFootstep(audio::MovementSoundManager::CharacterSize::MEDIUM); + } + auto* swimEffects = renderer_->getSwimEffects(); + auto* waterRenderer = renderer_->getWaterRenderer(); + if (swimEffects && waterRenderer) { + const glm::vec3& characterPosition = renderer_->getCharacterPosition(); + auto wh = waterRenderer->getWaterHeightAt(characterPosition.x, characterPosition.y); + if (wh) { + swimEffects->spawnFootSplash(characterPosition, *wh); + } + } + } + } + mountFootstepNormInitialized_ = false; + } else { + footstepNormInitialized_ = false; + mountFootstepNormInitialized_ = false; + } +} + +// ── Activity SFX state tracking ────────────────────────────────────────────── + +void AnimationController::updateSfxState(float deltaTime) { + auto* activitySoundManager = renderer_->getAudioCoordinator()->getActivitySoundManager(); + if (!activitySoundManager) return; + + auto* cameraController = renderer_->getCameraController(); + + activitySoundManager->update(deltaTime); + if (cameraController && cameraController->isThirdPerson()) { + bool grounded = cameraController->isGrounded(); + bool jumping = cameraController->isJumping(); + bool falling = cameraController->isFalling(); + bool swimming = cameraController->isSwimming(); + bool moving = cameraController->isMoving(); + + if (!sfxStateInitialized_) { + sfxPrevGrounded_ = grounded; + sfxPrevJumping_ = jumping; + sfxPrevFalling_ = falling; + sfxPrevSwimming_ = swimming; + sfxStateInitialized_ = true; + } + + if (jumping && !sfxPrevJumping_ && !swimming) { + activitySoundManager->playJump(); + } + + if (grounded && !sfxPrevGrounded_) { + bool hardLanding = sfxPrevFalling_; + activitySoundManager->playLanding(resolveFootstepSurface(), hardLanding); + } + + if (swimming && !sfxPrevSwimming_) { + activitySoundManager->playWaterEnter(); + } else if (!swimming && sfxPrevSwimming_) { + activitySoundManager->playWaterExit(); + } + + activitySoundManager->setSwimmingState(swimming, moving); + + if (renderer_->getAudioCoordinator()->getMusicManager()) { + renderer_->getAudioCoordinator()->getMusicManager()->setUnderwaterMode(swimming); + } + + sfxPrevGrounded_ = grounded; + sfxPrevJumping_ = jumping; + sfxPrevFalling_ = falling; + sfxPrevSwimming_ = swimming; + } else { + activitySoundManager->setSwimmingState(false, false); + if (renderer_->getAudioCoordinator()->getMusicManager()) { + renderer_->getAudioCoordinator()->getMusicManager()->setUnderwaterMode(false); + } + sfxStateInitialized_ = false; + } + + // Mount ambient sounds + if (renderer_->getAudioCoordinator()->getMountSoundManager()) { + renderer_->getAudioCoordinator()->getMountSoundManager()->update(deltaTime); + if (cameraController && isMounted()) { + bool isMoving = cameraController->isMoving(); + bool flying = taxiFlight_ || !cameraController->isGrounded(); + renderer_->getAudioCoordinator()->getMountSoundManager()->setMoving(isMoving); + renderer_->getAudioCoordinator()->getMountSoundManager()->setFlying(flying); + } + } +} + +} // namespace rendering +} // namespace wowee diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 5ce45f0f..ddd39746 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -59,6 +59,7 @@ #include "rendering/amd_fsr3_runtime.hpp" #include "rendering/spell_visual_system.hpp" #include "rendering/post_process_pipeline.hpp" +#include "rendering/animation_controller.hpp" #include #include #include @@ -86,34 +87,6 @@ namespace wowee { namespace rendering { -// Audio accessor pass-throughs — delegate to AudioCoordinator (owned by Application). -// These remain until §4.2 (AnimationController) removes Renderer's last audio usage. -audio::MusicManager* Renderer::getMusicManager() { return audioCoordinator_ ? audioCoordinator_->getMusicManager() : nullptr; } -audio::FootstepManager* Renderer::getFootstepManager() { return audioCoordinator_ ? audioCoordinator_->getFootstepManager() : nullptr; } -audio::ActivitySoundManager* Renderer::getActivitySoundManager() { return audioCoordinator_ ? audioCoordinator_->getActivitySoundManager() : nullptr; } -audio::MountSoundManager* Renderer::getMountSoundManager() { return audioCoordinator_ ? audioCoordinator_->getMountSoundManager() : nullptr; } -audio::NpcVoiceManager* Renderer::getNpcVoiceManager() { return audioCoordinator_ ? audioCoordinator_->getNpcVoiceManager() : nullptr; } -audio::AmbientSoundManager* Renderer::getAmbientSoundManager() { return audioCoordinator_ ? audioCoordinator_->getAmbientSoundManager() : nullptr; } -audio::UiSoundManager* Renderer::getUiSoundManager() { return audioCoordinator_ ? audioCoordinator_->getUiSoundManager() : nullptr; } -audio::CombatSoundManager* Renderer::getCombatSoundManager() { return audioCoordinator_ ? audioCoordinator_->getCombatSoundManager() : nullptr; } -audio::SpellSoundManager* Renderer::getSpellSoundManager() { return audioCoordinator_ ? audioCoordinator_->getSpellSoundManager() : nullptr; } -audio::MovementSoundManager* Renderer::getMovementSoundManager() { return audioCoordinator_ ? audioCoordinator_->getMovementSoundManager() : nullptr; } - -struct EmoteInfo { - uint32_t animId = 0; - uint32_t dbcId = 0; // EmotesText.dbc record ID (for CMSG_TEXT_EMOTE) - bool loop = false; - std::string textNoTarget; // sender sees, no target: "You dance." - std::string textTarget; // sender sees, with target: "You dance with %s." - std::string othersNoTarget; // others see, no target: "%s dances." - std::string othersTarget; // others see, with target: "%s dances with %s." - std::string command; -}; - -static std::unordered_map EMOTE_TABLE; -static std::unordered_map EMOTE_BY_DBCID; // reverse lookup: dbcId → EmoteInfo* -static bool emoteTableLoaded = false; - static bool envFlagEnabled(const char* key, bool defaultValue) { const char* raw = std::getenv(key); if (!raw || !*raw) return defaultValue; @@ -133,173 +106,6 @@ static int envIntOrDefault(const char* key, int defaultValue) { return static_cast(n); } - - -static std::vector parseEmoteCommands(const std::string& raw) { - std::vector out; - std::string cur; - for (char c : raw) { - if (std::isalnum(static_cast(c)) || c == '_') { - cur.push_back(static_cast(std::tolower(static_cast(c)))); - } else if (!cur.empty()) { - out.push_back(cur); - cur.clear(); - } - } - if (!cur.empty()) out.push_back(cur); - return out; -} - -static bool isLoopingEmote(const std::string& command) { - static const std::unordered_set kLooping = { - "dance", - "train", - }; - return kLooping.find(command) != kLooping.end(); -} - -static void loadFallbackEmotes() { - if (!EMOTE_TABLE.empty()) return; - EMOTE_TABLE = { - {"wave", {67, 0, false, "You wave.", "You wave at %s.", "%s waves.", "%s waves at %s.", "wave"}}, - {"bow", {66, 0, false, "You bow down graciously.", "You bow down before %s.", "%s bows down graciously.", "%s bows down before %s.", "bow"}}, - {"laugh", {70, 0, false, "You laugh.", "You laugh at %s.", "%s laughs.", "%s laughs at %s.", "laugh"}}, - {"point", {84, 0, false, "You point over yonder.", "You point at %s.", "%s points over yonder.", "%s points at %s.", "point"}}, - {"cheer", {68, 0, false, "You cheer!", "You cheer at %s.", "%s cheers!", "%s cheers at %s.", "cheer"}}, - {"dance", {69, 0, true, "You burst into dance.", "You dance with %s.", "%s bursts into dance.", "%s dances with %s.", "dance"}}, - {"kneel", {75, 0, false, "You kneel down.", "You kneel before %s.", "%s kneels down.", "%s kneels before %s.", "kneel"}}, - {"applaud", {80, 0, false, "You applaud. Bravo!", "You applaud at %s. Bravo!", "%s applauds. Bravo!", "%s applauds at %s. Bravo!", "applaud"}}, - {"shout", {81, 0, false, "You shout.", "You shout at %s.", "%s shouts.", "%s shouts at %s.", "shout"}}, - {"chicken", {78, 0, false, "With arms flapping, you strut around. Cluck, Cluck, Chicken!", - "With arms flapping, you strut around %s. Cluck, Cluck, Chicken!", - "%s struts around. Cluck, Cluck, Chicken!", "%s struts around %s. Cluck, Cluck, Chicken!", "chicken"}}, - {"cry", {77, 0, false, "You cry.", "You cry on %s's shoulder.", "%s cries.", "%s cries on %s's shoulder.", "cry"}}, - {"kiss", {76, 0, false, "You blow a kiss into the wind.", "You blow a kiss to %s.", "%s blows a kiss into the wind.", "%s blows a kiss to %s.", "kiss"}}, - {"roar", {74, 0, false, "You roar with bestial vigor. So fierce!", "You roar with bestial vigor at %s. So fierce!", "%s roars with bestial vigor. So fierce!", "%s roars with bestial vigor at %s. So fierce!", "roar"}}, - {"salute", {113, 0, false, "You salute.", "You salute %s with respect.", "%s salutes.", "%s salutes %s with respect.", "salute"}}, - {"rude", {73, 0, false, "You make a rude gesture.", "You make a rude gesture at %s.", "%s makes a rude gesture.", "%s makes a rude gesture at %s.", "rude"}}, - {"flex", {82, 0, false, "You flex your muscles. Oooooh so strong!", "You flex at %s. Oooooh so strong!", "%s flexes. Oooooh so strong!", "%s flexes at %s. Oooooh so strong!", "flex"}}, - {"shy", {83, 0, false, "You smile shyly.", "You smile shyly at %s.", "%s smiles shyly.", "%s smiles shyly at %s.", "shy"}}, - {"beg", {79, 0, false, "You beg everyone around you. How pathetic.", "You beg %s. How pathetic.", "%s begs everyone around. How pathetic.", "%s begs %s. How pathetic.", "beg"}}, - {"eat", {61, 0, false, "You begin to eat.", "You begin to eat in front of %s.", "%s begins to eat.", "%s begins to eat in front of %s.", "eat"}}, - }; -} - -static std::string replacePlaceholders(const std::string& text, const std::string* targetName) { - if (text.empty()) return text; - std::string out; - out.reserve(text.size() + 16); - for (size_t i = 0; i < text.size(); ++i) { - if (text[i] == '%' && i + 1 < text.size() && text[i + 1] == 's') { - if (targetName && !targetName->empty()) out += *targetName; - i++; - } else { - out.push_back(text[i]); - } - } - return out; -} - -static void loadEmotesFromDbc() { - if (emoteTableLoaded) return; - emoteTableLoaded = true; - - auto* assetManager = core::Application::getInstance().getAssetManager(); - if (!assetManager) { - LOG_WARNING("Emotes: no AssetManager"); - loadFallbackEmotes(); - return; - } - - auto emotesTextDbc = assetManager->loadDBC("EmotesText.dbc"); - auto emotesTextDataDbc = assetManager->loadDBC("EmotesTextData.dbc"); - if (!emotesTextDbc || !emotesTextDataDbc || !emotesTextDbc->isLoaded() || !emotesTextDataDbc->isLoaded()) { - LOG_WARNING("Emotes: DBCs not available (EmotesText/EmotesTextData)"); - loadFallbackEmotes(); - return; - } - - const auto* activeLayout = pipeline::getActiveDBCLayout(); - const auto* etdL = activeLayout ? activeLayout->getLayout("EmotesTextData") : nullptr; - const auto* emL = activeLayout ? activeLayout->getLayout("Emotes") : nullptr; - const auto* etL = activeLayout ? activeLayout->getLayout("EmotesText") : nullptr; - - std::unordered_map textData; - textData.reserve(emotesTextDataDbc->getRecordCount()); - for (uint32_t r = 0; r < emotesTextDataDbc->getRecordCount(); ++r) { - uint32_t id = emotesTextDataDbc->getUInt32(r, etdL ? (*etdL)["ID"] : 0); - std::string text = emotesTextDataDbc->getString(r, etdL ? (*etdL)["Text"] : 1); - if (!text.empty()) textData.emplace(id, std::move(text)); - } - - std::unordered_map emoteIdToAnim; - if (auto emotesDbc = assetManager->loadDBC("Emotes.dbc"); emotesDbc && emotesDbc->isLoaded()) { - emoteIdToAnim.reserve(emotesDbc->getRecordCount()); - for (uint32_t r = 0; r < emotesDbc->getRecordCount(); ++r) { - uint32_t emoteId = emotesDbc->getUInt32(r, emL ? (*emL)["ID"] : 0); - uint32_t animId = emotesDbc->getUInt32(r, emL ? (*emL)["AnimID"] : 2); - if (animId != 0) emoteIdToAnim[emoteId] = animId; - } - } - - EMOTE_TABLE.clear(); - EMOTE_TABLE.reserve(emotesTextDbc->getRecordCount()); - for (uint32_t r = 0; r < emotesTextDbc->getRecordCount(); ++r) { - uint32_t recordId = emotesTextDbc->getUInt32(r, etL ? (*etL)["ID"] : 0); - std::string cmdRaw = emotesTextDbc->getString(r, etL ? (*etL)["Command"] : 1); - if (cmdRaw.empty()) continue; - - uint32_t emoteRef = emotesTextDbc->getUInt32(r, etL ? (*etL)["EmoteRef"] : 2); - uint32_t animId = 0; - auto animIt = emoteIdToAnim.find(emoteRef); - if (animIt != emoteIdToAnim.end()) { - animId = animIt->second; - } else { - animId = emoteRef; // fallback if EmotesText stores animation id directly - } - - uint32_t senderTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderTargetTextID"] : 5); - uint32_t senderNoTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["SenderNoTargetTextID"] : 9); - uint32_t othersTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["OthersTargetTextID"] : 3); - uint32_t othersNoTargetTextId = emotesTextDbc->getUInt32(r, etL ? (*etL)["OthersNoTargetTextID"] : 7); - - std::string textTarget, textNoTarget, oTarget, oNoTarget; - if (auto it = textData.find(senderTargetTextId); it != textData.end()) textTarget = it->second; - if (auto it = textData.find(senderNoTargetTextId); it != textData.end()) textNoTarget = it->second; - if (auto it = textData.find(othersTargetTextId); it != textData.end()) oTarget = it->second; - if (auto it = textData.find(othersNoTargetTextId); it != textData.end()) oNoTarget = it->second; - - for (const std::string& cmd : parseEmoteCommands(cmdRaw)) { - if (cmd.empty()) continue; - EmoteInfo info; - info.animId = animId; - info.dbcId = recordId; - info.loop = isLoopingEmote(cmd); - info.textNoTarget = textNoTarget; - info.textTarget = textTarget; - info.othersNoTarget = oNoTarget; - info.othersTarget = oTarget; - info.command = cmd; - EMOTE_TABLE.emplace(cmd, std::move(info)); - } - } - - if (EMOTE_TABLE.empty()) { - LOG_WARNING("Emotes: DBC loaded but no commands parsed, using fallback list"); - loadFallbackEmotes(); - } else { - LOG_INFO("Emotes: loaded ", EMOTE_TABLE.size(), " commands from DBC"); - } - - // Build reverse lookup by dbcId (only first command per emote needed) - EMOTE_BY_DBCID.clear(); - for (auto& [cmd, info] : EMOTE_TABLE) { - if (info.dbcId != 0) { - EMOTE_BY_DBCID.emplace(info.dbcId, &info); - } - } -} - Renderer::Renderer() = default; Renderer::~Renderer() = default; @@ -827,6 +633,9 @@ void Renderer::shutdown() { characterRenderer.reset(); } + // Shutdown AnimationController before renderers it references (§4.2) + animationController_.reset(); + LOG_WARNING("Renderer::shutdown - wmoRenderer..."); if (wmoRenderer) { wmoRenderer->shutdown(); @@ -1175,1107 +984,57 @@ void Renderer::setCharacterFollow(uint32_t instanceId) { if (cameraController && instanceId > 0) { cameraController->setFollowTarget(&characterPosition); } + if (animationController_) animationController_->onCharacterFollow(instanceId); } void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset, const std::string& modelPath) { - mountInstanceId_ = mountInstId; - mountHeightOffset_ = heightOffset; - mountSeatAttachmentId_ = -1; - smoothedMountSeatPos_ = characterPosition; - mountSeatSmoothingInit_ = false; - mountAction_ = MountAction::None; // Clear mount action state - mountActionPhase_ = 0; - charAnimState = CharAnimState::MOUNT; - if (cameraController) { - cameraController->setMounted(true); - cameraController->setMountHeightOffset(heightOffset); - } - - // Debug: dump available mount animations - if (characterRenderer && mountInstId > 0) { - characterRenderer->dumpAnimations(mountInstId); - } - - // Discover mount animation capabilities (property-based, not hardcoded IDs) - LOG_DEBUG("=== Mount Animation Dump (Display ID ", mountDisplayId, ") ==="); - characterRenderer->dumpAnimations(mountInstId); - - // Get all sequences for property-based analysis - std::vector sequences; - if (!characterRenderer->getAnimationSequences(mountInstId, sequences)) { - LOG_WARNING("Failed to get animation sequences for mount, using fallback IDs"); - sequences.clear(); - } - - // Helper: ID-based fallback finder - auto findFirst = [&](std::initializer_list candidates) -> uint32_t { - for (uint32_t id : candidates) { - if (characterRenderer->hasAnimation(mountInstId, id)) { - return id; - } - } - return 0; - }; - - // Property-based jump animation discovery with chain-based scoring - auto discoverJumpSet = [&]() { - // Debug: log all sequences for analysis - LOG_DEBUG("=== Full sequence table for mount ==="); - for (const auto& seq : sequences) { - LOG_DEBUG("SEQ id=", seq.id, - " dur=", seq.duration, - " flags=0x", std::hex, seq.flags, std::dec, - " moveSpd=", seq.movingSpeed, - " blend=", seq.blendTime, - " next=", seq.nextAnimation, - " alias=", seq.aliasNext); - } - LOG_DEBUG("=== End sequence table ==="); - - // Known combat/bad animation IDs to avoid - std::set forbiddenIds = {53, 54, 16}; // jumpkick, attack - - auto scoreNear = [](int a, int b) -> int { - int d = std::abs(a - b); - return (d <= 8) ? (20 - d) : 0; // within 8 IDs gets points - }; - - auto isForbidden = [&](uint32_t id) { - return forbiddenIds.count(id) != 0; - }; - - auto findSeqById = [&](uint32_t id) -> const pipeline::M2Sequence* { - for (const auto& s : sequences) { - if (s.id == id) return &s; - } - return nullptr; - }; - - uint32_t runId = findFirst({5, 4}); - uint32_t standId = findFirst({0}); - - // Step A: Find loop candidates - std::vector loops; - for (const auto& seq : sequences) { - if (isForbidden(seq.id)) continue; - // Bit 0x01 NOT set = loops (0x20, 0x60), bit 0x01 set = non-looping (0x21, 0x61) - bool isLoop = (seq.flags & 0x01) == 0; - if (isLoop && seq.duration >= 350 && seq.duration <= 1000 && - seq.id != runId && seq.id != standId) { - loops.push_back(seq.id); - } - } - - // Choose loop: prefer one near known classic IDs (38), else best duration - uint32_t loop = 0; - if (!loops.empty()) { - uint32_t best = loops[0]; - int bestScore = -999; - for (uint32_t id : loops) { - int sc = 0; - sc += scoreNear(static_cast(id), 38); // classic hint - const auto* s = findSeqById(id); - if (s) sc += (s->duration >= 500 && s->duration <= 800) ? 5 : 0; - if (sc > bestScore) { - bestScore = sc; - best = id; - } - } - loop = best; - } - - // Step B: Score start/end candidates - uint32_t start = 0, end = 0; - int bestStart = -999, bestEnd = -999; - - for (const auto& seq : sequences) { - if (isForbidden(seq.id)) continue; - // Only consider non-looping animations for start/end - bool isLoop = (seq.flags & 0x01) == 0; - if (isLoop) continue; - - // Start window - if (seq.duration >= 450 && seq.duration <= 1100) { - int sc = 0; - if (loop) sc += scoreNear(static_cast(seq.id), static_cast(loop)); - // Chain bonus: if this start points at loop or near it - if (loop && (seq.nextAnimation == static_cast(loop) || seq.aliasNext == loop)) sc += 30; - if (loop && scoreNear(seq.nextAnimation, static_cast(loop)) > 0) sc += 10; - // Penalize "stop/brake-ish": very long blendTime can be a stop transition - if (seq.blendTime > 400) sc -= 5; - - if (sc > bestStart) { - bestStart = sc; - start = seq.id; - } - } - - // End window - if (seq.duration >= 650 && seq.duration <= 1600) { - int sc = 0; - if (loop) sc += scoreNear(static_cast(seq.id), static_cast(loop)); - // Chain bonus: end often points to run/stand or has no next - if (seq.nextAnimation == static_cast(runId) || seq.nextAnimation == static_cast(standId)) sc += 10; - if (seq.nextAnimation < 0) sc += 5; // no chain sometimes = terminal - if (sc > bestEnd) { - bestEnd = sc; - end = seq.id; - } - } - } - - LOG_DEBUG("Property-based jump discovery: start=", start, " loop=", loop, " end=", end, - " scores: start=", bestStart, " end=", bestEnd); - return std::make_tuple(start, loop, end); - }; - - auto [discoveredStart, discoveredLoop, discoveredEnd] = discoverJumpSet(); - - // Use discovered animations, fallback to known IDs if discovery fails - mountAnims_.jumpStart = discoveredStart > 0 ? discoveredStart : findFirst({40, 37}); - mountAnims_.jumpLoop = discoveredLoop > 0 ? discoveredLoop : findFirst({38}); - mountAnims_.jumpEnd = discoveredEnd > 0 ? discoveredEnd : findFirst({39}); - mountAnims_.rearUp = findFirst({94, 92, 40}); // RearUp/Special - mountAnims_.run = findFirst({5, 4}); // Run/Walk - mountAnims_.stand = findFirst({0}); // Stand (almost always 0) - - // Discover idle fidget animations using proper WoW M2 metadata (frequency, replay timers) - mountAnims_.fidgets.clear(); - core::Logger::getInstance().debug("Scanning for fidget animations in ", sequences.size(), " sequences"); - - // DEBUG: Log ALL non-looping, short, stationary animations to identify stamps/tosses - core::Logger::getInstance().debug("=== ALL potential fidgets (no metadata filter) ==="); - for (const auto& seq : sequences) { - bool isLoop = (seq.flags & 0x01) == 0; - bool isStationary = std::abs(seq.movingSpeed) < 0.05f; - bool reasonableDuration = seq.duration >= 400 && seq.duration <= 2500; - - if (!isLoop && reasonableDuration && isStationary) { - core::Logger::getInstance().debug(" ALL: id=", seq.id, - " dur=", seq.duration, "ms", - " freq=", seq.frequency, - " replay=", seq.replayMin, "-", seq.replayMax, - " flags=0x", std::hex, seq.flags, std::dec, - " next=", seq.nextAnimation); - } - } - - // Proper fidget discovery: frequency > 0 + replay timers indicate random idle animations - for (const auto& seq : sequences) { - bool isLoop = (seq.flags & 0x01) == 0; - bool hasFrequency = seq.frequency > 0; - bool hasReplay = seq.replayMax > 0; - bool isStationary = std::abs(seq.movingSpeed) < 0.05f; - bool reasonableDuration = seq.duration >= 400 && seq.duration <= 2500; - - // Log candidates with metadata - if (!isLoop && reasonableDuration && isStationary && (hasFrequency || hasReplay)) { - core::Logger::getInstance().debug(" Candidate: id=", seq.id, - " dur=", seq.duration, "ms", - " freq=", seq.frequency, - " replay=", seq.replayMin, "-", seq.replayMax, - " next=", seq.nextAnimation, - " speed=", seq.movingSpeed); - } - - // Exclude known problematic animations: death (5-6), wounds (7-9), combat (16-21), attacks (11-15) - bool isDeathOrWound = (seq.id >= 5 && seq.id <= 9); - bool isAttackOrCombat = (seq.id >= 11 && seq.id <= 21); - bool isSpecial = (seq.id == 2 || seq.id == 3); // Often aggressive specials - - // Select fidgets: (frequency OR replay) + exclude problematic ID ranges - // Relaxed back to OR since some mounts may only have one metadata marker - if (!isLoop && (hasFrequency || hasReplay) && isStationary && reasonableDuration && - !isDeathOrWound && !isAttackOrCombat && !isSpecial) { - // Bonus: chains back to stand (indicates idle behavior) - bool chainsToStand = (seq.nextAnimation == static_cast(mountAnims_.stand)) || - (seq.aliasNext == mountAnims_.stand) || - (seq.nextAnimation == -1); - - mountAnims_.fidgets.push_back(seq.id); - core::Logger::getInstance().debug(" >> Selected fidget: id=", seq.id, - (chainsToStand ? " (chains to stand)" : "")); - } - } - - // Ensure we have fallbacks for movement - if (mountAnims_.run == 0) mountAnims_.run = mountAnims_.stand; // Fallback to stand if no run - - core::Logger::getInstance().debug("Mount animation set: jumpStart=", mountAnims_.jumpStart, - " jumpLoop=", mountAnims_.jumpLoop, - " jumpEnd=", mountAnims_.jumpEnd, - " rearUp=", mountAnims_.rearUp, - " run=", mountAnims_.run, - " stand=", mountAnims_.stand, - " fidgets=", mountAnims_.fidgets.size()); - - // Notify mount sound manager - if (getMountSoundManager()) { - bool isFlying = taxiFlight_; // Taxi flights are flying mounts - getMountSoundManager()->onMount(mountDisplayId, isFlying, modelPath); - } + if (animationController_) animationController_->setMounted(mountInstId, mountDisplayId, heightOffset, modelPath); } void Renderer::clearMount() { - mountInstanceId_ = 0; - mountHeightOffset_ = 0.0f; - mountPitch_ = 0.0f; - mountRoll_ = 0.0f; - mountSeatAttachmentId_ = -1; - smoothedMountSeatPos_ = glm::vec3(0.0f); - mountSeatSmoothingInit_ = false; - mountAction_ = MountAction::None; - mountActionPhase_ = 0; - charAnimState = CharAnimState::IDLE; - if (cameraController) { - cameraController->setMounted(false); - cameraController->setMountHeightOffset(0.0f); - } - - // Notify mount sound manager - if (getMountSoundManager()) { - getMountSoundManager()->onDismount(); - } + if (animationController_) animationController_->clearMount(); } -uint32_t Renderer::resolveMeleeAnimId() { - if (!characterRenderer || characterInstanceId == 0) { - meleeAnimId = 0; - meleeAnimDurationMs = 0.0f; - return 0; - } - if (meleeAnimId != 0 && characterRenderer->hasAnimation(characterInstanceId, meleeAnimId)) { - return meleeAnimId; - } - - std::vector sequences; - if (!characterRenderer->getAnimationSequences(characterInstanceId, sequences)) { - meleeAnimId = 0; - meleeAnimDurationMs = 0.0f; - return 0; - } - - auto findDuration = [&](uint32_t id) -> float { - for (const auto& seq : sequences) { - if (seq.id == id && seq.duration > 0) { - return static_cast(seq.duration); - } - } - return 0.0f; - }; - - // Select animation priority based on equipped weapon type - // WoW inventory types: 17 = 2H weapon, 13/21 = 1H, 0 = unarmed - // WoW anim IDs: 16 = unarmed, 17 = 1H attack, 18 = 2H attack - const uint32_t* attackCandidates; - size_t candidateCount; - static const uint32_t candidates2H[] = {18, 17, 16, 19, 20, 21}; - static const uint32_t candidates1H[] = {17, 18, 16, 19, 20, 21}; - static const uint32_t candidatesUnarmed[] = {16, 17, 18, 19, 20, 21}; - if (equippedWeaponInvType_ == 17) { // INVTYPE_2HWEAPON - attackCandidates = candidates2H; - candidateCount = 6; - } else if (equippedWeaponInvType_ == 0) { - attackCandidates = candidatesUnarmed; - candidateCount = 6; - } else { - attackCandidates = candidates1H; - candidateCount = 6; - } - for (size_t ci = 0; ci < candidateCount; ci++) { - uint32_t id = attackCandidates[ci]; - if (characterRenderer->hasAnimation(characterInstanceId, id)) { - meleeAnimId = id; - meleeAnimDurationMs = findDuration(id); - return meleeAnimId; - } - } - - const uint32_t avoidIds[] = {0, 1, 4, 5, 11, 12, 13, 37, 38, 39, 41, 42, 97}; - auto isAvoid = [&](uint32_t id) -> bool { - for (uint32_t avoid : avoidIds) { - if (id == avoid) return true; - } - return false; - }; - - uint32_t bestId = 0; - uint32_t bestDuration = 0; - for (const auto& seq : sequences) { - if (seq.duration == 0) continue; - if (isAvoid(seq.id)) continue; - if (seq.movingSpeed > 0.1f) continue; - if (seq.duration < 150 || seq.duration > 2000) continue; - if (bestId == 0 || seq.duration < bestDuration) { - bestId = seq.id; - bestDuration = seq.duration; - } - } - - if (bestId == 0) { - for (const auto& seq : sequences) { - if (seq.duration == 0) continue; - if (isAvoid(seq.id)) continue; - if (bestId == 0 || seq.duration < bestDuration) { - bestId = seq.id; - bestDuration = seq.duration; - } - } - } - - meleeAnimId = bestId; - meleeAnimDurationMs = static_cast(bestDuration); - return meleeAnimId; -} - -void Renderer::updateCharacterAnimation() { - // WoW WotLK AnimationData.dbc IDs - constexpr uint32_t ANIM_STAND = 0; - constexpr uint32_t ANIM_WALK = 4; - constexpr uint32_t ANIM_RUN = 5; - // Candidate locomotion clips by common WotLK IDs. - constexpr uint32_t ANIM_STRAFE_RUN_RIGHT = 92; - constexpr uint32_t ANIM_STRAFE_RUN_LEFT = 93; - constexpr uint32_t ANIM_STRAFE_WALK_LEFT = 11; - constexpr uint32_t ANIM_STRAFE_WALK_RIGHT = 12; - constexpr uint32_t ANIM_BACKPEDAL = 13; - constexpr uint32_t ANIM_JUMP_START = 37; - constexpr uint32_t ANIM_JUMP_MID = 38; - constexpr uint32_t ANIM_JUMP_END = 39; - constexpr uint32_t ANIM_SIT_DOWN = 97; // SitGround — transition to sitting - constexpr uint32_t ANIM_SITTING = 97; // Hold on same animation (no separate idle) - constexpr uint32_t ANIM_SWIM_IDLE = 41; // Treading water (SwimIdle) - constexpr uint32_t ANIM_SWIM = 42; // Swimming forward (Swim) - constexpr uint32_t ANIM_MOUNT = 91; // Seated on mount - // Canonical player ready stances (AnimationData.dbc) - constexpr uint32_t ANIM_READY_UNARMED = 22; // ReadyUnarmed - constexpr uint32_t ANIM_READY_1H = 23; // Ready1H - constexpr uint32_t ANIM_READY_2H = 24; // Ready2H - constexpr uint32_t ANIM_READY_2H_L = 25; // Ready2HL (some 2H left-handed rigs) - constexpr uint32_t ANIM_FLY_IDLE = 158; // Flying mount idle/hover - constexpr uint32_t ANIM_FLY_FORWARD = 159; // Flying mount forward - - CharAnimState newState = charAnimState; - - const bool rawMoving = cameraController->isMoving(); - const bool rawSprinting = cameraController->isSprinting(); - constexpr float kLocomotionStopGraceSec = 0.12f; - if (rawMoving) { - locomotionStopGraceTimer_ = kLocomotionStopGraceSec; - locomotionWasSprinting_ = rawSprinting; - } else { - locomotionStopGraceTimer_ = std::max(0.0f, locomotionStopGraceTimer_ - lastDeltaTime_); - } - // Debounce brief input/state dropouts (notably during both-mouse steering) so - // locomotion clips do not restart every few frames. - bool moving = rawMoving || locomotionStopGraceTimer_ > 0.0f; - bool movingForward = cameraController->isMovingForward(); - bool movingBackward = cameraController->isMovingBackward(); - bool autoRunning = cameraController->isAutoRunning(); - bool strafeLeft = cameraController->isStrafingLeft(); - bool strafeRight = cameraController->isStrafingRight(); - // Strafe animation only plays during *pure* strafing (no forward/backward/autorun). - // When forward+strafe are both held, the walk/run animation plays — same as the real client. - bool pureStrafe = !movingForward && !movingBackward && !autoRunning; - bool anyStrafeLeft = strafeLeft && !strafeRight && pureStrafe; - bool anyStrafeRight = strafeRight && !strafeLeft && pureStrafe; - bool grounded = cameraController->isGrounded(); - bool jumping = cameraController->isJumping(); - bool sprinting = rawSprinting || (!rawMoving && moving && locomotionWasSprinting_); - bool sitting = cameraController->isSitting(); - bool swim = cameraController->isSwimming(); - bool forceMelee = meleeSwingTimer > 0.0f && grounded && !swim; - - // When mounted, force MOUNT state and skip normal transitions - if (isMounted()) { - newState = CharAnimState::MOUNT; - charAnimState = newState; - - // Play seated animation on player - uint32_t currentAnimId = 0; - float currentAnimTimeMs = 0.0f, currentAnimDurationMs = 0.0f; - bool haveState = characterRenderer->getAnimationState(characterInstanceId, currentAnimId, currentAnimTimeMs, currentAnimDurationMs); - if (!haveState || currentAnimId != ANIM_MOUNT) { - characterRenderer->playAnimation(characterInstanceId, ANIM_MOUNT, true); - } - - // Sync mount instance position and rotation - float mountBob = 0.0f; - float mountYawRad = glm::radians(characterYaw); - if (mountInstanceId_ > 0) { - characterRenderer->setInstancePosition(mountInstanceId_, characterPosition); - - // Procedural lean into turns (ground mounts only, optional enhancement) - if (!taxiFlight_ && moving && lastDeltaTime_ > 0.0f) { - float currentYawDeg = characterYaw; - float turnRate = (currentYawDeg - prevMountYaw_) / lastDeltaTime_; - // Normalize to [-180, 180] for wrap-around - while (turnRate > 180.0f) turnRate -= 360.0f; - while (turnRate < -180.0f) turnRate += 360.0f; - - float targetLean = glm::clamp(turnRate * 0.15f, -0.25f, 0.25f); - mountRoll_ = glm::mix(mountRoll_, targetLean, lastDeltaTime_ * 6.0f); - prevMountYaw_ = currentYawDeg; - } else { - // Return to upright when not turning - mountRoll_ = glm::mix(mountRoll_, 0.0f, lastDeltaTime_ * 8.0f); - } - - // Apply pitch (up/down), roll (banking), and yaw for realistic flight - characterRenderer->setInstanceRotation(mountInstanceId_, glm::vec3(mountPitch_, mountRoll_, mountYawRad)); - - // Drive mount model animation: idle when still, run when moving - auto pickMountAnim = [&](std::initializer_list candidates, uint32_t fallback) -> uint32_t { - for (uint32_t id : candidates) { - if (characterRenderer->hasAnimation(mountInstanceId_, id)) { - return id; - } - } - return fallback; - }; - - uint32_t mountAnimId = ANIM_STAND; - - // Get current mount animation state (used throughout) - uint32_t curMountAnim = 0; - float curMountTime = 0, curMountDur = 0; - bool haveMountState = characterRenderer->getAnimationState(mountInstanceId_, curMountAnim, curMountTime, curMountDur); - - // Taxi flight: use flying animations instead of ground movement - if (taxiFlight_) { - // Log available animations once when taxi starts - if (!taxiAnimsLogged_) { - taxiAnimsLogged_ = true; - LOG_INFO("Taxi flight active: mountInstanceId_=", mountInstanceId_, - " curMountAnim=", curMountAnim, " haveMountState=", haveMountState); - std::vector seqs; - if (characterRenderer->getAnimationSequences(mountInstanceId_, seqs)) { - std::string animList; - for (const auto& s : seqs) { - if (!animList.empty()) animList += ", "; - animList += std::to_string(s.id); - } - LOG_INFO("Taxi mount available animations: [", animList, "]"); - } - } - - // Try multiple flying animation IDs in priority order: - // 159=FlyForward, 158=FlyIdle (WotLK flying mounts) - // 234=FlyRun, 229=FlyStand (Vanilla creature fly anims) - // 233=FlyWalk, 141=FlyMounted, 369=FlyRun (alternate IDs) - // 6=Fly (classic creature fly) - // Fallback: Run, then Stand (hover) - uint32_t flyAnims[] = {ANIM_FLY_FORWARD, ANIM_FLY_IDLE, 234, 229, 233, 141, 369, 6, ANIM_RUN}; - mountAnimId = ANIM_STAND; // ultimate fallback: hover/idle - for (uint32_t fa : flyAnims) { - if (characterRenderer->hasAnimation(mountInstanceId_, fa)) { - mountAnimId = fa; - break; - } - } - - if (!haveMountState || curMountAnim != mountAnimId) { - LOG_INFO("Taxi mount: playing animation ", mountAnimId); - characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); - } - - // Skip all ground mount logic (jumps, fidgets, etc.) - goto taxi_mount_done; - } else { - taxiAnimsLogged_ = false; - } - - // Check for jump trigger - use cached per-mount animation IDs - if (cameraController->isJumpKeyPressed() && grounded && mountAction_ == MountAction::None) { - if (moving && mountAnims_.jumpLoop > 0) { - // Moving: skip JumpStart (looks like stopping), go straight to airborne loop - LOG_DEBUG("Mount jump triggered while moving: using jumpLoop anim ", mountAnims_.jumpLoop); - characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpLoop, true); - mountAction_ = MountAction::Jump; - mountActionPhase_ = 1; // Start in airborne phase - mountAnimId = mountAnims_.jumpLoop; - if (getMountSoundManager()) { - getMountSoundManager()->playJumpSound(); - } - if (cameraController) { - cameraController->triggerMountJump(); - } - } else if (!moving && mountAnims_.rearUp > 0) { - // Standing still: rear-up flourish - LOG_DEBUG("Mount rear-up triggered: playing rearUp anim ", mountAnims_.rearUp); - characterRenderer->playAnimation(mountInstanceId_, mountAnims_.rearUp, false); - mountAction_ = MountAction::RearUp; - mountActionPhase_ = 0; - mountAnimId = mountAnims_.rearUp; - // Trigger semantic rear-up sound - if (getMountSoundManager()) { - getMountSoundManager()->playRearUpSound(); - } - } - } - - // Handle active mount actions (jump chaining or rear-up) - if (mountAction_ != MountAction::None) { - bool animFinished = haveMountState && curMountDur > 0.1f && - (curMountTime >= curMountDur - 0.05f); - - if (mountAction_ == MountAction::Jump) { - // Jump sequence: start → loop → end (physics-driven) - if (mountActionPhase_ == 0 && animFinished && mountAnims_.jumpLoop > 0) { - // JumpStart finished, go to JumpLoop (airborne) - LOG_DEBUG("Mount jump: phase 0→1 (JumpStart→JumpLoop anim ", mountAnims_.jumpLoop, ")"); - characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpLoop, true); - mountActionPhase_ = 1; - mountAnimId = mountAnims_.jumpLoop; - } else if (mountActionPhase_ == 0 && animFinished && mountAnims_.jumpLoop == 0) { - // No JumpLoop, go straight to airborne phase 1 (hold JumpStart pose) - LOG_DEBUG("Mount jump: phase 0→1 (no JumpLoop, holding JumpStart)"); - mountActionPhase_ = 1; - } else if (mountActionPhase_ == 1 && grounded && mountAnims_.jumpEnd > 0) { - // Landed after airborne phase! Go to JumpEnd (grounded-triggered) - LOG_DEBUG("Mount jump: phase 1→2 (landed, JumpEnd anim ", mountAnims_.jumpEnd, ")"); - characterRenderer->playAnimation(mountInstanceId_, mountAnims_.jumpEnd, false); - mountActionPhase_ = 2; - mountAnimId = mountAnims_.jumpEnd; - // Trigger semantic landing sound - if (getMountSoundManager()) { - getMountSoundManager()->playLandSound(); - } - } else if (mountActionPhase_ == 1 && grounded && mountAnims_.jumpEnd == 0) { - // No JumpEnd animation, return directly to movement after landing - LOG_DEBUG("Mount jump: phase 1→done (landed, no JumpEnd, returning to ", - moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand), ")"); - mountAction_ = MountAction::None; - mountAnimId = moving ? mountAnims_.run : mountAnims_.stand; - characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); - } else if (mountActionPhase_ == 2 && animFinished) { - // JumpEnd finished, return to movement - LOG_DEBUG("Mount jump: phase 2→done (JumpEnd finished, returning to ", - moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand), ")"); - mountAction_ = MountAction::None; - mountAnimId = moving ? mountAnims_.run : mountAnims_.stand; - characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); - } else { - mountAnimId = curMountAnim; // Keep current jump animation - } - } else if (mountAction_ == MountAction::RearUp) { - // Rear-up: single animation, return to stand when done - if (animFinished) { - LOG_DEBUG("Mount rear-up: finished, returning to ", - moving ? "run" : "stand", " anim ", (moving ? mountAnims_.run : mountAnims_.stand)); - mountAction_ = MountAction::None; - mountAnimId = moving ? mountAnims_.run : mountAnims_.stand; - characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); - } else { - mountAnimId = curMountAnim; // Keep current rear-up animation - } - } - } else if (moving) { - // Normal movement animations - if (anyStrafeLeft) { - mountAnimId = pickMountAnim({ANIM_STRAFE_RUN_LEFT, ANIM_STRAFE_WALK_LEFT, ANIM_RUN}, ANIM_RUN); - } else if (anyStrafeRight) { - mountAnimId = pickMountAnim({ANIM_STRAFE_RUN_RIGHT, ANIM_STRAFE_WALK_RIGHT, ANIM_RUN}, ANIM_RUN); - } else if (movingBackward) { - mountAnimId = pickMountAnim({ANIM_BACKPEDAL}, ANIM_RUN); - } else { - mountAnimId = ANIM_RUN; - } - } - - // Cancel active fidget immediately if movement starts - if (moving && mountActiveFidget_ != 0) { - mountActiveFidget_ = 0; - // Force play run animation to stop fidget immediately - characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); - } - - // Check if active fidget has completed (only when not moving) - if (!moving && mountActiveFidget_ != 0) { - uint32_t curAnim = 0; - float curTime = 0.0f, curDur = 0.0f; - if (characterRenderer->getAnimationState(mountInstanceId_, curAnim, curTime, curDur)) { - // If animation changed or completed, clear active fidget - if (curAnim != mountActiveFidget_ || curTime >= curDur * 0.95f) { - mountActiveFidget_ = 0; - LOG_DEBUG("Mount fidget completed"); - } - } - } - - // Idle fidgets: random one-shot animations when standing still - if (!moving && mountAction_ == MountAction::None && mountActiveFidget_ == 0 && !mountAnims_.fidgets.empty()) { - mountIdleFidgetTimer_ += lastDeltaTime_; - // Use the seeded mt19937 for timing so fidgets aren't deterministic - // across launches (rand() without srand() always starts from seed 1). - static std::mt19937 idleRng(std::random_device{}()); - static float nextFidgetTime = std::uniform_real_distribution(6.0f, 12.0f)(idleRng); - - if (mountIdleFidgetTimer_ >= nextFidgetTime) { - std::uniform_int_distribution dist(0, mountAnims_.fidgets.size() - 1); - uint32_t fidgetAnim = mountAnims_.fidgets[dist(idleRng)]; - - characterRenderer->playAnimation(mountInstanceId_, fidgetAnim, false); - mountActiveFidget_ = fidgetAnim; - mountIdleFidgetTimer_ = 0.0f; - nextFidgetTime = std::uniform_real_distribution(6.0f, 12.0f)(idleRng); - - LOG_DEBUG("Mount idle fidget: playing anim ", fidgetAnim); - } - } - if (moving) { - mountIdleFidgetTimer_ = 0.0f; // Reset timer when moving - } - - // Idle ambient sounds: snorts and whinnies only, infrequent - if (!moving && getMountSoundManager()) { - mountIdleSoundTimer_ += lastDeltaTime_; - static std::mt19937 soundRng(std::random_device{}()); - static float nextIdleSoundTime = std::uniform_real_distribution(45.0f, 90.0f)(soundRng); - - if (mountIdleSoundTimer_ >= nextIdleSoundTime) { - getMountSoundManager()->playIdleSound(); - mountIdleSoundTimer_ = 0.0f; - nextIdleSoundTime = std::uniform_real_distribution(45.0f, 90.0f)(soundRng); - } - } else if (moving) { - mountIdleSoundTimer_ = 0.0f; // Reset timer when moving - } - - // Only update animation if it changed and we're not in an action sequence or playing a fidget - if (mountAction_ == MountAction::None && mountActiveFidget_ == 0 && (!haveMountState || curMountAnim != mountAnimId)) { - bool loop = true; // Normal movement animations loop - characterRenderer->playAnimation(mountInstanceId_, mountAnimId, loop); - } - - taxi_mount_done: - // Rider bob: sinusoidal motion synced to mount's run animation (only used in fallback positioning) - mountBob = 0.0f; - if (moving && haveMountState && curMountDur > 1.0f) { - // Wrap mount time preserving precision via subtraction instead of fmod - float wrappedTime = curMountTime; - while (wrappedTime >= curMountDur) { - wrappedTime -= curMountDur; - } - float norm = wrappedTime / curMountDur; - // One bounce per stride cycle - float bobSpeed = taxiFlight_ ? 2.0f : 1.0f; - mountBob = std::sin(norm * 2.0f * 3.14159f * bobSpeed) * 0.12f; - } - } - - // Use mount's attachment point for proper bone-driven rider positioning. - if (taxiFlight_) { - glm::mat4 mountSeatTransform(1.0f); - bool haveSeat = false; - static constexpr uint32_t kTaxiSeatAttachmentId = 0; // deterministic rider seat - if (mountSeatAttachmentId_ == -1) { - mountSeatAttachmentId_ = static_cast(kTaxiSeatAttachmentId); - } - if (mountSeatAttachmentId_ >= 0) { - haveSeat = characterRenderer->getAttachmentTransform( - mountInstanceId_, static_cast(mountSeatAttachmentId_), mountSeatTransform); - } - if (!haveSeat) { - mountSeatAttachmentId_ = -2; - } - - if (haveSeat) { - glm::vec3 targetRiderPos = glm::vec3(mountSeatTransform[3]) + glm::vec3(0.0f, 0.0f, 0.02f); - // Taxi passengers should be rigidly parented to mount attachment transforms. - // Smoothing here introduces visible seat lag/drift on turns. - mountSeatSmoothingInit_ = false; - smoothedMountSeatPos_ = targetRiderPos; - characterRenderer->setInstancePosition(characterInstanceId, targetRiderPos); - } else { - mountSeatSmoothingInit_ = false; - glm::vec3 playerPos = characterPosition + glm::vec3(0.0f, 0.0f, mountHeightOffset_ + 0.10f); - characterRenderer->setInstancePosition(characterInstanceId, playerPos); - } - - float riderPitch = mountPitch_ * 0.35f; - float riderRoll = mountRoll_ * 0.35f; - characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(riderPitch, riderRoll, mountYawRad)); - return; - } - - // Ground mounts: try a seat attachment first. - glm::mat4 mountSeatTransform; - bool haveSeat = false; - if (mountSeatAttachmentId_ >= 0) { - haveSeat = characterRenderer->getAttachmentTransform( - mountInstanceId_, static_cast(mountSeatAttachmentId_), mountSeatTransform); - } else if (mountSeatAttachmentId_ == -1) { - // Probe common rider seat attachment IDs once per mount. - static constexpr uint32_t kSeatAttachments[] = {0, 5, 6, 7, 8}; - for (uint32_t attId : kSeatAttachments) { - if (characterRenderer->getAttachmentTransform(mountInstanceId_, attId, mountSeatTransform)) { - mountSeatAttachmentId_ = static_cast(attId); - haveSeat = true; - break; - } - } - if (!haveSeat) { - mountSeatAttachmentId_ = -2; - } - } - - if (haveSeat) { - // Extract position from mount seat transform (attachment point already includes proper seat height) - glm::vec3 mountSeatPos = glm::vec3(mountSeatTransform[3]); - - // Keep seat offset minimal; large offsets amplify visible bobble. - glm::vec3 seatOffset = glm::vec3(0.0f, 0.0f, taxiFlight_ ? 0.04f : 0.08f); - glm::vec3 targetRiderPos = mountSeatPos + seatOffset; - // When moving, smoothing the seat position produces visible lag that looks like - // the rider sliding toward the rump. Anchor rigidly while moving. - if (moving) { - mountSeatSmoothingInit_ = false; - smoothedMountSeatPos_ = targetRiderPos; - } else if (!mountSeatSmoothingInit_) { - smoothedMountSeatPos_ = targetRiderPos; - mountSeatSmoothingInit_ = true; - } else { - float smoothHz = taxiFlight_ ? 10.0f : 14.0f; - float alpha = 1.0f - std::exp(-smoothHz * std::max(lastDeltaTime_, 0.001f)); - smoothedMountSeatPos_ = glm::mix(smoothedMountSeatPos_, targetRiderPos, alpha); - } - - // Position rider at mount seat - characterRenderer->setInstancePosition(characterInstanceId, smoothedMountSeatPos_); - - // Rider uses character facing yaw, not mount bone rotation - // (rider faces character direction, seat bone only provides position) - float yawRad = glm::radians(characterYaw); - float riderPitch = mountPitch_ * 0.35f; - float riderRoll = mountRoll_ * 0.35f; - characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(riderPitch, riderRoll, yawRad)); - } else { - // Fallback to old manual positioning if attachment not found - mountSeatSmoothingInit_ = false; - float yawRad = glm::radians(characterYaw); - glm::mat4 mountRotation = glm::mat4(1.0f); - mountRotation = glm::rotate(mountRotation, yawRad, glm::vec3(0.0f, 0.0f, 1.0f)); - mountRotation = glm::rotate(mountRotation, mountRoll_, glm::vec3(1.0f, 0.0f, 0.0f)); - mountRotation = glm::rotate(mountRotation, mountPitch_, glm::vec3(0.0f, 1.0f, 0.0f)); - glm::vec3 localOffset(0.0f, 0.0f, mountHeightOffset_ + mountBob); - glm::vec3 worldOffset = glm::vec3(mountRotation * glm::vec4(localOffset, 0.0f)); - glm::vec3 playerPos = characterPosition + worldOffset; - characterRenderer->setInstancePosition(characterInstanceId, playerPos); - characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(mountPitch_, mountRoll_, yawRad)); - } - return; - } - - if (!forceMelee) switch (charAnimState) { - case CharAnimState::IDLE: - if (swim) { - newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; - } else if (sitting && grounded) { - newState = CharAnimState::SIT_DOWN; - } else if (!grounded && jumping) { - newState = CharAnimState::JUMP_START; - } else if (!grounded) { - newState = CharAnimState::JUMP_MID; - } else if (moving && sprinting) { - newState = CharAnimState::RUN; - } else if (moving) { - newState = CharAnimState::WALK; - } else if (inCombat_ && grounded) { - newState = CharAnimState::COMBAT_IDLE; - } - break; - - case CharAnimState::WALK: - if (swim) { - newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; - } else if (!grounded && jumping) { - newState = CharAnimState::JUMP_START; - } else if (!grounded) { - newState = CharAnimState::JUMP_MID; - } else if (!moving) { - newState = CharAnimState::IDLE; - } else if (sprinting) { - newState = CharAnimState::RUN; - } - break; - - case CharAnimState::RUN: - if (swim) { - newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; - } else if (!grounded && jumping) { - newState = CharAnimState::JUMP_START; - } else if (!grounded) { - newState = CharAnimState::JUMP_MID; - } else if (!moving) { - newState = CharAnimState::IDLE; - } else if (!sprinting) { - newState = CharAnimState::WALK; - } - break; - - case CharAnimState::JUMP_START: - if (swim) { - newState = CharAnimState::SWIM_IDLE; - } else if (grounded) { - newState = CharAnimState::JUMP_END; - } else { - newState = CharAnimState::JUMP_MID; - } - break; - - case CharAnimState::JUMP_MID: - if (swim) { - newState = CharAnimState::SWIM_IDLE; - } else if (grounded) { - newState = CharAnimState::JUMP_END; - } - break; - - case CharAnimState::JUMP_END: - if (swim) { - newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; - } else if (moving && sprinting) { - newState = CharAnimState::RUN; - } else if (moving) { - newState = CharAnimState::WALK; - } else { - newState = CharAnimState::IDLE; - } - break; - - case CharAnimState::SIT_DOWN: - if (swim) { - newState = CharAnimState::SWIM_IDLE; - } else if (!sitting) { - newState = CharAnimState::IDLE; - } - break; - - case CharAnimState::SITTING: - if (swim) { - newState = CharAnimState::SWIM_IDLE; - } else if (!sitting) { - newState = CharAnimState::IDLE; - } - break; - - case CharAnimState::EMOTE: - if (swim) { - cancelEmote(); - newState = CharAnimState::SWIM_IDLE; - } else if (jumping || !grounded) { - cancelEmote(); - newState = CharAnimState::JUMP_START; - } else if (moving) { - cancelEmote(); - newState = sprinting ? CharAnimState::RUN : CharAnimState::WALK; - } else if (sitting) { - cancelEmote(); - newState = CharAnimState::SIT_DOWN; - } else if (!emoteLoop && characterRenderer && characterInstanceId > 0) { - // Auto-cancel non-looping emotes once animation completes - uint32_t curId = 0; float curT = 0.0f, curDur = 0.0f; - if (characterRenderer->getAnimationState(characterInstanceId, curId, curT, curDur) - && curDur > 0.1f && curT >= curDur - 0.05f) { - cancelEmote(); - newState = CharAnimState::IDLE; - } - } - break; - - case CharAnimState::SWIM_IDLE: - if (!swim) { - newState = moving ? CharAnimState::WALK : CharAnimState::IDLE; - } else if (moving) { - newState = CharAnimState::SWIM; - } - break; - - case CharAnimState::SWIM: - if (!swim) { - newState = moving ? CharAnimState::WALK : CharAnimState::IDLE; - } else if (!moving) { - newState = CharAnimState::SWIM_IDLE; - } - break; - - case CharAnimState::MELEE_SWING: - if (swim) { - newState = CharAnimState::SWIM_IDLE; - } else if (!grounded && jumping) { - newState = CharAnimState::JUMP_START; - } else if (!grounded) { - newState = CharAnimState::JUMP_MID; - } else if (moving && sprinting) { - newState = CharAnimState::RUN; - } else if (moving) { - newState = CharAnimState::WALK; - } else if (sitting) { - newState = CharAnimState::SIT_DOWN; - } else if (inCombat_) { - newState = CharAnimState::COMBAT_IDLE; - } else { - newState = CharAnimState::IDLE; - } - break; - - case CharAnimState::MOUNT: - // If we got here, the mount state was cleared externally but the - // animation state hasn't been reset yet. Fall back to normal logic. - if (swim) { - newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; - } else if (sitting && grounded) { - newState = CharAnimState::SIT_DOWN; - } else if (!grounded && jumping) { - newState = CharAnimState::JUMP_START; - } else if (!grounded) { - newState = CharAnimState::JUMP_MID; - } else if (moving && sprinting) { - newState = CharAnimState::RUN; - } else if (moving) { - newState = CharAnimState::WALK; - } else { - newState = CharAnimState::IDLE; - } - break; - - case CharAnimState::COMBAT_IDLE: - if (swim) { - newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE; - } else if (!grounded && jumping) { - newState = CharAnimState::JUMP_START; - } else if (!grounded) { - newState = CharAnimState::JUMP_MID; - } else if (moving && sprinting) { - newState = CharAnimState::RUN; - } else if (moving) { - newState = CharAnimState::WALK; - } else if (!inCombat_) { - newState = CharAnimState::IDLE; - } - break; - - case CharAnimState::CHARGE: - // Stay in CHARGE until charging_ is cleared - break; - } - - if (forceMelee) { - newState = CharAnimState::MELEE_SWING; - } - - if (charging_) { - newState = CharAnimState::CHARGE; - } - - if (newState != charAnimState) { - charAnimState = newState; - } - - auto pickFirstAvailable = [&](std::initializer_list candidates, uint32_t fallback) -> uint32_t { - for (uint32_t id : candidates) { - if (characterRenderer->hasAnimation(characterInstanceId, id)) { - return id; - } - } - return fallback; - }; - - uint32_t animId = ANIM_STAND; - bool loop = true; - - switch (charAnimState) { - case CharAnimState::IDLE: animId = ANIM_STAND; loop = true; break; - case CharAnimState::WALK: - if (movingBackward) { - animId = pickFirstAvailable({ANIM_BACKPEDAL}, ANIM_WALK); - } else if (anyStrafeLeft) { - animId = pickFirstAvailable({ANIM_STRAFE_WALK_LEFT, ANIM_STRAFE_RUN_LEFT}, ANIM_WALK); - } else if (anyStrafeRight) { - animId = pickFirstAvailable({ANIM_STRAFE_WALK_RIGHT, ANIM_STRAFE_RUN_RIGHT}, ANIM_WALK); - } else { - animId = pickFirstAvailable({ANIM_WALK, ANIM_RUN}, ANIM_STAND); - } - loop = true; - break; - case CharAnimState::RUN: - if (movingBackward) { - animId = pickFirstAvailable({ANIM_BACKPEDAL}, ANIM_WALK); - } else if (anyStrafeLeft) { - animId = pickFirstAvailable({ANIM_STRAFE_RUN_LEFT}, ANIM_RUN); - } else if (anyStrafeRight) { - animId = pickFirstAvailable({ANIM_STRAFE_RUN_RIGHT}, ANIM_RUN); - } else { - animId = pickFirstAvailable({ANIM_RUN, ANIM_WALK}, ANIM_STAND); - } - loop = true; - break; - case CharAnimState::JUMP_START: animId = ANIM_JUMP_START; loop = false; break; - case CharAnimState::JUMP_MID: animId = ANIM_JUMP_MID; loop = false; break; - case CharAnimState::JUMP_END: animId = ANIM_JUMP_END; loop = false; break; - case CharAnimState::SIT_DOWN: animId = ANIM_SIT_DOWN; loop = false; break; - case CharAnimState::SITTING: animId = ANIM_SITTING; loop = true; break; - case CharAnimState::EMOTE: animId = emoteAnimId; loop = emoteLoop; break; - case CharAnimState::SWIM_IDLE: animId = ANIM_SWIM_IDLE; loop = true; break; - case CharAnimState::SWIM: animId = ANIM_SWIM; loop = true; break; - case CharAnimState::MELEE_SWING: - animId = resolveMeleeAnimId(); - if (animId == 0) { - animId = ANIM_STAND; - } - loop = false; - break; - case CharAnimState::MOUNT: animId = ANIM_MOUNT; loop = true; break; - case CharAnimState::COMBAT_IDLE: - animId = pickFirstAvailable( - {ANIM_READY_1H, ANIM_READY_2H, ANIM_READY_2H_L, ANIM_READY_UNARMED}, - ANIM_STAND); - loop = true; - break; - case CharAnimState::CHARGE: - animId = ANIM_RUN; - loop = true; - break; - } - - uint32_t currentAnimId = 0; - float currentAnimTimeMs = 0.0f; - float currentAnimDurationMs = 0.0f; - bool haveState = characterRenderer->getAnimationState(characterInstanceId, currentAnimId, currentAnimTimeMs, currentAnimDurationMs); - // Some frames may transiently fail getAnimationState() while resources/instance state churn. - // Avoid reissuing the same clip on those frames, which restarts locomotion and causes hitches. - const bool requestChanged = (lastPlayerAnimRequest_ != animId) || (lastPlayerAnimLoopRequest_ != loop); - const bool shouldPlay = (haveState && currentAnimId != animId) || (!haveState && requestChanged); - if (shouldPlay) { - characterRenderer->playAnimation(characterInstanceId, animId, loop); - lastPlayerAnimRequest_ = animId; - lastPlayerAnimLoopRequest_ = loop; - } -} void Renderer::playEmote(const std::string& emoteName) { - loadEmotesFromDbc(); - auto it = EMOTE_TABLE.find(emoteName); - if (it == EMOTE_TABLE.end()) return; - - const auto& info = it->second; - if (info.animId == 0) return; - emoteActive = true; - emoteAnimId = info.animId; - emoteLoop = info.loop; - charAnimState = CharAnimState::EMOTE; - - if (characterRenderer && characterInstanceId > 0) { - characterRenderer->playAnimation(characterInstanceId, emoteAnimId, emoteLoop); - } + if (animationController_) animationController_->playEmote(emoteName); } void Renderer::cancelEmote() { - emoteActive = false; - emoteAnimId = 0; - emoteLoop = false; + if (animationController_) animationController_->cancelEmote(); +} + +bool Renderer::isEmoteActive() const { + return animationController_ && animationController_->isEmoteActive(); +} + +void Renderer::setInCombat(bool combat) { + if (animationController_) animationController_->setInCombat(combat); +} + +void Renderer::setEquippedWeaponType(uint32_t inventoryType) { + if (animationController_) animationController_->setEquippedWeaponType(inventoryType); +} + +void Renderer::setCharging(bool c) { + if (animationController_) animationController_->setCharging(c); +} + +bool Renderer::isCharging() const { + return animationController_ && animationController_->isCharging(); +} + +void Renderer::setTaxiFlight(bool taxi) { + if (animationController_) animationController_->setTaxiFlight(taxi); +} + +void Renderer::setMountPitchRoll(float pitch, float roll) { + if (animationController_) animationController_->setMountPitchRoll(pitch, roll); +} + +bool Renderer::isMounted() const { + return animationController_ && animationController_->isMounted(); } bool Renderer::captureScreenshot(const std::string& outputPath) { @@ -2374,56 +1133,19 @@ bool Renderer::captureScreenshot(const std::string& outputPath) { } void Renderer::triggerLevelUpEffect(const glm::vec3& position) { - if (!levelUpEffect) return; - - // Lazy-load the M2 model on first trigger - if (!levelUpEffect->isModelLoaded() && m2Renderer) { - if (!cachedAssetManager) { - cachedAssetManager = core::Application::getInstance().getAssetManager(); - } - if (!cachedAssetManager) { - LOG_WARNING("LevelUpEffect: no asset manager available"); - } else { - auto m2Data = cachedAssetManager->readFile("Spells\\LevelUp\\LevelUp.m2"); - auto skinData = cachedAssetManager->readFile("Spells\\LevelUp\\LevelUp00.skin"); - LOG_INFO("LevelUpEffect: m2Data=", m2Data.size(), " skinData=", skinData.size()); - if (!m2Data.empty()) { - levelUpEffect->loadModel(m2Renderer.get(), m2Data, skinData); - } else { - LOG_WARNING("LevelUpEffect: failed to read Spell\\LevelUp\\LevelUp.m2"); - } - } - } - - levelUpEffect->trigger(position); + if (animationController_) animationController_->triggerLevelUpEffect(position); } void Renderer::startChargeEffect(const glm::vec3& position, const glm::vec3& direction) { - if (!chargeEffect) return; - - // Lazy-load M2 models on first use - if (!chargeEffect->isActive() && m2Renderer) { - if (!cachedAssetManager) { - cachedAssetManager = core::Application::getInstance().getAssetManager(); - } - if (cachedAssetManager) { - chargeEffect->tryLoadM2Models(m2Renderer.get(), cachedAssetManager); - } - } - - chargeEffect->start(position, direction); + if (animationController_) animationController_->startChargeEffect(position, direction); } void Renderer::emitChargeEffect(const glm::vec3& position, const glm::vec3& direction) { - if (chargeEffect) { - chargeEffect->emit(position, direction); - } + if (animationController_) animationController_->emitChargeEffect(position, direction); } void Renderer::stopChargeEffect() { - if (chargeEffect) { - chargeEffect->stop(); - } + if (animationController_) animationController_->stopChargeEffect(); } // ─── Spell Visual Effects — delegated to SpellVisualSystem (§4.4) ──────────── @@ -2434,102 +1156,32 @@ void Renderer::playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition } void Renderer::triggerMeleeSwing() { - if (!characterRenderer || characterInstanceId == 0) return; - if (meleeSwingCooldown > 0.0f) return; - if (emoteActive) { - cancelEmote(); - } - resolveMeleeAnimId(); - meleeSwingCooldown = 0.1f; - float durationSec = meleeAnimDurationMs > 0.0f ? meleeAnimDurationMs / 1000.0f : 0.6f; - if (durationSec < 0.25f) durationSec = 0.25f; - if (durationSec > 1.0f) durationSec = 1.0f; - meleeSwingTimer = durationSec; - if (getActivitySoundManager()) { - getActivitySoundManager()->playMeleeSwing(); - } + if (animationController_) animationController_->triggerMeleeSwing(); } std::string Renderer::getEmoteText(const std::string& emoteName, const std::string* targetName) { - loadEmotesFromDbc(); - auto it = EMOTE_TABLE.find(emoteName); - if (it != EMOTE_TABLE.end()) { - const auto& info = it->second; - const std::string& base = (targetName ? info.textTarget : info.textNoTarget); - if (!base.empty()) { - return replacePlaceholders(base, targetName); - } - if (targetName && !targetName->empty()) { - return "You " + info.command + " at " + *targetName + "."; - } - return "You " + info.command + "."; - } - return ""; + return AnimationController::getEmoteText(emoteName, targetName); } uint32_t Renderer::getEmoteDbcId(const std::string& emoteName) { - loadEmotesFromDbc(); - auto it = EMOTE_TABLE.find(emoteName); - if (it != EMOTE_TABLE.end()) { - return it->second.dbcId; - } - return 0; + return AnimationController::getEmoteDbcId(emoteName); } std::string Renderer::getEmoteTextByDbcId(uint32_t dbcId, const std::string& senderName, const std::string* targetName) { - loadEmotesFromDbc(); - auto it = EMOTE_BY_DBCID.find(dbcId); - if (it == EMOTE_BY_DBCID.end()) return ""; - - const EmoteInfo& info = *it->second; - - // Use "others see" text templates: "%s dances." / "%s dances with %s." - if (targetName && !targetName->empty()) { - if (!info.othersTarget.empty()) { - // Replace first %s with sender, second %s with target - std::string out; - out.reserve(info.othersTarget.size() + senderName.size() + targetName->size()); - bool firstReplaced = false; - for (size_t i = 0; i < info.othersTarget.size(); ++i) { - if (info.othersTarget[i] == '%' && i + 1 < info.othersTarget.size() && info.othersTarget[i + 1] == 's') { - out += firstReplaced ? *targetName : senderName; - firstReplaced = true; - ++i; - } else { - out.push_back(info.othersTarget[i]); - } - } - return out; - } - return senderName + " " + info.command + "s at " + *targetName + "."; - } else { - if (!info.othersNoTarget.empty()) { - return replacePlaceholders(info.othersNoTarget, &senderName); - } - return senderName + " " + info.command + "s."; - } + return AnimationController::getEmoteTextByDbcId(dbcId, senderName, targetName); } uint32_t Renderer::getEmoteAnimByDbcId(uint32_t dbcId) { - loadEmotesFromDbc(); - auto it = EMOTE_BY_DBCID.find(dbcId); - if (it != EMOTE_BY_DBCID.end()) { - return it->second->animId; - } - return 0; + return AnimationController::getEmoteAnimByDbcId(dbcId); } void Renderer::setTargetPosition(const glm::vec3* pos) { - targetPosition = pos; + if (animationController_) animationController_->setTargetPosition(pos); } void Renderer::resetCombatVisualState() { - inCombat_ = false; - targetPosition = nullptr; - meleeSwingTimer = 0.0f; - meleeSwingCooldown = 0.0f; - // Clear lingering spell visual instances from the previous map/combat session. + if (animationController_) animationController_->resetCombatVisualState(); if (spellVisualSystem_) spellVisualSystem_->reset(); } @@ -2537,110 +1189,6 @@ bool Renderer::isMoving() const { return cameraController && cameraController->isMoving(); } -bool Renderer::isFootstepAnimationState() const { - return charAnimState == CharAnimState::WALK || charAnimState == CharAnimState::RUN; -} - -bool Renderer::shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs) { - if (animationDurationMs <= 1.0f) { - footstepNormInitialized = false; - return false; - } - - // Wrap animation time preserving precision via subtraction instead of fmod - float wrappedTime = animationTimeMs; - while (wrappedTime >= animationDurationMs) { - wrappedTime -= animationDurationMs; - } - if (wrappedTime < 0.0f) wrappedTime += animationDurationMs; - float norm = wrappedTime / animationDurationMs; - - if (animationId != footstepLastAnimationId) { - footstepLastAnimationId = animationId; - footstepLastNormTime = norm; - footstepNormInitialized = true; - return false; - } - - if (!footstepNormInitialized) { - footstepNormInitialized = true; - footstepLastNormTime = norm; - return false; - } - - auto crossed = [&](float eventNorm) { - if (footstepLastNormTime <= norm) { - return footstepLastNormTime < eventNorm && eventNorm <= norm; - } - return footstepLastNormTime < eventNorm || eventNorm <= norm; - }; - - bool trigger = crossed(0.22f) || crossed(0.72f); - footstepLastNormTime = norm; - return trigger; -} - -audio::FootstepSurface Renderer::resolveFootstepSurface() const { - if (!cameraController || !cameraController->isThirdPerson()) { - return audio::FootstepSurface::STONE; - } - - const glm::vec3& p = characterPosition; - - // Cache footstep surface to avoid expensive queries every step - // Only update if moved >1.5 units or timer expired (0.5s) - float distSq = glm::dot(p - cachedFootstepPosition, p - cachedFootstepPosition); - if (distSq < 2.25f && cachedFootstepUpdateTimer < 0.5f) { - return cachedFootstepSurface; - } - - // Update cache - cachedFootstepPosition = p; - cachedFootstepUpdateTimer = 0.0f; - - if (cameraController->isSwimming()) { - cachedFootstepSurface = audio::FootstepSurface::WATER; - return audio::FootstepSurface::WATER; - } - - if (waterRenderer) { - auto waterH = waterRenderer->getWaterHeightAt(p.x, p.y); - if (waterH && p.z < (*waterH + 0.25f)) { - cachedFootstepSurface = audio::FootstepSurface::WATER; - return audio::FootstepSurface::WATER; - } - } - - if (wmoRenderer) { - auto wmoFloor = wmoRenderer->getFloorHeight(p.x, p.y, p.z + 1.5f); - auto terrainFloor = terrainManager ? terrainManager->getHeightAt(p.x, p.y) : std::nullopt; - if (wmoFloor && (!terrainFloor || *wmoFloor >= *terrainFloor - 0.1f)) { - cachedFootstepSurface = audio::FootstepSurface::STONE; - return audio::FootstepSurface::STONE; - } - } - - // Determine surface type (expensive - only done when cache needs update) - audio::FootstepSurface surface = audio::FootstepSurface::STONE; - - if (terrainManager) { - auto texture = terrainManager->getDominantTextureAt(p.x, p.y); - if (texture) { - std::string t = *texture; - for (char& c : t) c = static_cast(std::tolower(static_cast(c))); - if (t.find("snow") != std::string::npos || t.find("ice") != std::string::npos) surface = audio::FootstepSurface::SNOW; - else if (t.find("grass") != std::string::npos || t.find("moss") != std::string::npos || t.find("leaf") != std::string::npos) surface = audio::FootstepSurface::GRASS; - else if (t.find("sand") != std::string::npos || t.find("dirt") != std::string::npos || t.find("mud") != std::string::npos) surface = audio::FootstepSurface::DIRT; - else if (t.find("wood") != std::string::npos || t.find("timber") != std::string::npos) surface = audio::FootstepSurface::WOOD; - else if (t.find("metal") != std::string::npos || t.find("iron") != std::string::npos) surface = audio::FootstepSurface::METAL; - else if (t.find("stone") != std::string::npos || t.find("rock") != std::string::npos || t.find("cobble") != std::string::npos || t.find("brick") != std::string::npos) surface = audio::FootstepSurface::STONE; - } - } - - cachedFootstepSurface = surface; - return surface; -} - void Renderer::update(float deltaTime) { globalTime += deltaTime; if (musicSwitchCooldown_ > 0.0f) { @@ -2649,7 +1197,7 @@ void Renderer::update(float deltaTime) { runDeferredWorldInitStep(deltaTime); auto updateStart = std::chrono::steady_clock::now(); - lastDeltaTime_ = deltaTime; // Cache for use in updateCharacterAnimation() + lastDeltaTime_ = deltaTime; if (wmoRenderer) wmoRenderer->resetQueryStats(); if (m2Renderer) m2Renderer->resetQueryStats(); @@ -2675,7 +1223,7 @@ void Renderer::update(float deltaTime) { // Visibility hardening: ensure player instance cannot stay hidden after // taxi/camera transitions, but preserve first-person self-hide. if (characterRenderer && characterInstanceId > 0 && cameraController) { - if ((cameraController->isThirdPerson() && !cameraController->isFirstPersonView()) || taxiFlight_) { + if ((cameraController->isThirdPerson() && !cameraController->isFirstPersonView()) || (animationController_ && animationController_->isTaxiFlight())) { characterRenderer->setInstanceVisible(characterInstanceId, true); } } @@ -2720,25 +1268,17 @@ void Renderer::update(float deltaTime) { // Sync character model position/rotation and animation with follow target if (characterInstanceId > 0 && characterRenderer && cameraController) { - if (meleeSwingCooldown > 0.0f) { - meleeSwingCooldown = std::max(0.0f, meleeSwingCooldown - deltaTime); - } - if (meleeSwingTimer > 0.0f) { - meleeSwingTimer = std::max(0.0f, meleeSwingTimer - deltaTime); - } - characterRenderer->setInstancePosition(characterInstanceId, characterPosition); // Movement-facing comes from camera controller and is decoupled from LMB orbit. - // During taxi flights, orientation is controlled by the flight path (not player input) - if (taxiFlight_) { - // Taxi flight: use orientation from flight path + bool taxiFlight = animationController_ && animationController_->isTaxiFlight(); + if (taxiFlight) { characterYaw = cameraController->getFacingYaw(); } else if (cameraController->isMoving() || cameraController->isRightMouseHeld()) { characterYaw = cameraController->getFacingYaw(); - } else if (inCombat_ && targetPosition && !emoteActive && !isMounted()) { - // Face target when in combat and idle - glm::vec3 toTarget = *targetPosition - characterPosition; + } else if (animationController_ && animationController_->isInCombat() && + animationController_->getTargetPosition() && !animationController_->isEmoteActive() && !isMounted()) { + glm::vec3 toTarget = *animationController_->getTargetPosition() - characterPosition; if (toTarget.x * toTarget.x + toTarget.y * toTarget.y > 0.01f) { float targetYaw = glm::degrees(std::atan2(toTarget.y, toTarget.x)); float diff = targetYaw - characterYaw; @@ -2755,8 +1295,12 @@ void Renderer::update(float deltaTime) { float yawRad = glm::radians(characterYaw); characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(0.0f, 0.0f, yawRad)); - // Update animation based on movement state - updateCharacterAnimation(); + // Update animation based on movement state (delegated to AnimationController §4.2) + if (animationController_) { + animationController_->updateMeleeTimers(deltaTime); + animationController_->setDeltaTime(deltaTime); + animationController_->updateCharacterAnimation(); + } } // Update terrain streaming @@ -2795,7 +1339,7 @@ void Renderer::update(float deltaTime) { mountDust->update(deltaTime); // Spawn dust when mounted and moving on ground - if (isMounted() && camera && cameraController && !taxiFlight_) { + if (isMounted() && camera && cameraController && !(animationController_ && animationController_->isTaxiFlight())) { bool isMoving = cameraController->isMoving(); bool onGround = cameraController->isGrounded(); @@ -2807,7 +1351,8 @@ void Renderer::update(float deltaTime) { velocity.z = 0.0f; // Ignore vertical component // Spawn dust at mount's feet (slightly below character position) - glm::vec3 dustPos = characterPosition - glm::vec3(0.0f, 0.0f, mountHeightOffset_ * 0.8f); + float mho = animationController_ ? animationController_->getMountHeightOffset() : 0.0f; + glm::vec3 dustPos = characterPosition - glm::vec3(0.0f, 0.0f, mho * 0.8f); mountDust->spawnDust(dustPos, velocity, isMoving); } } @@ -2846,144 +1391,11 @@ void Renderer::update(float deltaTime) { // Update AudioEngine (cleanup finished sounds, etc.) audio::AudioEngine::instance().update(deltaTime); - // Footsteps: animation-event driven + surface query at event time. - if (getFootstepManager()) { - getFootstepManager()->update(deltaTime); - cachedFootstepUpdateTimer += deltaTime; // Update surface cache timer - bool canPlayFootsteps = characterRenderer && characterInstanceId > 0 && - cameraController && cameraController->isThirdPerson() && - cameraController->isGrounded() && !cameraController->isSwimming(); + // Footsteps: delegated to AnimationController (§4.2) + if (animationController_) animationController_->updateFootsteps(deltaTime); - if (canPlayFootsteps && isMounted() && mountInstanceId_ > 0 && !taxiFlight_) { - // Mount footsteps: use mount's animation for timing - uint32_t animId = 0; - float animTimeMs = 0.0f, animDurationMs = 0.0f; - if (characterRenderer->getAnimationState(mountInstanceId_, animId, animTimeMs, animDurationMs) && - animDurationMs > 1.0f && cameraController->isMoving()) { - // Wrap animation time preserving precision via subtraction instead of fmod - float wrappedTime = animTimeMs; - while (wrappedTime >= animDurationMs) { - wrappedTime -= animDurationMs; - } - if (wrappedTime < 0.0f) wrappedTime += animDurationMs; - float norm = wrappedTime / animDurationMs; - - if (animId != mountFootstepLastAnimId) { - mountFootstepLastAnimId = animId; - mountFootstepLastNormTime = norm; - mountFootstepNormInitialized = true; - } else if (!mountFootstepNormInitialized) { - mountFootstepNormInitialized = true; - mountFootstepLastNormTime = norm; - } else { - // Mount gait: 2 hoofbeats per cycle (synced with animation) - auto crossed = [&](float eventNorm) { - if (mountFootstepLastNormTime <= norm) { - return mountFootstepLastNormTime < eventNorm && eventNorm <= norm; - } - return mountFootstepLastNormTime < eventNorm || eventNorm <= norm; - }; - if (crossed(0.25f) || crossed(0.75f)) { - getFootstepManager()->playFootstep(resolveFootstepSurface(), true); - } - mountFootstepLastNormTime = norm; - } - } else { - mountFootstepNormInitialized = false; - } - footstepNormInitialized = false; // Reset player footstep tracking - } else if (canPlayFootsteps && isFootstepAnimationState()) { - uint32_t animId = 0; - float animTimeMs = 0.0f; - float animDurationMs = 0.0f; - if (characterRenderer->getAnimationState(characterInstanceId, animId, animTimeMs, animDurationMs) && - shouldTriggerFootstepEvent(animId, animTimeMs, animDurationMs)) { - auto surface = resolveFootstepSurface(); - getFootstepManager()->playFootstep(surface, cameraController->isSprinting()); - // Play additional splash sound and spawn foot splash particles when wading - if (surface == audio::FootstepSurface::WATER) { - if (getMovementSoundManager()) { - getMovementSoundManager()->playWaterFootstep(audio::MovementSoundManager::CharacterSize::MEDIUM); - } - if (swimEffects && waterRenderer) { - auto wh = waterRenderer->getWaterHeightAt(characterPosition.x, characterPosition.y); - if (wh) { - swimEffects->spawnFootSplash(characterPosition, *wh); - } - } - } - } - mountFootstepNormInitialized = false; - } else { - footstepNormInitialized = false; - mountFootstepNormInitialized = false; - } - } - - // Activity SFX: animation/state-driven jump, landing, and swim loops/splashes. - if (getActivitySoundManager()) { - getActivitySoundManager()->update(deltaTime); - if (cameraController && cameraController->isThirdPerson()) { - bool grounded = cameraController->isGrounded(); - bool jumping = cameraController->isJumping(); - bool falling = cameraController->isFalling(); - bool swimming = cameraController->isSwimming(); - bool moving = cameraController->isMoving(); - - if (!sfxStateInitialized) { - sfxPrevGrounded = grounded; - sfxPrevJumping = jumping; - sfxPrevFalling = falling; - sfxPrevSwimming = swimming; - sfxStateInitialized = true; - } - - if (jumping && !sfxPrevJumping && !swimming) { - getActivitySoundManager()->playJump(); - } - - if (grounded && !sfxPrevGrounded) { - bool hardLanding = sfxPrevFalling; - getActivitySoundManager()->playLanding(resolveFootstepSurface(), hardLanding); - } - - if (swimming && !sfxPrevSwimming) { - getActivitySoundManager()->playWaterEnter(); - } else if (!swimming && sfxPrevSwimming) { - getActivitySoundManager()->playWaterExit(); - } - - getActivitySoundManager()->setSwimmingState(swimming, moving); - - // Fade music underwater - if (getMusicManager()) { - getMusicManager()->setUnderwaterMode(swimming); - } - - sfxPrevGrounded = grounded; - sfxPrevJumping = jumping; - sfxPrevFalling = falling; - sfxPrevSwimming = swimming; - } else { - getActivitySoundManager()->setSwimmingState(false, false); - // Restore music volume when activity sounds disabled - if (getMusicManager()) { - getMusicManager()->setUnderwaterMode(false); - } - sfxStateInitialized = false; - } - } - - // Mount ambient sounds: wing flaps, breathing, etc. - if (getMountSoundManager()) { - getMountSoundManager()->update(deltaTime); - if (cameraController && isMounted()) { - bool moving = cameraController->isMoving(); - bool flying = taxiFlight_ || !cameraController->isGrounded(); // Flying if taxi or airborne - getMountSoundManager()->setMoving(moving); - getMountSoundManager()->setFlying(flying); - } - } + // Activity SFX + mount ambient sounds: delegated to AnimationController (§4.2) + if (animationController_) animationController_->updateSfxState(deltaTime); const bool canQueryWmo = (camera && wmoRenderer); const glm::vec3 camPos = camera ? camera->getPosition() : glm::vec3(0.0f); @@ -2993,7 +1405,7 @@ void Renderer::update(float deltaTime) { playerIndoors_ = insideWmo; // Ambient environmental sounds: fireplaces, water, birds, etc. - if (getAmbientSoundManager() && camera && wmoRenderer && cameraController) { + if (audioCoordinator_->getAmbientSoundManager() && camera && wmoRenderer && cameraController) { bool isIndoor = insideWmo; bool isSwimming = cameraController->isSwimming(); @@ -3027,10 +1439,10 @@ void Renderer::update(float deltaTime) { } } - getAmbientSoundManager()->setWeather(audioWeatherType); + audioCoordinator_->getAmbientSoundManager()->setWeather(audioWeatherType); } - getAmbientSoundManager()->update(deltaTime, camPos, isIndoor, isSwimming, isBlacksmith); + audioCoordinator_->getAmbientSoundManager()->update(deltaTime, camPos, isIndoor, isSwimming, isBlacksmith); } // Wait for M2 doodad animation to finish (was launched earlier in parallel with character anim) @@ -3043,14 +1455,14 @@ void Renderer::update(float deltaTime) { auto playZoneMusic = [&](const std::string& music) { if (music.empty()) return; if (music.rfind("file:", 0) == 0) { - getMusicManager()->crossfadeToFile(music.substr(5)); + audioCoordinator_->getMusicManager()->crossfadeToFile(music.substr(5)); } else { - getMusicManager()->crossfadeTo(music); + audioCoordinator_->getMusicManager()->crossfadeTo(music); } }; // Update zone detection and music - if (zoneManager && getMusicManager() && terrainManager && camera) { + if (zoneManager && audioCoordinator_->getMusicManager() && terrainManager && camera) { // Prefer server-authoritative zone ID (from SMSG_INIT_WORLD_STATES); // fall back to tile-based lookup for single-player / offline mode. const auto* gh = core::Application::getInstance().getGameHandler(); @@ -3115,7 +1527,7 @@ void Renderer::update(float deltaTime) { if (!inTavern_ && !tavernMusic.empty()) { inTavern_ = true; LOG_INFO("Entered tavern"); - getMusicManager()->playMusic(tavernMusic, true); // Immediate playback, looping + audioCoordinator_->getMusicManager()->playMusic(tavernMusic, true); // Immediate playback, looping musicSwitchCooldown_ = 6.0f; } } else if (inTavern_) { @@ -3137,7 +1549,7 @@ void Renderer::update(float deltaTime) { if (!inBlacksmith_) { inBlacksmith_ = true; LOG_INFO("Entered blacksmith - stopping music"); - getMusicManager()->stopMusic(); + audioCoordinator_->getMusicManager()->stopMusic(); } } else if (inBlacksmith_) { // Exited blacksmith - restore zone music with crossfade @@ -3169,15 +1581,15 @@ void Renderer::update(float deltaTime) { } } // Update ambient sound manager zone type - if (getAmbientSoundManager()) { - getAmbientSoundManager()->setZoneId(zoneId); + if (audioCoordinator_->getAmbientSoundManager()) { + audioCoordinator_->getAmbientSoundManager()->setZoneId(zoneId); } } - getMusicManager()->update(deltaTime); + audioCoordinator_->getMusicManager()->update(deltaTime); // When a track finishes, pick a new random track from the current zone - if (!getMusicManager()->isPlaying() && !inTavern_ && !inBlacksmith_ && + if (!audioCoordinator_->getMusicManager()->isPlaying() && !inTavern_ && !inBlacksmith_ && currentZoneId != 0 && musicSwitchCooldown_ <= 0.0f) { std::string music = zoneManager->getRandomMusic(currentZoneId); if (!music.empty()) { @@ -3218,24 +1630,24 @@ void Renderer::runDeferredWorldInitStep(float deltaTime) { switch (deferredWorldInitStage_) { case 0: - if (getAmbientSoundManager()) { - getAmbientSoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getAmbientSoundManager()) { + audioCoordinator_->getAmbientSoundManager()->initialize(cachedAssetManager); } - if (terrainManager && getAmbientSoundManager()) { - terrainManager->setAmbientSoundManager(getAmbientSoundManager()); + if (terrainManager && audioCoordinator_->getAmbientSoundManager()) { + terrainManager->setAmbientSoundManager(audioCoordinator_->getAmbientSoundManager()); } break; case 1: - if (getUiSoundManager()) getUiSoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getUiSoundManager()) audioCoordinator_->getUiSoundManager()->initialize(cachedAssetManager); break; case 2: - if (getCombatSoundManager()) getCombatSoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getCombatSoundManager()) audioCoordinator_->getCombatSoundManager()->initialize(cachedAssetManager); break; case 3: - if (getSpellSoundManager()) getSpellSoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getSpellSoundManager()) audioCoordinator_->getSpellSoundManager()->initialize(cachedAssetManager); break; case 4: - if (getMovementSoundManager()) getMovementSoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getMovementSoundManager()) audioCoordinator_->getMovementSoundManager()->initialize(cachedAssetManager); break; case 5: if (questMarkerRenderer) questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, cachedAssetManager); @@ -4011,6 +2423,12 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s } } + // Initialize AnimationController (§4.2) + if (!animationController_) { + animationController_ = std::make_unique(); + animationController_->initialize(this); + } + // Create and initialize terrain manager if (!terrainManager) { terrainManager = std::make_unique(); @@ -4032,8 +2450,8 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s terrainManager->setWMORenderer(wmoRenderer.get()); } // Set ambient sound manager for environmental audio emitters - if (getAmbientSoundManager()) { - terrainManager->setAmbientSoundManager(getAmbientSoundManager()); + if (audioCoordinator_->getAmbientSoundManager()) { + terrainManager->setAmbientSoundManager(audioCoordinator_->getAmbientSoundManager()); } // Pass asset manager to character renderer for texture loading if (characterRenderer) { @@ -4064,36 +2482,36 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s if (worldMap) worldMap->setMapName(mapName); // Initialize audio managers - if (getMusicManager() && assetManager && !cachedAssetManager) { + if (audioCoordinator_->getMusicManager() && assetManager && !cachedAssetManager) { audio::AudioEngine::instance().setAssetManager(assetManager); - getMusicManager()->initialize(assetManager); - if (getFootstepManager()) { - getFootstepManager()->initialize(assetManager); + audioCoordinator_->getMusicManager()->initialize(assetManager); + if (audioCoordinator_->getFootstepManager()) { + audioCoordinator_->getFootstepManager()->initialize(assetManager); } - if (getActivitySoundManager()) { - getActivitySoundManager()->initialize(assetManager); + if (audioCoordinator_->getActivitySoundManager()) { + audioCoordinator_->getActivitySoundManager()->initialize(assetManager); } - if (getMountSoundManager()) { - getMountSoundManager()->initialize(assetManager); + if (audioCoordinator_->getMountSoundManager()) { + audioCoordinator_->getMountSoundManager()->initialize(assetManager); } - if (getNpcVoiceManager()) { - getNpcVoiceManager()->initialize(assetManager); + if (audioCoordinator_->getNpcVoiceManager()) { + audioCoordinator_->getNpcVoiceManager()->initialize(assetManager); } if (!deferredWorldInitEnabled_) { - if (getAmbientSoundManager()) { - getAmbientSoundManager()->initialize(assetManager); + if (audioCoordinator_->getAmbientSoundManager()) { + audioCoordinator_->getAmbientSoundManager()->initialize(assetManager); } - if (getUiSoundManager()) { - getUiSoundManager()->initialize(assetManager); + if (audioCoordinator_->getUiSoundManager()) { + audioCoordinator_->getUiSoundManager()->initialize(assetManager); } - if (getCombatSoundManager()) { - getCombatSoundManager()->initialize(assetManager); + if (audioCoordinator_->getCombatSoundManager()) { + audioCoordinator_->getCombatSoundManager()->initialize(assetManager); } - if (getSpellSoundManager()) { - getSpellSoundManager()->initialize(assetManager); + if (audioCoordinator_->getSpellSoundManager()) { + audioCoordinator_->getSpellSoundManager()->initialize(assetManager); } - if (getMovementSoundManager()) { - getMovementSoundManager()->initialize(assetManager); + if (audioCoordinator_->getMovementSoundManager()) { + audioCoordinator_->getMovementSoundManager()->initialize(assetManager); } if (questMarkerRenderer) { questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, assetManager); @@ -4102,7 +2520,7 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s if (envFlagEnabled("WOWEE_PREWARM_ZONE_MUSIC", false)) { if (zoneManager) { for (const auto& musicPath : zoneManager->getAllMusicPaths()) { - getMusicManager()->preloadMusic(musicPath); + audioCoordinator_->getMusicManager()->preloadMusic(musicPath); } } static const std::vector tavernTracks = { @@ -4112,7 +2530,7 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s "Sound\\Music\\ZoneMusic\\TavernHuman\\RA_HumanTavern2A.mp3", }; for (const auto& musicPath : tavernTracks) { - getMusicManager()->preloadMusic(musicPath); + audioCoordinator_->getMusicManager()->preloadMusic(musicPath); } } } else { @@ -4251,42 +2669,42 @@ bool Renderer::loadTerrainArea(const std::string& mapName, int centerX, int cent } // Initialize music manager with asset manager - if (getMusicManager() && cachedAssetManager) { - if (!getMusicManager()->isInitialized()) { - getMusicManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getMusicManager() && cachedAssetManager) { + if (!audioCoordinator_->getMusicManager()->isInitialized()) { + audioCoordinator_->getMusicManager()->initialize(cachedAssetManager); } } - if (getFootstepManager() && cachedAssetManager) { - if (!getFootstepManager()->isInitialized()) { - getFootstepManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getFootstepManager() && cachedAssetManager) { + if (!audioCoordinator_->getFootstepManager()->isInitialized()) { + audioCoordinator_->getFootstepManager()->initialize(cachedAssetManager); } } - if (getActivitySoundManager() && cachedAssetManager) { - if (!getActivitySoundManager()->isInitialized()) { - getActivitySoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getActivitySoundManager() && cachedAssetManager) { + if (!audioCoordinator_->getActivitySoundManager()->isInitialized()) { + audioCoordinator_->getActivitySoundManager()->initialize(cachedAssetManager); } } - if (getMountSoundManager() && cachedAssetManager) { - getMountSoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getMountSoundManager() && cachedAssetManager) { + audioCoordinator_->getMountSoundManager()->initialize(cachedAssetManager); } - if (getNpcVoiceManager() && cachedAssetManager) { - getNpcVoiceManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getNpcVoiceManager() && cachedAssetManager) { + audioCoordinator_->getNpcVoiceManager()->initialize(cachedAssetManager); } if (!deferredWorldInitEnabled_) { - if (getAmbientSoundManager() && cachedAssetManager) { - getAmbientSoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getAmbientSoundManager() && cachedAssetManager) { + audioCoordinator_->getAmbientSoundManager()->initialize(cachedAssetManager); } - if (getUiSoundManager() && cachedAssetManager) { - getUiSoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getUiSoundManager() && cachedAssetManager) { + audioCoordinator_->getUiSoundManager()->initialize(cachedAssetManager); } - if (getCombatSoundManager() && cachedAssetManager) { - getCombatSoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getCombatSoundManager() && cachedAssetManager) { + audioCoordinator_->getCombatSoundManager()->initialize(cachedAssetManager); } - if (getSpellSoundManager() && cachedAssetManager) { - getSpellSoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getSpellSoundManager() && cachedAssetManager) { + audioCoordinator_->getSpellSoundManager()->initialize(cachedAssetManager); } - if (getMovementSoundManager() && cachedAssetManager) { - getMovementSoundManager()->initialize(cachedAssetManager); + if (audioCoordinator_->getMovementSoundManager() && cachedAssetManager) { + audioCoordinator_->getMovementSoundManager()->initialize(cachedAssetManager); } if (questMarkerRenderer && cachedAssetManager) { questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, cachedAssetManager); @@ -4298,8 +2716,8 @@ bool Renderer::loadTerrainArea(const std::string& mapName, int centerX, int cent } // Wire ambient sound manager to terrain manager for emitter registration - if (terrainManager && getAmbientSoundManager()) { - terrainManager->setAmbientSoundManager(getAmbientSoundManager()); + if (terrainManager && audioCoordinator_->getAmbientSoundManager()) { + terrainManager->setAmbientSoundManager(audioCoordinator_->getAmbientSoundManager()); } // Wire WMO, M2, and water renderer to camera controller diff --git a/src/ui/auth_screen.cpp b/src/ui/auth_screen.cpp index 88264930..e30be6e4 100644 --- a/src/ui/auth_screen.cpp +++ b/src/ui/auth_screen.cpp @@ -6,6 +6,7 @@ #include "rendering/renderer.hpp" #include "rendering/vk_context.hpp" #include "pipeline/asset_manager.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/music_manager.hpp" #include "game/expansion_profile.hpp" #include @@ -199,20 +200,20 @@ void AuthScreen::render(auth::AuthHandler& authHandler) { } auto& app = core::Application::getInstance(); - auto* renderer = app.getRenderer(); + auto* ac = app.getAudioCoordinator(); if (!musicInitAttempted) { musicInitAttempted = true; auto* assets = app.getAssetManager(); - if (renderer) { - auto* music = renderer->getMusicManager(); + if (ac) { + auto* music = ac->getMusicManager(); if (music && assets && assets->isInitialized() && !music->isInitialized()) { music->initialize(assets); } } } // Login screen music - if (renderer) { - auto* music = renderer->getMusicManager(); + if (ac) { + auto* music = ac->getMusicManager(); if (music) { if (!loginMusicVolumeAdjusted_) { savedMusicVolume_ = music->getVolume(); @@ -506,9 +507,9 @@ void AuthScreen::render(auth::AuthHandler& authHandler) { void AuthScreen::stopLoginMusic() { auto& app = core::Application::getInstance(); - auto* renderer = app.getRenderer(); - if (!renderer) return; - auto* music = renderer->getMusicManager(); + auto* ac = app.getAudioCoordinator(); + if (!ac) return; + auto* music = ac->getMusicManager(); if (!music) return; if (musicPlaying) { music->stopMusic(500.0f); diff --git a/src/ui/chat_panel.cpp b/src/ui/chat_panel.cpp index 19ff5331..67fd4fc4 100644 --- a/src/ui/chat_panel.cpp +++ b/src/ui/chat_panel.cpp @@ -11,6 +11,7 @@ #include "rendering/renderer.hpp" #include "rendering/camera.hpp" #include "rendering/camera_controller.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/audio_engine.hpp" #include "audio/ui_sound_manager.hpp" #include "pipeline/asset_manager.hpp" @@ -1109,8 +1110,8 @@ void ChatPanel::render(game::GameHandler& gameHandler, std::string bodyLower = mMsg.message; for (auto& c : bodyLower) c = static_cast(std::tolower(static_cast(c))); if (bodyLower.find(selfNameLower) != std::string::npos) { - if (auto* renderer = services_.renderer) { - if (auto* ui = renderer->getUiSoundManager()) + if (auto* ac = services_.audioCoordinator) { + if (auto* ui = ac->getUiSoundManager()) ui->playWhisperReceived(); } break; // play at most once per scan pass diff --git a/src/ui/combat_ui.cpp b/src/ui/combat_ui.cpp index 17b6e0c9..ecaa88a5 100644 --- a/src/ui/combat_ui.cpp +++ b/src/ui/combat_ui.cpp @@ -15,6 +15,7 @@ #include "rendering/camera.hpp" #include "game/game_handler.hpp" #include "pipeline/asset_manager.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/audio_engine.hpp" #include "audio/ui_sound_manager.hpp" #include @@ -283,9 +284,11 @@ void CombatUI::renderRaidWarningOverlay(game::GameHandler& gameHandler) { raidWarnEntries_.erase(raidWarnEntries_.begin()); } // Whisper audio notification - if (msg.type == game::ChatType::WHISPER && renderer) { - if (auto* ui = renderer->getUiSoundManager()) - ui->playWhisperReceived(); + if (msg.type == game::ChatType::WHISPER) { + if (auto* ac = services_.audioCoordinator) { + if (auto* ui = ac->getUiSoundManager()) + ui->playWhisperReceived(); + } } } raidWarnChatSeenCount_ = newCount; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 1119a7d4..99247350 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -14,6 +14,7 @@ #include "rendering/character_renderer.hpp" #include "rendering/camera.hpp" #include "rendering/camera_controller.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/audio_engine.hpp" #include "audio/music_manager.hpp" #include "game/zone_manager.hpp" @@ -285,8 +286,8 @@ void GameScreen::render(game::GameHandler& gameHandler) { uiErrors_.push_back({msg, 0.0f}); if (uiErrors_.size() > 5) uiErrors_.erase(uiErrors_.begin()); // Play error sound for each new error (rate-limited by deque cap of 5) - if (auto* r = services_.renderer) { - if (auto* sfx = r->getUiSoundManager()) sfx->playError(); + if (auto* ac = services_.audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) sfx->playError(); } }); uiErrorCallbackSet_ = true; @@ -345,9 +346,9 @@ void GameScreen::render(game::GameHandler& gameHandler) { // Apply saved volume settings once when audio managers first become available if (!settingsPanel_.volumeSettingsApplied_) { - auto* renderer = services_.renderer; - if (renderer && renderer->getUiSoundManager()) { - settingsPanel_.applyAudioVolumes(renderer); + auto* ac = services_.audioCoordinator; + if (ac && ac->getUiSoundManager()) { + settingsPanel_.applyAudioVolumes(ac); settingsPanel_.volumeSettingsApplied_ = true; } } @@ -6525,38 +6526,38 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) { } auto applyMuteState = [&]() { - auto* activeRenderer = services_.renderer; + auto* ac = services_.audioCoordinator; float masterScale = settingsPanel_.soundMuted_ ? 0.0f : static_cast(settingsPanel_.pendingMasterVolume) / 100.0f; audio::AudioEngine::instance().setMasterVolume(masterScale); - if (!activeRenderer) return; - if (auto* music = activeRenderer->getMusicManager()) { + if (!ac) return; + if (auto* music = ac->getMusicManager()) { music->setVolume(settingsPanel_.pendingMusicVolume); } - if (auto* ambient = activeRenderer->getAmbientSoundManager()) { + if (auto* ambient = ac->getAmbientSoundManager()) { ambient->setVolumeScale(settingsPanel_.pendingAmbientVolume / 100.0f); } - if (auto* ui = activeRenderer->getUiSoundManager()) { + if (auto* ui = ac->getUiSoundManager()) { ui->setVolumeScale(settingsPanel_.pendingUiVolume / 100.0f); } - if (auto* combat = activeRenderer->getCombatSoundManager()) { + if (auto* combat = ac->getCombatSoundManager()) { combat->setVolumeScale(settingsPanel_.pendingCombatVolume / 100.0f); } - if (auto* spell = activeRenderer->getSpellSoundManager()) { + if (auto* spell = ac->getSpellSoundManager()) { spell->setVolumeScale(settingsPanel_.pendingSpellVolume / 100.0f); } - if (auto* movement = activeRenderer->getMovementSoundManager()) { + if (auto* movement = ac->getMovementSoundManager()) { movement->setVolumeScale(settingsPanel_.pendingMovementVolume / 100.0f); } - if (auto* footstep = activeRenderer->getFootstepManager()) { + if (auto* footstep = ac->getFootstepManager()) { footstep->setVolumeScale(settingsPanel_.pendingFootstepVolume / 100.0f); } - if (auto* npcVoice = activeRenderer->getNpcVoiceManager()) { + if (auto* npcVoice = ac->getNpcVoiceManager()) { npcVoice->setVolumeScale(settingsPanel_.pendingNpcVoiceVolume / 100.0f); } - if (auto* mount = activeRenderer->getMountSoundManager()) { + if (auto* mount = ac->getMountSoundManager()) { mount->setVolumeScale(settingsPanel_.pendingMountVolume / 100.0f); } - if (auto* activity = activeRenderer->getActivitySoundManager()) { + if (auto* activity = ac->getActivitySoundManager()) { activity->setVolumeScale(settingsPanel_.pendingActivityVolume / 100.0f); } }; diff --git a/src/ui/settings_panel.cpp b/src/ui/settings_panel.cpp index 15acfc13..f6353942 100644 --- a/src/ui/settings_panel.cpp +++ b/src/ui/settings_panel.cpp @@ -17,6 +17,7 @@ #include "rendering/wmo_renderer.hpp" #include "rendering/character_renderer.hpp" #include "game/zone_manager.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/audio_engine.hpp" #include "audio/music_manager.hpp" #include "audio/ambient_sound_manager.hpp" @@ -439,7 +440,7 @@ ImGui::BeginChild("AudioSettings", ImVec2(0, 360), true); // Helper lambda to apply audio settings auto applyAudioSettings = [&]() { - applyAudioVolumes(renderer); + applyAudioVolumes(services_.audioCoordinator); saveCallback(); }; @@ -1227,29 +1228,29 @@ std::string SettingsPanel::getSettingsPath() { return dir + "/settings.cfg"; } -void SettingsPanel::applyAudioVolumes(rendering::Renderer* renderer) { - if (!renderer) return; +void SettingsPanel::applyAudioVolumes(audio::AudioCoordinator* ac) { + if (!ac) return; float masterScale = soundMuted_ ? 0.0f : static_cast(pendingMasterVolume) / 100.0f; audio::AudioEngine::instance().setMasterVolume(masterScale); - if (auto* music = renderer->getMusicManager()) + if (auto* music = ac->getMusicManager()) music->setVolume(pendingMusicVolume); - if (auto* ambient = renderer->getAmbientSoundManager()) + if (auto* ambient = ac->getAmbientSoundManager()) ambient->setVolumeScale(pendingAmbientVolume / 100.0f); - if (auto* ui = renderer->getUiSoundManager()) + if (auto* ui = ac->getUiSoundManager()) ui->setVolumeScale(pendingUiVolume / 100.0f); - if (auto* combat = renderer->getCombatSoundManager()) + if (auto* combat = ac->getCombatSoundManager()) combat->setVolumeScale(pendingCombatVolume / 100.0f); - if (auto* spell = renderer->getSpellSoundManager()) + if (auto* spell = ac->getSpellSoundManager()) spell->setVolumeScale(pendingSpellVolume / 100.0f); - if (auto* movement = renderer->getMovementSoundManager()) + if (auto* movement = ac->getMovementSoundManager()) movement->setVolumeScale(pendingMovementVolume / 100.0f); - if (auto* footstep = renderer->getFootstepManager()) + if (auto* footstep = ac->getFootstepManager()) footstep->setVolumeScale(pendingFootstepVolume / 100.0f); - if (auto* npcVoice = renderer->getNpcVoiceManager()) + if (auto* npcVoice = ac->getNpcVoiceManager()) npcVoice->setVolumeScale(pendingNpcVoiceVolume / 100.0f); - if (auto* mount = renderer->getMountSoundManager()) + if (auto* mount = ac->getMountSoundManager()) mount->setVolumeScale(pendingMountVolume / 100.0f); - if (auto* activity = renderer->getActivitySoundManager()) + if (auto* activity = ac->getActivitySoundManager()) activity->setVolumeScale(pendingActivityVolume / 100.0f); } diff --git a/src/ui/toast_manager.cpp b/src/ui/toast_manager.cpp index 7f76c961..957f5e2a 100644 --- a/src/ui/toast_manager.cpp +++ b/src/ui/toast_manager.cpp @@ -2,6 +2,7 @@ #include "game/game_handler.hpp" #include "core/application.hpp" #include "rendering/renderer.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/ui_sound_manager.hpp" #include @@ -461,11 +462,13 @@ void ToastManager::triggerDing(uint32_t newLevel, uint32_t hpDelta, uint32_t man dingStats_[3] = intel; dingStats_[4] = spi; - auto* renderer = services_.renderer; - if (renderer) { - if (auto* sfx = renderer->getUiSoundManager()) { + auto* ac = services_.audioCoordinator; + if (ac) { + if (auto* sfx = ac->getUiSoundManager()) { sfx->playLevelUp(); } + } + if (auto* renderer = services_.renderer) { renderer->playEmote("cheer"); } } @@ -550,9 +553,9 @@ void ToastManager::triggerAchievementToast(uint32_t achievementId, std::string n achievementToastTimer_ = ACHIEVEMENT_TOAST_DURATION; // Play a UI sound if available - auto* renderer = services_.renderer; - if (renderer) { - if (auto* sfx = renderer->getUiSoundManager()) { + auto* ac = services_.audioCoordinator; + if (ac) { + if (auto* sfx = ac->getUiSoundManager()) { sfx->playAchievementAlert(); } } diff --git a/src/ui/window_manager.cpp b/src/ui/window_manager.cpp index c87a3f64..f25db424 100644 --- a/src/ui/window_manager.cpp +++ b/src/ui/window_manager.cpp @@ -16,6 +16,7 @@ #include "game/game_handler.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/dbc_layout.hpp" +#include "audio/audio_coordinator.hpp" #include "audio/ui_sound_manager.hpp" #include "audio/music_manager.hpp" #include @@ -1701,9 +1702,9 @@ void WindowManager::renderEscapeMenu(SettingsPanel& settingsPanel) { settingsPanel.showEscapeSettingsNotice = false; } if (ImGui::Button("Quit", ImVec2(-1, 0))) { - auto* renderer = services_.renderer; - if (renderer) { - if (auto* music = renderer->getMusicManager()) { + auto* ac = services_.audioCoordinator; + if (ac) { + if (auto* music = ac->getMusicManager()) { music->stopMusic(0.0f); } }