fix(vulkan,rendering,editor): vkQueueSubmit2 migration + deferred texture upload + async drag-drop - #774
Closed
JeanPhilippeKernel wants to merge 144 commits into
Closed
fix(vulkan,rendering,editor): vkQueueSubmit2 migration + deferred texture upload + async drag-drop#774JeanPhilippeKernel wants to merge 144 commits into
JeanPhilippeKernel wants to merge 144 commits into
Conversation
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* ci(build): drop macOS x64, add Ninja preset for Windows, cache LLVM and deps - Remove macOS x64 from all CI jobs and publish workflows; arm64 only - Add Windows_x64_Debug/Release_Ninja presets using Ninja Multi-Config generator - Add -Generator param to BuildEngine.ps1 (VisualStudio default for local dev) - CI passes -Generator Ninja to avoid MSBuild solution-parse hang - Add ilammy/msvc-dev-cmd to expose cl.exe/link.exe to Ninja on CI - Cache LLVM 20.1.7 and FetchContent _deps keyed on dependencies.cmake - Fix job-clangformat.yml missing on: workflow_call trigger - Remove stale matrix/srcDirectory reference in Engine-CI.yml * ci(build): skip LLVM install when already present on runner * ci(build): pin LLVM to 22.1.0 on Windows CI to match local clang-format version * ci(build): exclude workspace from Windows Defender to eliminate post-configure hang * ci(build): enable FETCHCONTENT_UPDATES_DISCONNECTED on Ninja preset to skip dep update checks * ci: skip build when only non-source files change * ci(build): revert Ninja generator, restore VS generator for Windows
…ne::Initialize (#575) * feat(core): add IsInitialized() to Logger and ThreadPoolHelper * feat(core): add IsInitialized() to Logger and ThreadPoolHelper * feat(engine): move VFS into EngineContext, pass MemoryManager to Engine::Initialize
…ntation (#576) * feat(core): add IsInitialized() to Logger and ThreadPoolHelper * feat(core): add IsInitialized() to Logger and ThreadPoolHelper * feat(engine): move VFS into EngineContext, pass MemoryManager to Engine::Initialize * docs(engine): reevaluate engine lifecycle doc against current implementation
* fix(engine): align shutdown sequence with engine-lifecycle spec - Fix shutdown order in Engine::Deinitialize(): set terminate flag first, join render thread, then AssetManager::Shutdown, AppRenderPipeline::Shutdown, VFSContext::Shutdown, VulkanDevice::Deinitialize, GameWindow::Deinitialize (was: Window first while render thread still running — use-after-free on surface) - Remove false s_request_terminate reset in Engine::Dispose() - Move AssetManager::Shutdown from Dispose into Deinitialize (correct order) - Move OnClosing() from GameApplication::Shutdown into Engine::Run(), firing before Deinitialize while all subsystems are live (spec: before shutdown Step 1) - Move OnClosed() to EntryPoint.cpp after manager.Shutdown(), before CrashHandler::Uninstall() (spec: after Step 17, before Step 18) - Add VFSMountTable::Clear() and VFSContext::Shutdown() (spec: shutdown Step 12) - Add IVFSContext::Shutdown() pure virtual - Add ThreadPoolHelper::Shutdown() static method; call it in EntryPoint.cpp (Step 15) - Add Logger::Flush() before Logger::Dispose() in EntryPoint.cpp (Step 16) - Update engine-lifecycle.md checklist to reflect completed items * fix(editor): move VFS mount from OnInitializing to OnInitialized Engine::GetContext()->VFS is null during OnInitializing — the engine has not been initialized yet at that call site. WorkingSpaceBackend can be initialized in OnInitializing (no engine dependency), but the Mount call must happen in OnInitialized where all engine subsystems are live. * fix(tests): add Shutdown() stub to MockVFSContext IVFSContext::Shutdown() is now a pure virtual; the mock in VFSScanner_test must implement it to remain concrete. * fix(renderer): add missing depth prepass fragment shader; enable VMA/validation debug - Add depth_prepass_scene.frag (no-op) — MoltenVK requires a fragment function even on depth-only pipelines; its absence caused a null-pointer dereference inside MVKGraphicsPipeline::newMTLRenderPipelineDescriptor - Enable ENABLE_VULKAN_VALIDATION_LAYER for Debug builds - Enable VMA_DEBUG_INITIALIZE_ALLOCATIONS and VMA_DEBUG_DETECT_CORRUPTION for Debug builds; VMA leak reports are written to stderr via VMA_DEBUG_LOG_FORMAT * fix(vma): fix dedicated image leak on resize and add VMA_DEBUG_MARGIN - DeviceSwapchain::Present() was calling Texture::Dispose() (a no-op) instead of Image2DBuffer::Dispose() when draining TextureHandleToDispose, causing every render target discarded during RenderGraph::Resize to leak its VMA dedicated allocation for the lifetime of the session - VulkanDevice::Deinitialize() now drains TextureHandleToDispose before GlobalTextures/Image2DBufferManager teardown, using the same correct Image2DBuffer::Dispose() path, so any residual entries at shutdown are also freed - Replace VMA_DEBUG_INITIALIZE_ALLOCATIONS with VMA_DEBUG_MARGIN=16 so VMA_DEBUG_DETECT_CORRUPTION actually places guard bytes; without a non-zero margin the corruption checker was a silent no-op Verified: 102 dedicated allocs, 102 frees, no assertion on clean shutdown * fix(vma): route VMA debug log to OutputDebugStringA on Windows On Windows, fprintf(stderr, ...) is not visible in the Visual Studio Output window. Route VMA_DEBUG_LOG_FORMAT through OutputDebugStringA (and still mirror to stderr) on _WIN32; keep the plain fprintf path on Linux and macOS. * style: apply clang-format to VMA_DEBUG_LOG_FORMAT macro * fix(engine): align lifecycle with spec — AppRenderPipeline ownership and OnClosing ordering Finding 14: move AppRenderPipeline construction out of Engine::Initialize into GameApplication::Initialize, after the device is live and before OnInitialized fires. Removes the static g_appRenderPipeline pointer; Deinitialize now shuts it down via g_app->RenderPipeline. Finding 16: split the shutdown signal into two flags so OnClosing fires while all subsystems are fully live. OnEngineClosed now sets s_close_requested (exits the main loop) instead of s_request_terminate. Deinitialize sets s_request_terminate to stop the render thread. OnClosing in Engine::Run fires between the two, matching the spec requirement that the game DLL can save state before any teardown begins. * style: clang-format Engine.cpp after lifecycle fix
New documents: - gpu-allocator-rearchitecture.md: VMA v3.3.0 redesign with segregated pools, StagingRing, DeferredFreeQueue, and GpuMemoryDomain budget - per-frame-upload-heap.md: single 64 MB bump-pointer frame heap replacing per-upload vkAllocateMemory - rendering-flow.md: full per-frame pipeline from main thread to GPU submission, post-rearchitecture flow changes and known gaps - render-graph-redesign.md: production-grade render graph with automatic barrier insertion, transient memory aliasing, RGTransientPool, kAccessTable, and compute pass support - draw-call-sorting.md: 64-bit sort key, LSD radix sort, opaque front-to-back and transparent back-to-front split, DrawCommandBuffer - compute-pipeline.md: ComputePipeline struct, CommandBuffer::Dispatch, ShaderType::COMPUTE, IComputeCallbackPass, three concrete examples Correctness fixes to existing documents: - render-resource-manager.md: FRAMES_IN_FLIGHT 2->3 in checklist, staging path updated to Ring.Allocate, DeferredRelease description corrected, EndFrame DeferFree call fixed - gizmo-3d-pass.md: raw VkDeviceMemory fields replaced with VmaAllocation - shadows.md: ShadowUniformBuffer upload attributed to PerFrameUploadHeap - post-processing.md: all passes converted to DOD vtable pattern, SSAOPass converted, checklist and file layout updated - render-graph-integration.md: post-process registration updated to PostProcessStack::AddPass factory pattern, enable/disable section updated to PostProcessStack::SetEnabled - profiling.md: Execute examples updated to free-function signatures
…nd test suite (#578) - Add LogChannel (10 channels) and LogLevel enums to Logger.h - Replace std::function LogEventHandler with plain fn-ptr + void* struct (zero allocation) - Add unified Logger::Log(channel, level, string_view) dispatch - Add per-channel runtime level gate via constexpr k_min_level[] table - Fix unguarded write in AddEventHandler; replace copy-on-log pattern with shared_mutex - Implement arena-backed ring buffer (s_log_message_rb / s_log_raw_string_rb) - Add Logger::FlushRingBufferToCrashLog; wire into CrashHandler via SetPreCrashCallback - Add LoggerConfiguration::CrashLogDir and RingBufferSize fields - Replace LoggerDefinition.h macros with channel/level compile-time filtering system - Add backwards-compat ZENGINE_CORE_* aliases routed to ENGINE channel - Add Scripts/CMake/LoggingDefaults.cmake with per-channel per-build-type level defaults - Update LogUIComponent handler registration to use new LogEventHandler struct - Reorder LogMessage fields for 40-byte layout (was 48 bytes) - Fix Logger::Initialize to treat OutputDirectory as absolute when already absolute - Fix Logger::Dispose to call spdlog::drop() per logger and reset s_crash_log_dir - Add ZEngine/tests/Logging/Logger_test.cpp with 13 tests covering handler registration/removal, color derivation, backwards-compat macros, runtime level filter, ring buffer wrap, FlushRingBufferToCrashLog ordering, no-arg overload, and concurrent Add/Log thread safety - Update logging-policy.md to reflect implementation state and completed checklist
…defined before ZEngine target uses them
…MinLevelAllChannels for test override in Release builds
…_INFO is compile-time silenced in Release
…ots (#582) Add ZEngine/Input/ module: - InputTypes.h: InputActionType, InputBinding, InputAction, InputButtonState, InputAxisState, InputFrame (64 action slots, POD, networking-ready) - InputManager.h/.cpp: RegisterAction, BindKey, BindMouseButton, BindScrollAxis, Poll(GLFWwindow*), GetButton/GetAxis/GetAxis2D, GetMouseDelta/GetMousePosition/ GetScrollDelta; FNV-32 name hashing; scroll accumulator fed by GLFW callback Wire into engine: - EngineContext gains InputArena and InputManager* fields - Engine::Initialize creates InputManager in a dedicated budgeted arena, registers glfwSetScrollCallback trampoline to AccumulateScroll - GameApplication::Update calls InputManager::Poll before CameraController::Update - MemoryBudgetConfig gains Input SubArenaConfig (1 MB in Default/Editor/Server) - CMakeLists.txt: ZENGINE_SOURCES_INPUT glob for Input/*.cpp Tests: - 21 InputManager unit tests covering registration, binding API, default state, scroll accumulation, InputFrame trivial-copyability, and capacity limits
* feat(camera): redesign FlyCamera with InputManager integration Replace the event-callback camera system with a poll-based design that reads from InputManager, eliminating stuck keys, HiDPI sensitivity bugs, and raw GLFW event interception. FlyCameraInput / FlyCameraState (new): - Plain struct owning all input state: buttons, keys[512], mouse deltas, scroll delta, viewport-relative cursor position - Explicit state enum: Free, Orbit, Pan, Animating - Reset() clears all state; FlushDeltas() zeroes per-frame deltas FlyCamera: - Single OnUpdate(float dt) entry point — no more OnMouseMove/OnKeyDown - Explicit state machine — pan as first-class state, orbit entry/exit driven by AltDown+LeftDown, animation saves/restores previous state - All mouse math divides by m_logicalW/H — HiDPI correct - GetRayFromViewport takes viewport-relative logical coords - AdaptiveSpeed uses Hooks.Raycast; FocusOn uses Hooks.GetSelectionBounds - CameraSetting: remove unused MoveSpeed and FastMoveSpeed fields FlyCameraController: - Drops IMouseEventCallback / IKeyboardEventCallback inheritance - Initialize(InputManager*, ArenaAllocator*) registers 14 camera action slots (WASDQE as Axis1D, buttons, scroll via BindScrollAxis) - Update fills FlyCameraInput from InputManager then calls OnUpdate - PauseEventProcessing calls Input.Reset() — single guaranteed clear point - SetViewportOrigin stores viewport origin for scroll-toward-cursor ray EditorCameraController: - Initialize gains InputManager* parameter - Explicit CameraSetting with all fields; SetViewportSize in logical pixels - Default no-op Hooks wired at construction ICameraController: - Add Update, OnEvent, SetViewport, SetViewportOrigin, ResumeEventProcessing, PauseEventProcessing as pure virtuals SceneViewportUIComponent: - Remove reinterpret_cast to EditorCameraControllerPtr - Call SetViewportOrigin every frame from m_viewport_bounds[0] HierarchyViewUIComponent: - Remove EditorCameraController cast; F-to-frame handled by InputManager * fix(camera): bind both left and right Alt/Shift/Ctrl for orbit and fast-move
…ion (#581) * feat(memory): GpuAllocator rearchitecture — VMA pools, staging ring, deferred free Replace flat VMA usage in VulkanDevice with a GpuAllocator struct owning segregated memory pools, a persistent 64 MB staging ring, and a timeline-gated DeferredFreeQueue. Removes the DirtyCollector thread and all HandleManager-based dirty queues. Fixes: - C1: DEDICATED_MEMORY_BIT hardcoded on every image — removed, now driver-queried via vmaCreateImage - C2: WriteTextureData staging used HOST_ACCESS_RANDOM — replaced with StagingRingBuffer (sequential write, persistent map) - C3: VmaAllocatorCreateInfo had no flags — BDA and budget bits wired - C4: No vmaGetHeapBudgets call — SampleBudgets() added, called from TickMemory() each frame - H2: Per-upload vmaCreateBuffer/vmaDestroyBuffer — replaced with ring allocation; one-shot fallback on ring-full - H3: UniformBuffer missing ALLOW_TRANSFER_INSTEAD — domain flag set - H5: ClearBuffer skipped vmaFlushAllocation on non-coherent memory — fixed Key changes: - GpuAllocator.h/cpp: VMA segregated pools per GpuMemoryDomain, StagingRingBuffer (64 MB persistent map, timeline-drained), SampleBudgets with vmaCalculateStatistics fallback, VMA leak check on shutdown; VMA_STATIC_VULKAN_FUNCTIONS=1 to avoid dynamic dispatch blocking under api_dump layer - DeferredFreeQueue.h: inline Enqueue/Drain replacing DirtyCollector thread; dispatches FreeBuffer/FreeImage/vkDestroy* by entry kind - VulkanDevice.h/cpp: GpuMem + PendingFree members; TickMemory/DeferFree wired; CreateBuffer/CreateImage route through GpuMem; typed buffer callsites pass GpuMemoryDomain; double PendingFree.Drain at shutdown to free all Dispose-enqueued resources before vmaDestroyAllocator - AsyncResourceLoader.h/cpp: UploadFromStagingBuffer and ClearBuffer use ring with one-shot fallback; retire loops use GpuMem.FreeBuffer - DeviceSwapchain.cpp: AcquireNextImage calls TickMemory; Clear uses DeferFree - Rendering callsites (Fence, Semaphore, Attachment, Pipeline, FrameBuffer, Shader): migrated to DeferFree - ZEngine/tests/CMakeLists.txt: fix IN_LIST check for Xcode generator - docs: gpu-allocator-rearchitecture.md corrected Verified: engine initializes and renders on Apple M4 Pro via MoltenVK; VMA reports no leaks on clean shutdown. * feat(rendering): implement PerFrameUploadHeap steps 5-10 Migrate camera UBO and indirect commands from per-buffer VmaAllocation to the PerFrameUploadHeap, and remove UniformBuffer/IndirectBuffer types. Step 5 — Shader::MarkBindingAsDynamic: patches binding type to UNIFORM_BUFFER_DYNAMIC, rebuilds the VkDescriptorSetLayout, recreates the descriptor pool and reallocates descriptor sets for the patched set. Step 6 — Camera UBO migration: GraphicRenderer::DrawScene pushes UBOCameraLayout into FrameHeaps[fi] each frame; CameraHeapOffset stored in SceneData; SetInputFromHeap writes UNIFORM_BUFFER_DYNAMIC descriptor; BindDescriptorSets counts actual dynamic bindings before passing offsets to avoid validation errors on passes without dynamic UBOs. Step 7 — Indirect draw migration: AppRenderPipeline pushes VkDrawIndirectCommand array into the heap; IndirectHeapOffset and IndirectCommandCount stored in SceneData; DrawIndirect overload takes VkBuffer + offset + count directly from the heap. Step 8 — Remove SceneCameraBufferHandle and IndirectBufferHandle from SceneData and GraphicRenderer; remove CreateUniformBufferSet and CreateIndirectBufferSet call sites. Step 9 — Remove IndirectBufferSetManager accessors from active pass Execute methods. Step 10 — Full cleanup: remove UniformBuffer, UniformBufferSet, IndirectBuffer, IndirectBufferSet type definitions and all associated HandleManager specializations, Create* methods, and implementations from VulkanDevice; remove GetBufferUniformSet, GetIndirectBufferSet, AttachBuffer(UniformBufferSetHandle) from RenderGraph; remove SetInput(UniformBufferSetHandle) from RenderPass; update DrawIndirect and DrawIndexedIndirect to take VkBuffer + offset directly. Verified: engine initializes and renders with dynamic UBO offsets on Apple M4 Pro via MoltenVK. * feat(rendering): move env map to project assets, disable skybox when absent Environment map is now a project-owned asset, not an engine resource. - Remove Resources/Editor/Settings/EnvironmentMaps/ from engine source; bergen_4k.hdr and bergen_4k.zenvmap no longer ship with the engine - Add environmentMapDir field to project.json (default: Assets/EnvironmentMaps); env map filename referenced under sky.environmentMap - EditorConfiguration gains EnvironmentMapImportPath and ActiveEnvironmentMapPath; ReadConfig parses both from project.json - GameApplication::GetActiveEnvironmentMapPath() virtual hook; Editor overrides it to return Configuration->ActiveEnvironmentMapPath - AppRenderPipeline::ActiveEnvironmentMapPath forwarded to SceneRenderer before Initialize so GraphicRenderer::Initialize sees it - SkyboxPass: EnvMapPath field set by GraphicRenderer before Setup; pass is disabled via RenderGraph node when path is null or not found in VFS; Execute bails early when m_env_map is invalid - Env map importer output path changed from Settings/EnvironmentMaps to EditorConfiguration::EnvironmentMapImportPath (project/Assets/EnvironmentMaps) - Add SampleProject/ to .gitignore — user projects are not engine source - docs/future-plan/sky-rendering.md: detailed design for SkyAtmospherePass, SkyLightPass, HDRIBackdropPass, SkySpherePass, and SkySystem Verified: engine runs without env map — skybox pass disabled, grid renders cleanly, no validation errors. * feat(config): adopt assetDirs structure, expand EditorConfiguration paths Align EditorConfiguration with the new project.json assetDirs layout: - Replace DefaultImportTexturePath/DefaultImportSoundPath with TexturePath, SoundPath, MeshPath, MaterialPath, SpritePath, EnvironmentMapImportPath — all resolved under Assets/ - ReadConfig: parse assetDirs (new format) with backward-compat fallback to defaultImportDir for existing project files - $(workingSpace) token expanded for all assetDirs entries - DockspaceUIComponent: use TexturePath instead of removed DefaultImportTexturePath for texture import output path * docs(config): update project.json examples to assetDirs structure * feat(rendering): per-scene sky config with runtime ApplySkyConfig Sky configuration (mode + environment map) is now fully per-scene. Each .zescene carries its own SkyConfig; switching scenes replaces the skybox immediately on the render thread. - SkyConfig lives only on RenderScene — removed from SceneData (GPU buffer handles have no business storing sky mode) - RenderScene gains SkyDirty[3] atomic flag, set for all 3 frame slots when a scene is deserialized; consumed by AppRenderPipeline::RenderScene - GraphicRenderer::ApplySkyConfig(const SkyConfig&) replaces the init-time resolve_skybox lambda; resolves env map via VFS, sets SkyboxPass::EnvMapPath, toggles NodeMap["Skybox Pass"].Enabled - GraphicRenderer::Initialize always registers Skybox Pass as disabled; first scene load with IsHDRI() == true enables it - Remove ActiveEnvironmentMapPath from IRenderer, AppRenderPipeline, GameApplication, and EditorConfiguration — no longer needed since sky config flows through scene load, not renderer init - Remove GetActiveEnvironmentMapPath() virtual from GameApplication and Editor; remove implementation from Editor.cpp - Remove sky.environmentMap / legacy environmentMap read from EditorConfiguration::ReadConfig — project.json no longer owns sky - Bump SCENE_FILE_VERSION 1.0.0 -> 1.1.0 (breaking: sky fields added to .zescene binary stream; old files are not forward-compatible) * style: apply clang-format * fix(rendering): default clear color to black when no sky is active * fix(rendering): use dark carbon (#1C1C1C) as default clear color * fix(vulkan): remove api_dump from default validation layer stack api_dump intercepts every Vulkan call and serializes full parameter structs — 50+ MB per run at 60fps. It belongs in targeted debugging sessions only, enabled via VK_INSTANCE_LAYERS env var. * fix(rendering): skip SkyboxPass Verify() when env map is not set
(#584) Frame color/depth render targets and g-buffer attachments were hardcoded to 1280x780. When the editor viewport panel was larger the scene geometry was clipped at the RT boundary. All transient render targets now initialise to SwapchainImageWidth/Height at startup. The existing resize path via RenderTargetResizeRequests handles subsequent viewport changes. Also updates input-system.md to include BindScrollAxis and scroll delta accessor documented during the InputManager session.
…inotify, RDCW) (#580) Introduces a file watching system wired into VFSContext: Platform backends: - VFSFSEventsWatcher (macOS): FSEvents-based, run-loop threaded, stream rebuilt on the run loop thread via CFRunLoopPerformBlock - VFSInotifyWatcher (Linux): inotify-based with poll + wake pipe, recursive subtree support via IN_ONLYDIR and auto-watch of new directories - VFSRDCWatcher (Windows): ReadDirectoryChangesW over an IOCP, recursive flag forwarded to the OS VFSFileWatcher: - Debounce layer (default 80 ms) over any IVFSPlatformWatcher - Path-prefix routing to per-watch callbacks; longest-prefix match wins - Overflow events broadcast to all registered watchers VFSContext integration: - InitWatcher allocates the platform and file watcher from the arena (ZAlloc + placement new) and wires a root watch that invalidates VFSDirectoryCache and triggers VFSScanner on each event - ShutdownWatcher stops the background thread and calls destructors explicitly; arena owns the memory - Tick() drives the debounce flush each frame - All silent failure paths log via ZENGINE_LOG_VFS_ERR API: - IVFSPlatformWatcher::Poll uses a C function pointer void(*)(void*, const VFSWatchEvent&) instead of std::function to avoid heap allocation on the hot poll path - CMakeLists: add VFSFSEventsWatcher.mm to ZENGINE_SOURCES_CORE on Apple and link CoreServices framework Tests: VFSFileWatcherTest (mock-based), vfs_fsevents_test, vfs_inotify_test, vfs_rdc_test cover creation, modification, deletion, rename, overflow, debounce, routing, and recursive watching. Co-authored-by: Jean Philippe <jeanphilippe02666592@outlook.fr>
… (Ticket 5) (#585) Introduces MetaFileIO, MetaFileData, and ImportStatus so every project asset receives a stable UUID on first import that survives editor restarts, reimports, and multi-engineer checkouts. - Add ZEngine/Core/VFS/Meta/{ImportStatus,MetaFileData,MetaFileIO}.h/.cpp - SHA-256 hashing (self-contained, no new dependency) - Atomic write protocol: write to .tmp then rename, crash-safe - GetOrCreate: New / Stale / UpToDate classification per asset - Add AssetManager::GetOrCreateUUID replacing per-launch uuid_random_generator - Extend VFSScanner: MetasCreated/Updated/UpToDate stats, all atomics converted to PaddedAtomic, callback changed to void*/fn-ptr to avoid heap - Add 10 unit tests in tests/VFS/vfs_meta_test.cpp (all passing)
Mark VFS tickets 1-6, InputManager, FlyCamera, logging policy, and engine lifecycle as Implemented. Mark GpuAllocator and profiling as Partially implemented. Update scene-serialization deps to reflect VFS 5-6 unblocking.
…iling, gpu-allocator - input-system: Partially implemented (InputManager done, ECS/networking pending) - engine-lifecycle: Partially implemented (core lifecycle done, ECS/audio/physics pending) - profiling: Partially implemented (macros/Tracy/MemoryProfiler done, GPU timestamps/overlay pending) - gpu-allocator: Implemented (all C1-C4/H2/H3/H5 bugs fixed in PR #581)
…on macOS release EXPECT_DEATH spawns a child process that calls OnCrash, which pumps the NSRunLoop waiting for a Cocoa dialog that never appears in headless CI. The budget is still validated by InitialisesWithinAssetManagerBudget.
…ler docs (#586) ECS core: - EntityID (generational index), ComponentTypeID (PaddedAtomic counter), ArchetypeMask (uint64_t bitmask, 64-type v1 cap with MaskBit/MaskMatches) - ComponentStorage<T>: sparse-set, generation check on Get/Has/Remove, swap-and-pop - EntityRegistry: MAX_ENTITIES=65536, free-list, generational slots - Scene: CreateEntity/DestroyEntity/AddComponent/ForEach with O(1) mask guard, SnapshotTransforms, FillRenderableTransforms for fixed-timestep interpolation - WorldCommands: deferred mutations (spawn/destroy/add/remove), fn-ptr callbacks - Query<Ts...>: reusable pre-computed ArchetypeMask wrapper - RenderableTransform: plain struct for per-frame renderer packet Actor system: - Actor: arena-allocated, no Ref<>/RefCounted, explicit lifetime via ActorManager - ActorManager: HandleManager<Actor*>, MAX_ACTORS=1024, Create<T>/Destroy/Tick/Shutdown - ECS::Components::TransformComponent: plain data (Vec3f pos/rot/scale + previousPosition) Engine wiring: - ECSArena sub-arena (128 MB budget), Scene/ActorManager/WorldCommands in EngineContext - ActorManager::Shutdown before Scene::Shutdown in Deinitialize lifecycle order Tests: - 19 tests in tests/ECS/ECSTest.cpp, passing Debug and Release - FSEvents drain: spin-poll loop (2s deadline) replaces fixed 400ms sleep Docs updated: - actor-ecs-architecture.md: Handle<Actor> rationale, MAX_ACTORS/MAX_ENTITIES, memory layout section, archetype and sparse-set concept explanations - system-scheduler.md: Why This Exists section, WorldCommands& in SystemFn, spin-yield barrier with cv.wait fallback, inline single-system wave path, commitlint reference (section 13)
- RegisterSystem(fn, deps) returns stable SystemID; MAX_SYSTEMS=64 - OrderBefore(a, b) declares explicit execution ordering - Commit(): BuildEdges detects conflicts and asserts on missing OrderBefore; TopologicalSort uses Kahn's algorithm to group systems into parallel waves; asserts on cycles - Tick(): single-system waves run inline (zero thread pool overhead); multi-system waves use spin-yield + cv.wait fallback barrier - WorldTick wired into EngineContext under ECSArena - 5 tests in SchedulerTest.cpp: independent wave, separate waves, write-before-read ordering verified via state, WorldCommands deferred spawn, three-system chain
* feat(engine): implement fixed-timestep game loop - ZEngine::Timing::FrameTimer — wall-clock delta, 250ms spike clamp, 8-sample smoothed delta for UI and camera - ZEngine::Timing::FixedTimestepAccumulator — fix-your-timestep pattern, 60Hz fixed step, spiral-of-death guard (max 5 steps), Alpha() for renderer lerp - ZEngine::Timing::FrameRateCap — sleep + spin-wait frame budget, 300fps default, bypassed when vsync is on - ZEngine::Timing::FramePacket — per-frame snapshot with Alpha and ECS RenderableTransform array; reuses ECS::RenderableTransform - Engine::MainThreadRun rewritten: FrameTimer -> accumulator -> fixed-step loop (WorldTick + Flush + ActorManager + SnapshotTransforms) -> alpha -> render payload -> frame rate cap; existing PrepareScene/overlay path preserved unchanged - Namespace ZEngine::Timing avoids collision with ZEngine::Engine struct * refactor: remove dead code — GraphicSceneEntity, serializers, ValidComponent, NameComponent GraphicSceneEntity and GraphicScene3DSerializer had all logic commented out and no external callers. ValidComponent and NameComponent had zero live references. The live rendering path (RenderScene, GraphicScene, Tetragrama) is unaffected.
- Add GltfImporter (fastgltf v0.9) for .glb/.gltf; AssimpImporter now handles .fbx/.obj only - Add fastgltf v0.9.0 dependency via FetchContent - Wire VFSBackendCaps::Write on WorkingSpaceBackend so MetaFileIO can create .meta sidecar files - Filter .meta files from VFSFileWatcher Enqueue to prevent self-import loop after sidecar creation - Fix AssetIndex::Register to allow empty-path records (sub-assets such as MESH_HIERARCHY have no VFS path) - Fire hot-reload callback in AssetRegistry::SetState when state transitions to Loaded; IngestMesh explicitly calls SetState for both mesh and hierarchy UUIDs so Editor callback enqueues handle into PendingOnLoadHierarchies - Fix missing NodeHierarchyUUID copy in AssetManager::IngestMesh - Stable arena-backed filenames for AsyncResLoader texture Submit calls - Guard duplicate ingest in GltfImporter; re-drop of already-loaded asset is a no-op (multi-instance deferred to RRM) - Add ImportCoordinator error log for MetaFileIO write failures - Flush spdlog on critical level so assert messages reach disk - Fix flaky FSEvents test: accumulate events across polls instead of clearing between iterations; bump timeout to 3000ms for CI headroom
…redesign (#590) * feat(rrm): RenderResourceManager, packed geometry buffers, and scene redesign Introduce RenderResourceManager (RRM) as the single authority over GPU resource lifetime, replacing AsyncResourceLoader and IGraphicBuffer entirely. Add packed global geometry buffers (256 MB vertex + 256 MB index) shared across all meshes, and redesign the scene system with a flat MeshInstance list protected by a seqlock. RenderResourceManager - Full GPU buffer and texture upload ownership (vertex, index, textures) - Dedicated transfer command buffer slot (GEOMETRY_UPLOAD_SLOT = 15) - Timeline semaphore infrastructure for texture retire - HostUniform buffers use VMA_MEMORY_USAGE_AUTO for HOST_VISIBLE guarantee on all GPU types (discrete and integrated) - RecordAndSubmit helper: wait-fence -> reset -> begin -> record -> end -> submit -> wait, using CommandBufferMgr directly Packed global geometry buffers - Two 256 MB device-local VkBuffers (m_global_vertex_buf, m_global_index_buf) - Append-only watermark cursor; per-mesh element-count offsets in MeshSlot - All shaders index into the global buffers via DrawData.VertexOffset / DrawData.IndexOffset - SetInputByBinding(set, binding, buffer) bypasses ValidateInput name lookup for VertexSB / IndexSB (SPIR-V Cross reflection gap) Scene system redesign - Replace node hierarchy with flat Array<MeshInstance> + seqlock (PaddedAtomic<uint64_t> m_seq: even = stable, odd = writing) - MeshInstance: { Id, MeshUUID, Transform, Name[128] } - GetInstancesSnapshot spins on odd seq, copies, verifies unchanged - Same-mesh re-drop handled via ImportCoordinator::Enqueue returning UUID - CachedDrawCmds[512] in SceneData fixes stale FrameHeap offset bug (heap resets every frame; draw commands now pushed unconditionally) - SCENE_FILE_VERSION bumped to 2.0.0; serializer writes instance array Shader / descriptor fixes - UBCamera (set=0, binding=0) declared UNIFORM_BUFFER_DYNAMIC at SPIR-V reflection time -- eliminates MarkBindingAsDynamic and the incomplete descriptor layout rebuild it caused - pool_needs_update_after_bind guard: UPDATE_AFTER_BIND_BIT only set when a binding actually uses it -- fixes Vulkan validation crash when pool has UPDATE_AFTER_BIND and layout has UNIFORM_BUFFER_DYNAMIC ImGui per-frame buffers - VBHandles[3] / IdxBHandles[3] replace single shared VBHandle / IdxBHandle to eliminate write-after-read flickering across frames * fix(vfs): StartThread blocks until FSEvents stream is live ReportsFileCreation was flaky on slow CI runners because StartThread() returned immediately after spawning the thread, before FSEventStreamStart was called. Any file written in that window was missed. Fix: add a condition_variable that the background thread signals once RebuildStream() completes (FSEventStreamStart called). StartThread() now blocks until the signal arrives before returning, so callers can safely produce filesystem events immediately after the call.
Full-file re-reads (not shallow grep checks) surfaced real drift between docs and shipped code across the completed/ and future-plan/ sets: - ui-system.md: font backend is FreeType not stb_truetype; "Migrated Editor Panels" and "ImGui Coexistence" sections described an intermediate dev snapshot superseded by ZUIPanelManagerComponent + Tetragrama/Panels/*; ImGui/ImGuizmo are unused dependencies, not coexisting passes; several widget names drifted. - vfs-ticket2-mount-backends.md: VFSMountTable::Resolve() picks the longest matching prefix, not first-priority-match; VFSDiskBackend has no path-traversal sandbox check at all (pure concatenation); VFSZipBackend serializes all decompression under one mutex instead of the planned lock-free per-file design, and VFSZipFile::m_decompressed is an unguarded bool (real data race on concurrent Read()); Array now has insert()/erase() so the manual-shift workaround is stale. - vfs-ticket6-asset-registry.md: CollectCascade never fails with OutOfMemory; it rehashes and continues. - asset-manager.md: memory layout is a direct parent Arena + separate 256MB TLSFSlab ContainerSlab, not a 512MB carve; dedup uses per-type maps (MeshToHierarchySlot/UUIDToTextureHandle/UUIDToMaterialSlot), not IsRegistered (which VFSScanner pre-registration makes unreliable here); UUIDToMaterialSlot was missing from the doc entirely. - logging-policy.md: k_min_level is a mutable array with SetMinLevel/ SetMinLevelAllChannels, contradicting the doc's own "runtime adjustment not supported" claim; LogMessageType alias was never added; LogUIComponent doesn't exist, the real panel is ConsolePanel (ZUI-based, not ImGui-based). - gpu-allocator-rearchitecture.md, render-resource-manager.md: moved back to future-plan/ — findings were severe enough that "completed" was the wrong status. GPU allocator's headline "VMA segregated pools" feature was never built (GpuAllocator::Pools[5] is populated nowhere); AsyncResourceLoader.cpp (its stated fix location) no longer exists. RRM's SwapKind::Buffer is silently dropped in EndFrame, ScheduleSwap is a stub for both resource kinds, the documented deferred-release ring and HandleManager/ABA protection don't exist. Filed #740 for the two real runtime-risk findings (buffer hot-reload no-op, no ABA protection). vfs-design.md, memory-allocator-audit.md, system-scheduler.md carry smaller corrections from the same pass (VFSDiskContext naming, 16-not-13 bug count, two fabricated test sub-bullets).
…738) * fix(vulkan): remove backward timeline clamp in swapchain recreation — closes #736 DeviceSwapchain::AcquireNextImage's recreation path resynced RenderTimelineNextValue to vkGetSemaphoreCounterValue's current 'completed' value after waiting on ImageInFlights/PresentCompletes. That wait set is sized SwapchainImageCount (3), while the engine allows up to FrameContextPoolSize (BufferedFrameCount * 4, e.g. 8-12) frames' command buffers to be concurrently in-flight — so 'completed' could be lower than the real signal target of a still-executing frame beyond those 3 waited-on slots. VulkanDevice::DeferFree unconditionally stamps every deferred-free entry (both this function's own Clear() and unrelated callers like RenderGraph::Resize disposing viewport render targets) with RenderTimelineNextValue. Rewinding that counter backward meant any resource deferred-freed shortly after made Drain() treat it as already safe to destroy — even while a genuinely in-flight command buffer from a frame beyond the waited-on slots still referenced it. This produced the 'vkDestroyFramebuffer/vkDestroyImage ... currently in use by VkCommandBuffer' validation errors (and crash on some drivers) reported on fast/rapid window or viewport resize. RenderTimelineNextValue is already a correct, strictly monotonic counter — it is incremented exactly once per real submission (the two ++RenderTimelineNextValue call sites, one per submit). It never needed external resyncing; removing the resync removes the only place it could move backward. Sanity-checked on macOS: 5 rapid resizes in immediate succession via System Events, Khronos validation layer active, no validation errors, engine remained alive throughout. The actual reported race is Windows + Intel iGPU driver timing specific and needs verification on that platform. * fix(vulkan): wait on full frame-context pool before swapchain recreation (#739) Option B from the #736 analysis, stacked on Option A (PR #738). ImageInFlights/PresentCompletes are sized SwapchainImageCount (typically 3), but the engine allows up to FrameContextPoolSize (BufferredFrameCount * 4, e.g. 8-12) frames' command buffers to be concurrently in-flight. The existing wait loop covers only the smaller set, so a command buffer using a frame context beyond those tracked slots could still be executing when recreation proceeds to destroy old resources. Add a wait over every FrameContexts[i].Fence (checking Submitted state first, mirroring the existing PresentCompletes pattern) before Clear() and the timeline read. This closes the actual undersized-wait gap independently of Option A's timeline-clamp fix — even if some other code path reintroduces a timeline miscalculation in the future, this ensures recreation genuinely waits for the full in-flight depth the engine allows before destroying anything. Sanity-checked on macOS with both fixes stacked: 15 rapid random resizes, Khronos validation layer active, no errors, engine remained alive.
Calling Log before Initialize (or after Dispose) divided by a zero-sized ring buffer and could recurse through assert macros. Guard with IsInitialized() and cover before-init / after-dispose paths. Fixes #746 Co-authored-by: Yuzhong Zhang <BetterAndBetterII@users.noreply.github.com>
…ools, RRM hot-reload (#748) * feat(render-graph): real topological sort with RAW/WAW/WAR hazard edges BuildTopology was a flat insertion-order loop despite RGPass already recording per-pass Reads/Writes — no actual dependency sort ever ran. Binds each resource's readers directly to its sole writer regardless of declared order, which is what lets a pass registered before its producer get fixed instead of just reproducing declaration order (a single forward scan can never do this, since it can only emit edges to a later index — checked this against a first design that compiled fine but, per its own tests, could never reorder anything or detect a cycle). Falls back to declaration order and logs on a cycle instead of crashing. Also fixes the resulting Compile() ordering bug: BuildLifetimes must run after BuildTopology now, since it indexes by sorted execution order, and one unrelated pre-existing bug found in the same function (a resource index compared against a pass index, which could never match). 7 new device-free tests in RenderGraphTest.cpp. * feat(gpu-allocator): real segregated VMA pools for GpuAllocator domains Pools[5] was declared but never populated — every allocation went through VMA's default pool regardless of domain. Adds real vmaCreatePool calls for DeviceGeometry/DeviceTexture/HostUniform/HostStaging (RenderTarget intentionally stays unpooled — it benefits from VMA's automatic dedicated-allocation promotion, which pooling would work against). Wired into AllocateBuffer/AllocateImage with a fallback retry against the default pool on exhaustion. Building and testing this against a real headless Vulkan device (no window/surface needed — GpuAllocator only touches raw Vulkan handles) caught three real bugs no amount of review would have found: - VK_ERROR_OUT_OF_POOL_MEMORY doesn't exist in this VMA version at all — it's a different Vulkan concept (VkDescriptorPool exhaustion). VMA signals VmaPool exhaustion via VK_ERROR_OUT_OF_DEVICE_MEMORY instead. - A fixed-blockSize pool sized exactly equal to an allocation's byte count fails — a block needs alignment headroom beyond its raw size. Fixed HostStaging (built around the ring's exact-size buffer) and DeviceTexture (single large textures can exceed a fixed block) with blockSize=0 (auto-sized), which also preserves dedicated-allocation fallback for oversized textures. - Every custom pool must be destroyed before the VmaAllocator, or vmaDestroyAllocator hits VMA's own internal assert. 4 new tests against a real headless Vulkan device in GpuAllocatorTest.cpp. * feat(rrm): implement hot-reload swap mechanism + ABA fix for handle pools ScheduleSwap(BufferHandle,...) and ScheduleSwap(ImageHandle,...) were both literal no-op stubs — neither ever constructed a SwapEntry or incremented m_swap_count, so EndFrame's swap-processing loop never ran for either kind (the Image branch was correctly written but permanently unreachable, since the queue it drained was always empty). ScheduleSwap now enqueues onto a mutex-guarded m_pending_swaps queue — a dedicated mutex, not m_pending_mutex, which SubmitTextureFile holds across file I/O and a GPU call. A new FlushPendingSwaps (render-thread only, called from BeginFrame) drains it and applies each swap immediately: no frame-in-flight delay, since tracing every consumer of RRM handles in this engine found none that needs one (the old SwapSafeFrame gating was independently broken anyway — it compared against a wrapped 0..2 swapchain slot index that could never satisfy frame_index + FRAMES_IN_FLIGHT). Mesh swaps repoint the slot's offsets at freshly-appended data via a new shared AppendMeshData helper (no GPU free needed — the global buffer is append-only); image swaps re-upload and DeferFree the old image. SwapEntry/m_swaps/EndFrame's old drain loop are removed entirely — the design collapsed to one stage, not two. Also fixes the ABA bug: AllocMeshSlot/AllocImageSlot/AllocGBufSlot assigned a deterministic idx+1 generation on slot reuse, so a stale handle from a released slot could alias whatever got allocated into it next. Now assigns a never-reset-by-Release monotonic counter per slot. Also fixes DoUploadTexture silently overwriting AllocImageSlot's already-correct generation with the old idx+1 scheme right after allocating it — found while extending AllocImageSlot for the ABA fix. New tests are honestly skip-gated in RenderResourceManagerHotReloadTest.cpp: no test in this codebase constructs a real RenderResourceManager today, and doing so needs a fully-initialized VulkanDevice (Arena, GpuMem, a DeviceSwapchain with a real timeline semaphore, ThreadPoolHelper::Pool, CommandPool/CommandBuffer) — a materially bigger fixture than GpuAllocatorTest's raw-Vulkan-handle approach, left as a documented follow-up rather than distorting the class for testability. Fixes #740. * style: apply clang-format to this month's rendering-foundation changes
#751) RRM's own ImageHandle/m_image_slots texture system had zero real consumers and was disconnected from what actually renders (materials sample via a raw bindless index into Device->GlobalTextures). Delete it in favor of Rendering::Textures::TextureHandle everywhere, and fix the three structural gaps that came with it: - No working texture disposal: TextureHandleToDispose had a consumer but no producer. VulkanDevice::DestroyTexture is now the sole producer, timeline- gating both the VkImage free and the bindless slot reclaim in Present(). - No real hot-reload trigger: AssetManager::IngestTexture's dedup silently blocked re-ingest. A new TextureImporter routes texture files through ImportCoordinator like every other asset type, and a dedup hit now calls RenderResourceManager::ScheduleTextureReload instead of no-op'ing. - No reference safety: materials stored texture refs as a bare uint64_t index with no generation. AssetManager::ReleaseTexture/FlushTextureReleases patch every referencing material to the INVALID_MAP_HANDLE sentinel before the underlying bindless slot can ever be reused. Also: VulkanDevice::ReconstructTexture generalizes the in-place resize pattern (same handle, same slot) previously duplicated inline in RenderGraph::Resize; TextureHandleToDispose is now a lock-free SPSC queue since producer and consumer are both render-thread only; AssetRegistry:: InferTypeFromExtension recognizes all 8 raster extensions TextureImporter claims plus the pre-existing .exr gap; Image2DBuffer renamed to ImageBuffer (it holds 2D, cube, and array images, not just 2D); and the fully dead Texture2D.h/.cpp (a superseded Ref<T>-based texture class, zero callers) is removed. Adversarially reviewed in 4 parallel passes; 2 real bugs found and fixed (an unlocked concurrent read, and a missing arena Clear() that would have grown unboundedly and crashed on exhaustion). 554/554 tests passing, 5 new (AssetRegistry extension coverage + OnRemoved callback firing, TextureImporter::CanImport coverage). Verified live in Obelisk under an aggressive resize stress test with no leaks or crashes.
…e redesign (#752) The doc still described RRM's ImageHandle/GPUImage/ScheduleSwap(ImageHandle,...) system as if it existed. PR #751 deleted that system outright (zero real consumers) in favor of Rendering::Textures::TextureHandle everywhere. Update the status header, naming note, and the two most misleading checklist items to point at the real current API (IngestTexture, ScheduleTextureReload, ReleaseTexture, GetTexture, VulkanDevice::DestroyTexture) instead of the removed one. Sections 2-10's prose is left as-is and explicitly scoped as accurate for buffers/meshes only, consistent with this doc's existing correction-callout convention.
… fallback (#754) * docs(rrm): correct render-resource-manager.md for the texture pipeline redesign The doc still described RRM's ImageHandle/GPUImage/ScheduleSwap(ImageHandle,...) system as if it existed. PR #751 deleted that system outright (zero real consumers) in favor of Rendering::Textures::TextureHandle everywhere. Update the status header, naming note, and the two most misleading checklist items to point at the real current API (IngestTexture, ScheduleTextureReload, ReleaseTexture, GetTexture, VulkanDevice::DestroyTexture) instead of the removed one. Sections 2-10's prose is left as-is and explicitly scoped as accurate for buffers/meshes only, consistent with this doc's existing correction-callout convention. * fix(vulkan): guard QueueSubmit's optional args, fix GetQueue transfer fallback VulkanDevice::QueueSubmit(wait_stage_flag, command_buffer, signal_semaphore, fence) declares signal_semaphore/fence with nullptr defaults but dereferenced both unconditionally — any caller relying on the documented default crashed. Guard every dereference; skip the fence wait when no fence was given instead of trying to wait on nothing. VulkanDevice::GetQueue(TRANSFER_QUEUE) computed the family index with the correct HasSeperateTransfertQueueFamily fallback, but looked up the queue *handle* using the unadjusted type, which m_queue_map never has an entry for on devices without a separate transfer queue family — a hard abort via UnorderedHashMap::at. Mirror QueueWait's existing type-reassignment guard. Closes #741, #743.
… and scan (#758) Three independent UUID-minting paths could diverge for the same file: artifact .meta writes never captured the mesh's own embedded UUID, VFSScanner minted a random UUID for self-describing files that already had one baked in, and mesh-import texture UUIDs never touched MetaFileIO at all. Align all three around the file's own stable UUID so AssetRegistry::FindByUUID stays reliable after a re-scan, not just during the same-session fresh import. Fixes #755
…rop (#756) * fix(editor): wire up Content Browser context menus, File menu, and drag-and-drop Content Browser: - Create Folder/Rename/Delete context menus and modals were built but never wired correctly — a popup-timing race meant the modal always closed itself before it could render, and the name field never received keyboard focus. - Polished the modal UI (centered, right-aligned buttons, Enter/Escape, disable-when-empty, red Delete styling, pre-selected Rename text). - The grid's context menu used one shared popup key for every card, so opening one card's menu opened every card's; fixed with a per-card key. - Right-click on empty grid space now opens New File / New Folder. File menu: - New Scene only cleared the selection — now actually clears the scene (ActorManager::DestroyAll + scene->Reset()). - Open Scene just logged a hint; now opens a native file dialog defaulting to the project's scene folder. - Save Scene / Save Scene As constructed the serializer as a local stack variable handed to a background thread pool that outlived it — a dangling-pointer bug. Scene serialization is being rebuilt from scratch (#713-#719), so these are stubbed behind Editor::SaveScene/SaveSceneAs rather than patched against a format about to be replaced. - Quit called glfwSetWindowShouldClose, which nothing in the main loop reads — added Engine::RequestClose(), which does what the real OS window-close path does. - Removed a leftover dev shortcut that fired a window-close event on every Escape press, regardless of context — harmless when this was a bare test window, but closes the whole editor today when Escape cancels a modal. - Deleted Tetragrama/Messengers/ and MessageToken.h: every token they carried had zero consumers repo-wide, leftover from the pre-ZUI editor. Native file dialog: - NSOpenPanel's runModal blocked the main thread for as long as the dialog was open, freezing the whole engine loop. Switched to beginWithCompletionHandler, bridged into the existing coroutine scheduler. Adds a default-directory and message parameter (Windows/Linux best-effort, untested on this machine). Drag-and-drop: - Two bugs in ZUIInteractionPass made every drag-and-drop in the app non-functional: the drop target was read from ctx->HotKey, which is frozen while the mouse is held, so it always resolved to wherever the drag started rather than where it was released; and the payload length was zeroed in the same statement that reported the drop as successful, so every drop reported success with an empty payload. - Dropping a .zemesh/.zescene onto the viewport now resolves the VFS-style drop path to native and ingests the mesh explicitly (the registry's auto-ingest path is dead code, tracked separately in #755). Material/ texture resolution is intentionally left out — the mesh's embedded material UUID and the registry's UUID for that file come from independent, uncoordinated code paths today (#755). - Added a small drag-ghost indicator so a drag in progress is visible. Also fixes the Inspector's generic field renderer reinterpreting a uuids::uuid as a C string instead of formatting it, and runs clang-format over the touched trees. * fix(windows): remove invalid WinRT SuggestedStartFolder call FileOpenPicker has no SuggestedStartFolder API — WinRT pickers only support a fixed PickerLocationId enum, not an arbitrary start folder, which broke the Windows CI build (C2039). default_dir/message have no effect on Windows as a result; ComputerFolder is used unconditionally.
#757) * feat(ecs): add component button * feat(ecs): AddComponentRaw * fix(ecs): move Add Component button to header, fix popup edge clipping Move "+ Add Component" from below the component list into the actor header next to the name field, matching the reference layout. The relocated button sits flush against the panel's right edge, which exposed a real bug: ZUIOpenPopup defaults to opening at the click position with no edge clamping, so the popup's component list was clipped off the window's right edge. Anchor it leftward from the button's own screen position instead, same pattern ZUIBeginCombo already uses for its dropdown. --------- Co-authored-by: Jean Philippe <jeanphilippe02666592@outlook.fr>
…orter's own Arena (#761) AssetImporterPanel::m_local_arena is a 64KB arena sized for a handful of path strings, but AssetImporterPanel::StartImport() was also handing it to AssimpImporter/GltfImporter/FbxImporter::ImportFile as their scratch arena for node-hierarchy, mesh, material, and texture data — guaranteed to exhaust it on any real import (two Mat4f arrays alone need ~384KB). When the arena ran out, the OOM assert didn't halt execution without a debugger attached, so Array::reserve() silently left m_capacity inconsistent with a null m_data, crashing on the next push(). Each importer already carves its own generously-sized private Arena at Initialize() (128MB/64MB/512MB) and already uses it for the file-watcher hot-reload path (Import()) — ImportFile() just wasn't using it. Route ImportFile() through the same private Arena instead, matching each importer's own existing scratch-arena convention. Fixes #760
… on re-ingest (#763) IngestMesh silently dropped re-ingestion of an already-loaded MeshUUID, so re-importing a mesh through the GUI panel never reached the hot-reload swap path. Fixing that dead end surfaced three more bugs blocking the same real-world workflow: ImportFile minted a fresh random UUID on every re-cook (mesh and material both), the OnImportFileComplete .meta write never actually ran due to a bad path-prefix check, and ReadAssetMeshFileHeader was handed a VFS-relative path instead of a native one. - AssetManager::IngestMesh now overwrites the existing mesh/hierarchy slots in place on reload and calls the registry's new MarkStale (UUID-based, no VFSPath needed) instead of SetState(Loaded), which RenderResourceManager's OnReady callback ignores for already-uploaded UUIDs. - AssimpImporter/GltfImporter/FbxImporter's ImportFile now reads the destination's existing .meta to keep mesh and material UUIDs stable across re-imports instead of minting new ones each time. FbxImporter additionally remaps its SubMeshes' by-value MaterialUUID copies. - AssetImporterPanel::OnImportFileComplete's mesh .meta write now parses the already-VFS-relative output path directly instead of stripping a native workspace prefix that was never there, and resolves a native path before calling ReadAssetMeshFileHeader. A new block writes a .meta for every cooked material, which previously had none at all. - VFSFileWatcher's debounce map capacity raised from 64 to 256; a material-heavy import's new per-material .meta writes could push the pending-entry count past the load-factor assert. Verified live end-to-end for both meshes and materials: re-importing the same source keeps the same UUID (confirmed byte-for-byte in the .meta and the re-cooked artifact's own embedded UUID) and the viewport hot-swaps to the new geometry/material without a restart. Fixes #762
… the viewport (#766) * fix(assets): load materials/textures when dragging a cooked mesh onto the viewport Dragging an already-cooked .zemesh from the Content Browser onto the viewport (ViewportPanel::SpawnDroppedMesh) only ever loaded geometry and hierarchy via IngestMesh — it never resolved or ingested the mesh's referenced materials or their textures. If none of a mesh's materials happened to already be resident from an earlier import this session, the dropped mesh rendered with whatever material happened to occupy slot 0, i.e. an arbitrary, unrelated material. - AssetManager gains IngestMaterialFromUUID, the single-UUID counterpart to the existing (uncalled) ReloadFromDisk's material block: resolve the UUID via the registry, deserialize its .zematerial, and ingest it (which transitively ingests its textures). SpawnDroppedMesh now calls this for every submesh's MaterialUUID before the mesh data is moved into IngestMesh. - Reserved slot 0 in Materials/GPUMeshMaterials as an intentional magenta fallback material, created once at AssetManager::Initialize. AppRenderPipeline's render-time material lookup already defaulted an unresolved submesh to index 0 — without a reserved slot, that meant aliasing to whichever real material happened to be ingested first, silently rendering the wrong material instead of a recognizable "unresolved" signal. - GetAsset<AssetMaterial, uuid> now checks AssetRecord::IsLoaded(), not just non-null, closing a related false-positive: VFSScanner pre-registers every .zematerial UUID with SlotHandle=0 before any ingest happens, so the old check returned Materials[0] for any UUID that was scanned but never actually ingested. Verifying the above live surfaced two more, unrelated pre-existing bugs in the file-watcher path that made every freshly-imported texture fail to resolve: - VFSPath::FromNative (used by VFSContext's file-watcher callback) is just Parse — it has no notion of the project root, so a native watch-event path came out still fully absolute. Every downstream consumer (AssetRegistry, ImportCoordinator's importers) expects a workspace-relative VFSPath and prepends the workspace root itself, so the absolute one got double-prefixed. The watcher callback now strips the project root before parsing. - TextureImporter::Import used path.ToNative() (bare separator conversion) instead of ResolveNative(workspace_root, ...) for its own file probe, which only "worked" by accident while the path above was wrongly absolute already. Fixed to resolve against the workspace root like every other importer's native probe. A separate, still-open rendering bug (freshly-imported textures render as flat unlit background color despite fully correct data end-to-end) is tracked in #764 for dedicated GPU-level investigation. Fixes #764 * fix(vfs): wire up VFSScanner so pre-existing project assets get registered at startup InitWatcher was always called with cache=nullptr and scanner=nullptr, so VFSScanner was never constructed anywhere in the live engine — not even the reactive rescan-on-file-change ever ran (it's gated on m_scanner being non-null). The only way an asset's UUID ever reached AssetRegistry was a fresh import this session; anything cooked in a previous session and never touched since was invisible to any UUID-based lookup (e.g. IngestMaterialFromUUID from the previous commit), even though its .zematerial/.zemesh file was sitting right there on disk. - Engine::Initialize now constructs a real VFSScanner + VFSDirectoryCache and wires the scanner to the registry, then passes both into InitWatcher instead of nullptr, nullptr. - New VFSContext::ScanProject() walks the whole project once, via the now-live scanner, registering every asset it finds. - GameApplication::Initialize calls ScanProject() right after the project's own VFS backend is actually mounted at "/" — mounting happens after Engine::Initialize returns, so scanning any earlier would hit an empty mount table and silently register nothing. Confirmed via live process inspection: a material UUID that previously returned nullptr from AssetRegistry::FindByUUID (reported during manual testing of #766) now resolves to a Loaded record with a valid slot, without any import happening this session. * refactor(engine): move Engine::Initialize's static locals into EngineContext Every importer, the VFS scanner/directory cache, the app pointer, the render thread handle, and the two lifecycle flags were function-local statics inside Engine::Initialize (or free functions in the same translation unit) instead of living on the context object that already exists to own engine-lifetime state. Moves them all onto EngineContext: - GltfImporter/FbxImporter/AssimpImporter/EnvironmentMapImporter/ TextureImporter, VFSDirectoryCache, VFSScanner — were static locals inside Initialize(), addresses taken and handed to ImportCoordinator/ InitWatcher. EngineContext is arena-allocated once and never moved, so this is the same stable-address guarantee a static gave, just owned by the object whose lifetime they actually track. - App, RenderThread, RequestTerminate, CloseRequested — were namespace-scope statics used across Initialize/Run/Deinitialize/ MainThreadRun/RenderThreadRun. RequestTerminate/CloseRequested use PaddedAtomic<bool>, matching the rest of the codebase's convention for cross-thread flags instead of a bare std::atomic_bool. g_engine_ctx itself stays a free static — it's the pointer *to* the context, so it can't be a member of the thing it points to. No behavior change: same construction timing (ZPushStructCtor still placement-news real constructors), same addresses-are-stable-for- engine-lifetime property, same call sites otherwise untouched. * fix(gpu): stop DeviceGeometry/HostUniform VMA pools from exhausting immediately Both pools used a fixed block size exactly equal to their known-largest single allocation (DeviceGeometry: RRM's global vertex/index buffers, 512 MB each, blockSize=512 MB; HostUniform: PerFrameUploadHeap alone requests a full 64 MB, blockSize=64 MB). A VMA block needs bookkeeping headroom beyond its raw byte count, so a block exactly equal to an allocation's size can't actually fit it — this exact failure mode was already known and fixed for HostStaging, just not applied to these two. In practice this meant every DeviceGeometry/HostUniform allocation fell back to the default (unpooled) allocator from the very first frame — "[GPU] Pool for domain N exhausted" fired at startup, before any user action. Running entirely on the fallback path for the engine's two busiest domains left no headroom for further growth: dragging a few more meshes onto the viewport reliably triggered a GPU device-loss timeout a few seconds later (VK_TIMEOUT / Lost VkDevice, observed consistently after 3 drops). Fixed by giving both pools blockSize=0 (auto-sized) and maxBlockCount=0 (unlimited), matching DeviceTexture's already-correct configuration for the same reason. Confirmed: pool-exhaustion warnings are gone on a clean launch, and 8+ consecutive mesh drag-drops that previously crashed within 3 now run without issue. * fix(gpu): run ThreadPool worker init in-loop, fix ring buffer offset/retirement ThreadPool::WorkerRun only checked the init callback once before entering its loop, but RegisterWorkerInit is always called after workers are already idle-waiting, so notify_one() never resumed at that check — t_worker_slab stayed null on every real worker, silently skipping all async texture pixel uploads. Move the check inside the loop, deduped by last-seen callback pointer. CommandBuffer::CopyBufferToImage hardcoded bufferOffset=0, so the GPU copy for the shared-ring texture upload path always read from the start of the ring buffer instead of the texture's actual ring_offset, reading stale bytes. Plumb the offset through as a new parameter. WriteTextureData's ring allocations were also never paired with a Ring.Submit() call, leaving the ring unaware those regions were still in flight. Thread an out_ring_offset through the upload call sites and submit against the appropriate retirement marker for each queue path. Fixes #764 * fix(rrm): dedicated batch timeline semaphore, drop m_upload_fences Started as the #764 GPU-hang follow-up: added VulkanDevice::CheckDeviceLost wired into every QueueSubmit/Present/AcquireNextImage call site plus a render-loop guard that freezes safely instead of continuing to call into a lost device (calling Vulkan after VK_ERROR_DEVICE_LOST is UB and has been observed to segfault inside the loader). Extracted CommandBufferManager out of VulkanDevice.h/.cpp so it's includable standalone. Along the way, found and fixed a real, independently-confirmed bug: mesh/ font-atlas uploads reset and resubmitted a single shared command buffer indefinitely across the life of the process, and three call sites freed staging buffers immediately after a blocking fence wait even though a fence signal isn't trustworthy proof of GPU completion once any command buffer on the queue has already timed out. Gave uploads dedicated per-frame command buffers and routed staging frees through timeline-gated deferral instead. Also fixed a real VFSDiskBackend data race (VFSScanner fans directory scans across ThreadPool workers against an unsynchronized file-object pool). Root cause of a separate, Windows-only bug (Intel driver reporting UINT64_MAX / non-monotonic timeline semaphore values right after a mesh drag-drop): RenderResourceManager's deferred batch upload was signalling DeviceSwapchain::RenderTimeline, which Present() also drives independently every frame — two writers on one timeline. Fixed by giving the batch its own dedicated, single-writer timeline semaphore (m_batch_timeline) instead. With that in place, unified the remaining synchronous, fence-blocking upload paths (hot-reload mesh swaps, UpdateBuffer's staging sub-case) into the same per-frame deferred batch mesh uploads already used, opened lazily via EnsureBatchOpen and closed once per frame from the new RRM::EndFrame. m_upload_fences (one fence per frame index) is gone; the two paths that must stay synchronous — UpdateBuffer's ring sub-case (GpuAllocator::Ring retires against a single hardcoded semaphore, so it can't safely move to a second one without a larger change there) and UploadFontAtlas (runs once, pre-render-thread) — now share one fence instead of a per-frame array, since neither can ever be in flight while the other runs. RecordStagingCopy gained the VkBufferMemoryBarrier it was missing, needed now that its caller no longer just blocks until the copy is visible everywhere. Also seeds Present()'s wait-semaphore aggregation with RenderTimeline's own value instead of pushing it separately, so an AsyncGPUOperation that also targets RenderTimeline merges instead of appearing twice in one submit's wait list with two different values. * refactor(rrm): drop RecordAndSubmit's dead wait-semaphore params Its only remaining caller (UpdateBuffer's ring path) never passed them — leftover from the deleted synchronous AppendToGlobalBuffer branch. Also fixes a BeginBatchUpload comment that still named that deleted branch. * fix(rrm): explicit BatchFrameState{} init, fixes GCC brace-init rejection
… add watermark (#768) Three gaps in the bindless texture system closed (design decision in bindless-descriptor-architecture.md, closes #665): - zui_draw.frag was missing nonuniformEXT() around the TextureArray index despite requiring GL_EXT_nonuniform_qualifier — spec violation that produces silently wrong results on hardware with divergent warp indexing. g_buffer.frag already used it correctly. - Present()'s descriptor update loop called vkUpdateDescriptorSets once per dequeued texture (N calls for N textures in the same frame). Now accumulates all writes across the full drain and emits one call. - No observable signal existed when the 8192-slot pool approached exhaustion — HandleManager::Size() existed but nothing read it. Adds a one-shot WARN at 75% capacity (6144 slots) so pressure is caught before CreateTexture starts silently returning null handles.
…n, compaction, VRAM auto-detect (#769) * feat(streaming): virtual geometry streaming — pool allocator, eviction, compaction, VRAM auto-detect Closes #622. GeometryPool (GeometryPool.h/.cpp): variable-size region allocator over the streaming VB/IB. PoolAllocator-backed intrusive sorted free lists — O(1) node alloc/free, first-fit search, merge-on-free, FragmentationRatio(). 24 unit tests (GeometryPoolTest + GeometryStreamingManagerTest). RRM wiring: replaced the append-only cursors with GeometryPool m_pool. AppendMeshData calls m_pool.Allocate(); ReleaseMeshGeometry and FlushPendingSwaps call m_pool.Free(). Hot-reload swap now frees the old pool region before repointing the slot (leak fix). Swapped slot is immediately set to Resident so it renders without a 1-frame gap. Builtin geometry fix: RegisterBuiltinGeometry writes into separate pinned m_builtin_vertex_buf / m_builtin_index_buf buffers, fixing a silent corruption where ResetGeometryBuffers would overwrite builtin draw offsets on scene reload. StreamingState + draw guard: MeshSlot gains StreamingState (Unloaded/Pending/Resident/Evicting), Referenced (clock-hand bit), and Pinned. RenderScene calls IsMeshResident before emitting draw commands and MarkMeshReferenced for every mesh that draws; non-resident meshes queue a reload via RequestMeshLoad. GeometryStreamingManager (GeometryStreamingManager.h/.cpp): clock-hand eviction sweep (second-chance, one eviction per call), fragmentation- triggered compaction request, SPSCQueue<StreamRequest,256>-backed RequestLoad (render thread producer, Tick consumer), direct-execution RequestEvict. Tick() called from RRM::BeginFrame. Compaction (RunCompaction): resets the pool, re-uploads every Resident mesh from CPU asset data into fresh packed regions via the existing m_batch_timeline path. Triggered when fragmentation > 0.30. VRAM auto-detection: DeriveGeometryBudget() computes 15% of the largest device-local heap (PhysicalDeviceMemoryProperties), clamped to [128, 512] MB per axis. Optional override via project.json "memory": { "geometry_streaming_mb": N } — pre-parsed by Engine::Initialize before RRM::Initialize runs. On Apple M4 Pro: auto-detects 512 MB per axis. Private section reorganised for alignment: packed bool/uint8 batch fields together to eliminate ~12 bytes of padding; mutexes co-located with the data they protect; all methods in one contiguous block. * fix(memory): default page_size=0 to 4096 in ArenaAllocator::Initialize On Windows the commit-size mask (offset + size + m_mem_page_size - 1) & ~(m_mem_page_size - 1) evaluates to 0 when m_mem_page_size is 0, causing VirtualAlloc(MEM_COMMIT, 0) to fail. PoolAllocator::Initialize then hits its null-check assert and aborts. macOS/Linux are immune because m_committed_size = size skips the commit block. Fix: treat page_size=0 as 4096 (the default Windows page size) — the same value all production callers already pass explicitly. Fixes GeometryPool.FreshPoolIsEmpty crash on Windows CI.
…stry, YAMLSceneSerializer Closes #713, #714, #715. Partial #719 (YAML tests deferred). Adds the foundation of the scene serialization system: - **SceneSnapshot** — lightweight snapshot struct (UUID, name, entity list); Create() factory ensures Entities is initialized before use - **ISceneSerializer** — abstract Serialize/Deserialize interface over VFS paths - **ComponentSerializerRegistry** — dynamic dispatch table mapping ComponentTypeID to C-style YAML/binary callbacks; arena-backed; Initialize(arena, capacity) guard prevents double-init - **YAMLSceneSerializer** — editor-only (#ifdef ZENGINE_EDITOR); human-readable YAML via yaml-cpp; validates all *_uuid fields before mutating the scene; unknown component keys skipped for forward compatibility; entity count sized from the actual YAML sequence; DeserializeYAML callbacks must use safe yaml-cpp APIs (single YAML::Load exception boundary retained as unavoidable third-party constraint) Reviewed and approved.
…ec4f (#771) * refactor(rendering): drop GPUTypes.h — replace gpuvec3/gpuvec4 with Vec4f gpuvec3 had no callers — dead code removed outright. gpuvec4 was a 16-byte struct with only 4-byte alignment (alignof=4), making it subtly wrong for GPU std140/std430 buffers if ever placed after a non-16-byte member. Fix: add alignas(4 * sizeof(T)) to Vec4 in Vec.h so Vec4f is always 16-byte aligned by the type itself, no per-member alignas needed. Replace all gpuvec4 usages with Vec4f (MeshMaterial, GpuDirectionalLight, GpuPointLight) and delete GPUTypes.h. Array-subscript assignments that relied on gpuvec4::operator=(float[4]) are expanded to element-wise brace-init at the three call sites in AssetManager.cpp. * refactor(rendering): split RendererPasses into Graphics/ and Compute/, rename RenderPasses/ to Base/ * feat(rendering): IComputeCallbackPass base, update compute stubs IComputeCallbackPass (Base/) is the helper base class for all compute dispatch passes. It sits on top of IRenderGraphCallbackPass so compute passes plug into the render graph identically to graphics passes: - Setup() → delegates to SetupCompute(device, res_builder) - Compile() → stub for ComputePassBuilder + ComputePipeline (compute-pipeline.md §2/§5) - Execute() → stub for pipeline bind + delegates to ExecuteCompute() Subclasses implement only SetupCompute, ExecuteCompute, and GetShaderName. GetPushConstantSize() is optional (default 0). The four Compute/ stubs (FrustumCullingPass, SSAOPass, BloomPass, SkinningPass) are updated to inherit IComputeCallbackPass and implement the correct two-method interface instead of the graphics-only IRenderGraphCallbackPass methods. * refactor(rendering): flatten Contracts/ — RendererDataContract.h → RendererContracts.h Moves UBOCameraLayout from the Contracts/ subdirectory up to Renderers/ (RendererContracts.h) and drops the Contracts:: namespace qualifier. Deletes the now-empty Contracts/ directory. All seven callers updated: GraphicRenderer.cpp and the five Graphics/ pass .cpp files that use sizeof(UBOCameraLayout) for SetDynamicUniform. * style: remove section-divider comment from IComputeCallbackPass
… guard in VFSDiskBackend (#745) (#772) #744: VFSZipFile::m_decompressed was a plain bool with no synchronization. Two threads calling Read() concurrently on the same VFSZipFile could both observe m_decompressed == false, both allocate and extract, and race on the m_data/m_decompressed writes. Fixed with double-checked locking: m_decompressed is now std::atomic<bool> (acquire load on fast path, release store on write), guarded by a per-file m_decompress_mutex on the slow path. #745: VFSDiskBackend::ResolveNativePath had no containment check — it was pure string concatenation with no verification that the composed path stays within m_native_root. VFSPath::Parse already rejects '..' segments so current risk is low, but the backend's own design doc specifies this check as defense-in-depth for any future code path that bypasses Parse. Added a std::memcmp prefix check + separator guard after building the path.
…l sites
Adds a compile-time-removable macro that calls vkGetSemaphoreCounterValue
before every timeline semaphore signal and asserts two invariants:
1. current != UINT64_MAX — Intel driver reports UINT64_MAX when the
semaphore has already been corrupted by a prior non-monotonic signal.
Firing here means corruption happened upstream.
2. next_signal_value > current — directly detects the non-monotonic
signal that causes the UINT64_MAX corruption on Intel Windows.
Guards are placed at all 6 signal sites:
- VulkanDevice::QueueSubmit (timeline overload) — covers all async
texture upload submissions
- DeviceSwapchain::Present frame_start_value — acquire bridge signal
- DeviceSwapchain::Present work_complete_value — render work signal
- RenderResourceManager::EndBatchUpload — m_batch_timeline signal
- RenderResourceManager::UploadTextureBuffer (x2) — transfer and
graphics texture timeline signals
The assert message names the semaphore and value so the crash log
identifies which site fires first, narrowing the root cause.
Previous assert only printed a static string — on Windows the engine log is the only artifact before the crash. Now ZENGINE_CORE_ERROR logs the semaphore handle address, current driver value, and attempted next signal value so the log tells us: - 'ALREADY CORRUPTED': corruption happened upstream of this site - 'NON-MONOTONIC': THIS site is the origin, with exact values Also confirmed: assert fires at frame_start_value (first RenderTimeline signal in Present()), meaning by the time that Present() runs, the GPU has already processed a signal >= frame_start_value. Something between the previous Present() and this one submitted to RenderTimeline with a value that bypassed or overtook RenderTimelineNextValue.
Root cause of Intel Windows timeline semaphore corruption (UINT64_MAX): submit2 (the present bridge) used VkTimelineSemaphoreSubmitInfo with signalSemaphoreValueCount=1 and pSignalSemaphoreValues=&dummy_signal_val where dummy_signal_val=0. The signal semaphore (render_complete) is BINARY so the spec says pSignalSemaphoreValues[0] is ignored — but Intel's driver was treating the zero as a timeline signal of value 0, which is non-monotonic when RenderTimeline's current value > 0. The driver responds by setting the semaphore's current value to UINT64_MAX, corrupting all subsequent signals. Fix: signalSemaphoreValueCount=0 / pSignalSemaphoreValues=nullptr, since no timeline semaphore is being signalled in submit2. The diagnostic guards from the diag/timeline-semaphore-intel-guard branch confirmed this site via the log: dummy signal value 334 was rejected with current=UINT64_MAX immediately after a mesh was uploaded, right before the first ASSERT_TIMELINE_MONOTONIC guard fired on the next Present() call.
…hore handles Two new diagnostic probes: 1. Present() entry log: vkGetSemaphoreCounterValue before any logic so we can tell whether corruption arrived from OUTSIDE Present() (EndFrame, SubmitAsyncUploads, etc.) or from within it. If UINT64_MAX on entry, logs 'corruption happened OUTSIDE Present()'. 2. TRACE log in QueueSubmit(timeline): logs the signal semaphore handle, signal value, wait semaphore handle, and wait value for every async upload submission. Cross-referencing with the RenderTimeline handle from the DIAG-PRESENT log will reveal if any async submission is accidentally targeting RenderTimeline.
…ine self-wait Two root causes of Intel Windows timeline semaphore corruption (UINT64_MAX): 1. submit_0 (acquire bridge): waitSemaphoreValueCount=1 with a BINARY wait semaphore (Acquired). Per spec the value is ignored for binary semaphores, but Intel's driver was reading ignored_wait_val=0 as a timeline wait at 0, corrupting RenderTimeline to UINT64_MAX. Fix: waitSemaphoreValueCount=0 / pWaitSemaphoreValues=nullptr. 2. submit_1 (render work): RenderTimeline appeared in both pWaitSemaphores (seeded into max_val_timeline_semaphores at frame_start_value) AND pSignalSemaphores (to work_complete_value). Intel corrupts semaphores to UINT64_MAX when the same semaphore is in both lists of one VkSubmitInfo. Fix: remove the RenderTimeline seed. submit_0 and submit_1 are on the same graphics queue — queue ordering ensures submit_0 completes before submit_1 begins without an explicit timeline wait. For NVIDIA (HasSeperateTransfertQueueFamily=true), m_tex_transfer_timelines remain in the wait list via AsyncGPUOperations drain for cross-queue texture sync. submit_2 (present bridge) signalSemaphoreValueCount=0 fix is preserved. Diagnostic overhead (DIAG-PRESENT, DIAG-SUBMIT1, DIAG-QSUBMIT TRACE) removed. ASSERT_TIMELINE_MONOTONIC guards remain for Windows test verification.
…stage flag types Replace vkQueueSubmit+VkTimelineSemaphoreSubmitInfo with vkQueueSubmit2+VkSemaphoreSubmitInfo in Present() and QueueSubmit(timeline) — each semaphore carries its own VkSemaphoreSubmitInfo struct, eliminating the parallel-array count ambiguity that caused Intel Windows driver corruption (UINT64_MAX semaphore value). Enable VkPhysicalDeviceSynchronization2Features in VkDeviceCreateInfo — the extension was loaded but the feature flag was never set, causing vkQueueSubmit2 validation errors. Fix type correctness: AsyncGPUOperationHandle::StageFlags, AsyncUploadJob::WaitFlag, and QueueSubmit(timeline) wait_flag now use VkPipelineStageFlags2 instead of uint32_t. Remove ASSERT_TIMELINE_MONOTONIC macro and all 6 call sites — root cause is fixed at the API level; the per-frame vkGetSemaphoreCounterValue calls were expensive on MoltenVK.
…all on drag-drop Texture uploads via AsyncUploadQueue now land in DeferredAsyncGPUOperations (SPSC) and DeferredTextureDescriptorUpdates (SPSC) instead of the live queues. Present() runs before SubmitAsyncUploads() so by the time Present() drains the deferred queues they only hold prior-frame items; submit_1 waits on them at zero cost since the GPU transfer is done. RequestDeferredDescriptorUpdate writes the fallback image view to the new slot synchronously (PARTIALLY_BOUND set — no validation error) so the mesh renders with the fallback pink for at most one frame while the upload is in-flight, then with the correct texture on the next. FallbackDescriptorImageInfo is cached from GetOrCreateFallbackTexture so the fallback view is always ready before any user-triggered texture upload can occur.
…er-thread file stall DeserializeMeshAssetFile and IngestMaterialFromUUID are synchronous file reads that were blocking the render thread on every drag-drop, causing a visible frame stall proportional to mesh file size. Move both to a ThreadPool worker via ThreadPoolHelper::Submit. A dedicated ArenaAllocator (4x file size + 8 MB) is allocated up front and passed through a heap-allocated MeshPayload struct so AssetMesh/AssetNodeHierarchy Array<T> data stays valid across the thread boundary (scratch arenas are unsafe here — ZReleaseScratch frees the backing memory before the callback runs). The main-thread callback via MainThreadScheduler::Post handles only the lightweight IngestMesh + actor create, then shuts down and deletes the arena.
…ync drag-drop changes
Owner
Author
|
Closing — duplicate of #773 which targets develop correctly. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
vkQueueSubmit2 migration — replace
vkQueueSubmit+VkTimelineSemaphoreSubmitInfowithvkQueueSubmit2+VkSemaphoreSubmitInfoin all timeline semaphore submit paths (DeviceSwapchain::Present()andVulkanDevice::QueueSubmit(timeline)). Each semaphore carries its own struct, eliminating the parallel-array count ambiguity that caused Intel Windows driver corruption (UINT64_MAX semaphore value). EnableVkPhysicalDeviceSynchronization2FeaturesinVkDeviceCreateInfo— the extension was loaded but the feature flag was never set. Fix type correctness:AsyncGPUOperationHandle::StageFlags,AsyncUploadJob::WaitFlag, andQueueSubmit(timeline)wait_flagnow useVkPipelineStageFlags2. RemoveASSERT_TIMELINE_MONOTONICmacro and all 6 call sites — root cause fixed at the API level; the per-framevkGetSemaphoreCounterValuecalls were expensive on MoltenVK.Deferred texture upload — texture async ops and descriptor updates go into 1-frame SPSC deferred queues (
DeferredAsyncGPUOperations,DeferredTextureDescriptorUpdates).Present()runs beforeSubmitAsyncUploads()so the same-frame GPU stall on drag-drop is eliminated.RequestDeferredDescriptorUpdatepre-fills the new slot with the fallback image view (pink) for the one-frame upload window so the mesh renders correctly immediately.Async drag-drop mesh deserialization —
DeserializeMeshAssetFileandIngestMaterialFromUUIDare synchronous file reads that were blocking the render thread on every drag-drop. Both now run on aThreadPoolworker thread using a dedicated per-dropArenaAllocator(4× file size + 8 MB) that outlives the lambda. The main-thread callback viaMainThreadScheduler::Posthandles only the lightweightIngestMesh+ actor create.Result
Stable 121 fps through mesh drag-drop with textures. Multiple meshes rendered correctly.
Test plan
.zemeshasset onto the viewport — FPS stays stable, mesh appears with correct texturesUINT64_MAX) withvkQueueSubmit2