Commit Graph
76 Commits
Author SHA1 Message Date
emil28092005 e36cbc7774 feat: diffuse lighting — normals passed to fragment shader
- Vertex shader: pass normal through mat3(model) to fragment
- Fragment shader: directional light (0.5, 0.8, 0.3) with 0.25 ambient
  + 0.75 diffuse = faces now have depth and 3D form
- Torus knot has smooth normals (vn in OBJ), cubes have face normals
- 0 validation errors, ~2300 FPS
2026-06-18 17:03:15 +03:00
emil28092005 a4914f28c5 feat: ECS scene with 7 objects — torus knot + 6 rotating cubes
- VulkanRenderer: mesh cache by entity ID, lazy buffer creation
- RenderWorld iterates entities with Transform+Mesh, draws each with
  per-entity model matrix via push constants
- Program.cs: creates scene with 1 torus knot (center, scale 1.5) +
  6 cubes at different positions, all rotating around Y at different speeds
- Camera at (0, 2, -8) looking at origin, FreeFly controls
- 0 validation errors, ~2100 FPS with 7 objects
2026-06-18 17:00:14 +03:00
emil28092005 398cc88ae2 fix: remove row_major from GLSL — fixes fish-eye and broken projection
Root cause: System.Numerics.Matrix4x4 uses row-major storage with row-vector
convention (v * M). GLSL with layout(row_major) does column-vector (M * v),
which effectively transposes the matrix, breaking the projection.

Fix: remove row_major from layout qualifiers. GLSL then interprets the
row-major data as column-major (effectively transposing it), so M * v in
GLSL equals v * M in System.Numerics — correct result.

The Y-flip (M22 *= -1) is still needed for Vulkan's Y-down clip space.
2026-06-18 16:56:44 +03:00
emil28092005 44bae31495 fix: flip projection Y for Vulkan (M22 *= -1) — fixes fish-eye distortion
System.Numerics.CreatePerspectiveFieldOfView uses DirectX convention (Y up).
Vulkan clip space has Y down. Negating M22 corrects the projection.
Depth is already [0,1] in System.Numerics (DirectX-style), no remap needed.
2026-06-18 16:53:48 +03:00
emil28092005 fcbc42959e feat: FreeFlyCameraController — interactive WASD + mouse look
- Program.cs: create Camera entity in ECS World, FreeFlyCameraController
  with WASD movement, Q/E up/down, Shift boost, right-click mouse look
- VulkanRenderer.RenderWorld: queries Camera from World, computes VP
  from Camera.GetViewMatrix() * GetProjectionMatrix()
- Camera aspect ratio auto-updated on window resize
- Fallback to hardcoded VP if no Camera entity found
- Torus knot still rotating via push constant model matrix
- 0 validation errors
2026-06-18 16:49:54 +03:00
emil28092005 2886dd77f2 feat: render torus knot (4800 verts, 9600 faces, smooth normals)
- Load Content/torusknot.obj instead of cube.obj
- 28800 vertices (duplicated per-face), 28800 indices
- OBJ has vn lines so smooth normals are used (not face normals)
- Removed slide animation, kept Y-axis rotation
- Camera z=-6, FOV 45°
- 0 validation errors, ~3000 FPS
2026-06-18 16:46:27 +03:00
emil28092005 0978b82322 fix: remove projection matrix hacks — back to clean FOV 45° + z=-5
- Removed M22 Y-flip and M33/M43 depth remap that caused edge distortion
- System.Numerics.CreatePerspectiveFieldOfView works as-is with row_major
- FOV 45°, camera z=-5, 1 cube sliding left-right + rotating
2026-06-18 13:29:26 +03:00
emil28092005 00f23187d5 feat: 1 cube slowly sliding left-right (sin wave) + rotating 2026-06-18 13:27:36 +03:00
emil28092005 a801e7fb16 fix: Vulkan projection matrix — Y-flip + depth [0,1] remap
- Negate proj.M22 for Vulkan Y-down coordinate system
- Remap depth from OpenGL [-1,1] to Vulkan [0,1]: M33*0.5+0.5, M43*0.5+0.5
- FOV 45°, camera z=-5, cubes at x=±2
- 0 validation errors
2026-06-18 13:25:59 +03:00
emil28092005 ce3dc8c41b fix: FOV 72°, camera z=-10, cubes 6 units apart 2026-06-18 13:24:26 +03:00
emil28092005 1a277abc7a fix: wider FOV (81°) for more distance 2026-06-18 13:23:19 +03:00
emil28092005 c9358bf080 fix: wider FOV (60°) + camera z=-6 for better view 2026-06-18 13:22:41 +03:00
emil28092005 51f94b8923 fix: move camera to z=-12 2026-06-18 13:21:39 +03:00
emil28092005 45fcccbd34 fix: move camera to z=-7 for more distance 2026-06-18 13:20:50 +03:00
emil28092005 edfcf33578 feat: render 2 rotating cubes side by side
- Push constant changed from float angle (4B) to mat4 model (64B)
- Two draw calls with different model matrices (x=-1.5 and x=+1.5)
- Both cubes rotate around Y axis at 0.2 rad/s
- 0 validation errors
2026-06-18 13:20:22 +03:00
emil28092005 567d88825d fix: slow down rotation to 0.2 rad/s 2026-06-18 13:18:45 +03:00
emil28092005 0541f5d13c feat: rotate cube around Y axis instead of Z 2026-06-18 13:18:07 +03:00
emil28092005 f8cbacb422 feat: slow cube rotation (0.5 rad/s) 2026-06-18 13:16:46 +03:00
emil28092005 441ce1f511 fix: move camera to z=-4 so cube is further away 2026-06-18 13:15:17 +03:00
emil28092005 bdd4d3eaf8 feat: cube from OBJ with depth buffer, no rotation
- Load Content/cube.obj via ObjLoader (36 vertices, 36 indices)
- Depth buffer re-enabled: depth test + write, CompareOp=Less
- Depth image transition + depth attachment in rendering info
- Push constant angle=0 (no rotation)
- Camera at z=-2, perspective FOV 45°
- UINT32 index type (fixed in previous commit)
- Face normals computed by ObjLoader (no vn in cube.obj)
- 0 validation errors
2026-06-18 13:14:13 +03:00
emil28092005 237474014c test: 206 tests covering Vulkan types, struct sizes, enum values, OBJ normals
New test files:
- VulkanStructSizeTests.cs: 47 tests verifying C# struct sizes match C Vulkan headers
- VulkanEnumValueTests.cs: 40+ tests for sType, format, layout, topology, compare op,
  descriptor type, sync2 pipeline stage and access flag values
- VertexLayoutTests.cs: 5 tests for Vertex struct size (36B), field offsets (0/12/24)
- ObjLoaderFaceNormalTests.cs: 5 tests for face normal computation when OBJ has no vn lines

Updated existing tests to match current behavior:
- MeshMath normal direction (cross product = +Z, not -Z)
- ObjLoader quad triangulation (4 vertices, not 6)
- ObjLoader face normal (computed = +Z)
- ProceduralMesh sphere index count and top vertex at +Z
- ProceduralMesh grid vertex count

All 206 tests pass. Tests would have caught:
- VkPhysicalDeviceMemoryProperties size (was 264, should be 520)
- VkPhysicalDeviceLimits fields (size_t fields were uint, not ulong)
- Synchronization2Features sType (was 1000257000, should be 1000314007)
- Access flag values (COLOR_ATTACHMENT_WRITE was 0x800, should be 0x100)
- Index type mismatch (UINT16 vs UINT32)
2026-06-18 13:09:51 +03:00
emil28092005 233041ab00 fix: VK_INDEX_TYPE_UINT32 (was UINT16) + remove unused depth from pipeline
- vkCmdBindIndexBuffer indexType changed from 0 (UINT16) to 1 (UINT32)
  to match uint[] index data — this was causing black screen
- Removed depth stencil state, depth attachment format, and depth
  rendering attachment from pipeline/renderer (depth buffer still
  created in swapchain but unused)
2026-06-18 12:59:01 +03:00
emil28092005 aca8d1ab4d fix: correct matrix order (proj*view) + Z depth range for Vulkan
- VP matrix order: proj*view (GLSL column-vector convention)
- Z correction in vertex shader: gl_Position.z = (z+w)*0.5 (OpenGL [-1,1] → Vulkan [0,1])
- Depth test temporarily disabled for debugging
2026-06-18 12:47:20 +03:00
emil28092005 d620bd537f revert: back to triangle with 3D rotation + depth buffer
- Hardcoded triangle (RGB vertices) with index buffer
- 3D rotation: RotateZ * RotateX
- Depth buffer + UBO + push constant model matrix retained
- Camera at z=-3, perspective FOV 45°
2026-06-18 12:44:06 +03:00
emil28092005 e740cf1214 fix: compute face normals when OBJ has no vn lines
- ObjLoader.ParseFace: if no normals in OBJ, compute face normal from
  first triangle using MeshMath.ComputeFaceNormal and apply to all face vertices
- cube.obj has no vn lines, so all faces previously had normal (0,1,0)
- Now each face has correct outward-facing normal for proper lighting
2026-06-18 12:42:13 +03:00
emil28092005 d6ca80c678 fix: add normal-based diffuse lighting so cube is visible as 3D
- Fragment shader: directional light dot(normal, lightDir) with 0.2 ambient
- Vertex shader: pass normal through mat3(model) to fragment
- Same-color faces now have different shading based on orientation
2026-06-18 12:39:51 +03:00
emil28092005 d6bd1807cf feat: depth buffer + 3D model matrix — rotating cube now looks correct
- Depth image (D32_SFLOAT) + image view created in VulkanSwapchain
- VkPipelineDepthStencilStateCreateInfo: depth test + write enabled, CompareOp=Less
- Push constant changed from float angle (4B) to mat4 model (64B)
- 3D rotation: RotateY * RotateX in renderer, applied via push constant
- Vertex shader: gl_Position = vp * model * vec4(pos, 1.0)
- Depth attachment in VkRenderingInfo + depth clear (1.0)
- Depth image layout transition (Undefined → DepthStencilAttachmentOptimal)
- New Vulkan functions: vkCreateImage, vkDestroyImage, vkGetImageMemoryRequirements, vkBindImageMemory
- New structs: VkPipelineDepthStencilStateCreateInfo, VkStencilOpState, VkImageCreateInfo
- New enums: VkCompareOp, VkImageType, VkImageTiling
- 0 validation errors
2026-06-18 02:14:54 +03:00
emil28092005 fde8b0f63a feat: index buffer + OBJ cube rendering
- VulkanIndexBuffer.cs: staging → device-local index buffer (uint16 indices)
- vkCmdBindIndexBuffer + vkCmdDrawIndexed added to Vk.cs
- VulkanRenderer loads Content/cube.obj via ObjLoader (36 vertices, 12 faces)
- Replaced hardcoded triangle with rotating cube
- 0 validation errors, ~3800 FPS
2026-06-18 02:05:15 +03:00
emil28092005 4f3810010a feat: push constants + UBO descriptor sets — rotating triangle with VP matrix
- Push constants: rotation angle (float) sent to vertex shader via vkCmdPushConstants
- UBO + descriptor sets: VP matrix (mat4) in uniform buffer, bound via VkDescriptorSet
- New Vulkan functions: vkCreateDescriptorSetLayout, vkCreateDescriptorPool,
  vkAllocateDescriptorSets, vkUpdateDescriptorSets, vkCmdBindDescriptorSets,
  vkCmdPushConstants
- New structs: VkPushConstantRange, VkDescriptorSetLayoutBinding,
  VkDescriptorSetLayoutCreateInfo, VkDescriptorPoolCreateInfo,
  VkDescriptorPoolSize, VkDescriptorSetAllocateInfo, VkWriteDescriptorSet,
  VkDescriptorBufferInfo
- Per-frame UBO buffers (HOST_VISIBLE|HOST_COHERENT, 64 bytes each)
- Perspective projection + look-at camera (static, no ECS yet)
- Cleaned up debug logging in VulkanContext
- Zero validation errors
2026-06-18 01:59:41 +03:00
emil28092005 2e0970e769 feat: rebuild Vulkan renderer from scratch — pure P/Invoke triangle (Vulkan 1.3)
- Complete rewrite of Engine.Graphics.Vulkan with pure P/Invoke (no wrapper libs)
- Vulkan 1.3: dynamic rendering (vkCmdBeginRendering/vkCmdEndRendering),
  synchronization2 (vkQueueSubmit2, vkCmdPipelineBarrier2)
- Split types into VulkanHandles.cs, VulkanEnums.cs, VulkanStructs.cs
- Staging buffer → device-local vertex buffer pattern
- Correct swapchain semaphore indexing (per-image, not per-frame-in-flight)
- VK_EXT_debug_utils debug messenger with validation layer fallback
- Dynamic viewport/scissor (no pipeline recreation on resize)
- Simplified Program.cs to triangle-only rendering
- Removed old Silk.NET renderer, ImGui, PBR shaders, screenshot code
- Updated VULKAN_IMPLEMENTATION_PLAN.md with full architecture decisions
2026-06-18 01:49:25 +03:00
emil28092005 ee98e4ad08 fix(window): ensure SDL3/Wayland window is mapped by pumping events after ShowWindow
- Remove ALWAYS_ON_TOP and debug print hacks.
- Pump SDL events after SDL_ShowWindow so the Wayland compositor maps the window.
- Clean run.sh: do not force DISPLAY or SDL_VIDEODRIVER, let SDL3 auto-detect.
2026-06-18 00:38:16 +03:00
emil28092005 aa93b55a74 fix: window now visible — SDL_ShowWindow + Wayland support in run.sh
- Added SDL_ShowWindow + SDL_RaiseWindow after window creation
- run.sh: auto-detect Wayland vs X11, set SDL_VIDEODRIVER accordingly
- Wayland session uses SDL_VIDEODRIVER=wayland (bundled SDL3 lacks X11 support)
- Engine runs at 1600+ FPS, window visible on Wayland desktop
- Screenshots confirm 3D geometry rendering correctly
2026-06-18 00:22:27 +03:00
emil28092005 46a04850e3 fix: rendering now works — three critical bugs fixed
1. VkColorComponentFlags: wrong bit values (0x10-0x80 instead of 0x01-0x08)
   → colorWriteMask was 0xF0 instead of 0x0F → no color channels written

2. VkClearValue: was Sequential layout instead of Explicit (union)
   → depthStencil clear value written at wrong offset → depth buffer
   cleared to 0.0 instead of 1.0 → all fragments failed depth test

3. Vulkan clip space correction in vertex shader:
   gl_Position.y = -gl_Position.y (Vulkan Y-down vs OpenGL Y-up)
   gl_Position.z = (gl_Position.z + gl_Position.w) * 0.5 (Z [0,1] vs [-1,1])

Also:
- PushConstantSize fixed from 128 to 144 (two mat4 + vec4)
- Dynamic viewport/scissor state added to pipeline
- Push constants use column_major (default) to match System.Numerics row-major layout
- Camera.GetProjectionMatrix reverted to pure OpenGL-style (correction in shader)
- Screenshots show actual 3D geometry (calibration cubes visible)
- 66/66 tests pass
2026-06-18 00:14:40 +03:00
emil28092005 749fbc8df4 feat: ImGui integration + real screenshot capture via Vulkan
ImGui:
- ImGui.NET 1.91.6.1 NuGet package
- VulkanImGui: font texture upload, ImGui pipeline (blending, no depth), vertex/index buffer streaming
- GLSL 450 ImGui shaders (vert: scale/translate push constants, frag: font texture sampler)
- Compiled to SPIR-V via @webgpu/glslang
- Debug overlay: FPS, camera position, entity count, light list
- 1600+ FPS with ImGui active

Screenshot:
- vkCmdCopyImageToBuffer embedded in main command buffer
- Layout transition PresentSrcKHR→TransferSrcOptimal→PresentSrcKHR
- Deferred read: staging buffer read on next frame after fence
- BMP format written directly to FileStream (row by row)
- All 16 camera tour screenshots captured (1280x720x32bpp)

All 66 tests pass.
2026-06-17 23:39:38 +03:00
emil28092005 544f6b59c0 feat: real screenshot capture — BMP from swapchain image via vkCmdCopyImageToBuffer
- Added vkCmdCopyImageToBuffer to P/Invoke layer
- Screenshot embedded in main command buffer (barrier PresentSrcKHR→TransferSrc, copy, barrier back)
- Deferred read: staging buffer read on next frame after fence signaled
- BMP format written directly to FileStream (row by row, avoids large heap allocations)
- BGRA→BGRA direct copy for B8G8R8A8 swapchain format
- All 16 camera tour screenshots captured successfully (1280x720x32bpp)
- 66/66 tests pass
2026-06-17 23:27:58 +03:00
emil28092005 80238b08f5 feat: pure Vulkan P/Invoke renderer — no wrappers, SDL3 window, SPIR-V shaders
- Engine.Graphics: restored interfaces (IRenderContext, IRenderer, RenderBackendFactory),
  loaders (ObjLoader, GltfLoader), ProceduralMesh, MeshMath, SceneSerializer
- Engine.Graphics.Vulkan: pure P/Invoke to libvulkan.so.1/vulkan-1.dll
  - VulkanNative: library loading, function pointer loading via vkGetInstanceProcAddr
  - Vk: static function cache for ~80 Vulkan functions, all delegate types
  - VulkanTypes: ~50 structs, ~20 enums matching Vulkan C headers
  - VulkanContext: instance, physical device, logical device, surface, memory types
  - VulkanSwapchain: swapchain, image views, depth image, render pass, framebuffers
  - VulkanPipeline: graphics pipeline, descriptor set layout, shader modules
  - VulkanBuffer: vertex/index/uniform buffers, staging, memory allocation
  - VulkanRenderer: command buffers, sync (semaphores/fences), render loop, 2 frames in flight
  - GLSL 450 shaders: PBR lighting (Fresnel, ACES, gamma), directional+point lights
  - Compiled to SPIR-V via @webgpu/glslang WASM
- CortexEngine.App: switched to vulkan backend, removed Raylib/OpenTK/ImGui refs
- Tests: all 66 pass (ObjLoader, MeshMath, ProceduralMesh, SceneSerializer, etc.)
- Camera tour: 16 poses, screenshots captured, clean shutdown
2026-06-17 23:03:24 +03:00
emil28092005 dd105ea4af chore: remove all graphics backends (Raylib, OpenTK, Vulkan/Silk.NET) — prepare for pure Vulkan P/Invoke rewrite 2026-06-17 22:12:51 +03:00
emil28092005 ff4a3faae9 revert: switch back to Raylib default, keep OpenTK for future
OpenTK renderer has matrix/swap issues causing garbled output.
Raylib restored as default. OpenTK project preserved for debugging.
Normal PBR shader restored in OpenTKRenderer.
2026-06-17 21:35:20 +03:00
emil28092005 f0af0a6b5e fix: rewrite OpenTK renderer from scratch — correct matrix transpose
- SetUniformMat4: copies Matrix4 to float[16] row-major, passes with
  transpose=true (OpenGL transposes row-major → column-major)
- Previous version used transpose=false with manual column-major copy
  which caused double-transpose → garbage rendering (strobe)
- VertexAttribPointer with stride=0 (tightly packed per-attribute buffers)
- MakeCurrent() explicitly in constructor to ensure GL context is ready
- 66/66 tests, 1400+ FPS
2026-06-17 21:28:53 +03:00
emil28092005 270806a189 feat: OpenTK 4.x OpenGL backend — replaces Silk.NET, default renderer
- Deleted Engine.Graphics.OpenGL (Silk.NET) project
- Created Engine.Graphics.OpenTK with OpenTK 4.9.4
- OpenTKWindow: GameWindow + ProcessEvents() (non-blocking), SwapBuffers()
- OpenTKInputState: KeyboardState/MouseState → Engine.Core.Key
- OpenTKRenderer: raw OpenGL 3.3 Core, managed arrays (no unsafe),
  OpenTK.Mathematics.Matrix4 (column-major, no transpose needed),
  shadow FBO with depth texture, PCF 3x3, full PBR shader
- OpenTKRenderContext + OpenTKBackendRegistrar ('opentk')
- Program.cs: default backend is 'opentk'
- 66/66 tests, 0 errors, 1600 FPS (VSync off)
2026-06-17 21:24:33 +03:00
emil28092005 5687e97a09 revert: switch back to Raylib as default backend
OpenGL backend has rendering issues (stripes) that need visual debugging.
Raylib restored as default. OpenGL backend code preserved for future work.
2026-06-17 21:05:59 +03:00
emil28092005 a306439f22 wip: OpenGL backend — pinned buffers, void* offsets, manual swap
- GCHandle.Alloc(Pinned) for BufferData to prevent GC relocation
- void* null for VertexAttribPointer offsets
- ShouldSwapAutomatically=false, manual SwapBuffers
- Solid color debug shader active
- Still has rendering issues — needs visual debugging
2026-06-17 21:05:17 +03:00
emil28092005 3c67790947 fix: column-major matrix copy for OpenGL
CopyMatrixToBuffer now transposes during copy (column-major output),
UniformMatrix4 uses transpose=false. Fixes garbage rendering.
2026-06-17 20:59:15 +03:00
emil28092005 07754a31bf fix: transpose matrices for OpenGL (row-major → column-major)
System.Numerics.Matrix4x4 is row-major, OpenGL UniformMatrix4 with
transpose=false expects column-major. Changed all UniformMatrix4 calls
to transpose=true. Also explicitly enable depth test + disable cull face
for main pass. Fixes strobe/flickering issue.
2026-06-17 20:55:59 +03:00
emil28092005 417915e861 feat: switch default backend to Silk.NET OpenGL
- OpenGLRenderContext: Initialize() + CreateOpenGL() for non-blocking
- OpenGLWindow: Initialize() instead of Run(), CreateGL() method
- Program.cs: default backend is now 'opengl', SwapBuffers per frame
- ImGui guarded: only initialized for RaylibRenderer, ObjectManipulator
  checks _imGuiAvailable before calling ImGui.GetIO()
- 66/66 tests, 145 FPS with OpenGL backend
2026-06-17 20:14:15 +03:00
emil28092005 cc9a1a4602 feat: Silk.NET OpenGL backend compiles — full shadow mapping support
- Fixed all Silk.NET 2.21 API issues:
  - BufferData with void* + fixed blocks
  - UniformMatrix4 with float[] buffer (CopyMatrixToBuffer helper)
  - DrawBuffer/ReadBuffer with DrawBufferMode/ReadBufferMode
  - Clear with (uint) cast
  - Input delegates: KeyDown(IKeyboard,Key,int), MouseDown(IMouse,MouseButton), Scroll(IMouse,ScrollWheel)
  - GetProgram with ProgramPropertyARB
  - IWindow alias SilkWindow to avoid ambiguity
- OpenGLRenderer: shadow FBO with depth texture, PCF 3x3, full PBR shader
- OpenGLRenderContext: Silk.NET window + GL context
- OpenGLBackendRegistrar registered as 'opengl'
- 66/66 tests, 0 errors
2026-06-17 18:59:36 +03:00
emil28092005 6e65eedf1d wip: Silk.NET OpenGL backend — project structure, partial implementation
- Engine.Graphics.OpenGL project with Silk.NET.OpenGL, Windowing, Input
- OpenGLWindow (Silk.NET.Windowing), OpenGLInputState (Silk.NET.Input)
- OpenGLRenderer: full shader pipeline with shadow mapping, VAO/VBO upload
- OpenGLRenderContext, OpenGLBackendRegistrar
- Build errors due to Silk.NET 2.21 API differences — needs API investigation
- Raylib and Vulkan backends unchanged
2026-06-17 18:48:36 +03:00
emil28092005 e452a294cc revert: remove shadow mapping attempts, restore clean renderer
Shadow mapping via Raylib doesn't work reliably:
- Rlgl.GetMatrixModelview returns zeros with custom FBO
- MaterialMapIndex binding doesn't map to expected texture units
- DrawModelEx resets texture state internally
Restored to last working state without shadows.
Consider direct OpenGL backend for shadow mapping.
2026-06-17 18:40:30 +03:00
emil28092005 573872298f fix: shadow map binding before draw + correct matrix transpose
- Use BeginShaderMode/EndShaderMode around DrawModelEx to keep shader active
- Bind shadow map on texture unit 1 BEFORE DrawModelEx (not after)
- Remove manual Matrix4x4.Transpose — SetShaderValueMatrix already handles
  row-major to column-major conversion via glUniformMatrix4fv(transpose=false)
- 66/66 tests, 145 FPS
2026-06-17 18:11:08 +03:00
emil28092005 6d16287bcb fix: shadow map re-bind after DrawModelEx + transpose lightViewProj
- Bind shadow map on MaterialMapIndex.Emission (unit 1) per entity
- Re-bind via Rlgl.ActiveTextureSlot(1)+EnableTexture after DrawModelEx
  (DrawModelEx resets texture state internally)
- Transpose lightViewProj matrix (Raylib row-major → OpenGL column-major)
- 66/66 tests, 145 FPS
2026-06-17 18:07:13 +03:00