diff --git a/docs/GameAPI.md b/docs/GameAPI.md index 2cdcc36..ad3b2f3 100644 --- a/docs/GameAPI.md +++ b/docs/GameAPI.md @@ -10,6 +10,180 @@ For details on the underlying systems, see also: --- +## `GameAPI::Engine` (High‑Level Game Wrapper) + +Header: `src/core/game_api.h` +Implementation: `src/core/game_api.cpp` + +`GameAPI::Engine` is a thin, game‑friendly wrapper around `VulkanEngine`. It exposes stable, snake_case methods grouped by responsibility: + +- Texture streaming and VRAM budget. +- Shadows and reflections. +- IBL volumes. +- Instances and animation. +- Post‑processing (tonemap, bloom, FXAA). +- Camera control. +- Picking and render‑graph pass toggles. + +Typical creation: + +```cpp +#include "core/engine.h" +#include "core/game_api.h" + +VulkanEngine engine; +engine.init(); + +GameAPI::Engine api(&engine); // non‑owning +``` + +You then call `api.*` from your game loop to spawn content and tweak settings. + +### Texture Streaming & VRAM Budget + +Relevant methods: + +- `size_t get_texture_budget() const;` +- `void set_texture_loads_per_frame(int count);` +- `void set_texture_upload_budget(size_t bytes);` +- `void set_cpu_source_budget(size_t bytes);` +- `void set_max_upload_dimension(uint32_t dim);` +- `void set_keep_source_bytes(bool keep);` +- `void evict_textures_to_budget();` + +At a lower level, `VulkanEngine::query_texture_budget_bytes()` computes a conservative per‑frame texture budget using VMA heap info and constants in `src/core/config.h`: + +- `kTextureBudgetFraction` – fraction of total device‑local VRAM reserved for streamed textures (default `0.35`). +- `kTextureBudgetFallbackBytes` – fallback budget when memory properties are unavailable (default `512 MiB`). +- `kTextureBudgetMinBytes` – minimum budget clamp (default `128 MiB`). + +To globally change how aggressive streaming can be, edit these constants in `config.h` and rebuild. Use the `GameAPI::Engine` setters for per‑scene tuning (e.g. reducing upload bandwidth on low‑end machines). + +### Shadows: Resolution, Quality, and RT Modes + +Shadows are controlled by a combination of: + +- Global settings in `EngineContext::shadowSettings`. +- Config constants in `src/core/config.h`. +- The `ShadowPass` render pass and lighting shader (`shadow.vert`, `deferred_lighting.frag`). + +High‑level game‑side controls: + +- `void set_shadows_enabled(bool enabled);` +- `void set_shadow_mode(ShadowMode mode);` + - `ClipmapOnly` – cascaded shadow maps only. + - `ClipmapPlusRT` – cascades + optional ray‑query assist. + - `RTOnly` – ray‑traced shadows only (no raster maps). +- `void set_hybrid_ray_cascade_mask(uint32_t mask);` +- `void set_hybrid_ray_threshold(float threshold);` + +These map directly onto `EngineContext::shadowSettings` and are also visualized in the ImGui “Shadows / Ray Query” tab. + +#### Shadow Map Resolution (`kShadowMapResolution`) + +The shadow map resolution is driven by `kShadowMapResolution` in `src/core/config.h`: + +- Used for: + - The actual depth image size created for each cascaded shadow map in `VulkanEngine::draw()` (`shadowExtent`). + - Texel snapping for cascade stabilization in `SceneManager::update_scene()`: + - `texel = (2.0f * cover) / float(kShadowMapResolution);` +- Default: `2048.0f`, which gives a good compromise between quality and VRAM usage on mid‑range GPUs. + +Increasing `kShadowMapResolution` has two important effects: + +- **VRAM cost grows quadratically.** + - Depth D32F, per cascade: + - 2048 → ~16 MB. + - 4096 → ~64 MB. + - With 4 cascades, 4096×4096 can consume ~256 MB just for shadow depth, on top of swapchain, HDR, G‑buffers, IBL, and other images. +- **Allocation failures can effectively “kill” shadows.** + - All shadow maps are created as transient RenderGraph images each frame run. + - If VMA runs out of suitable device‑local memory, `vmaCreateImage` will fail, and the engine will assert via `VK_CHECK`. In practice (especially in a release build), this often manifests as: + - No shadow rendering, or + - The app aborting when the first frame tries to allocate these images. + +Practical guidance: + +- Prefer 2048 or 3072 on consumer hardware unless you have headroom and have profiled memory. +- If you push to 4096 and shadows “disappear”, suspect VRAM pressure: + - Try reducing `kTextureBudgetFraction` so textures use less VRAM. + - Or bring `kShadowMapResolution` back down and re‑test. + +The following quality‑related shadow constants also live in `config.h`: + +- `kShadowCascadeCount`, `kShadowCSMFar`, `kShadowCascadeRadiusScale`, `kShadowCascadeRadiusMargin`. +- `kShadowBorderSmoothNDC`, `kShadowPCFBaseRadius`, `kShadowPCFCascadeGain`. +- `kShadowDepthBiasConstant`, `kShadowDepthBiasSlope`. + +These affect how cascades are distributed and how soft/filtered the resulting shadows are. Changing them is safe but should be tested against your content and FOV ranges. + +### Reflections and Post‑Processing + +Game‑side reflection controls: + +- `void set_ssr_enabled(bool enabled);` +- `void set_reflection_mode(ReflectionMode mode);` + (`SSROnly`, `SSRPlusRT`, `RTOnly`) + +Tone mapping and bloom: + +- `void set_exposure(float exposure);` +- `void set_tonemap_operator(TonemapOperator op);` (`Reinhard`, `ACES`) +- `void set_bloom_enabled(bool enabled);` +- `void set_bloom_threshold(float threshold);` +- `void set_bloom_intensity(float intensity);` + +These wrap `TonemapPass` parameters and are equivalent to flipping the corresponding ImGui controls at runtime. + +FXAA: + +- `void set_fxaa_enabled(bool enabled);` +- `void set_fxaa_edge_threshold(float threshold);` +- `void set_fxaa_edge_threshold_min(float threshold);` + +### Camera and Render Scale + +Camera: + +- `void set_camera_position(const glm::vec3 &position);` +- `glm::vec3 get_camera_position() const;` +- `void set_camera_rotation(float pitchDeg, float yawDeg);` +- `void get_camera_rotation(float &pitchDeg, float &yawDeg) const;` +- `void set_camera_fov(float fovDegrees);` +- `float get_camera_fov() const;` +- `void camera_look_at(const glm::vec3 &target);` + +These functions internally manipulate the quaternion‑based `Camera::orientation` and `position` in `SceneManager`. They respect the engine’s `-Z` forward convention. + +Render resolution scaling: + +- `void set_render_scale(float scale); // 0.3–1.0` +- `float get_render_scale() const;` + +This scales the internal draw extent relative to the swapchain and main HDR image sizes, trading resolution for performance. + +### Picking & Pass Toggles + +Picking: + +- `Engine::PickResult get_last_pick() const;` +- `void set_use_id_buffer_picking(bool use);` +- `bool get_use_id_buffer_picking() const;` + +These mirror `VulkanEngine::get_last_pick()` and `_useIdBufferPicking`, letting you choose between: + +- CPU raycast picking (immediate, cheaper VRAM). +- ID‑buffer based picking (async, 1‑frame latency, robust for dense scenes). + +Render‑graph pass toggles: + +- `void set_pass_enabled(const std::string &passName, bool enabled);` +- `bool get_pass_enabled(const std::string &passName) const;` + +This writes into `VulkanEngine::_rgPassToggles` and is applied during RenderGraph compilation. It allows you to permanently disable or enable named passes (e.g. `"ShadowMap[0]"`, `"FXAA"`, `"SSR"`) from game code, not just via the debug UI. + +--- + ## VulkanEngine Helpers Header: `src/core/engine.h` diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 45861e0..629893f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -10,6 +10,8 @@ add_executable (vulkan_engine core/engine.h core/engine.cpp core/engine_ui.cpp + core/game_api.h + core/game_api.cpp # core/device core/device/device.h core/device/device.cpp diff --git a/src/core/config.h b/src/core/config.h index 0dc1342..e968d4f 100644 --- a/src/core/config.h +++ b/src/core/config.h @@ -38,11 +38,11 @@ inline constexpr float kShadowCascadeRadiusMargin = 10.0f; inline constexpr float kShadowClipBaseRadius = 20.0f; // When using dynamic pullback, compute it from the covered XY range of each level. // pullback = max(kShadowClipPullbackMin, cover * kShadowClipPullbackFactor) -inline constexpr float kShadowClipPullbackFactor = 1.5f; // fraction of XY half-size behind center -inline constexpr float kShadowClipForwardFactor = 1.5f; // fraction of XY half-size in front of center for zFar -inline constexpr float kShadowClipPullbackMin = 40.0f; // lower bound on pullback so near levels don’t collapse +inline constexpr float kShadowClipPullbackFactor = 1.2f; // fraction of XY half-size behind center +inline constexpr float kShadowClipForwardFactor = 1.2f; // fraction of XY half-size in front of center for zFar +inline constexpr float kShadowClipPullbackMin = 20.0f; // lower bound on pullback so near levels don’t collapse // Additional Z padding for the orthographic frustum along light direction -inline constexpr float kShadowClipZPadding = 40.0f; +inline constexpr float kShadowClipZPadding = 20.0f; // Shadow quality & filtering // Soft cross-fade band between cascades in light-space NDC (0..1) @@ -55,3 +55,12 @@ inline constexpr float kShadowPCFCascadeGain = 2.0f; // Raster depth-bias parameters for shadow map rendering (tuned conservatively) inline constexpr float kShadowDepthBiasConstant = 1.25f; inline constexpr float kShadowDepthBiasSlope = 1.5f; + +// Texture streaming / VRAM budget configuration +// Fraction of total device-local VRAM reserved for streamed textures. +// The remaining budget is left for attachments, swapchain images, meshes, AS, etc. +inline constexpr double kTextureBudgetFraction = 0.35; +// Fallback texture budget in bytes when Vulkan memory properties are unavailable. +inline constexpr size_t kTextureBudgetFallbackBytes = 512ull * 1024ull * 1024ull; +// Minimum texture budget clamp in bytes. +inline constexpr size_t kTextureBudgetMinBytes = 128ull * 1024ull * 1024ull; diff --git a/src/core/engine.cpp b/src/core/engine.cpp index 9a2a640..03eb28d 100644 --- a/src/core/engine.cpp +++ b/src/core/engine.cpp @@ -107,13 +107,13 @@ static void dump_vma_json(DeviceManager* dev, const char* tag) size_t VulkanEngine::query_texture_budget_bytes() const { DeviceManager *dev = _deviceManager.get(); - if (!dev) return 512ull * 1024ull * 1024ull; // fallback + if (!dev) return kTextureBudgetFallbackBytes; // fallback VmaAllocator alloc = dev->allocator(); - if (!alloc) return 512ull * 1024ull * 1024ull; + if (!alloc) return kTextureBudgetFallbackBytes; const VkPhysicalDeviceMemoryProperties *memProps = nullptr; vmaGetMemoryProperties(alloc, &memProps); - if (!memProps) return 512ull * 1024ull * 1024ull; + if (!memProps) return kTextureBudgetFallbackBytes; VmaBudget budgets[VK_MAX_MEMORY_HEAPS] = {}; vmaGetHeapBudgets(alloc, budgets); @@ -128,14 +128,13 @@ size_t VulkanEngine::query_texture_budget_bytes() const totalUsage += budgets[i].usage; } } - if (totalBudget == 0) return 512ull * 1024ull * 1024ull; + if (totalBudget == 0) return kTextureBudgetFallbackBytes; - // Reserve ~65% of VRAM for attachments, swapchain, meshes, AS, etc. - unsigned long long cap = static_cast(double(totalBudget) * 0.35); + unsigned long long cap = static_cast(double(totalBudget) * kTextureBudgetFraction); // If usage is already near the cap, still allow current textures to live; eviction will trim. - // Clamp to at least 128 MB, at most totalBudget. - unsigned long long minCap = 128ull * 1024ull * 1024ull; + // Clamp to at least a minimum budget, at most totalBudget. + unsigned long long minCap = static_cast(kTextureBudgetMinBytes); if (cap < minCap) cap = minCap; if (cap > totalBudget) cap = totalBudget; return static_cast(cap); @@ -827,7 +826,9 @@ void VulkanEngine::draw() RGImageHandle hDebugColor = hDraw; // Create transient depth targets for cascaded shadow maps (even if RT-only / disabled, to keep descriptors stable) - const VkExtent2D shadowExtent{2048, 2048}; + const VkExtent2D shadowExtent{ + static_cast(kShadowMapResolution), + static_cast(kShadowMapResolution)}; std::array hShadowCascades{}; for (int i = 0; i < kShadowCascadeCount; ++i) { diff --git a/src/core/game_api.cpp b/src/core/game_api.cpp new file mode 100644 index 0000000..5a24ceb --- /dev/null +++ b/src/core/game_api.cpp @@ -0,0 +1,921 @@ +#include "game_api.h" +#include "engine.h" +#include "context.h" +#include "core/assets/texture_cache.h" +#include "core/assets/ibl_manager.h" +#include "core/pipeline/manager.h" +#include "render/passes/tonemap.h" +#include "render/passes/fxaa.h" +#include "render/renderpass.h" +#include "scene/vk_scene.h" +#include "scene/camera.h" + +#include +#include + +namespace GameAPI +{ + +// ============================================================================ +// Transform helpers +// ============================================================================ + +glm::mat4 Transform::to_matrix() const +{ + glm::mat4 T = glm::translate(glm::mat4(1.0f), position); + glm::mat4 R = glm::mat4_cast(rotation); + glm::mat4 S = glm::scale(glm::mat4(1.0f), scale); + return T * R * S; +} + +Transform Transform::from_matrix(const glm::mat4& m) +{ + Transform t; + glm::vec3 skew; + glm::vec4 perspective; + glm::decompose(m, t.scale, t.rotation, t.position, skew, perspective); + return t; +} + +// ============================================================================ +// Engine Implementation +// ============================================================================ + +Engine::Engine(VulkanEngine* engine) + : _engine(engine) +{ +} + +// ---------------------------------------------------------------------------- +// Memory / Texture Streaming +// ---------------------------------------------------------------------------- + +size_t Engine::get_texture_budget() const +{ + return _engine->query_texture_budget_bytes(); +} + +void Engine::set_texture_loads_per_frame(int count) +{ + if (_engine->_textureCache) + { + _engine->_textureCache->set_max_loads_per_pump(count); + } +} + +int Engine::get_texture_loads_per_frame() const +{ + return _engine->_textureCache ? _engine->_textureCache->max_loads_per_pump() : 0; +} + +void Engine::set_texture_upload_budget(size_t bytes) +{ + if (_engine->_textureCache) + { + _engine->_textureCache->set_max_bytes_per_pump(bytes); + } +} + +size_t Engine::get_texture_upload_budget() const +{ + return _engine->_textureCache ? _engine->_textureCache->max_bytes_per_pump() : 0; +} + +void Engine::set_cpu_source_budget(size_t bytes) +{ + if (_engine->_textureCache) + { + _engine->_textureCache->set_cpu_source_budget(bytes); + } +} + +size_t Engine::get_cpu_source_budget() const +{ + return _engine->_textureCache ? _engine->_textureCache->cpu_source_budget() : 0; +} + +void Engine::set_max_upload_dimension(uint32_t dim) +{ + if (_engine->_textureCache) + { + _engine->_textureCache->set_max_upload_dimension(dim); + } +} + +uint32_t Engine::get_max_upload_dimension() const +{ + return _engine->_textureCache ? _engine->_textureCache->max_upload_dimension() : 0; +} + +void Engine::set_keep_source_bytes(bool keep) +{ + if (_engine->_textureCache) + { + _engine->_textureCache->set_keep_source_bytes(keep); + } +} + +bool Engine::get_keep_source_bytes() const +{ + return _engine->_textureCache ? _engine->_textureCache->keep_source_bytes() : false; +} + +void Engine::evict_textures_to_budget() +{ + if (_engine->_textureCache) + { + size_t budget = _engine->query_texture_budget_bytes(); + _engine->_textureCache->evictToBudget(budget); + } +} + +// ---------------------------------------------------------------------------- +// Shadows +// ---------------------------------------------------------------------------- + +void Engine::set_shadows_enabled(bool enabled) +{ + if (_engine->_context) + { + _engine->_context->shadowSettings.enabled = enabled; + } +} + +bool Engine::get_shadows_enabled() const +{ + return _engine->_context ? _engine->_context->shadowSettings.enabled : false; +} + +void Engine::set_shadow_mode(ShadowMode mode) +{ + if (_engine->_context) + { + // Guard against requesting RT modes on unsupported hardware. + if (mode != ShadowMode::ClipmapOnly) + { + if (!_engine->_deviceManager + || !_engine->_deviceManager->supportsRayQuery() + || !_engine->_deviceManager->supportsAccelerationStructure()) + { + mode = ShadowMode::ClipmapOnly; + } + } + + _engine->_context->shadowSettings.mode = static_cast(mode); + _engine->_context->shadowSettings.hybridRayQueryEnabled = + _engine->_context->shadowSettings.enabled && (mode != ShadowMode::ClipmapOnly); + } +} + +ShadowMode Engine::get_shadow_mode() const +{ + if (!_engine->_context) return ShadowMode::ClipmapOnly; + return static_cast(_engine->_context->shadowSettings.mode); +} + +void Engine::set_hybrid_ray_cascade_mask(uint32_t mask) +{ + if (_engine->_context) + { + _engine->_context->shadowSettings.hybridRayCascadesMask = mask & 0xF; + } +} + +uint32_t Engine::get_hybrid_ray_cascade_mask() const +{ + return _engine->_context ? _engine->_context->shadowSettings.hybridRayCascadesMask : 0; +} + +void Engine::set_hybrid_ray_threshold(float threshold) +{ + if (_engine->_context) + { + _engine->_context->shadowSettings.hybridRayNoLThreshold = glm::clamp(threshold, 0.0f, 1.0f); + } +} + +float Engine::get_hybrid_ray_threshold() const +{ + return _engine->_context ? _engine->_context->shadowSettings.hybridRayNoLThreshold : 0.25f; +} + +// ---------------------------------------------------------------------------- +// IBL (Image-Based Lighting) +// ---------------------------------------------------------------------------- + +static ::IBLPaths to_internal_ibl_paths(const IBLPaths& p) +{ + ::IBLPaths out; + out.specularCube = p.specularCube; + out.diffuseCube = p.diffuseCube; + out.brdfLut2D = p.brdfLut; + out.background2D = p.background; + return out; +} + +static IBLPaths from_internal_ibl_paths(const ::IBLPaths& p) +{ + IBLPaths out; + out.specularCube = p.specularCube; + out.diffuseCube = p.diffuseCube; + out.brdfLut = p.brdfLut2D; + out.background = p.background2D; + return out; +} + +bool Engine::load_global_ibl(const IBLPaths& paths) +{ + if (!_engine->_iblManager) return false; + + ::IBLPaths internal = to_internal_ibl_paths(paths); + _engine->_globalIBLPaths = internal; + + if (_engine->_iblManager->load_async(internal)) + { + _engine->_pendingIBLRequest.active = true; + _engine->_pendingIBLRequest.targetVolume = -1; + _engine->_pendingIBLRequest.paths = internal; + _engine->_hasGlobalIBL = false; + return true; + } + return false; +} + +IBLPaths Engine::get_global_ibl_paths() const +{ + return from_internal_ibl_paths(_engine->_globalIBLPaths); +} + +void Engine::set_global_ibl_paths(const IBLPaths& paths) +{ + _engine->_globalIBLPaths = to_internal_ibl_paths(paths); +} + +size_t Engine::add_ibl_volume(const IBLVolume& volume) +{ + VulkanEngine::IBLVolume v; + v.center = volume.center; + v.halfExtents = volume.halfExtents; + v.paths = to_internal_ibl_paths(volume.paths); + v.enabled = volume.enabled; + + _engine->_iblVolumes.push_back(v); + return _engine->_iblVolumes.size() - 1; +} + +bool Engine::remove_ibl_volume(size_t index) +{ + if (index >= _engine->_iblVolumes.size()) return false; + + if (_engine->_activeIBLVolume == static_cast(index)) + { + _engine->_activeIBLVolume = -1; + } + else if (_engine->_activeIBLVolume > static_cast(index)) + { + _engine->_activeIBLVolume -= 1; + } + + _engine->_iblVolumes.erase(_engine->_iblVolumes.begin() + index); + return true; +} + +bool Engine::get_ibl_volume(size_t index, IBLVolume& out) const +{ + if (index >= _engine->_iblVolumes.size()) return false; + + const auto& v = _engine->_iblVolumes[index]; + out.center = v.center; + out.halfExtents = v.halfExtents; + out.paths = from_internal_ibl_paths(v.paths); + out.enabled = v.enabled; + return true; +} + +bool Engine::set_ibl_volume(size_t index, const IBLVolume& volume) +{ + if (index >= _engine->_iblVolumes.size()) return false; + + auto& v = _engine->_iblVolumes[index]; + v.center = volume.center; + v.halfExtents = volume.halfExtents; + v.paths = to_internal_ibl_paths(volume.paths); + v.enabled = volume.enabled; + return true; +} + +int Engine::get_active_ibl_volume() const +{ + return _engine->_activeIBLVolume; +} + +size_t Engine::get_ibl_volume_count() const +{ + return _engine->_iblVolumes.size(); +} + +void Engine::clear_ibl_volumes() +{ + _engine->_iblVolumes.clear(); + _engine->_activeIBLVolume = -1; +} + +// ---------------------------------------------------------------------------- +// Objects / Instances +// ---------------------------------------------------------------------------- + +bool Engine::add_gltf_instance(const std::string& name, + const std::string& modelPath, + const Transform& transform, + bool preloadTextures) +{ + return _engine->addGLTFInstance(name, modelPath, transform.to_matrix(), preloadTextures); +} + +uint32_t Engine::add_gltf_instance_async(const std::string& name, + const std::string& modelPath, + const Transform& transform, + bool preloadTextures) +{ + return _engine->loadGLTFAsync(name, modelPath, transform.to_matrix(), preloadTextures); +} + +bool Engine::remove_gltf_instance(const std::string& name) +{ + return _engine->_sceneManager ? _engine->_sceneManager->removeGLTFInstance(name) : false; +} + +bool Engine::get_gltf_instance_transform(const std::string& name, Transform& out) const +{ + if (!_engine->_sceneManager) return false; + + glm::mat4 m; + if (_engine->_sceneManager->getGLTFInstanceTransform(name, m)) + { + out = Transform::from_matrix(m); + return true; + } + return false; +} + +bool Engine::set_gltf_instance_transform(const std::string& name, const Transform& transform) +{ + return _engine->_sceneManager + ? _engine->_sceneManager->setGLTFInstanceTransform(name, transform.to_matrix()) + : false; +} + +bool Engine::add_primitive_instance(const std::string& name, + PrimitiveType type, + const Transform& transform) +{ + AssetManager::MeshGeometryDesc::Type geomType; + switch (type) + { + case PrimitiveType::Cube: geomType = AssetManager::MeshGeometryDesc::Type::Cube; break; + case PrimitiveType::Sphere: geomType = AssetManager::MeshGeometryDesc::Type::Sphere; break; + case PrimitiveType::Plane: geomType = AssetManager::MeshGeometryDesc::Type::Plane; break; + case PrimitiveType::Capsule: geomType = AssetManager::MeshGeometryDesc::Type::Capsule; break; + default: return false; + } + + return _engine->addPrimitiveInstance(name, geomType, transform.to_matrix()); +} + +bool Engine::remove_mesh_instance(const std::string& name) +{ + return _engine->_sceneManager ? _engine->_sceneManager->removeMeshInstance(name) : false; +} + +bool Engine::get_mesh_instance_transform(const std::string& name, Transform& out) const +{ + if (!_engine->_sceneManager) return false; + + glm::mat4 m; + if (_engine->_sceneManager->getMeshInstanceTransform(name, m)) + { + out = Transform::from_matrix(m); + return true; + } + return false; +} + +bool Engine::set_mesh_instance_transform(const std::string& name, const Transform& transform) +{ + return _engine->_sceneManager + ? _engine->_sceneManager->setMeshInstanceTransform(name, transform.to_matrix()) + : false; +} + +void Engine::preload_instance_textures(const std::string& name) +{ + _engine->preloadInstanceTextures(name); +} + +void Engine::clear_all_instances() +{ + if (_engine->_sceneManager) + { + _engine->_sceneManager->clearGLTFInstances(); + _engine->_sceneManager->clearMeshInstances(); + } +} + +// ---------------------------------------------------------------------------- +// Animation +// ---------------------------------------------------------------------------- + +bool Engine::set_instance_animation(const std::string& instanceName, int animationIndex, bool resetTime) +{ + return _engine->_sceneManager + ? _engine->_sceneManager->setGLTFInstanceAnimation(instanceName, animationIndex, resetTime) + : false; +} + +bool Engine::set_instance_animation(const std::string& instanceName, const std::string& animationName, bool resetTime) +{ + return _engine->_sceneManager + ? _engine->_sceneManager->setGLTFInstanceAnimation(instanceName, animationName, resetTime) + : false; +} + +bool Engine::set_instance_animation_loop(const std::string& instanceName, bool loop) +{ + return _engine->_sceneManager + ? _engine->_sceneManager->setGLTFInstanceAnimationLoop(instanceName, loop) + : false; +} + +bool Engine::set_instance_node_offset(const std::string& instanceName, const std::string& nodeName, const glm::mat4& offset) +{ + return _engine->_sceneManager + ? _engine->_sceneManager->setGLTFInstanceNodeOffset(instanceName, nodeName, offset) + : false; +} + +bool Engine::clear_instance_node_offset(const std::string& instanceName, const std::string& nodeName) +{ + return _engine->_sceneManager + ? _engine->_sceneManager->clearGLTFInstanceNodeOffset(instanceName, nodeName) + : false; +} + +void Engine::clear_all_instance_node_offsets(const std::string& instanceName) +{ + if (_engine->_sceneManager) + { + _engine->_sceneManager->clearGLTFInstanceNodeOffsets(instanceName); + } +} + +// ---------------------------------------------------------------------------- +// Lighting +// ---------------------------------------------------------------------------- + +size_t Engine::add_point_light(const PointLight& light) +{ + if (!_engine->_sceneManager) return 0; + + SceneManager::PointLight pl; + pl.position = light.position; + pl.radius = light.radius; + pl.color = light.color; + pl.intensity = light.intensity; + + size_t idx = _engine->_sceneManager->getPointLightCount(); + _engine->_sceneManager->addPointLight(pl); + return idx; +} + +bool Engine::remove_point_light(size_t index) +{ + return _engine->_sceneManager ? _engine->_sceneManager->removePointLight(index) : false; +} + +bool Engine::get_point_light(size_t index, PointLight& out) const +{ + if (!_engine->_sceneManager) return false; + + SceneManager::PointLight pl; + if (_engine->_sceneManager->getPointLight(index, pl)) + { + out.position = pl.position; + out.radius = pl.radius; + out.color = pl.color; + out.intensity = pl.intensity; + return true; + } + return false; +} + +bool Engine::set_point_light(size_t index, const PointLight& light) +{ + if (!_engine->_sceneManager) return false; + + SceneManager::PointLight pl; + pl.position = light.position; + pl.radius = light.radius; + pl.color = light.color; + pl.intensity = light.intensity; + + return _engine->_sceneManager->setPointLight(index, pl); +} + +size_t Engine::get_point_light_count() const +{ + return _engine->_sceneManager ? _engine->_sceneManager->getPointLightCount() : 0; +} + +void Engine::clear_point_lights() +{ + if (_engine->_sceneManager) + { + _engine->_sceneManager->clearPointLights(); + } +} + +// ---------------------------------------------------------------------------- +// Post Processing - FXAA +// ---------------------------------------------------------------------------- + +void Engine::set_fxaa_enabled(bool enabled) +{ + if (!_engine->_renderPassManager) return; + if (auto* fxaa = _engine->_renderPassManager->getPass()) + { + fxaa->set_enabled(enabled); + } +} + +bool Engine::get_fxaa_enabled() const +{ + if (!_engine->_renderPassManager) return false; + if (auto* fxaa = _engine->_renderPassManager->getPass()) + { + return fxaa->enabled(); + } + return false; +} + +void Engine::set_fxaa_edge_threshold(float threshold) +{ + if (!_engine->_renderPassManager) return; + if (auto* fxaa = _engine->_renderPassManager->getPass()) + { + fxaa->set_edge_threshold(threshold); + } +} + +float Engine::get_fxaa_edge_threshold() const +{ + if (!_engine->_renderPassManager) return 0.125f; + if (auto* fxaa = _engine->_renderPassManager->getPass()) + { + return fxaa->edge_threshold(); + } + return 0.125f; +} + +void Engine::set_fxaa_edge_threshold_min(float threshold) +{ + if (!_engine->_renderPassManager) return; + if (auto* fxaa = _engine->_renderPassManager->getPass()) + { + fxaa->set_edge_threshold_min(threshold); + } +} + +float Engine::get_fxaa_edge_threshold_min() const +{ + if (!_engine->_renderPassManager) return 0.0312f; + if (auto* fxaa = _engine->_renderPassManager->getPass()) + { + return fxaa->edge_threshold_min(); + } + return 0.0312f; +} + +// ---------------------------------------------------------------------------- +// Post Processing - SSR +// ---------------------------------------------------------------------------- + +void Engine::set_ssr_enabled(bool enabled) +{ + if (_engine->_context) + { + _engine->_context->enableSSR = enabled; + } +} + +bool Engine::get_ssr_enabled() const +{ + return _engine->_context ? _engine->_context->enableSSR : false; +} + +void Engine::set_reflection_mode(ReflectionMode mode) +{ + if (_engine->_context) + { + // Guard against requesting RT reflection modes on unsupported hardware. + if (mode != ReflectionMode::SSROnly) + { + if (!_engine->_deviceManager + || !_engine->_deviceManager->supportsRayQuery() + || !_engine->_deviceManager->supportsAccelerationStructure()) + { + mode = ReflectionMode::SSROnly; + } + } + + _engine->_context->reflectionMode = static_cast(mode); + } +} + +ReflectionMode Engine::get_reflection_mode() const +{ + if (!_engine->_context) return ReflectionMode::SSROnly; + return static_cast(_engine->_context->reflectionMode); +} + +// ---------------------------------------------------------------------------- +// Post Processing - Tonemapping +// ---------------------------------------------------------------------------- + +void Engine::set_exposure(float exposure) +{ + if (!_engine->_renderPassManager) return; + if (auto* tonemap = _engine->_renderPassManager->getPass()) + { + tonemap->setExposure(exposure); + } +} + +float Engine::get_exposure() const +{ + if (!_engine->_renderPassManager) return 1.0f; + if (auto* tonemap = _engine->_renderPassManager->getPass()) + { + return tonemap->exposure(); + } + return 1.0f; +} + +void Engine::set_tonemap_operator(TonemapOperator op) +{ + if (!_engine->_renderPassManager) return; + if (auto* tonemap = _engine->_renderPassManager->getPass()) + { + tonemap->setMode(static_cast(op)); + } +} + +TonemapOperator Engine::get_tonemap_operator() const +{ + if (!_engine->_renderPassManager) return TonemapOperator::ACES; + if (auto* tonemap = _engine->_renderPassManager->getPass()) + { + return static_cast(tonemap->mode()); + } + return TonemapOperator::ACES; +} + +// ---------------------------------------------------------------------------- +// Post Processing - Bloom +// ---------------------------------------------------------------------------- + +void Engine::set_bloom_enabled(bool enabled) +{ + if (!_engine->_renderPassManager) return; + if (auto* tonemap = _engine->_renderPassManager->getPass()) + { + tonemap->setBloomEnabled(enabled); + } +} + +bool Engine::get_bloom_enabled() const +{ + if (!_engine->_renderPassManager) return false; + if (auto* tonemap = _engine->_renderPassManager->getPass()) + { + return tonemap->bloomEnabled(); + } + return false; +} + +void Engine::set_bloom_threshold(float threshold) +{ + if (!_engine->_renderPassManager) return; + if (auto* tonemap = _engine->_renderPassManager->getPass()) + { + tonemap->setBloomThreshold(threshold); + } +} + +float Engine::get_bloom_threshold() const +{ + if (!_engine->_renderPassManager) return 1.0f; + if (auto* tonemap = _engine->_renderPassManager->getPass()) + { + return tonemap->bloomThreshold(); + } + return 1.0f; +} + +void Engine::set_bloom_intensity(float intensity) +{ + if (!_engine->_renderPassManager) return; + if (auto* tonemap = _engine->_renderPassManager->getPass()) + { + tonemap->setBloomIntensity(intensity); + } +} + +float Engine::get_bloom_intensity() const +{ + if (!_engine->_renderPassManager) return 0.7f; + if (auto* tonemap = _engine->_renderPassManager->getPass()) + { + return tonemap->bloomIntensity(); + } + return 0.7f; +} + +// ---------------------------------------------------------------------------- +// Camera +// ---------------------------------------------------------------------------- + +void Engine::set_camera_position(const glm::vec3& position) +{ + if (_engine->_sceneManager) + { + _engine->_sceneManager->getMainCamera().position = position; + } +} + +glm::vec3 Engine::get_camera_position() const +{ + if (_engine->_sceneManager) + { + return _engine->_sceneManager->getMainCamera().position; + } + return glm::vec3(0.0f); +} + +void Engine::set_camera_rotation(float pitch, float yaw) +{ + if (_engine->_sceneManager) + { + Camera& cam = _engine->_sceneManager->getMainCamera(); + + // Convert degrees to radians. + float pitchRad = glm::radians(pitch); + float yawRad = glm::radians(yaw); + + // -Z forward convention: yaw around +Y, then pitch around local +X. + glm::quat yawQ = glm::angleAxis(yawRad, glm::vec3(0.0f, 1.0f, 0.0f)); + glm::quat pitchQ = glm::angleAxis(pitchRad, glm::vec3(1.0f, 0.0f, 0.0f)); + + cam.orientation = glm::normalize(yawQ * pitchQ); + } +} + +void Engine::get_camera_rotation(float& pitch, float& yaw) const +{ + if (_engine->_sceneManager) + { + const Camera& cam = _engine->_sceneManager->getMainCamera(); + + // Derive forward from orientation and convert to pitch/yaw (degrees). + glm::vec3 forward = glm::rotate(cam.orientation, glm::vec3(0.0f, 0.0f, -1.0f)); + forward = glm::normalize(forward); + + pitch = glm::degrees(asinf(-forward.y)); + yaw = glm::degrees(atan2f(forward.x, forward.z)); + } + else + { + pitch = 0.0f; + yaw = 0.0f; + } +} + +void Engine::set_camera_fov(float fovDegrees) +{ + if (_engine->_sceneManager) + { + _engine->_sceneManager->getMainCamera().fovDegrees = fovDegrees; + } +} + +float Engine::get_camera_fov() const +{ + if (_engine->_sceneManager) + { + return _engine->_sceneManager->getMainCamera().fovDegrees; + } + return 70.0f; +} + +void Engine::camera_look_at(const glm::vec3& target) +{ + if (!_engine->_sceneManager) return; + + Camera& cam = _engine->_sceneManager->getMainCamera(); + glm::vec3 dir = glm::normalize(target - cam.position); + + // For a -Z forward convention, build a quaternion that rotates -Z into dir. + // Use glm's lookAt-style helper via matrices, then convert to a quaternion. + glm::vec3 up(0.0f, 1.0f, 0.0f); + if (glm::length2(glm::cross(dir, up)) < 1e-6f) + { + up = glm::vec3(0.0f, 0.0f, 1.0f); + } + + glm::vec3 f = dir; + glm::vec3 r = glm::normalize(glm::cross(up, f)); + glm::vec3 u = glm::cross(f, r); + + glm::mat3 rot; + rot[0] = r; + rot[1] = u; + rot[2] = -f; // -Z forward + + cam.orientation = glm::quat_cast(rot); +} + +// ---------------------------------------------------------------------------- +// Rendering +// ---------------------------------------------------------------------------- + +void Engine::set_render_scale(float scale) +{ + _engine->renderScale = glm::clamp(scale, 0.3f, 1.0f); +} + +float Engine::get_render_scale() const +{ + return _engine->renderScale; +} + +void Engine::set_pass_enabled(const std::string& passName, bool enabled) +{ + _engine->_rgPassToggles[passName] = enabled; +} + +bool Engine::get_pass_enabled(const std::string& passName) const +{ + auto it = _engine->_rgPassToggles.find(passName); + if (it != _engine->_rgPassToggles.end()) + { + return it->second; + } + return true; // Default to enabled if not in map +} + +void Engine::hot_reload_shaders() +{ + if (_engine->_pipelineManager) + { + _engine->_pipelineManager->hotReloadChanged(); + } +} + +// ---------------------------------------------------------------------------- +// Statistics +// ---------------------------------------------------------------------------- + +Stats Engine::get_stats() const +{ + Stats s; + s.frametime = _engine->stats.frametime; + s.drawTime = _engine->stats.mesh_draw_time; + s.sceneUpdateTime = _engine->stats.scene_update_time; + s.triangleCount = _engine->stats.triangle_count; + s.drawCallCount = _engine->stats.drawcall_count; + return s; +} + +// ---------------------------------------------------------------------------- +// Picking / Selection +// ---------------------------------------------------------------------------- + +Engine::PickResult Engine::get_last_pick() const +{ + PickResult r; + r.valid = _engine->_lastPick.valid; + r.ownerName = _engine->_lastPick.ownerName; + r.worldPosition = _engine->_lastPick.worldPos; + return r; +} + +void Engine::set_use_id_buffer_picking(bool use) +{ + _engine->_useIdBufferPicking = use; +} + +bool Engine::get_use_id_buffer_picking() const +{ + return _engine->_useIdBufferPicking; +} + +} // namespace GameAPI diff --git a/src/core/game_api.h b/src/core/game_api.h new file mode 100644 index 0000000..c117c22 --- /dev/null +++ b/src/core/game_api.h @@ -0,0 +1,377 @@ +#pragma once + +// GameAPI: High-level interface for game development +// Wraps VulkanEngine internals and exposes clean, game-friendly functions. + +#include +#include +#include +#include +#include +#include + +class VulkanEngine; + +namespace GameAPI +{ + +// ============================================================================ +// Forward declarations and simple POD types +// ============================================================================ + +// Shadow rendering mode +enum class ShadowMode : uint32_t +{ + ClipmapOnly = 0, // Raster shadow maps with PCF + ClipmapPlusRT = 1, // Shadow maps + ray-traced assist at low N.L angles + RTOnly = 2 // Pure ray-traced shadows (no shadow maps) +}; + +// Reflection rendering mode +enum class ReflectionMode : uint32_t +{ + SSROnly = 0, // Screen-space reflections only + SSRPlusRT = 1, // SSR with ray-traced fallback + RTOnly = 2 // Pure ray-traced reflections +}; + +// Tone mapping operator +enum class TonemapOperator : int +{ + Reinhard = 0, + ACES = 1 +}; + +// Primitive geometry types +enum class PrimitiveType +{ + Cube, + Sphere, + Plane, + Capsule +}; + +// Point light data +struct PointLight +{ + glm::vec3 position{0.0f}; + float radius{10.0f}; + glm::vec3 color{1.0f}; + float intensity{1.0f}; +}; + +// IBL (Image-Based Lighting) paths +struct IBLPaths +{ + std::string specularCube; // .ktx2 specular cubemap + std::string diffuseCube; // .ktx2 diffuse cubemap + std::string brdfLut; // .ktx2 BRDF lookup table + std::string background; // .ktx2 background (optional, falls back to specular) +}; + +// IBL Volume (local reflection probe) +struct IBLVolume +{ + glm::vec3 center{0.0f}; + glm::vec3 halfExtents{10.0f}; + IBLPaths paths; + bool enabled{true}; +}; + +// Transform decomposition +struct Transform +{ + glm::vec3 position{0.0f}; + glm::quat rotation{1.0f, 0.0f, 0.0f, 0.0f}; + glm::vec3 scale{1.0f}; + + glm::mat4 to_matrix() const; + static Transform from_matrix(const glm::mat4& m); +}; + +// Engine statistics (read-only) +struct Stats +{ + float frametime{0.0f}; // ms + float drawTime{0.0f}; // ms + float sceneUpdateTime{0.0f}; // ms + int triangleCount{0}; + int drawCallCount{0}; +}; + +// ============================================================================ +// Main API Class +// ============================================================================ + +class Engine +{ +public: + explicit Engine(VulkanEngine* engine); + ~Engine() = default; + + // Non-copyable + Engine(const Engine&) = delete; + Engine& operator=(const Engine&) = delete; + + // ------------------------------------------------------------------------ + // Memory / Texture Streaming + // ------------------------------------------------------------------------ + + // Query current VRAM texture budget (bytes) + size_t get_texture_budget() const; + + // Set maximum textures loaded per frame (1-16) + void set_texture_loads_per_frame(int count); + int get_texture_loads_per_frame() const; + + // Set upload budget per frame (bytes, e.g., 128*1024*1024 = 128 MiB) + void set_texture_upload_budget(size_t bytes); + size_t get_texture_upload_budget() const; + + // Set CPU source data budget (bytes) + void set_cpu_source_budget(size_t bytes); + size_t get_cpu_source_budget() const; + + // Set maximum upload dimension (clamps large textures) + void set_max_upload_dimension(uint32_t dim); + uint32_t get_max_upload_dimension() const; + + // Keep CPU source data after GPU upload (useful for streaming) + void set_keep_source_bytes(bool keep); + bool get_keep_source_bytes() const; + + // Force eviction to budget (call after loading large assets) + void evict_textures_to_budget(); + + // ------------------------------------------------------------------------ + // Shadows + // ------------------------------------------------------------------------ + + void set_shadows_enabled(bool enabled); + bool get_shadows_enabled() const; + + void set_shadow_mode(ShadowMode mode); + ShadowMode get_shadow_mode() const; + + // For hybrid mode: which cascades use ray assist (bitmask, bits 0-3) + void set_hybrid_ray_cascade_mask(uint32_t mask); + uint32_t get_hybrid_ray_cascade_mask() const; + + // N.L threshold for hybrid ray shadows (0.0 - 1.0) + void set_hybrid_ray_threshold(float threshold); + float get_hybrid_ray_threshold() const; + + // ------------------------------------------------------------------------ + // IBL (Image-Based Lighting) + // ------------------------------------------------------------------------ + + // Load global IBL asynchronously (returns false if failed to queue) + bool load_global_ibl(const IBLPaths& paths); + + // Get/set global IBL paths (does not trigger reload) + IBLPaths get_global_ibl_paths() const; + void set_global_ibl_paths(const IBLPaths& paths); + + // Add a local IBL volume (returns volume index) + size_t add_ibl_volume(const IBLVolume& volume); + + // Remove IBL volume by index + bool remove_ibl_volume(size_t index); + + // Get/set IBL volume properties + bool get_ibl_volume(size_t index, IBLVolume& out) const; + bool set_ibl_volume(size_t index, const IBLVolume& volume); + + // Get current active IBL volume index (-1 = global) + int get_active_ibl_volume() const; + + // Get IBL volume count + size_t get_ibl_volume_count() const; + + // Clear all IBL volumes + void clear_ibl_volumes(); + + // ------------------------------------------------------------------------ + // Objects / Instances + // ------------------------------------------------------------------------ + + // Add glTF model instance (path relative to assets/models/) + bool add_gltf_instance(const std::string& name, + const std::string& modelPath, + const Transform& transform = {}, + bool preloadTextures = true); + + // Add glTF model asynchronously (returns job ID, 0 on failure) + uint32_t add_gltf_instance_async(const std::string& name, + const std::string& modelPath, + const Transform& transform = {}, + bool preloadTextures = true); + + // Remove glTF instance + bool remove_gltf_instance(const std::string& name); + + // Get/set glTF instance transform + bool get_gltf_instance_transform(const std::string& name, Transform& out) const; + bool set_gltf_instance_transform(const std::string& name, const Transform& transform); + + // Add primitive mesh instance + bool add_primitive_instance(const std::string& name, + PrimitiveType type, + const Transform& transform = {}); + + // Remove mesh instance (primitives or custom meshes) + bool remove_mesh_instance(const std::string& name); + + // Get/set mesh instance transform + bool get_mesh_instance_transform(const std::string& name, Transform& out) const; + bool set_mesh_instance_transform(const std::string& name, const Transform& transform); + + // Preload textures for an instance (useful before it becomes visible) + void preload_instance_textures(const std::string& name); + + // Clear all dynamic instances + void clear_all_instances(); + + // ------------------------------------------------------------------------ + // Animation + // ------------------------------------------------------------------------ + + // Set animation by index for a glTF instance (-1 to disable) + bool set_instance_animation(const std::string& instanceName, int animationIndex, bool resetTime = true); + + // Set animation by name for a glTF instance + bool set_instance_animation(const std::string& instanceName, const std::string& animationName, bool resetTime = true); + + // Set animation looping for a glTF instance + bool set_instance_animation_loop(const std::string& instanceName, bool loop); + + // Per-node transform offset (local space, layered on animation) + bool set_instance_node_offset(const std::string& instanceName, const std::string& nodeName, const glm::mat4& offset); + bool clear_instance_node_offset(const std::string& instanceName, const std::string& nodeName); + void clear_all_instance_node_offsets(const std::string& instanceName); + + // ------------------------------------------------------------------------ + // Lighting + // ------------------------------------------------------------------------ + + // Add point light (returns index) + size_t add_point_light(const PointLight& light); + + // Remove point light by index + bool remove_point_light(size_t index); + + // Get/set point light properties + bool get_point_light(size_t index, PointLight& out) const; + bool set_point_light(size_t index, const PointLight& light); + + // Get point light count + size_t get_point_light_count() const; + + // Clear all point lights + void clear_point_lights(); + + // ------------------------------------------------------------------------ + // Post Processing - FXAA + // ------------------------------------------------------------------------ + + void set_fxaa_enabled(bool enabled); + bool get_fxaa_enabled() const; + + void set_fxaa_edge_threshold(float threshold); + float get_fxaa_edge_threshold() const; + + void set_fxaa_edge_threshold_min(float threshold); + float get_fxaa_edge_threshold_min() const; + + // ------------------------------------------------------------------------ + // Post Processing - SSR (Screen Space Reflections) + // ------------------------------------------------------------------------ + + void set_ssr_enabled(bool enabled); + bool get_ssr_enabled() const; + + void set_reflection_mode(ReflectionMode mode); + ReflectionMode get_reflection_mode() const; + + // ------------------------------------------------------------------------ + // Post Processing - Tonemapping + // ------------------------------------------------------------------------ + + void set_exposure(float exposure); + float get_exposure() const; + + void set_tonemap_operator(TonemapOperator op); + TonemapOperator get_tonemap_operator() const; + + // ------------------------------------------------------------------------ + // Post Processing - Bloom + // ------------------------------------------------------------------------ + + void set_bloom_enabled(bool enabled); + bool get_bloom_enabled() const; + + void set_bloom_threshold(float threshold); + float get_bloom_threshold() const; + + void set_bloom_intensity(float intensity); + float get_bloom_intensity() const; + + // ------------------------------------------------------------------------ + // Camera + // ------------------------------------------------------------------------ + + void set_camera_position(const glm::vec3& position); + glm::vec3 get_camera_position() const; + + void set_camera_rotation(float pitch, float yaw); + void get_camera_rotation(float& pitch, float& yaw) const; + + void set_camera_fov(float fovDegrees); + float get_camera_fov() const; + + // Look at a target position + void camera_look_at(const glm::vec3& target); + + // ------------------------------------------------------------------------ + // Rendering + // ------------------------------------------------------------------------ + + void set_render_scale(float scale); // 0.3 - 1.0 + float get_render_scale() const; + + // Enable/disable specific render passes by name + void set_pass_enabled(const std::string& passName, bool enabled); + bool get_pass_enabled(const std::string& passName) const; + + // Hot reload all changed shaders + void hot_reload_shaders(); + + // ------------------------------------------------------------------------ + // Statistics (read-only) + // ------------------------------------------------------------------------ + + Stats get_stats() const; + + // ------------------------------------------------------------------------ + // Picking / Selection + // ------------------------------------------------------------------------ + + struct PickResult + { + bool valid{false}; + std::string ownerName; + glm::vec3 worldPosition{0.0f}; + }; + + // Get last click selection result + PickResult get_last_pick() const; + + // Enable/disable ID buffer picking (vs CPU raycast) + void set_use_id_buffer_picking(bool use); + bool get_use_id_buffer_picking() const; + +private: + VulkanEngine* _engine; +}; + +} // namespace GameAPI