feat(engine)!: refactor graphics, ECS, and logging APIs

Major refactor of graphics and ECS infrastructure:
- Removed IResourceManager, IRenderSystem, IFenceSynchronizer interfaces; ResourceManager and RenderSystem are now concrete classes.
- Updated all render graph, pipeline, and context code to use concrete ResourceManager.
- Refactored camera/frustum math and render extraction for clarity and correctness; frustum now uses inline arrays.
- RenderingLayerMask is now an immutable struct with bitwise operators.
- Meshlet and meshlet group data structures improved; meshlet build callback signature updated.
- Logging system overhauled: LogMessage is now a class, LogCollection supports change events, and Logger is used directly in the debug console.
- ECS query API: ChunkView.Count renamed to EntityCount; query builder/iterators use VirtualStack.Scope.
- Updated render pipeline and passes for new resource manager and render list APIs.
- Cleaned up obsolete files, improved code style, and updated documentation.
- HLSL meshlet shader updated for new struct layout.
- Debug console now uses new logger and log collection.

BREAKING CHANGE: Public APIs for resource management, rendering, ECS queries, and logging have changed. Interfaces removed; use new concrete types and updated method signatures.
This commit is contained in:
2026-03-21 22:10:28 +09:00
parent 793df1af4f
commit 37f4795b4f
45 changed files with 1007 additions and 840 deletions

View File

@@ -11,13 +11,13 @@ public unsafe struct Camera : IComponent
public float nearClipPlane;
public float farClipPlane;
public float2 sensorSize;
public float2 sensorSize; // mm
public GateFit gateFit;
public float iso;
public float shutterSpeed;
public float aperture;
public float focalLength;
public float focusDistance;
public float focalLength; // mm
public float focusDistance; // m
public RenderingLayerMask renderingLayerMask;
@@ -29,5 +29,5 @@ public unsafe struct Camera : IComponent
// TODO: Add more render targets like motion vector, etc.
// Custim render function. If it's not null, the render system will call this function instead of the default render pipeline.
public delegate*<ref readonly RenderingContext, ref readonly RenderRequest, void> renderFunc;
public delegate*<ref readonly RenderContext, ref readonly RenderRequest, void> renderFunc;
}

View File

@@ -8,7 +8,7 @@ public struct MeshInstance : IComponent
{
public Handle<Mesh> mesh;
public Identifier<MaterialPalette> materialPalette;
public ShadowCastingMode shadowCastingMode;
public RenderingLayerMask renderingLayerMask;
public ShadowCastingMode shadowCastingMode;
public bool staticShadowCaster;
}
}

View File

@@ -107,7 +107,7 @@ public static class SceneManager
var entities = chunk.GetEntities();
var sceneIDs = chunk.GetComponentData<Components.SceneID>();
for (var i = 0; i < chunk.Count; i++)
for (var i = 0; i < chunk.EntityCount; i++)
{
if (sceneIDs[i].scene.ID == scene.ID)
{
@@ -140,7 +140,7 @@ public static class SceneManager
var chunkEntities = chunk.GetEntities();
var sceneIDs = chunk.GetComponentData<Components.SceneID>();
for (var i = 0; i < chunk.Count; i++)
for (var i = 0; i < chunk.EntityCount; i++)
{
if (sceneIDs[i].scene.ID == scene.ID)
{

View File

@@ -4,31 +4,24 @@ using Misaki.HighPerformance.Jobs;
namespace Ghost.Engine;
public interface IEngineContext : IDisposable
{
IJobScheduler JobScheduler { get; }
IRenderSystem RenderSystem { get; }
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
internal class EngineEntryAttribute : Attribute
{
}
[EngineEntry]
internal sealed partial class EngineCore : IEngineContext
public sealed partial class EngineCore : IDisposable
{
private readonly JobScheduler _jobScheduler;
private readonly RenderSystem _renderSystem;
public IJobScheduler JobScheduler => _jobScheduler;
public IRenderSystem RenderSystem => _renderSystem;
public JobScheduler JobScheduler => _jobScheduler;
public RenderSystem RenderSystem => _renderSystem;
public EngineCore()
internal EngineCore()
{
_jobScheduler = new JobScheduler(Environment.ProcessorCount - 2); // We -2 here, one for main thread, one for render thread
// TODO: Remove the windows dependency from RenderSystem.
var renderingConfig = new RenderSystemDesc
{
FrameBufferCount = 2,
@@ -40,10 +33,6 @@ internal sealed partial class EngineCore : IEngineContext
ComponentRegistry.GetOrRegisterComponentID<ManagedEntityRef>();
}
public void Init()
{
}
public void Dispose()
{
_jobScheduler.Dispose();

View File

@@ -2,55 +2,241 @@ using Ghost.Core;
using Ghost.Engine.Components;
using Ghost.Entities;
using Ghost.Graphics.Core;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.Mathematics;
using Misaki.HighPerformance.Mathematics.Geometry;
namespace Ghost.Engine.Systems;
public class RenderExtractionSystem : ISystem
{
private Identifier<EntityQuery> _queryID;
private IGraphicsEngine _graphicsEngine = null!;
private Identifier<EntityQuery> _cameraQueryID;
private Identifier<EntityQuery> _meshQueryID;
public void Initialize(ref readonly SystemAPI systemAPI)
{
_queryID = new QueryBuilder()
_graphicsEngine = systemAPI.World.GetResource<IGraphicsEngine>();
var builder = new QueryBuilder();
_cameraQueryID = builder
.WithAll<Camera, LocalToWorld>()
.Build(systemAPI.World, false);
_meshQueryID = builder
.WithAll<MeshInstance, LocalToWorld>()
.Build(systemAPI.World);
.Build(systemAPI.World, true);
}
public void Update(ref readonly SystemAPI systemAPI)
private static float3 IntersectFrustumPlanes(float4 p0, float4 p1, float4 p2)
{
if (_queryID.IsInvalid)
float3 n0 = p0.xyz;
float3 n1 = p1.xyz;
float3 n2 = p2.xyz;
float det = math.dot(math.cross(n0, n1), n2);
return (math.cross(n2, n1) * p0.w + math.cross(n0, n2) * p1.w - math.cross(n0, n1) * p2.w) * (1.0f / det);
}
private static Frustum CreateFrustum(Camera camRef, float4x4 vp, float3 viewDir, float3 viewPos)
{
var frustum = new Frustum();
Frustum.CalculateFrustumPlanes(vp, ref frustum.planes);
// We need to recalculate the near and far planes otherwise it does not work for oblique projection matrices used for reflection.
var nearPlane = Plane.CreateFromUnitNormalAndPointInPlane(viewDir, viewPos);
nearPlane.Distance -= camRef.nearClipPlane;
var farPlane = Plane.CreateFromUnitNormalAndPointInPlane(-viewDir, viewPos);
farPlane.Distance += camRef.farClipPlane;
frustum.planes[4] = nearPlane;
frustum.planes[5] = farPlane;
frustum.corners[0] = IntersectFrustumPlanes(frustum.planes[0], frustum.planes[3], frustum.planes[4]);
frustum.corners[1] = IntersectFrustumPlanes(frustum.planes[1], frustum.planes[3], frustum.planes[4]);
frustum.corners[2] = IntersectFrustumPlanes(frustum.planes[0], frustum.planes[2], frustum.planes[4]);
frustum.corners[3] = IntersectFrustumPlanes(frustum.planes[1], frustum.planes[2], frustum.planes[4]);
frustum.corners[4] = IntersectFrustumPlanes(frustum.planes[0], frustum.planes[3], frustum.planes[5]);
frustum.corners[5] = IntersectFrustumPlanes(frustum.planes[1], frustum.planes[3], frustum.planes[5]);
frustum.corners[6] = IntersectFrustumPlanes(frustum.planes[0], frustum.planes[2], frustum.planes[5]);
frustum.corners[7] = IntersectFrustumPlanes(frustum.planes[1], frustum.planes[2], frustum.planes[5]);
return frustum;
}
public unsafe void Update(ref readonly SystemAPI systemAPI)
{
if (_meshQueryID.IsInvalid)
{
return;
}
ref var query = ref systemAPI.World.ComponentManager.GetEntityQueryReference(_queryID);
var renderList = new RenderList(1, 64, Allocator.Temp);
ref var cameraQuery = ref systemAPI.World.ComponentManager.GetEntityQueryReference(_cameraQueryID);
ref var meshQuery = ref systemAPI.World.ComponentManager.GetEntityQueryReference(_meshQueryID);
// TODO: We should extract the render record for each camera because different cameras may have different culling results.
// TODO: This chould be done in parallel jobs.
foreach (var chunk in query.GetChunkIterator())
foreach (var (cam, camLtw) in cameraQuery.GetComponentIterator<Camera, LocalToWorld>())
{
var meshInstances = chunk.GetComponentData<MeshInstance>();
var localToWorlds = chunk.GetComponentData<LocalToWorld>();
ref readonly var camRef = ref cam.Get();
ref readonly var camLtwRef = ref camLtw.Get();
for (var i = 0; i < chunk.Count; i++)
var rtResult = _graphicsEngine.ResourceDatabase.GetResourceDescription(camRef.colorTarget.AsResource());
if (rtResult.IsFailure)
{
ref readonly var meshInstance = ref meshInstances[i];
ref readonly var localToWorld = ref localToWorlds[i];
renderList.Add(new RenderRecord
{
localToWorld = localToWorld.matrix,
mesh = meshInstance.mesh,
materialPalette = meshInstance.materialPalette,
renderingLayerMask = meshInstance.renderingLayerMask,
}, 0);
continue;
}
}
// TODO: Send render list to render pipeline.
var rtSize = new uint2(rtResult.Value.TextureDescription.Width, rtResult.Value.TextureDescription.Height);
var aspectScreen = (float)rtSize.x / rtSize.y;
var renderList = new RenderList(1, 64, Allocator.Temp);
var shadowCasterRenderList = new RenderList(1, 64, Allocator.Temp);
// TODO: This chould be done in parallel jobs.
foreach (var chunk in meshQuery.GetChunkIterator())
{
var meshInstances = chunk.GetComponentData<MeshInstance>();
var localToWorlds = chunk.GetComponentData<LocalToWorld>();
for (var i = 0; i < chunk.EntityCount; i++)
{
ref readonly var meshInstance = ref meshInstances[i];
if ((meshInstance.renderingLayerMask & camRef.renderingLayerMask) == 0u)
{
continue;
}
ref readonly var meshLtw = ref localToWorlds[i];
var meshPosition = meshLtw.matrix.c3.xyz;
var camPosition = camLtwRef.matrix.c3.xyz;
var distance = math.distance(meshPosition, camPosition);
if (distance < camRef.nearClipPlane || distance > camRef.farClipPlane)
{
continue;
}
if (meshInstance.shadowCastingMode != ShadowCastingMode.ShadowsOnly)
{
renderList.Add(new RenderRecord
{
localToWorld = meshLtw.matrix,
mesh = meshInstance.mesh,
materialPalette = meshInstance.materialPalette,
renderingLayerMask = meshInstance.renderingLayerMask,
}, 0);
}
if (meshInstance.shadowCastingMode != ShadowCastingMode.Off)
{
shadowCasterRenderList.Add(new RenderRecord
{
localToWorld = meshLtw.matrix,
mesh = meshInstance.mesh,
materialPalette = meshInstance.materialPalette,
renderingLayerMask = meshInstance.renderingLayerMask,
}, 0);
}
}
}
// NOTE: We assume camera's scale is always (1, 1, 1). Otherwise fastinverse will fail and we need to use regular inverse which is more expensive.
var viewMatrix = math.fastinverse(camLtwRef.matrix);
var vfov = 2.0f * math.atan(camRef.sensorSize.y / 2.0f * camRef.focalLength);
var hfov = 2.0f * math.atan(camRef.sensorSize.x / 2.0f * camRef.focalLength);
var aspectSensor = camRef.sensorSize.x / camRef.sensorSize.y;
float vfovF;
switch (camRef.gateFit)
{
case GateFit.Vertical:
vfovF = vfov;
break;
case GateFit.Horizontal:
// Adjust VFOV so that the sensor width fits the screen width
var horizontalAspectBuffer = math.tan(hfov * 0.5f);
vfovF = 2.0f * math.atan(horizontalAspectBuffer / aspectScreen);
break;
case GateFit.Fill:
if (aspectSensor > aspectScreen)
{
goto case GateFit.Vertical;
}
else
{
goto case GateFit.Horizontal;
}
case GateFit.Overscan:
if (aspectSensor > aspectScreen)
{
goto case GateFit.Horizontal;
}
else
{
goto case GateFit.Vertical;
}
default:
vfovF = vfov;
break;
}
var m_00 = 1.0f / aspectScreen * math.tan(vfovF * 0.5f);
var m_11 = 1.0f / math.tan(vfovF * 0.5f);
var m_22 = -(camRef.farClipPlane + camRef.nearClipPlane) / (camRef.farClipPlane - camRef.nearClipPlane);
var m_23 = -(2.0f * camRef.farClipPlane * camRef.nearClipPlane) / (camRef.farClipPlane - camRef.nearClipPlane);
var projectionMatrix = new float4x4
(
m_00, 0, 0, 0,
0, m_11, 0, 0,
0, 0, m_22, m_23,
0, 0, -1, 0
);
var vp = math.mul(projectionMatrix, viewMatrix);
var viewDir = math.normalize(camLtwRef.matrix.c2.xyz);
var viewPos = camLtwRef.matrix.c3.xyz;
var frustum = CreateFrustum(camRef, vp, viewDir, viewPos);
// TODO: Send this to render pipeline.
var request = new RenderRequest
{
colorTarget = camRef.colorTarget,
depthTarget = camRef.depthTarget,
opaqueRenderList = renderList,
shadowCasterRenderList = shadowCasterRenderList,
transparentRenderList = default,
renderFunc = camRef.renderFunc,
view = new RenderView
{
viewMatrix = viewMatrix,
projectionMatrix = projectionMatrix,
position = camLtwRef.matrix.c3.xyz,
frustum = frustum,
nearClipPlane = camRef.nearClipPlane,
farClipPlane = camRef.farClipPlane,
sensorSize = camRef.sensorSize,
gateFit = camRef.gateFit,
iso = camRef.iso,
shutterSpeed = camRef.shutterSpeed,
aperture = camRef.aperture,
focalLength = camRef.focalLength,
focusDistance = camRef.focusDistance,
renderingLayerMask = camRef.renderingLayerMask,
},
};
}
}
public void Cleanup(ref readonly SystemAPI systemAPI)