Root cause: after Destruct(), physics world still references dead
entities in its internal dictionary. SyncTransforms iterates dead
entity IDs → Flecs assert ecs_is_alive.
Fix:
- Pause physics during reset
- For each deleted entity: check Has<RigidBody>, call physicsWorld.RemoveBody
to clean up Jolt body + dictionary entries BEFORE Destruct
- Resume physics after CreateObjects
- New objects get IsInitialized=false, physics bodies created next frame
- renderTimer accumulates deltaTime each tick
- Render only when renderTimer >= 16.67ms
- If timer overshoots (slow frame), clamp to 0 to prevent spiral of death
- No Thread.Sleep/continue — loop runs continuously, just skips render
when not enough time has passed
- Physics + input + light animation: run at full speed every iteration
- Render: accumulator-based 60 FPS limiter — only renders when enough
time has accumulated (16.67ms). If not, Thread.Sleep(1) and continue.
- Video: captured at exactly 60 FPS, FFmpeg downsamples to 30
- Physics stays smooth even if render can't keep up
Root cause: driver returned format=58 (A2B10G10R10_PACK32) as fallback
when B8G8R8A8_SRGB wasn't found. 10-bit packed format has different
byte layout → vkCmdCopyImageToBuffer gives garbage when interpreted as
8-bit BGRA → acid colors in video.
Fix:
- QuerySurfaceFormat: try B8G8R8A8_UNORM first (NVIDIA supports it)
- Fallback: B8G8R8A8_SRGB, then first available
- Removed SRGB LUT conversion — UNORM data is already linear
- Raw BGRA bytes go directly to FFmpeg (pixel_format=bgra)
Three fixes:
1. imageCubeArray: enabled via VkPhysicalDeviceFeatures (core 1.0 feature)
instead of VkPhysicalDeviceImageCubeArrayFeatures (wrong sType in pNext
chain was interpreted as VkExternalMemoryImageCreateInfoNV)
2. Swapchain format: reverted to B8G8R8A8_SRGB (was UNORM which driver
didn't support → fell back to A2B10G10R10 10-bit format=58 → wrong
pixel layout for capture)
3. Video colors: SRGB→linear conversion via 256-entry LUT in
ReadCapturedBuffer. SRGB swapchain gives gamma-compressed bytes,
FFmpeg expects linear. LUT converts each B/G/R channel using
standard SRGB formula: s<=0.04045 ? s/12.92 : ((s+0.055)/1.055)^2.4
Also: video captures before ImGui (no UI in recording)
1. No UI in video: end render pass before ImGui, capture, start new
render pass with loadOp=LOAD for ImGui, end again. Video captures
only the 3D scene.
2. Acid colors fixed: swapchain format changed from B8G8R8A8_SRGB to
B8G8R8A8_UNORM. SRGB format stores gamma-compressed values that
vkCmdCopyImageToBuffer reads raw — FFmpeg treated them as linear
causing acid colors. UNORM stores linear values, copy gives linear
data, FFmpeg bgra→yuv420p conversion is correct.
Swapchain format is B8G8R8A8_SRGB — vkCmdCopyImageToBuffer gives BGRA
data. Was converting BGRA→RGBA in C# and telling FFmpeg 'rgba' —
but SRGB data needs to stay as-is. Now:
- No BGRA→RGBA swap in ReadCapturedBuffer
- FFmpeg pixel_format=bgra (matches swapchain)
- FFmpeg handles SRGB→yuv420p conversion correctly
Root cause: CaptureFrame returned null and was assigned to CapturedFrame,
overwriting the data that ReadCapturedBuffer had just set at the start
of the same frame.
Fix: CaptureFrame is now void — only records GPU copy commands.
ReadCapturedBuffer runs at start of next frame (after WaitFrame/fence)
and sets CapturedFrame with actual pixel data.
Program.cs reads CapturedFrame after RenderWorld — now contains data
from the previous frame's GPU copy.
- Removed skipFrames logic — capture every frame for reliability
- FFmpeg framerate set to actual FPS (capped at 60) instead of hardcoded 30
- Added -vf fps=30 filter to downsample to 30fps in output
- First frame: CapturedFrame is null (no previous capture), skipped naturally
- Second frame onwards: data available from previous frame's GPU copy
0x2000 was VK_ACCESS_MEMORY_READ_BIT in Vulkan 1.0 but maps to
VK_ACCESS_2_HOST_READ_BIT in sync2. Replaced with 0x1000
(VK_ACCESS_2_TRANSFER_WRITE_BIT) for dst access on transfer layout.
Three validation errors fixed:
1. vkCmdCopyImageToBuffer inside render pass → moved AFTER vkCmdEndRendering
2. Swapchain images missing TRANSFER_SRC usage → added to swapchain creation
3. Wrong layout transitions → ColorAttachment→TransferSrc→copy→ColorAttachment
Architecture change:
- CaptureFrame now only records commands (returns null)
- ReadCapturedBuffer called at start of NEXT frame (after WaitFrame/fence)
GPU has finished by then, safe to map memory
- CapturedFrame available 1 frame late (acceptable for video)
- Image transitions: ColorAttachmentOptimal→TransferSrcOptimal→ColorAttachmentOptimal
(present transition still works because image is back in ColorAttachmentOptimal)
- All 3 lights: intensity 8.0, range 15.5 (was 12/8/8, 60/40/40)
- Copy Parameters: also logs to console for debugging
- Clipboard text via ImGui.SetClipboardText (standard API)
Root cause: descriptor set layout binding 0 (SceneUBO) had stageFlags=Vertex
only. Fragment shader reads numLights, lights[], shadowParams[] from UBO
but couldn't access it. Changed to Vertex|Fragment.
CULL_MODE_BACK was culling faces with inconsistent winding order in
sphere.obj, torus.obj, diamond.obj, cone.obj. Many faces disappeared,
making objects look like wireframe/grids.
Reverted both main and shadow pipelines to CULL_MODE_NONE.
Shadow acne is handled by low bias (0.0001) which works fine.
- pyramid.obj: verified CCW via cross-product for all 6 faces
- diamond.obj: verified CCW for all 8 octahedron faces
- torus.obj: verified CCW — normal points outward from torus center
- cone.obj: verified CCW for side faces (radial outward) and bottom (-Y)
- Removed Grid entity — balls were falling through it (no physics)
- Removed 'Grid' from castShadow exclusion list
- All objects now render correctly with CULL_MODE_BACK
- AiCommandProcessor: checks if modelPath contains 'sphere' (case-insensitive)
- If detected as sphere: uses DynamicSphere physics body (was DynamicBox)
- Works with both shape='sphere' parameter and modelPath='Content/sphere.obj'
- No need to explicitly pass shape parameter when loading sphere.obj
- Changed from Thread to Task.Run for MCP server
- Start delay: 10 frames (was 3) — let scene fully initialize first
- Kestrel on ThreadPool instead of dedicated thread
Root cause: CULL_MODE_NONE in both main and shadow pipelines caused
back faces to render, creating self-shadowing acne on all objects:
- Shadow map: back faces written at closer depth → false shadows on front
- Main render: back faces visible through objects → wrong lighting
Fix:
- Main pipeline: CULL_MODE_BACK (was None) — only front faces render
- Shadow pipeline: CULL_MODE_BACK (was None) — only front faces cast shadows
- Removed normal flip hack (if dot(N,V) < 0 N = -N) — no longer needed
with proper culling + correct CCW winding in cube.obj
- This is the standard approach: correct geometry + back-face culling
Root cause: ALL 12 faces in cube.obj had reversed winding order.
This caused every face normal to point inward, breaking:
- Diffuse lighting (NdotL < 0 → no light on any face)
- Shadow bias (slope-dependent bias used wrong NdotL)
- Shadow self-shadowing (both sides rendered with CULL_NONE)
Fix: reversed all face indices to correct CCW winding:
- Back face: f 1 4 3, f 1 3 2 (was f 1 2 3, f 1 3 4)
- Front face: f 5 6 7, f 5 7 8 (was f 5 7 6, f 5 8 7)
- Left/Right/Bottom/Top: all reversed
Now MeshMath.ComputeFaceNormal produces correct outward normals.
Normal flip in shader (dot(N,V) < 0) is still kept as safety for
double-sided rendering, but cubes now have correct normals natively.
Previous fix flipped normal by both light AND view direction:
if (dot(N,L) < 0) N = -N;
if (dot(N,V) < 0) N = -N;
This double-flip broke spheres: when light is behind sphere, front faces
get N flipped by light (now facing away from camera), then flipped back
by view — but back faces get flipped once, causing inconsistency.
Correct approach: flip ONLY by view direction:
if (dot(N, V) < 0.0) N = -N;
This ensures the normal always faces the camera. Faces pointing away
from light naturally get NdotL=0 (no lighting) — correct behavior.
Spheres keep smooth normals, cubes get inward normals fixed.
Root cause: cube.obj has inconsistent winding order. Many faces have
normals pointing inward (e.g. back face normal = +Z instead of -Z).
With cullMode=None, both sides render, but inward normals cause:
- No diffuse lighting (NdotL < 0 → max() = 0)
- Wrong shadow bias (slope-dependent bias uses wrong NdotL)
- Inconsistent shadow edges on cube faces
Fix: in fragment shader, flip normal if dot(N,L) < 0 or dot(N,V) < 0
This is the standard approach for double-sided rendering with
non-watertight geometry or inconsistent winding order.