161 Commits
Author SHA1 Message Date
emil28092005 8e40d8c7a7 docs: add Arena Strike UI screenshot 2026-07-19 17:44:49 +03:00
emil28092005 a99559744f docs: clarify Arena Strike demo render 2026-07-19 17:42:35 +03:00
emil28092005 bb8ea6d6ec docs: add Arena Strike gameplay demo 2026-07-19 17:40:14 +03:00
emil28092005 89924fded6 feat: add playable Arena Strike FPS scene 2026-07-19 14:20:38 +03:00
emil28092005 cbe9121cf2 feat: add Solar Sanctuary demo scene 2026-07-19 01:44:09 +03:00
emil28092005 bfe5a7f44f fix: make Vulkan swapchain recreation resilient 2026-07-19 01:38:02 +03:00
emil28092005 96c32296d1 docs: add demo GIF to README
- cortex_demo.gif: 400px, 10fps, palette-optimized (833KB)
- Converted from cortex.mp4 (3.8s, 1280x720, 30fps)
- Shows physics simulation with PBR + shadows + multi-light
- README.md: embedded GIF at top
- .gitignore: added cortex.mp4 (source video, too large for git)
2026-06-19 17:21:40 +03:00
emil28092005 c57bd5229f docs: update all docs for publication
- README.md: new — features, quick start, controls, MCP tools, requirements
- AGENTS.md: rewritten for pure P/Invoke Vulkan 1.3 (was Raylib/OpenGL)
- CORTEX_ENGINE_ARCHITECTURE.md: complete rewrite — current architecture,
  frame loop, UBO layout, push constants, shadow mapping, PBR, AI/MCP,
  physics, ImGui, video recording, content, testing
- VULKAN_IMPLEMENTATION_PLAN.md: marked as COMPLETE with all 21 phases
- scripts/run.sh: updated examples
- .gitignore: added Videos/, imgui.ini, cortex.mp4
- Removed tracked imgui.ini and video files
2026-06-19 17:02:33 +03:00
emil28092005 9d68559962 fix: reset scene crash — remove physics bodies before entity destruct
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
2026-06-19 16:23:03 +03:00
emil28092005 194848d074 fix: physics pause/resume crash — IsAlive checks + safe deletion 2026-06-19 09:44:56 +03:00
emil28092005 ef7c808304 feat: pause/resume physics + reset scene buttons in ImGui
- 'Pause Physics' / 'Resume Physics' toggle button in debug panel
- 'Reset Scene' button: deletes all objects (except Camera, lights, Floor),
  recreates scene via CreateObjects()
- CreateScene split: CreateScene (floor + lights) vs CreateObjects (all
  dynamic/decorative objects) — Reset only recreates objects
- physicsEnabled flag controls physicsWorld.Update + SyncTransforms
- Removed duplicate light creation in CreateObjects
2026-06-19 09:39:33 +03:00
emil28092005 74dcc45543 feat: adjustable ambient lighting via ImGui sliders
- SceneUBO expanded: added vec4 ambientColor at offset 400
- UBO size: 448 → 464 bytes
- Fragment shader: ambient from UBO instead of hardcoded constant
- VulkanRenderer: AmbientColor property (default 0.01, 0.01, 0.02)
- ImGui: R/G/B sliders for ambient (0.0-0.5 range)
- Packed at offset 400 after 4 shadowParams (64 bytes at 336)
2026-06-19 09:34:09 +03:00
emil28092005 9555de60ef fix: render timer — no hang, spiral-of-death prevention
- 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
2026-06-19 09:29:45 +03:00
emil28092005 f2ae4d0f8f fix: fixed 60 FPS render rate, uncapped physics/input
- 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
2026-06-19 09:27:42 +03:00
emil28092005 e3b2cab9f0 feat: fixed 60 FPS render loop + 30 FPS video output
- Main loop: frame limiter at 60 FPS via Thread.Sleep when frame completes
  faster than target (16.67ms). Prevents unnecessary GPU/CPU usage.
- FFmpeg: fixed framerate 60 input, -vf fps=30 downsamples to 30 FPS output
- Removed unused skipFrames variable
- Stable timing for physics + video recording
2026-06-19 09:25:10 +03:00
emil28092005 e813e5e90b fix: force B8G8R8A8_UNORM swapchain — fixes acid video colors
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)
2026-06-19 09:21:26 +03:00
emil28092005 9531b1e9d5 fix: imageCubeArray via core features + SRGB swapchain + LUT color conversion
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)
2026-06-19 09:17:35 +03:00
emil28092005 d6939e9efa fix: video without UI + correct colors
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.
2026-06-19 09:13:04 +03:00
emil28092005 2daf10b3b1 debug: check if captured frame has non-zero data 2026-06-19 09:09:29 +03:00
emil28092005 277c8aea93 fix: video colors — use BGRA pixel format for FFmpeg, remove swap
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
2026-06-19 09:07:19 +03:00
emil28092005 b41e64db4a fix: CaptureFrame no longer overwrites CapturedFrame with null
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.
2026-06-19 09:04:03 +03:00
emil28092005 00b6f39201 fix: video recording captures every frame + dynamic FPS for FFmpeg
- 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
2026-06-19 09:01:55 +03:00
emil28092005 ab1a2d05c3 fix: enable imageCubeArray feature + transition all 24 shadow layers
Two validation errors fixed:
1. imageCubeArray feature not enabled → added VkPhysicalDeviceImageCubeArrayFeatures
   to device creation chain (pNext: cubeArray → sync2 → dynamicRendering)
2. Shadow layers 18-23 in UNDEFINED layout → transition ALL 24 layers
   (MaxShadowLights * 6) instead of only numShadowLights * 6
   Descriptor references all 24 layers via CubeArray view
2026-06-19 08:59:22 +03:00
emil28092005 d7b6851217 fix: replace all 0x2000 with 0x1000 in capture barriers
0x2000 = VK_ACCESS_2_HOST_READ_BIT (wrong for GPU transfer)
0x1000 = VK_ACCESS_2_TRANSFER_WRITE_BIT (correct for copy operations)
2026-06-19 08:56:25 +03:00
emil28092005 f8384a8672 fix: ImGui font upload barriers — use correct sync2 access flags
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.
2026-06-19 08:54:54 +03:00
emil28092005 9b80ab7881 fix: video recording — proper layout transitions + deferred buffer read
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)
2026-06-19 08:52:41 +03:00
emil28092005 b6299e8048 fix: capture video frame before ImGui — no UI in recordings
Moved CaptureFrame call before _imGui.Render() so the screenshot
buffer contains only the 3D scene without ImGui overlay.
2026-06-19 08:46:22 +03:00
emil28092005 346b422cb0 fix: all lights default to intensity=8 range=15.5 + clipboard logging
- 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)
2026-06-19 08:44:07 +03:00
emil28092005 ace815c77d feat: video recording via FFmpeg pipe
- vkCmdCopyImageToBuffer added to Vk.cs
- VulkanRenderer.CaptureFrame: copies swapchain image to staging buffer,
  converts BGRA→RGBA, returns byte[]
- ImGui 'Video Recording' panel with Start/Stop buttons
- Start: launches FFmpeg process (rawvideo rgba → libx264 mp4)
- Each 2nd frame: writes RGBA pixels to FFmpeg stdin pipe
- Output: Videos/cortex_<timestamp>.mp4 at 30fps
- Stop: closes stdin, waits for FFmpeg to finish
- Requires ffmpeg installed (already available on system)
- Captures at 1280x720 resolution
2026-06-19 08:37:37 +03:00
emil28092005 4955a042e9 fix: restore Copy Parameters button with all 3 lights
- Button outputs: shadow params + all 3 light intensity/range/color
- Format: light1Intensity=12.0, light1Color=(1.00,0.95,0.85), etc.
2026-06-19 08:16:56 +03:00
emil28092005 ad92e758de feat: 3 dynamic point lights with shadows
- MainLight: warm (1.0, 0.95, 0.85), intensity 12, range 60
- SecondLight: cool blue (0.3, 0.6, 1.0), intensity 8, range 40
- ThirdLight: red (0.9, 0.2, 0.3), intensity 8, range 40
- All 3 orbit in chaotic circles at different phases
- All 3 cast shadows (18 shadow passes per frame = 3 × 6)
- ImGui: per-light sliders for all 3 lights
- Reduced intensities slightly to balance 3 overlapping lights
2026-06-19 08:12:18 +03:00
emil28092005 d0af9b5c2d fix: UBO binding 0 visible to fragment shader — lights now work
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.
2026-06-19 08:07:44 +03:00
emil28092005 c3cca30bba feat: multi-light system with per-light cubemap array shadows
Architecture:
- SceneUBO (448B): mat4 vp + numLights + numShadowLights + LightData[8] + shadowParams[4]
- LightData: vec4 posAndIntensity + vec4 colorAndRange (32B per light)
- Push constants (main): mat4 model only (64B) — all light data in UBO
- Push constants (shadow): model(64) + lightViewProj(64) + lightPos(16) + shadowParams(16) = 160B
- Shadow: cubemap array (24 layers = 4 lights × 6 faces), samplerCubeArray in shader
- Shadow passes: numShadowLights × 6 faces (2 lights = 12 passes)
- Fragment shader: loop over lights, calcPBR per light, shadow from corresponding cubemap layer

New OBJ files with verified CCW winding:
- pyramid.obj, diamond.obj, torus.obj, cone.obj, sphere.obj

Scene 'Chaos Architecture':
- 28 sphere pyramid (physics)
- 30 falling diamonds (physics)
- 8 floating torus rings (decorative)
- 4 pyramid decorations (static)
- 4 cone pillars (static)
- Central torus knot (floating)
- 2 dynamic point lights (warm + cool), both cast shadows
- Both lights orbit in chaotic circles

ImGui panel:
- Per-light intensity, range, RGB sliders
- Shadow bias, sample radius, far plane sliders

227 tests, all pass
2026-06-19 07:15:29 +03:00
emil28092005 86d24e7254 fix: revert to CULL_MODE_NONE — no more holes in geometry
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.
2026-06-18 22:50:58 +03:00
emil28092005 19bed7fe1a fix: regenerate all OBJ files with verified CCW winding + remove grid
- 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
2026-06-18 22:41:54 +03:00
emil28092005 cd8c6de191 feat: new default scene — chaos architecture
- 28 sphere pyramid (7 layers, physics, DynamicSphere colliders)
- 30 falling diamonds (octahedrons, physics, DynamicBox)
- 8 floating torus rings (decorative, rotated to face center)
- 4 pyramid decorations (static, at corners)
- 4 cone pillars (static, at diagonals, taller scale)
- Central floating torus knot (no physics)
- Dynamic point light moving in chaotic circle
- Floor + grid
- Camera at (0, 5, -15)
- All with PBR + cubemap shadows + soft PCF
2026-06-18 22:37:16 +03:00
emil28092005 55c3a16e61 feat: new OBJ files + chaos architecture scene via MCP
New OBJ files:
- pyramid.obj: 4-sided pyramid with CCW winding
- diamond.obj: octahedron (8 faces, CCW)
- torus.obj: donut shape (24x16 segments)
- cone.obj: 24-sector cone with bottom cap

Scene created via MCP (spawn_model with physics):
- 28 sphere pyramid (7 layers, physics)
- 30 falling diamonds (physics)
- 8 floating torus rings (no physics, decorative)
- 4 pyramid decorations (static)
- 4 cone pillars (static)
2026-06-18 22:35:10 +03:00
emil28092005 4eb0584f6d fix: sphere collider matches visual size — create at radius 0.5
Root cause: CreateSphere(r) baked scale into mesh radius, then Transform
applied scale again → double scaling. Visual radius was scale*0.5*scale
but collider was scale*0.5.

Fix: always create sphere at radius 0.5 (unit sphere, like cube.obj).
Scale is applied through Transform only. Collider radius = maxScale*0.5
matches visual radius = 0.5 * scale.
2026-06-18 22:28:33 +03:00
emil28092005 243e430eab fix: auto-detect sphere from model path — correct sphere colliders
- 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
2026-06-18 22:25:11 +03:00
emil28092005 0119cd765b feat: add sphere.obj — 24x16 UV sphere with CCW winding
- 425 vertices, 720 triangles
- Radius 0.5, CCW winding from outside
- Can be loaded via spawn_model with modelPath='Content/sphere.obj'
2026-06-18 22:21:50 +03:00
emil28092005 24eb547711 feat: spawn spheres via MCP — shape parameter cube/sphere
- SpawnModelCommand: added Shape property (default 'cube')
- AiCommandProcessor: if shape='sphere', uses ProceduralMesh.CreateSphere
  instead of loading OBJ file; physics uses DynamicSphere instead of DynamicBox
- EngineMcpTools: added 'shape' parameter to SpawnModel tool
- Engine.AI now references Engine.Graphics for ProceduralMesh access
2026-06-18 22:19:42 +03:00
emil28092005 a90195c600 feat: optional physics in spawn_model MCP command
- SpawnModelCommand: added Physics bool property (default false)
- AiCommandProcessor.SpawnModel: when Physics=true, adds RigidBody.DynamicBox
  with half-extent = max(scale) * 0.5, mass = max(scale) * 2
- EngineMcpTools.SpawnModel: added 'physics' parameter (default false)
- Physics is optional — spawn without physics by default, enable when needed
- Physics bodies are initialized by the existing per-frame IsInitialized check
2026-06-18 22:16:13 +03:00
emil28092005 f8504cd44c fix: MCP server on Task.Run + start after 10th frame
- 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
2026-06-18 22:11:24 +03:00
emil28092005 5312625920 feat: 50 physics balls in scene — stress test for shadows + physics 2026-06-18 22:07:51 +03:00
emil28092005 eb2cfec41f fix: reduce shadow bias to 0.0001 — near zero with back-face culling 2026-06-18 22:03:02 +03:00
emil28092005 576d4586a3 fix: reduce default shadow bias to 0.01 — minimal acne with back-face culling
Back-face culling eliminated most self-shadowing, so bias can be much
lower. Default 0.01 (was 0.08). Slider still allows 0.001-0.5 tuning.
2026-06-18 22:01:43 +03:00
emil28092005 ed919edb0a fix: enable back-face culling — removes self-shadowing artifacts
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
2026-06-18 22:00:03 +03:00
emil28092005 d2078f3ee0 fix: cube.obj winding order — all faces now CCW from outside
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.
2026-06-18 21:57:32 +03:00
emil28092005 6a473bdc53 fix: flip normal only by view direction — fixes both cubes and spheres
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.
2026-06-18 21:56:10 +03:00
emil28092005 2f7430e3b1 fix: flip normals when facing away from light/view — fixes cube lighting
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.
2026-06-18 21:53:15 +03:00