From 923087cd254d0aba25e252d5ade3577f6cbedf95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=B6eh=20Matt?= <5415177+ZehMatt@users.noreply.github.com> Date: Fri, 16 Oct 2026 21:37:14 +0300 Subject: [PATCH 1/8] Split render context state into MatRenderContextBase --- .../IMatRenderContextInternal.cs | 6 +- Source.MaterialSystem/MatRenderContext.cs | 480 ++--------------- Source.MaterialSystem/MatRenderContextBase.cs | 505 ++++++++++++++++++ 3 files changed, 545 insertions(+), 446 deletions(-) create mode 100644 Source.MaterialSystem/MatRenderContextBase.cs diff --git a/Source.MaterialSystem/IMatRenderContextInternal.cs b/Source.MaterialSystem/IMatRenderContextInternal.cs index e5c11ee79..c7f65519c 100644 --- a/Source.MaterialSystem/IMatRenderContextInternal.cs +++ b/Source.MaterialSystem/IMatRenderContextInternal.cs @@ -28,8 +28,8 @@ public interface IMatRenderContextInternal : IMatRenderContext bool OnDrawMesh(IMesh mesh, int firstIndex, int numIndices); unsafe bool OnDrawMesh(IMesh mesh, PrimList* pLists, int nLists); - bool OnSetFlexMesh(IMesh staticMesh, IMesh mesh, int nVertexOffsetInBytes); - bool OnSetColorMesh(IMesh staticMesh, IMesh mesh, int nVertexOffsetInBytes); + bool OnSetFlexMesh(IMesh staticMesh, IMesh? mesh, int nVertexOffsetInBytes); + bool OnSetColorMesh(IMesh staticMesh, IMesh? mesh, int nVertexOffsetInBytes); bool OnSetPrimitiveType(IMesh mesh, MaterialPrimitiveType type); bool OnFlushBufferedPrimitives(); @@ -43,7 +43,7 @@ public interface IMatRenderContextInternal : IMatRenderContext void SetFrameTime(TimeUnit_t frameTime); void SetCurrentProxy(object? proxy); void MarkRenderDataUnused(bool bBeginFrame); - MatCallQueue GetCallQueueInternal(); + MatCallQueue? GetCallQueueInternal(); #if GMOD_DLL bool GMOD_IsLowOnMemory() { return false; } diff --git a/Source.MaterialSystem/MatRenderContext.cs b/Source.MaterialSystem/MatRenderContext.cs index d1819e1e5..74a1e54d0 100644 --- a/Source.MaterialSystem/MatRenderContext.cs +++ b/Source.MaterialSystem/MatRenderContext.cs @@ -12,42 +12,13 @@ namespace Source.MaterialSystem; -public class MatRenderContext : IMatRenderContextInternal +public class MatRenderContext : MatRenderContextBase, IMatRenderContextInternal { - readonly MaterialSystem materials; readonly IShaderAPI shaderAPI; - public MatRenderContext(MaterialSystem materials) { - this.materials = materials; - RenderTargetStack = new RefStack(); - MatrixStacks = new RefStack[(int)MaterialMatrixMode.Count]; - MatrixStacksDirtyStates = new bool[(int)MaterialMatrixMode.Count]; - for (int i = 0; i < MatrixStacks.Length; i++) { - MatrixStacks[i] = new(); - ref MatrixStackItem item = ref MatrixStacks[i].Push(); - MatrixStacksDirtyStates[i] = true; - item.Matrix = Matrix4x4.Identity; - } - RenderTargetStackElement initialElement = new() { - DepthTexture = null, - ViewX = 0, - ViewY = 0, - ViewW = -1, - ViewH = -1 - }; - RenderTargetStack.Push(initialElement); + public MatRenderContext(MaterialSystem materials) : base(materials) { shaderAPI = materials.ShaderAPI; - - DirtyViewState = true; - DirtyViewProjState = true; } - RefStack RenderTargetStack; - RefStack[] MatrixStacks; - bool[] MatrixStacksDirtyStates; - MaterialMatrixMode matrixMode; - ShaderViewport ActiveViewport = new(0, 0, 0, 0, 0, 1); - public ref MatrixStackItem CurMatrixItem => ref MatrixStacks[(int)matrixMode].Top(); - public ref RenderTargetStackElement CurRenderTargetStack => ref RenderTargetStack.Top(); public void BeginRender() { @@ -83,13 +54,13 @@ public void EndRender() { } - public void Flush(bool flushHardware = false) { + public override void Flush(bool flushHardware = false) { shaderAPI.FlushBufferedPrimitives(); } public void GMOD_ForceFilterMode(bool min, int mode) => shaderAPI.GMOD_ForceFilterMode(min, mode); - public void GetViewport(out int x, out int y, out int width, out int height) { + public override void GetViewport(out int x, out int y, out int width, out int height) { Assert(RenderTargetStack.Count > 0); ref RenderTargetStackElement element = ref RenderTargetStack.Top(); if (element.ViewW <= 0 || element.ViewH <= 0) { @@ -111,59 +82,13 @@ public void GetViewport(out int x, out int y, out int width, out int height) { } } - public void LoadIdentity() { - ref MatrixStackItem item = ref CurMatrixItem; - item.Matrix = Matrix4x4.Identity; - CurrentMatrixChanged(); - } - - public void LoadMatrix(in Matrix4x4 matrix) { - ref MatrixStackItem item = ref CurMatrixItem; - item.Matrix = matrix; - CurrentMatrixChanged(); - } - - public void LoadMatrix(Matrix3x4 matrix) { - ref MatrixStackItem item = ref CurMatrixItem; - item.Matrix = matrix; - CurrentMatrixChanged(); - } - - private void MarkDirty() => MatrixStacksDirtyStates[(int)matrixMode] = true; - - public void MatrixMode(MaterialMatrixMode mode) { - matrixMode = mode; - } - - public void PushMatrix() { - RefStack curStack = MatrixStacks[(int)matrixMode]; - curStack.Push(CurMatrixItem); - CurrentMatrixChanged(); + public override void PushMatrix() { + base.PushMatrix(); shaderAPI.PushMatrix(); } - private void CurrentMatrixChanged() { - MarkDirty(); - - if (matrixMode == MaterialMatrixMode.View) { - DirtyViewState = true; - DirtyViewProjState = true; - } - else if (matrixMode == MaterialMatrixMode.Projection) - DirtyViewProjState = true; - } - - public void Viewport(int x, int y, int width, int height) { - Dbg.Assert(RenderTargetStack.Count > 0); - - RenderTargetStackElement newTOS = new(); - newTOS = CurRenderTargetStack; // copy - newTOS.ViewX = x; - newTOS.ViewY = y; - newTOS.ViewW = width; - newTOS.ViewH = height; - RenderTargetStack.Pop(); - RenderTargetStack.Push(newTOS); + public override void Viewport(int x, int y, int width, int height) { + base.Viewport(x, y, width, height); if ((width < 0) || (height < 0)) { ITexture? target = RenderTargetStack.Top().RenderTarget0; @@ -191,9 +116,7 @@ public void Viewport(int x, int y, int width, int height) { } } - IMaterialInternal? currentMaterial; - - public void Bind(IMaterial iMaterial, object? proxyData) { + public override void Bind(IMaterial iMaterial, object? proxyData) { if (iMaterial == null) { if (materials.errorMaterial == null) return; @@ -207,50 +130,29 @@ public void Bind(IMaterial iMaterial, object? proxyData) { IMaterialInternal material = (IMaterialInternal)iMaterial; material = material.GetRealTimeVersion(); // TODO: figure out how to do this. - SyncMatrices(); - if (material == null) { Warning("Programming error: MatRenderContext.Bind NULL material\n"); material = ((MaterialSystem)materials).errorMaterial; } - if (GetCurrentMaterialInternal() != material) { - if (!material.IsPrecached()) { - material.Precache(); - } - SetCurrentMaterialInternal(material); - } - SetCurrentProxy(proxyData); - - if (materials.GetThreadMode() == MaterialThreadMode.SingleThreaded) - GetCurrentMaterialInternal()!.CallBindProxy(proxyData); + base.Bind(material, proxyData); - shaderAPI.Bind(GetCurrentMaterialInternal()); - } + IMaterialInternal? matInternal = GetCurrentMaterialInternal(); + Assert(matInternal != null); - public IMaterialInternal? GetCurrentMaterialInternal() { - return currentMaterial; - } - public void SetCurrentMaterialInternal(IMaterialInternal? mat) { - currentMaterial = mat; - } + SyncMatrices(); + if (materials.GetThreadMode() == MaterialThreadMode.SingleThreaded) + matInternal!.CallBindProxy(proxyData); - public IMaterial? GetCurrentMaterial() { - return currentMaterial; + shaderAPI.Bind(matInternal); } - object? CurrentProxyData; - public object? GetCurrentProxy() => CurrentProxyData; - public void SetCurrentProxy(object? proxyData) => CurrentProxyData = proxyData; - public int GetCurrentNumBones() => shaderAPI.GetCurrentNumBones(); - public void PopMatrix() { + public override void PopMatrix() { shaderAPI.PopMatrix(); // We need to tell ShaderAPI *NOW* so it can flush primitives trigger matrix sync etc // ^^ is NOT source behavior. But I think, for all intents and purposes, it will act as such (we'll see if I eat my words on that) - RefStack curStack = MatrixStacks[(int)matrixMode]; - curStack.Pop(); - CurrentMatrixChanged(); + base.PopMatrix(); } public IShaderAPI GetShaderAPI() { @@ -313,13 +215,6 @@ public void EndBatch() { BatchMesh = null; } - bool FlashlightEnable; - bool DirtyViewState; - bool DirtyViewProjState; - bool EnableClippingValue = true; - MaterialHeightClipMode HeightClipMode; - float HeightClipZ; - struct PlaneStackElement { public InlineArray4 Values; @@ -328,33 +223,7 @@ struct PlaneStackElement readonly List CustomClipPlanes = []; - public bool EnableClipping(bool enable) { - if (enable != EnableClippingValue) { - EnableClippingValue = enable; - ApplyCustomClipPlanes(); - - return !enable; - } - return enable; - } - - public MaterialHeightClipMode GetHeightClipMode() => HeightClipMode; - - public void SetHeightClipMode(MaterialHeightClipMode heightClipMode) { - if (HeightClipMode != heightClipMode) { - HeightClipMode = heightClipMode; - UpdateHeightClipUserClipPlane(); - } - } - - public void SetHeightClipZ(float z) { - if (z != HeightClipZ) { - HeightClipZ = z; - UpdateHeightClipUserClipPlane(); - } - } - - void UpdateHeightClipUserClipPlane() { + protected override void UpdateHeightClipUserClipPlane() { PlaneStackElement pse = default; pse.Hack_IsHeightClipPlane = true; @@ -416,7 +285,7 @@ public void PopCustomClipPlane() { ApplyCustomClipPlanes(); } - void ApplyCustomClipPlanes() { + protected override void ApplyCustomClipPlanes() { int maxClipPlanes = materials.HardwareConfig.MaxUserClipPlanes(); int customPlanes = EnableClippingValue ? CustomClipPlanes.Count : 0; @@ -451,10 +320,8 @@ public bool InFlashlightMode() { return FlashlightEnable; } - Vector3 LastSetToneMapScale = new(1, 1, 1); float CurToneMapScale = 1.0f; float GoalToneMapScale = 1.0f; - TimeUnit_t FrameTime; static readonly ConVar mat_accelerate_adjust_exposure_down = new("mat_accelerate_adjust_exposure_down", "3.0", FCvar.Cheat); static readonly ConVar mat_hdr_manual_tonemap_rate = new("mat_hdr_manual_tonemap_rate", "1.0"); @@ -500,14 +367,6 @@ public void TurnOnToneMapping() { public void BeginFrame() => shaderAPI.BeginFrame(); public void EndFrame() => shaderAPI.EndFrame(); - public void MarkRenderDataUnused(bool v) { - - } - - public void SetFrameTime(double frameTime) { - FrameTime = frameTime; - } - public void SwapBuffers() { materials.OcclusionQueryMgr.AdvanceFrame(); materials.ShaderDevice.Present(); @@ -542,7 +401,7 @@ public void ForceSyncMatrix(MaterialMatrixMode mode) { } } - public void SyncMatrices() { + public override void SyncMatrices() { if (!ShouldValidateMatrices() && AllowLazyMatrixSync()) { for (int i = 0; i < (int)MaterialMatrixMode.Count; i++) { ref MatrixStackItem top = ref MatrixStacks[i].Top(); @@ -562,35 +421,12 @@ public void SyncMatrices() { } } - public void SyncMatrix(MaterialMatrixMode mode) { + public override void SyncMatrix(MaterialMatrixMode mode) { if (!ShouldValidateMatrices() && AllowLazyMatrixSync()) ForceSyncMatrix(mode); } - public void Translate(float x, float y, float z) { - MathLib.MatrixBuildTranslation(out Matrix4x4 mat, x, y, z); - MultMatrixLocal(in mat); - } - - public void Scale(float x, float y, float z) { - MathLib.MatrixBuildScale(out Matrix4x4 mat, x, y, z); - MultMatrixLocal(in mat); - } - - - public void MultMatrixLocal(in Matrix4x4 mat) { - MathLib.MatrixMultiply(in CurMatrixItem.Matrix, in mat, out Matrix4x4 result); - CurMatrixItem.Matrix = result; - CurrentMatrixChanged(); - } - - public void Ortho(double left, double top, double right, double bottom, double near, double far) { - ref Matrix4x4 item = ref CurMatrixItem.Matrix; - MathLib.MatrixOrtho(ref item, (float)left, (float)right, (float)bottom, (float)top, (float)near, (float)far); - CurrentMatrixChanged(); - } - - private void CommitRenderTargetAndViewport() { + protected override void CommitRenderTargetAndViewport() { Assert(RenderTargetStack.Count > 0); ref RenderTargetStackElement element = ref RenderTargetStack.Top(); @@ -644,18 +480,6 @@ private void CommitRenderTargetAndViewport() { } } - public void PushRenderTargetAndViewport(ITexture? thisTexture, int x, int y, int w, int h) { - RenderTargetStackElement element = new(thisTexture, x, y, w, h); - RenderTargetStack.Push(element); - CommitRenderTargetAndViewport(); - } - - public void PushRenderTargetAndViewport(ITexture? colorTexture, ITexture? depthTexture, int x, int y, int w, int h) { - RenderTargetStackElement element = new(colorTexture, depthTexture, x, y, w, h); - RenderTargetStack.Push(element); - CommitRenderTargetAndViewport(); - } - public void CopyRenderTargetToTexture(ITexture texture) => CopyRenderTargetToTextureEx(texture, 0, ref Unsafe.NullRef(), ref Unsafe.NullRef()); public void CopyRenderTargetToTextureEx(ITexture? texture, int renderTargetID, ref System.Drawing.Rectangle srcRect, ref System.Drawing.Rectangle dstRect) { @@ -668,35 +492,10 @@ public void CopyRenderTargetToTextureEx(ITexture? texture, int renderTargetID, r ((ITextureInternal)texture).CopyFrameBufferToMe(renderTargetID, Unsafe.IsNullRef(ref srcRect) ? null : srcRect, Unsafe.IsNullRef(ref dstRect) ? null : dstRect); } - public void PushRenderTargetAndViewport(ITexture? thisTexture) { - RenderTargetStackElement element = new(thisTexture, 0, 0, -1, -1); - RenderTargetStack.Push(element); - CommitRenderTargetAndViewport(); - } - - public void PopRenderTargetAndViewport() { - if (RenderTargetStack.Count == 0) { - AssertMsg(false, "MatRenderContext.PopRenderTargetAndViewport: Stack is empty!\n"); - return; - } - - Flush(); - - RenderTargetStack.Pop(); - CommitRenderTargetAndViewport(); - } - public void GetWindowSize(out int w, out int h) { shaderAPI.GetBackBufferDimensions(out w, out h); } - public ITexture? GetRenderTarget() { - if (RenderTargetStack.Count > 0) - return RenderTargetStack.Top().RenderTarget0; - else - return null; - } - public IMesh CreateStaticMesh(VertexFormat format, ReadOnlySpan textureGroup, IMaterial? material) { return materials.ShaderDevice.CreateStaticMesh(format, textureGroup, material); } @@ -708,98 +507,6 @@ public void DestroyStaticMesh(IMesh mesh) { public int GetMaxVerticesToRender(IMaterial material) => materials.ShaderAPI.GetMaxVerticesToRender(material); public int GetMaxIndicesToRender() => materials.ShaderAPI.GetMaxIndicesToRender(); - public float ComputePixelDiameterOfSphere(in Vector3 absOrigin, float radius) { - RecomputeViewState(); - RecomputeViewProjState(); - // This is sort of faked, but it's faster that way - // FIXME: Also, there's a much faster way to do this with similar triangles - // but I want to make sure it exactly matches the current matrices, so - // for now, I do it this conservative way - Vector4 testPoint1 = default, testPoint2 = default; - MathLib.VectorMA(in absOrigin, radius, in VecViewUp, out testPoint1.AsVector3D()); - MathLib.VectorMA(in absOrigin, -radius, in VecViewUp, out testPoint2.AsVector3D()); - testPoint1.W = testPoint2.W = 1.0f; - - Vector4 clipPos1 = default, clipPos2 = default; - MathLib.Vector4DMultiply(ViewProjMatrix, in testPoint1, ref clipPos1); - MathLib.Vector4DMultiply(ViewProjMatrix, in testPoint2, ref clipPos2); - - if (clipPos1.W >= 0.001f) - clipPos1.Y /= clipPos1.W; - else - clipPos1.Y *= 1000; - - if (clipPos2.W >= 0.001f) - clipPos2.Y /= clipPos2.W; - else - clipPos2.Y *= 1000; - - GetViewport(out int vx, out int vy, out int vwidth, out int vheight); - - return vheight * MathF.Abs(clipPos2.Y - clipPos1.Y) / 2.0f; - } - - Vector3 VecViewOrigin; - Vector3 VecViewForward; - Vector3 VecViewRight; - Vector3 VecViewUp; - - private void RecomputeViewProjState() { - if (DirtyViewProjState) { - Matrix4x4 viewMatrix, projMatrix; - - // FIXME: Should consider caching this upon change for projection or view matrix. - GetMatrix(MaterialMatrixMode.View, out viewMatrix); - GetMatrix(MaterialMatrixMode.Projection, out projMatrix); - ViewProjMatrix = projMatrix * viewMatrix; - DirtyViewProjState = false; - } - } - Matrix4x4 ViewProjMatrix; - - private void RecomputeViewState() { - if (!DirtyViewState) - return; - DirtyViewState = false; - - // FIXME: Cache this off to make it less expensive? - Matrix4x4 viewMatrix; - GetMatrix(MaterialMatrixMode.View, out viewMatrix); - VecViewOrigin.X = - -(viewMatrix[0][3] * viewMatrix[0][0] + - viewMatrix[1][3] * viewMatrix[1][0] + - viewMatrix[2][3] * viewMatrix[2][0]); - VecViewOrigin.Y = - -(viewMatrix[0][3] * viewMatrix[0][1] + - viewMatrix[1][3] * viewMatrix[1][1] + - viewMatrix[2][3] * viewMatrix[2][1]); - VecViewOrigin.Z = - -(viewMatrix[0][3] * viewMatrix[0][2] + - viewMatrix[1][3] * viewMatrix[1][2] + - viewMatrix[2][3] * viewMatrix[2][2]); - - // FIXME Implement computation of m_vecViewForward, etc - VecViewForward = new(); - VecViewRight = new(); - - // FIXME: Is this correct? - VecViewUp = new(viewMatrix[1][0], viewMatrix[1][1], viewMatrix[1][2]); - } - - public void GetMatrix(MaterialMatrixMode mode, out Matrix4x4 viewMatrix) { - var stack = MatrixStacks[(int)mode]; - if (stack.Count == 0) { - viewMatrix = Matrix4x4.Identity; - return; - } - - viewMatrix = stack.Top().Matrix; - } - - public float ComputePixelWidthOfSphere(in Vector3 origin, float radius) { - return ComputePixelDiameterOfSphere(origin, radius) * 2.0f; - } - public void SetNumBoneWeights(int numBones) { shaderAPI.SetNumBoneWeights(numBones); } @@ -810,17 +517,12 @@ public void LoadBoneMatrix(int boneIndex, in Matrix3x4 matrix) { shaderAPI.LoadBoneMatrix(boneIndex, in matrix); } - public void GetWorldSpaceCameraPosition(out Vector3 vecCameraPos) { - vecCameraPos = VecViewOrigin; - } - - int LightmapPageID; - public void BindLightmapPage(int lightmapPageID) { + public override void BindLightmapPage(int lightmapPageID) { if (lightmapPageID == LightmapPageID) return; shaderAPI.FlushBufferedPrimitives(); - LightmapPageID = lightmapPageID; + base.BindLightmapPage(lightmapPageID); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public ShaderAPITextureHandle_t GetBlackTextureHandle() => materials.GetBlackTextureHandle(); @@ -850,10 +552,7 @@ public void BindStandardTexture(Sampler sampler, StandardTextureId id) { } } - public const int MAX_FB_TEXTURES = 4; - readonly ITexture?[] CurrentFrameBufferCopyTexture = new ITexture?[MAX_FB_TEXTURES]; - - public void SetFrameBufferCopyTexture(ITexture? texture, int textureIndex = 0) { + public override void SetFrameBufferCopyTexture(ITexture? texture, int textureIndex = 0) { if (textureIndex < 0 || textureIndex >= MAX_FB_TEXTURES) { Assert(false); return; @@ -865,20 +564,10 @@ public void SetFrameBufferCopyTexture(ITexture? texture, int textureIndex = 0) { CurrentFrameBufferCopyTexture[textureIndex] = texture; } - public ITexture? GetFrameBufferCopyTexture(int textureIndex) { - if (textureIndex < 0 || textureIndex >= MAX_FB_TEXTURES) { - Assert(false); - return null; // FIXME! This should return the error texture. - } - return CurrentFrameBufferCopyTexture[textureIndex]; - } - - ITexture? LocalCubemapTexture; - - public void BindLocalCubemap(ITexture? texture) { + public override void BindLocalCubemap(ITexture? texture) { ITexture? previousTexture = LocalCubemapTexture; - LocalCubemapTexture = texture ?? materials.TextureSystem.ErrorTexture(); + base.BindLocalCubemap(texture); if (LocalCubemapTexture != previousTexture) shaderAPI.FlushBufferedPrimitives(); @@ -950,10 +639,6 @@ public ColorCorrectionHandle_t AddLookup(ReadOnlySpan name) { throw new NotImplementedException("Incomplete port of IMatRenderContext"); } - public void AddRefRenderData() { - throw new NotImplementedException("Incomplete port of IMatRenderContext"); - } - public void BeginMorphAccumulation() { throw new NotImplementedException("Incomplete port of IMatRenderContext"); } @@ -1078,9 +763,7 @@ public void GMOD_FlushQueue() { throw new NotImplementedException("Incomplete port of IMatRenderContext"); } - public ICallQueue? GetCallQueue() { - throw new NotImplementedException("Incomplete port of IMatRenderContext"); - } + public ICallQueue? GetCallQueue() => null; public void GetFogColor(out Color rgb) => shaderAPI.GetSceneFogColor(out rgb); @@ -1088,10 +771,6 @@ public void GMOD_FlushQueue() { public MaterialFogMode GetFogMode() => shaderAPI.GetSceneFogMode(); - public void GetMatrix(MaterialMatrixMode matrixMode, out Matrix3x4 matrix) { - throw new NotImplementedException("Incomplete port of IMatRenderContext"); - } - public void GetMaxToRender(IMesh? pMesh, bool bMaxUntilFlush, Span maxVerts, Span maxIndices) { throw new NotImplementedException("Incomplete port of IMatRenderContext"); } @@ -1104,25 +783,6 @@ public int GetNumActiveDeformations() { throw new NotImplementedException("Incomplete port of IMatRenderContext"); } - public Vector3 GetToneMappingScaleLinear() { - if (materials.HardwareConfig.GetHDRType() == HDRType.None) - return new(1, 1, 1); - else - return LastSetToneMapScale; - } - - public void GetWorldSpaceCameraVectors(out Vector3 forward, out Vector3 right, out Vector3 up) { - throw new NotImplementedException("Incomplete port of IMatRenderContext"); - } - - public bool IsRenderData(object? data) { - throw new NotImplementedException("Incomplete port of IMatRenderContext"); - } - - public float Knob(Span knobname, Span setvalue = default) { - throw new NotImplementedException("Incomplete port of IMatRenderContext"); - } - public void LoadLookup(ColorCorrectionHandle_t handle, ReadOnlySpan lookuname) { throw new NotImplementedException("Incomplete port of IMatRenderContext"); } @@ -1135,26 +795,10 @@ public void LockLookup(ColorCorrectionHandle_t handle) { throw new NotImplementedException("Incomplete port of IMatRenderContext"); } - public object? LockRenderData(int sizeInBytes) { - throw new NotImplementedException("Incomplete port of IMatRenderContext"); - } - public E? LockRenderDataTyped(int count, E? srcData = null) where E : class { throw new NotImplementedException("Incomplete port of IMatRenderContext"); } - public void MultMatrix(in Matrix3x4 matrix) { - throw new NotImplementedException("Incomplete port of IMatRenderContext"); - } - - public void MultMatrix(in Matrix4x4 matrix) { - throw new NotImplementedException("Incomplete port of IMatRenderContext"); - } - - public void MultMatrixLocal(in Matrix3x4 matrix) { - throw new NotImplementedException("Incomplete port of IMatRenderContext"); - } - public int OcclusionQuery_GetNumPixelsRendered(OcclusionQueryObjectHandle_t handle) => materials.OcclusionQueryMgr.OcclusionQuery_GetNumPixelsRendered(handle, true); public void OverrideAlphaWriteEnable(bool enable, bool alphaWriteEnable) => shaderAPI.OverrideAlphaWriteEnable(enable, alphaWriteEnable); @@ -1175,18 +819,6 @@ public void PerformFullScreenStencilOperation() { throw new NotImplementedException("Incomplete port of IMatRenderContext"); } - public void PerspectiveOffCenterX(double fovx, double aspect, double zNear, double zFar, double bottom, double top, double left, double right) { - throw new NotImplementedException("Incomplete port of IMatRenderContext"); - } - - public void PerspectiveX(double fovx, double aspect, double zNear, double zFar) { - throw new NotImplementedException("Incomplete port of IMatRenderContext"); - } - - public void PickMatrix(int x, int y, int width, int height) { - throw new NotImplementedException("Incomplete port of IMatRenderContext"); - } - public void PopDeformation() { throw new NotImplementedException("Incomplete port of IMatRenderContext"); } @@ -1199,12 +831,6 @@ public void PushDeformation(ref readonly DeformationBase deformation) { throw new NotImplementedException("Incomplete port of IMatRenderContext"); } - public void PushRenderTargetAndViewport() { - RenderTargetStackElement top = RenderTargetStack.Top(); - RenderTargetStack.Push(top); - CommitRenderTargetAndViewport(); - } - public void PushSelectionName(int name) { throw new NotImplementedException("Incomplete port of IMatRenderContext"); } @@ -1219,10 +845,6 @@ public void RefreshFrontBufferNonInteractive() { throw new NotImplementedException("Incomplete port of IMatRenderContext"); } - public void ReleaseRenderData() { - throw new NotImplementedException("Incomplete port of IMatRenderContext"); - } - public bool RemoveLookup(ColorCorrectionHandle_t handle) { throw new NotImplementedException("Incomplete port of IMatRenderContext"); } @@ -1240,10 +862,6 @@ public void ResetToneMappingScale(float monoscale) { GoalToneMapScale = 1; } - public void Rotate(float angle, float x, float y, float z) { - throw new NotImplementedException("Incomplete port of IMatRenderContext"); - } - public void SelectionBuffer(Span buffer) { throw new NotImplementedException("Incomplete port of IMatRenderContext"); } @@ -1284,17 +902,12 @@ public void SetPIXMarker(Color color, ReadOnlySpan name) { throw new NotImplementedException("Incomplete port of IMatRenderContext"); } - public void SetRenderTarget(ITexture? texture) => SetRenderTargetEx(0, texture); - - public void SetRenderTargetEx(int renderTargetID, ITexture? newTarget) { + public override void SetRenderTargetEx(int renderTargetID, ITexture? newTarget) { Assert(RenderTargetStack.Count > 0); ITexture? oldTarget = RenderTargetStack.Top()[renderTargetID]; - RenderTargetStackElement newTOS = RenderTargetStack.Top(); - newTOS[renderTargetID] = newTarget; - RenderTargetStack.Pop(); - RenderTargetStack.Push(newTOS); + base.SetRenderTargetEx(renderTargetID, newTarget); if (newTarget != oldTarget) { if (newTarget == null) { @@ -1348,10 +961,6 @@ public void UnlockLookup(ColorCorrectionHandle_t handle) { throw new NotImplementedException("Incomplete port of IMatRenderContext"); } - public void UnlockRenderData(object? data) { - throw new NotImplementedException("Incomplete port of IMatRenderContext"); - } - public void UserClipTransform(in Matrix4x4 worldToView) { throw new NotImplementedException("Incomplete port of IMatRenderContext"); } @@ -1372,9 +981,7 @@ public MorphFormatFlags GetBoundMorphFormat() { throw new NotImplementedException("Incomplete port of IMatRenderContextInternal"); } - public MatCallQueue GetCallQueueInternal() { - throw new NotImplementedException("Incomplete port of IMatRenderContextInternal"); - } + public MatCallQueue? GetCallQueueInternal() => null; public float GetFloatRenderingParameter(int parmNumber) => shaderAPI.GetFloatRenderingParameter(parmNumber); @@ -1384,31 +991,18 @@ public void GetLightmapDimensions(out int w, out int h) { throw new NotImplementedException("Incomplete port of IMatRenderContextInternal"); } - public int GetLightmapPage() { - throw new NotImplementedException("Incomplete port of IMatRenderContextInternal"); - } - - public ITexture? GetRenderTargetEx(int renderTargetId) { - throw new NotImplementedException("Incomplete port of IMatRenderContextInternal"); - } - public Vector3 GetVectorRenderingParameter(int parmNumber) => shaderAPI.GetVectorRenderingParameter(parmNumber); public unsafe bool OnDrawMesh(IMesh mesh, PrimList* pLists, int nLists) { - throw new NotImplementedException("Incomplete port of IMatRenderContextInternal"); + SyncMatrices(); + return true; } - public bool OnFlushBufferedPrimitives() { - throw new NotImplementedException("Incomplete port of IMatRenderContextInternal"); - } + public bool OnFlushBufferedPrimitives() => true; - public bool OnSetColorMesh(IMesh staticMesh, IMesh mesh, int nVertexOffsetInBytes) { - throw new NotImplementedException("Incomplete port of IMatRenderContextInternal"); - } + public bool OnSetColorMesh(IMesh staticMesh, IMesh? mesh, int nVertexOffsetInBytes) => true; - public bool OnSetFlexMesh(IMesh staticMesh, IMesh mesh, int nVertexOffsetInBytes) { - throw new NotImplementedException("Incomplete port of IMatRenderContextInternal"); - } + public bool OnSetFlexMesh(IMesh staticMesh, IMesh? mesh, int nVertexOffsetInBytes) => true; public int AddRef() { throw new NotImplementedException("Incomplete port of IRefCounted"); diff --git a/Source.MaterialSystem/MatRenderContextBase.cs b/Source.MaterialSystem/MatRenderContextBase.cs new file mode 100644 index 000000000..2bc1fbc20 --- /dev/null +++ b/Source.MaterialSystem/MatRenderContextBase.cs @@ -0,0 +1,505 @@ +using Source.Common; +using Source.Common.MaterialSystem; +using Source.Common.Mathematics; +using Source.Common.ShaderAPI; +using Source.Common.Utilities; + +using System.Numerics; + +namespace Source.MaterialSystem; + +public abstract class MatRenderContextBase +{ + protected readonly MaterialSystem materials; + + protected MatRenderContextBase(MaterialSystem materials) { + this.materials = materials; + RenderTargetStack = new RefStack(); + MatrixStacks = new RefStack[(int)MaterialMatrixMode.Count]; + MatrixStacksDirtyStates = new bool[(int)MaterialMatrixMode.Count]; + for (int i = 0; i < MatrixStacks.Length; i++) { + MatrixStacks[i] = new(); + ref MatrixStackItem item = ref MatrixStacks[i].Push(); + MatrixStacksDirtyStates[i] = true; + item.Matrix = Matrix4x4.Identity; + } + RenderTargetStackElement initialElement = new() { + DepthTexture = null, + ViewX = 0, + ViewY = 0, + ViewW = -1, + ViewH = -1 + }; + RenderTargetStack.Push(initialElement); + + DirtyViewState = true; + DirtyViewProjState = true; + } + protected RefStack RenderTargetStack; + protected RefStack[] MatrixStacks; + protected bool[] MatrixStacksDirtyStates; + protected MaterialMatrixMode matrixMode; + protected ShaderViewport ActiveViewport = new(0, 0, 0, 0, 0, 1); + public ref MatrixStackItem CurMatrixItem => ref MatrixStacks[(int)matrixMode].Top(); + public ref RenderTargetStackElement CurRenderTargetStack => ref RenderTargetStack.Top(); + + protected IMaterialInternal? currentMaterial; + protected object? CurrentProxyData; + protected int LightmapPageID; + protected ITexture? LocalCubemapTexture; + + public const int MAX_FB_TEXTURES = 4; + protected readonly ITexture?[] CurrentFrameBufferCopyTexture = new ITexture?[MAX_FB_TEXTURES]; + + protected bool FlashlightEnable; + protected bool DirtyViewState; + protected bool DirtyViewProjState; + protected bool EnableClippingValue = true; + protected MaterialHeightClipMode HeightClipMode; + protected float HeightClipZ; + + protected Vector3 LastSetToneMapScale = new(1, 1, 1); + protected TimeUnit_t FrameTime; + + protected Vector3 VecViewOrigin; + protected Vector3 VecViewForward; + protected Vector3 VecViewRight; + protected Vector3 VecViewUp; + protected Matrix4x4 ViewProjMatrix; + + public virtual void InitializeFrom(MatRenderContextBase initialState) { + currentMaterial = initialState.currentMaterial; + CurrentProxyData = initialState.CurrentProxyData; + LightmapPageID = initialState.LightmapPageID; + LocalCubemapTexture = initialState.LocalCubemapTexture; + + initialState.CurrentFrameBufferCopyTexture.CopyTo(CurrentFrameBufferCopyTexture, 0); + + EnableClippingValue = initialState.EnableClippingValue; + + HeightClipMode = initialState.HeightClipMode; + HeightClipZ = initialState.HeightClipZ; + + while (RenderTargetStack.Count > 0) + RenderTargetStack.Pop(); + for (int i = 0; i < initialState.RenderTargetStack.Count; i++) + RenderTargetStack.Push(initialState.RenderTargetStack[i]); + + matrixMode = initialState.matrixMode; + for (int i = 0; i < MatrixStacks.Length; i++) { + RefStack stack = MatrixStacks[i]; + RefStack initialStack = initialState.MatrixStacks[i]; + while (stack.Count > 0) + stack.Pop(); + for (int j = 0; j < initialStack.Count; j++) + stack.Push(initialStack[j]); + MatrixStacksDirtyStates[i] = initialState.MatrixStacksDirtyStates[i]; + } + + FlashlightEnable = initialState.FlashlightEnable; + LastSetToneMapScale = initialState.LastSetToneMapScale; + } + + public virtual void SetFrameTime(double frameTime) { + FrameTime = frameTime; + } + + public ITexture? GetRenderTarget() { + if (RenderTargetStack.Count > 0) + return RenderTargetStack.Top().RenderTarget0; + else + return null; + } + + public ITexture? GetRenderTargetEx(int renderTargetId) { + throw new NotImplementedException("Incomplete port of IMatRenderContextInternal"); + } + + public IMaterialInternal? GetCurrentMaterialInternal() { + return currentMaterial; + } + public virtual void SetCurrentMaterialInternal(IMaterialInternal? mat) { + currentMaterial = mat; + } + + public IMaterial? GetCurrentMaterial() { + return currentMaterial; + } + + public virtual object? GetCurrentProxy() => CurrentProxyData; + public virtual void SetCurrentProxy(object? proxyData) => CurrentProxyData = proxyData; + + public virtual void Bind(IMaterial iMaterial, object? proxyData) { + IMaterialInternal material = (IMaterialInternal)iMaterial; + if (material == null) { + Warning("Programming error: MatRenderContextBase.Bind: NULL material\n"); + material = materials.errorMaterial; + } + + material = material.GetRealTimeVersion(); + + if (currentMaterial != material) { + if (!material.IsPrecached()) + material.Precache(); + currentMaterial = material; + } + + CurrentProxyData = proxyData; + } + + public virtual void BindLightmapPage(int lightmapPageID) { + LightmapPageID = lightmapPageID; + } + + public virtual void BindLocalCubemap(ITexture? texture) { + LocalCubemapTexture = texture ?? materials.TextureSystem.ErrorTexture(); + } + + public virtual void MatrixMode(MaterialMatrixMode mode) { + matrixMode = mode; + } + + public virtual void PushMatrix() { + RefStack curStack = MatrixStacks[(int)matrixMode]; + curStack.Push(CurMatrixItem); + CurrentMatrixChanged(); + } + + public virtual void PopMatrix() { + RefStack curStack = MatrixStacks[(int)matrixMode]; + curStack.Pop(); + CurrentMatrixChanged(); + } + + public virtual void LoadMatrix(in Matrix4x4 matrix) { + ref MatrixStackItem item = ref CurMatrixItem; + item.Matrix = matrix; + CurrentMatrixChanged(); + } + + public virtual void LoadMatrix(Matrix3x4 matrix) { + ref MatrixStackItem item = ref CurMatrixItem; + item.Matrix = matrix; + CurrentMatrixChanged(); + } + + public virtual void MultMatrix(in Matrix3x4 matrix) { + throw new NotImplementedException("Incomplete port of IMatRenderContext"); + } + + public virtual void MultMatrix(in Matrix4x4 matrix) { + throw new NotImplementedException("Incomplete port of IMatRenderContext"); + } + + public virtual void MultMatrixLocal(in Matrix4x4 mat) { + MathLib.MatrixMultiply(in CurMatrixItem.Matrix, in mat, out Matrix4x4 result); + CurMatrixItem.Matrix = result; + CurrentMatrixChanged(); + } + + public virtual void MultMatrixLocal(in Matrix3x4 matrix) { + throw new NotImplementedException("Incomplete port of IMatRenderContext"); + } + + public void GetMatrix(MaterialMatrixMode mode, out Matrix4x4 viewMatrix) { + var stack = MatrixStacks[(int)mode]; + if (stack.Count == 0) { + viewMatrix = Matrix4x4.Identity; + return; + } + + viewMatrix = stack.Top().Matrix; + } + + public void GetMatrix(MaterialMatrixMode matrixMode, out Matrix3x4 matrix) { + throw new NotImplementedException("Incomplete port of IMatRenderContext"); + } + + public virtual void LoadIdentity() { + ref MatrixStackItem item = ref CurMatrixItem; + item.Matrix = Matrix4x4.Identity; + CurrentMatrixChanged(); + } + + public virtual void Ortho(double left, double top, double right, double bottom, double near, double far) { + ref Matrix4x4 item = ref CurMatrixItem.Matrix; + MathLib.MatrixOrtho(ref item, (float)left, (float)right, (float)bottom, (float)top, (float)near, (float)far); + CurrentMatrixChanged(); + } + + public virtual void PerspectiveX(double fovx, double aspect, double zNear, double zFar) { + throw new NotImplementedException("Incomplete port of IMatRenderContext"); + } + + public virtual void PerspectiveOffCenterX(double fovx, double aspect, double zNear, double zFar, double bottom, double top, double left, double right) { + throw new NotImplementedException("Incomplete port of IMatRenderContext"); + } + + public virtual void PickMatrix(int x, int y, int width, int height) { + throw new NotImplementedException("Incomplete port of IMatRenderContext"); + } + + public virtual void Rotate(float angle, float x, float y, float z) { + throw new NotImplementedException("Incomplete port of IMatRenderContext"); + } + + public virtual void Translate(float x, float y, float z) { + MathLib.MatrixBuildTranslation(out Matrix4x4 mat, x, y, z); + MultMatrixLocal(in mat); + } + + public virtual void Scale(float x, float y, float z) { + MathLib.MatrixBuildScale(out Matrix4x4 mat, x, y, z); + MultMatrixLocal(in mat); + } + + public virtual void SetFrameBufferCopyTexture(ITexture? texture, int textureIndex = 0) { + if (textureIndex < 0 || textureIndex >= MAX_FB_TEXTURES) { + Assert(false); + return; + } + + CurrentFrameBufferCopyTexture[textureIndex] = texture; + } + + public ITexture? GetFrameBufferCopyTexture(int textureIndex) { + if (textureIndex < 0 || textureIndex >= MAX_FB_TEXTURES) { + Assert(false); + return null; // FIXME! This should return the error texture. + } + return CurrentFrameBufferCopyTexture[textureIndex]; + } + + public MaterialHeightClipMode GetHeightClipMode() => HeightClipMode; + + public virtual void SetHeightClipMode(MaterialHeightClipMode heightClipMode) { + if (HeightClipMode != heightClipMode) { + HeightClipMode = heightClipMode; + UpdateHeightClipUserClipPlane(); + } + } + + public virtual void SetHeightClipZ(float z) { + if (z != HeightClipZ) { + HeightClipZ = z; + UpdateHeightClipUserClipPlane(); + } + } + + public virtual bool EnableClipping(bool enable) { + if (enable != EnableClippingValue) { + EnableClippingValue = enable; + ApplyCustomClipPlanes(); + + return !enable; + } + return enable; + } + + public void SetRenderTarget(ITexture? texture) => SetRenderTargetEx(0, texture); + + public virtual void SetRenderTargetEx(int renderTargetID, ITexture? newTarget) { + Assert(RenderTargetStack.Count > 0); + + RenderTargetStackElement newTOS = RenderTargetStack.Top(); + newTOS[renderTargetID] = newTarget; + RenderTargetStack.Pop(); + RenderTargetStack.Push(newTOS); + } + + public virtual void Viewport(int x, int y, int width, int height) { + Dbg.Assert(RenderTargetStack.Count > 0); + + RenderTargetStackElement newTOS = new(); + newTOS = CurRenderTargetStack; // copy + newTOS.ViewX = x; + newTOS.ViewY = y; + newTOS.ViewW = width; + newTOS.ViewH = height; + RenderTargetStack.Pop(); + RenderTargetStack.Push(newTOS); + } + + public virtual void PushRenderTargetAndViewport() { + RenderTargetStackElement top = RenderTargetStack.Top(); + RenderTargetStack.Push(top); + CommitRenderTargetAndViewport(); + } + + public virtual void PushRenderTargetAndViewport(ITexture? thisTexture) { + RenderTargetStackElement element = new(thisTexture, 0, 0, -1, -1); + RenderTargetStack.Push(element); + CommitRenderTargetAndViewport(); + } + + public virtual void PushRenderTargetAndViewport(ITexture? thisTexture, int x, int y, int w, int h) { + RenderTargetStackElement element = new(thisTexture, x, y, w, h); + RenderTargetStack.Push(element); + CommitRenderTargetAndViewport(); + } + + public virtual void PushRenderTargetAndViewport(ITexture? colorTexture, ITexture? depthTexture, int x, int y, int w, int h) { + RenderTargetStackElement element = new(colorTexture, depthTexture, x, y, w, h); + RenderTargetStack.Push(element); + CommitRenderTargetAndViewport(); + } + + public virtual void PopRenderTargetAndViewport() { + if (RenderTargetStack.Count == 0) { + AssertMsg(false, "MatRenderContext.PopRenderTargetAndViewport: Stack is empty!\n"); + return; + } + + Flush(); + + RenderTargetStack.Pop(); + CommitRenderTargetAndViewport(); + } + + public virtual void SyncMatrices() { } + public virtual void SyncMatrix(MaterialMatrixMode mode) { } + + protected virtual void UpdateHeightClipUserClipPlane() { } + protected virtual void ApplyCustomClipPlanes() { } + + public float ComputePixelDiameterOfSphere(in Vector3 absOrigin, float radius) { + RecomputeViewState(); + RecomputeViewProjState(); + // This is sort of faked, but it's faster that way + // FIXME: Also, there's a much faster way to do this with similar triangles + // but I want to make sure it exactly matches the current matrices, so + // for now, I do it this conservative way + Vector4 testPoint1 = default, testPoint2 = default; + MathLib.VectorMA(in absOrigin, radius, in VecViewUp, out testPoint1.AsVector3D()); + MathLib.VectorMA(in absOrigin, -radius, in VecViewUp, out testPoint2.AsVector3D()); + testPoint1.W = testPoint2.W = 1.0f; + + Vector4 clipPos1 = default, clipPos2 = default; + MathLib.Vector4DMultiply(ViewProjMatrix, in testPoint1, ref clipPos1); + MathLib.Vector4DMultiply(ViewProjMatrix, in testPoint2, ref clipPos2); + + if (clipPos1.W >= 0.001f) + clipPos1.Y /= clipPos1.W; + else + clipPos1.Y *= 1000; + + if (clipPos2.W >= 0.001f) + clipPos2.Y /= clipPos2.W; + else + clipPos2.Y *= 1000; + + GetViewport(out int vx, out int vy, out int vwidth, out int vheight); + + return vheight * MathF.Abs(clipPos2.Y - clipPos1.Y) / 2.0f; + } + + public float ComputePixelWidthOfSphere(in Vector3 origin, float radius) { + return ComputePixelDiameterOfSphere(origin, radius) * 2.0f; + } + + public virtual void GetWorldSpaceCameraPosition(out Vector3 vecCameraPos) { + vecCameraPos = VecViewOrigin; + } + + public virtual void GetWorldSpaceCameraVectors(out Vector3 forward, out Vector3 right, out Vector3 up) { + throw new NotImplementedException("Incomplete port of IMatRenderContext"); + } + + public virtual Vector3 GetToneMappingScaleLinear() { + if (materials.HardwareConfig.GetHDRType() == HDRType.None) + return new(1, 1, 1); + else + return LastSetToneMapScale; + } + + public virtual int GetLightmapPage() { + throw new NotImplementedException("Incomplete port of IMatRenderContextInternal"); + } + + public virtual object? LockRenderData(int sizeInBytes) { + throw new NotImplementedException("Incomplete port of IMatRenderContext"); + } + + public virtual void UnlockRenderData(object? data) { + throw new NotImplementedException("Incomplete port of IMatRenderContext"); + } + + public virtual void AddRefRenderData() { + throw new NotImplementedException("Incomplete port of IMatRenderContext"); + } + + public virtual void ReleaseRenderData() { + throw new NotImplementedException("Incomplete port of IMatRenderContext"); + } + + public virtual bool IsRenderData(object? data) { + throw new NotImplementedException("Incomplete port of IMatRenderContext"); + } + + public void MarkRenderDataUnused(bool v) { + + } + + public virtual float Knob(Span knobname, Span setvalue = default) { + throw new NotImplementedException("Incomplete port of IMatRenderContext"); + } + + protected virtual void CommitRenderTargetAndViewport() { } + + protected void RecomputeViewState() { + if (!DirtyViewState) + return; + DirtyViewState = false; + + // FIXME: Cache this off to make it less expensive? + Matrix4x4 viewMatrix; + GetMatrix(MaterialMatrixMode.View, out viewMatrix); + VecViewOrigin.X = + -(viewMatrix[0][3] * viewMatrix[0][0] + + viewMatrix[1][3] * viewMatrix[1][0] + + viewMatrix[2][3] * viewMatrix[2][0]); + VecViewOrigin.Y = + -(viewMatrix[0][3] * viewMatrix[0][1] + + viewMatrix[1][3] * viewMatrix[1][1] + + viewMatrix[2][3] * viewMatrix[2][1]); + VecViewOrigin.Z = + -(viewMatrix[0][3] * viewMatrix[0][2] + + viewMatrix[1][3] * viewMatrix[1][2] + + viewMatrix[2][3] * viewMatrix[2][2]); + + // FIXME Implement computation of m_vecViewForward, etc + VecViewForward = new(); + VecViewRight = new(); + + // FIXME: Is this correct? + VecViewUp = new(viewMatrix[1][0], viewMatrix[1][1], viewMatrix[1][2]); + } + + protected void RecomputeViewProjState() { + if (DirtyViewProjState) { + Matrix4x4 viewMatrix, projMatrix; + + // FIXME: Should consider caching this upon change for projection or view matrix. + GetMatrix(MaterialMatrixMode.View, out viewMatrix); + GetMatrix(MaterialMatrixMode.Projection, out projMatrix); + ViewProjMatrix = projMatrix * viewMatrix; + DirtyViewProjState = false; + } + } + + protected void MarkDirty() => MatrixStacksDirtyStates[(int)matrixMode] = true; + + protected void CurrentMatrixChanged() { + MarkDirty(); + + if (matrixMode == MaterialMatrixMode.View) { + DirtyViewState = true; + DirtyViewProjState = true; + } + else if (matrixMode == MaterialMatrixMode.Projection) + DirtyViewProjState = true; + } + + public abstract void GetViewport(out int x, out int y, out int width, out int height); + public abstract void Flush(bool flushHardware = false); +} From 243ebe31084607ae5c6b2637dac9d912803ff59e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=B6eh=20Matt?= <5415177+ZehMatt@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:52:41 +0300 Subject: [PATCH 2/8] Implement MatCallQueue --- Source.Common/ICallQueue.cs | 3 + .../IMaterialSystemInternal.cs | 118 +++++++++++++++++- 2 files changed, 119 insertions(+), 2 deletions(-) diff --git a/Source.Common/ICallQueue.cs b/Source.Common/ICallQueue.cs index 0dd0de804..6bafcbeea 100644 --- a/Source.Common/ICallQueue.cs +++ b/Source.Common/ICallQueue.cs @@ -20,4 +20,7 @@ public void QueueFunctor(ref Functor functor) { } void QueueFunctorInternal(ref Functor functor); + + void QueueCall(Action func); + void QueueCall(Action func, in T args); } diff --git a/Source.MaterialSystem/IMaterialSystemInternal.cs b/Source.MaterialSystem/IMaterialSystemInternal.cs index 3e0c2d396..eed1bedb2 100644 --- a/Source.MaterialSystem/IMaterialSystemInternal.cs +++ b/Source.MaterialSystem/IMaterialSystemInternal.cs @@ -4,13 +4,127 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Text; namespace Source.MaterialSystem; -public class MatCallQueue : ICallQueue +public sealed class MatCallQueue : ICallQueue { + abstract class CallBuffer + { + public bool Used; + public abstract void Execute(int index); + public abstract void Clear(); + } + + sealed class ActionBuffer : CallBuffer + { + Action[] Funcs = new Action[64]; + int Count; + + public int Add(Action func) { + if (Count == Funcs.Length) + Array.Resize(ref Funcs, Count * 2); + Funcs[Count] = func; + return Count++; + } + + public override void Execute(int index) => Funcs[index](); + + public override void Clear() { + Array.Clear(Funcs, 0, Count); + Count = 0; + } + } + + sealed class CallBuffer : CallBuffer + { + Action[] Funcs = new Action[16]; + T[] Args = new T[16]; + int Count; + + public int Add(Action func, in T args) { + if (Count == Args.Length) { + Array.Resize(ref Funcs, Count * 2); + Array.Resize(ref Args, Count * 2); + } + Funcs[Count] = func; + Args[Count] = args; + return Count++; + } + + public override void Execute(int index) => Funcs[index](Args[index]); + + public override void Clear() { + Array.Clear(Funcs, 0, Count); + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + Array.Clear(Args, 0, Count); + Count = 0; + } + } + + struct Entry + { + public CallBuffer Buffer; + public int Index; + } + + static int NextTypeID = 1; + static class TypeID + { + public static readonly int Value = Interlocked.Increment(ref NextTypeID) - 1; + } + + readonly ActionBuffer Actions = new(); + CallBuffer?[] Buffers = new CallBuffer?[64]; + readonly List UsedBuffers = []; + Entry[] Entries = new Entry[4096]; + int EntryCount; + + public int Count() => EntryCount; + + void AddEntry(CallBuffer buffer, int index) { + if (!buffer.Used) { + buffer.Used = true; + UsedBuffers.Add(buffer); + } + if (EntryCount == Entries.Length) + Array.Resize(ref Entries, EntryCount * 2); + ref Entry entry = ref Entries[EntryCount++]; + entry.Buffer = buffer; + entry.Index = index; + } + + public void QueueCall(Action func) => AddEntry(Actions, Actions.Add(func)); + + public void QueueCall(Action func, in T args) { + int id = TypeID.Value; + if (id >= Buffers.Length) + Array.Resize(ref Buffers, Math.Max(id + 1, Buffers.Length * 2)); + CallBuffer buffer = (CallBuffer?)Buffers[id] ?? (CallBuffer)(Buffers[id] = new CallBuffer()); + AddEntry(buffer, buffer.Add(func, in args)); + } + + public void CallQueued() { + for (int i = 0; i < EntryCount; i++) { + ref Entry entry = ref Entries[i]; + entry.Buffer.Execute(entry.Index); + } + Flush(); + } + + public void Flush() { + Array.Clear(Entries, 0, EntryCount); + EntryCount = 0; + for (int i = 0; i < UsedBuffers.Count; i++) { + UsedBuffers[i].Clear(); + UsedBuffers[i].Used = false; + } + UsedBuffers.Clear(); + } + public void QueueFunctorInternal(ref Functor functor) { throw new NotImplementedException(); } @@ -42,7 +156,7 @@ public interface IMaterialSystemInternal : IMaterialSystem bool CanUseEditorMaterials(); ReadOnlySpan GetForcedTextureLoadPathID(); - MatCallQueue GetRenderCallQueue(); + MatCallQueue? GetRenderCallQueue(); void UnbindMaterial(IMaterial? material); uint GetRenderThreadId(); From 9e4b857ddfd67d8be91fb181dffd0dcec5266832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=B6eh=20Matt?= <5415177+ZehMatt@users.noreply.github.com> Date: Sun, 18 Oct 2026 01:26:08 +0300 Subject: [PATCH 3/8] Guard occlusion query objects with a mutex --- Source.MaterialSystem/OcclusionQueryMgr.cs | 57 +++++++++++++++------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/Source.MaterialSystem/OcclusionQueryMgr.cs b/Source.MaterialSystem/OcclusionQueryMgr.cs index 11d23589e..eba7e039e 100644 --- a/Source.MaterialSystem/OcclusionQueryMgr.cs +++ b/Source.MaterialSystem/OcclusionQueryMgr.cs @@ -16,31 +16,42 @@ class OcclusionQueryObject } readonly Dictionary OcclusionQueryObjects = []; + readonly object Mutex = new(); + + bool TryGetObject(OcclusionQueryObjectHandle_t h, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out OcclusionQueryObject? obj) { + lock (Mutex) + return OcclusionQueryObjects.TryGetValue(h, out obj); + } OcclusionQueryObjectHandle_t NextHandle = 1; int FrameCount = 0; IShaderAPI ShaderAPI => materials.ShaderAPI; public OcclusionQueryObjectHandle_t CreateOcclusionQueryObject() { - OcclusionQueryObjectHandle_t h = NextHandle++; - OcclusionQueryObjects.Add(h, new()); - return h; + lock (Mutex) { + OcclusionQueryObjectHandle_t h = NextHandle++; + OcclusionQueryObjects.Add(h, new()); + return h; + } } public void OnCreateOcclusionQueryObject(OcclusionQueryObjectHandle_t h) { - OcclusionQueryObject obj = OcclusionQueryObjects[h]; + if (!TryGetObject(h, out OcclusionQueryObject? obj)) + return; for (int i = 0; i < COUNT_OCCLUSION_QUERY_STACK; i++) obj.QueryHandle[i] = ShaderAPI.CreateOcclusionQueryObject(); } public void DestroyOcclusionQueryObject(OcclusionQueryObjectHandle_t occlusionQuery) { - Assert(OcclusionQueryObjects.ContainsKey(occlusionQuery)); - if (OcclusionQueryObjects.TryGetValue(occlusionQuery, out OcclusionQueryObject? obj)) { + bool found = TryGetObject(occlusionQuery, out OcclusionQueryObject? obj); + Assert(found); + if (found) { for (int i = 0; i < COUNT_OCCLUSION_QUERY_STACK; i++) { if (obj.QueryHandle[i] != IShaderAPI.INVALID_SHADERAPI_OCCLUSION_QUERY_HANDLE) ShaderAPI.DestroyOcclusionQueryObject(obj.QueryHandle[i]); } - OcclusionQueryObjects.Remove(occlusionQuery); + lock (Mutex) + OcclusionQueryObjects.Remove(occlusionQuery); } } @@ -49,7 +60,10 @@ public void AdvanceFrame() { } public void AllocOcclusionQueryObjects() { - foreach (OcclusionQueryObject obj in OcclusionQueryObjects.Values) { + OcclusionQueryObject[] objects; + lock (Mutex) + objects = [.. OcclusionQueryObjects.Values]; + foreach (OcclusionQueryObject obj in objects) { for (int i = 0; i < COUNT_OCCLUSION_QUERY_STACK; i++) { obj.QueryHandle[i] = ShaderAPI.CreateOcclusionQueryObject(); obj.HasBeenIssued[i] = false; @@ -58,7 +72,10 @@ public void AllocOcclusionQueryObjects() { } public void FreeOcclusionQueryObjects() { - foreach (OcclusionQueryObject obj in OcclusionQueryObjects.Values) { + OcclusionQueryObject[] objects; + lock (Mutex) + objects = [.. OcclusionQueryObjects.Values]; + foreach (OcclusionQueryObject obj in objects) { for (int i = 0; i < COUNT_OCCLUSION_QUERY_STACK; i++) { if (obj.QueryHandle[i] != IShaderAPI.INVALID_SHADERAPI_OCCLUSION_QUERY_HANDLE) { ShaderAPI.DestroyOcclusionQueryObject(obj.QueryHandle[i]); @@ -70,8 +87,9 @@ public void FreeOcclusionQueryObjects() { } public void ResetOcclusionQueryObject(OcclusionQueryObjectHandle_t occlusionQuery) { - Assert(OcclusionQueryObjects.ContainsKey(occlusionQuery)); - if (OcclusionQueryObjects.TryGetValue(occlusionQuery, out OcclusionQueryObject? obj)) { + bool found = TryGetObject(occlusionQuery, out OcclusionQueryObject? obj); + Assert(found); + if (found) { for (int i = 0; i < COUNT_OCCLUSION_QUERY_STACK; i++) obj.HasBeenIssued[i] = false; @@ -81,8 +99,9 @@ public void ResetOcclusionQueryObject(OcclusionQueryObjectHandle_t occlusionQuer } public void BeginOcclusionQueryDrawing(OcclusionQueryObjectHandle_t occlusionQuery) { - Assert(OcclusionQueryObjects.ContainsKey(occlusionQuery)); - if (OcclusionQueryObjects.TryGetValue(occlusionQuery, out OcclusionQueryObject? obj)) { + bool found = TryGetObject(occlusionQuery, out OcclusionQueryObject? obj); + Assert(found); + if (found) { int current = obj.CurrentIssue; ShaderAPIOcclusionQuery_t query = obj.QueryHandle[current]; if (query != IShaderAPI.INVALID_SHADERAPI_OCCLUSION_QUERY_HANDLE) { @@ -105,8 +124,9 @@ public void BeginOcclusionQueryDrawing(OcclusionQueryObjectHandle_t occlusionQue static int s_WarnCount = 0; public void EndOcclusionQueryDrawing(OcclusionQueryObjectHandle_t occlusionQuery) { - Assert(OcclusionQueryObjects.ContainsKey(occlusionQuery)); - if (OcclusionQueryObjects.TryGetValue(occlusionQuery, out OcclusionQueryObject? obj)) { + bool found = TryGetObject(occlusionQuery, out OcclusionQueryObject? obj); + Assert(found); + if (found) { int current = obj.CurrentIssue; ShaderAPIOcclusionQuery_t query = obj.QueryHandle[current]; if (query != IShaderAPI.INVALID_SHADERAPI_OCCLUSION_QUERY_HANDLE) { @@ -122,8 +142,9 @@ public void EndOcclusionQueryDrawing(OcclusionQueryObjectHandle_t occlusionQuery } public void OcclusionQuery_IssueNumPixelsRenderedQuery(OcclusionQueryObjectHandle_t occlusionQuery) { - Assert(OcclusionQueryObjects.ContainsKey(occlusionQuery)); - if (OcclusionQueryObjects.TryGetValue(occlusionQuery, out OcclusionQueryObject? obj)) { + bool found = TryGetObject(occlusionQuery, out OcclusionQueryObject? obj); + Assert(found); + if (found) { for (int i = 0; i < COUNT_OCCLUSION_QUERY_STACK; i++) { int index = (obj.CurrentIssue + i) % COUNT_OCCLUSION_QUERY_STACK; ShaderAPIOcclusionQuery_t query = obj.QueryHandle[index]; @@ -144,6 +165,6 @@ public int OcclusionQuery_GetNumPixelsRendered(OcclusionQueryObjectHandle_t h, b if (doQuery) OcclusionQuery_IssueNumPixelsRenderedQuery(h); - return OcclusionQueryObjects[h].LastResult; + return TryGetObject(h, out OcclusionQueryObject? obj) ? obj.LastResult : -1; } } From d3c718f9b5e92c90ab2bb856a5aa9b5c4b8fb030 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=B6eh=20Matt?= <5415177+ZehMatt@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:44:53 +0300 Subject: [PATCH 4/8] Port the queued render context for mat_queue_mode --- Source.Common/Launcher/IGraphicsContext.cs | 1 + Source.Common/MaterialSystem/IMaterialVar.cs | 6 +- Source.Common/ShaderAPI/IShaderUtil.cs | 5 +- Source.Engine/Host.cs | 6 +- Source.MaterialSystem/MatMemoryStack.cs | 89 ++ Source.MaterialSystem/MatQueueThread.cs | 99 ++ Source.MaterialSystem/MatQueuedMesh.cs | 381 ++++++++ .../MatQueuedRenderContext.cs | 874 ++++++++++++++++++ Source.MaterialSystem/MatStub.cs | 3 + Source.MaterialSystem/Material.cs | 45 +- Source.MaterialSystem/MaterialSystem.cs | 314 ++++++- Source.MaterialSystem/MaterialVar.cs | 138 ++- Source.MaterialSystem/Texture.cs | 2 + Source.SDLManager/SDL3_LauncherManager.cs | 5 + Source.ShaderAPI.Gl46/MeshGl46.cs | 19 +- Source.ShaderAPI.Gl46/ShaderAPIGl46.cs | 51 +- Source.ShaderAPI.Gl46/ShaderShadow.cs | 2 +- Source.StudioRender/StudioRenderContext.cs | 2 + 18 files changed, 1967 insertions(+), 75 deletions(-) create mode 100644 Source.MaterialSystem/MatMemoryStack.cs create mode 100644 Source.MaterialSystem/MatQueueThread.cs create mode 100644 Source.MaterialSystem/MatQueuedMesh.cs create mode 100644 Source.MaterialSystem/MatQueuedRenderContext.cs diff --git a/Source.Common/Launcher/IGraphicsContext.cs b/Source.Common/Launcher/IGraphicsContext.cs index 03c7a15e3..dfe29798a 100644 --- a/Source.Common/Launcher/IGraphicsContext.cs +++ b/Source.Common/Launcher/IGraphicsContext.cs @@ -4,6 +4,7 @@ public interface IGraphicsContext { nint HardwareHandle { get; } void MakeCurrent(); + void ReleaseCurrent(); void SetSwapInterval(float swapInterval); void SwapBuffers(); } diff --git a/Source.Common/MaterialSystem/IMaterialVar.cs b/Source.Common/MaterialSystem/IMaterialVar.cs index 27353c281..29358e648 100644 --- a/Source.Common/MaterialSystem/IMaterialVar.cs +++ b/Source.Common/MaterialSystem/IMaterialVar.cs @@ -47,7 +47,7 @@ public abstract class IMaterialVar protected ITexture? TextureValue; protected byte NumVectorComps; protected bool FakeMaterialVar; - protected byte TempIndex; + protected byte TempIndex = 0xFF; protected UtlSymbol Name = new(); public MaterialVarGPU GPU; @@ -162,10 +162,10 @@ public Vector4 GetVec4Value() { protected abstract int VectorSizeInternal(); public int GetIntValue() { - return IntVal; + return TempIndex == 0xFF ? IntVal : GetIntValueInternal(); } public float GetFloatValue() { - return VecVal.X; + return TempIndex == 0xFF ? VecVal.X : GetFloatValueInternal(); } public static readonly UtlSymbolTableMT MaterialVarSymbols = new(true); diff --git a/Source.Common/ShaderAPI/IShaderUtil.cs b/Source.Common/ShaderAPI/IShaderUtil.cs index b53466913..d97c8ad87 100644 --- a/Source.Common/ShaderAPI/IShaderUtil.cs +++ b/Source.Common/ShaderAPI/IShaderUtil.cs @@ -6,10 +6,13 @@ public interface IShaderUtil { bool InFlashlightMode(); bool OnDrawMesh(IMesh mesh, int firstIndex, int indexCount); + unsafe bool OnDrawMesh(IMesh mesh, PrimList* lists, int numLists); + bool OnSetFlexMesh(IMesh staticMesh, IMesh? mesh, int vertexOffsetInBytes); + bool OnSetColorMesh(IMesh staticMesh, IMesh? mesh, int vertexOffsetInBytes); bool OnSetPrimitiveType(IMesh mesh, MaterialPrimitiveType type); bool OnFlushBufferedPrimitives(); void SyncMatrices(); void SyncMatrix(MaterialMatrixMode mode); void RestoreShaderObjects(IServiceProvider services, int changeFlags = 0); void BindStandardTexture(Sampler sampler, StandardTextureId id); -} \ No newline at end of file +} diff --git a/Source.Engine/Host.cs b/Source.Engine/Host.cs index 195845f37..39ea00c54 100644 --- a/Source.Engine/Host.cs +++ b/Source.Engine/Host.cs @@ -1311,10 +1311,12 @@ public int CountVariablesWithFlags(FCvar flags, bool nonDefault) { return count; } - internal void AllowQueuedMaterialSystem(bool v) { - // todo + internal bool AllowQueuedMaterialSystem(bool v) { + return services.GetRequiredService().AllowThreading(v, MaterialSystemThread); } + public static int MaterialSystemThread = 0; + [ConCommand(helpText: "Reload the most recent saved game (add setpos to jump to current view position on reload).")] void reload(in TokenizedCommand args, CommandSource source, int clientSlot = -1) { if ( diff --git a/Source.MaterialSystem/MatMemoryStack.cs b/Source.MaterialSystem/MatMemoryStack.cs new file mode 100644 index 000000000..9a4404ba5 --- /dev/null +++ b/Source.MaterialSystem/MatMemoryStack.cs @@ -0,0 +1,89 @@ +using System.Runtime.InteropServices; + +namespace Source.MaterialSystem; + +public sealed unsafe class MatMemoryStack(int blockSize, int alignment) : IDisposable +{ + struct Block + { + public byte* Base; + public int Size; + public int Used; + } + + readonly List Blocks = []; + int CurrentBlock = -1; + int TotalUsed; + + public int GetUsed() => TotalUsed; + + static int AlignValue(int value, int alignment) => (value + alignment - 1) & ~(alignment - 1); + + public byte* Alloc(int size) { + if (size <= 0) + return null; + + int alignedSize = AlignValue(size, alignment); + + while (CurrentBlock >= 0 && CurrentBlock < Blocks.Count) { + Block block = Blocks[CurrentBlock]; + if (block.Size - block.Used >= alignedSize) { + byte* result = block.Base + block.Used; + block.Used += alignedSize; + Blocks[CurrentBlock] = block; + TotalUsed += alignedSize; + return result; + } + + if (CurrentBlock + 1 >= Blocks.Count) + break; + CurrentBlock++; + } + + int newSize = Math.Max(blockSize, alignedSize); + Block newBlock = new() { + Base = (byte*)NativeMemory.AlignedAlloc((nuint)newSize, (nuint)alignment), + Size = newSize, + Used = alignedSize + }; + Blocks.Add(newBlock); + CurrentBlock = Blocks.Count - 1; + TotalUsed += alignedSize; + return newBlock.Base; + } + + public void ShrinkLast(byte* ptr, int oldSize, int newSize) { + if (ptr == null || CurrentBlock < 0) + return; + + Block block = Blocks[CurrentBlock]; + int oldAligned = AlignValue(oldSize, alignment); + if (block.Base + block.Used - oldAligned != ptr) + return; + + int newAligned = AlignValue(newSize, alignment); + block.Used -= oldAligned - newAligned; + TotalUsed -= oldAligned - newAligned; + Blocks[CurrentBlock] = block; + } + + public void FreeAll() { + for (int i = 0; i < Blocks.Count; i++) { + Block block = Blocks[i]; + block.Used = 0; + Blocks[i] = block; + } + CurrentBlock = Blocks.Count > 0 ? 0 : -1; + TotalUsed = 0; + } + + public void Term() { + for (int i = 0; i < Blocks.Count; i++) + NativeMemory.AlignedFree(Blocks[i].Base); + Blocks.Clear(); + CurrentBlock = -1; + TotalUsed = 0; + } + + public void Dispose() => Term(); +} diff --git a/Source.MaterialSystem/MatQueueThread.cs b/Source.MaterialSystem/MatQueueThread.cs new file mode 100644 index 000000000..c614f094a --- /dev/null +++ b/Source.MaterialSystem/MatQueueThread.cs @@ -0,0 +1,99 @@ +using System.Runtime.ExceptionServices; + +namespace Source.MaterialSystem; + +public sealed class MatQueueJob(MatQueueJob.JobKind kind, MatQueuedRenderContext? context = null) +{ + public enum JobKind + { + AcquireThreadOwnership, + ReleaseThreadOwnership, + ExecuteQueuedContext + } + + public readonly JobKind Kind = kind; + public readonly MatQueuedRenderContext? Context = context; + + readonly ManualResetEventSlim Done = new(false); + ExceptionDispatchInfo? Exception; + + public bool IsFinished() => Done.IsSet; + + public void Reset() { + Exception = null; + Done.Reset(); + } + + internal void Complete(Exception? exception) { + if (exception != null) + Exception = ExceptionDispatchInfo.Capture(exception); + Done.Set(); + } + + public void WaitForFinish() { + Done.Wait(); + ExceptionDispatchInfo? exception = Exception; + if (exception != null) { + Exception = null; + exception.Throw(); + } + } +} + +public sealed class MatQueueThread +{ + readonly MaterialSystem Materials; + readonly Thread Thread; + readonly Queue Jobs = new(); + readonly object Sync = new(); + bool Stopping; + + public MatQueueThread(MaterialSystem materials) { + Materials = materials; + Thread = new Thread(Run) { + Name = "MatQueue", + IsBackground = true + }; + Thread.Start(); + } + + public void AddJob(MatQueueJob job) { + lock (Sync) { + Jobs.Enqueue(job); + Monitor.Pulse(Sync); + } + } + + public void Stop() { + lock (Sync) { + Stopping = true; + Monitor.Pulse(Sync); + } + Thread.Join(); + } + + void Run() { + while (true) { + MatQueueJob job; + lock (Sync) { + while (Jobs.Count == 0 && !Stopping) + Monitor.Wait(Sync); + + if (Jobs.Count == 0) + return; + + job = Jobs.Dequeue(); + } + + Exception? exception = null; + try { + Materials.ExecuteJob(job); + } + catch (Exception e) { + Warning($"MatQueue job {job.Kind} failed: {e}\n"); + exception = e; + } + job.Complete(exception); + } + } +} diff --git a/Source.MaterialSystem/MatQueuedMesh.cs b/Source.MaterialSystem/MatQueuedMesh.cs new file mode 100644 index 000000000..266ca1d4d --- /dev/null +++ b/Source.MaterialSystem/MatQueuedMesh.cs @@ -0,0 +1,381 @@ +using Source.Common.MaterialSystem; + +using System.Runtime.InteropServices; + +namespace Source.MaterialSystem; + +public sealed unsafe class MatQueuedMesh : IMesh +{ + public const uint MQM_BUFFERED = 1 << 0; + + public struct MST_MeshInfo + { + public IMaterialInternal? Material; + public VertexFormat VertexFormat; + public uint Flags; + public IMesh? VertexOverride; + public IMesh? IndexOverride; + public byte* VertexData; + public int VertexCount; + public int VertexSizeInBytes; + public ushort* IndexData; + public int IndexCount; + public MaterialPrimitiveType PrimitiveType; + } + + public struct MST_DrawInfo + { + public MaterialPrimitiveType Type; + public int FirstIndex; + public int IndexCount; + } + + static readonly ushort* ScratchIndexBuffer = (ushort*)NativeMemory.AllocZeroed(6 * sizeof(ushort)); + + readonly MatQueuedRenderContext Owner; + readonly MatCallQueue CallQueue; + readonly IMatRenderContextInternal HardwareContext; + readonly bool FlexMesh; + + int MSTActualVertexOffsetInBytes; + IMesh? MSTActualMesh; + + VertexFormat Format; + byte* VertexData; + ushort* IndexData; + int Verts; + int Indices; + int VertexSize; + uint Flags; + bool CanSetAuxMeshes; + MaterialPrimitiveType Type = MaterialPrimitiveType.Triangles; + IMesh? VertexOverride; + IMesh? IndexOverride; + + public MatQueuedMesh(MatQueuedRenderContext owner, IMatRenderContextInternal hardwareContext, bool flexMesh) { + Owner = owner; + CallQueue = owner.GetCallQueueInternal(); + HardwareContext = hardwareContext; + FlexMesh = flexMesh; + } + + public static IMesh? ResolveMesh(IMesh? mesh) => mesh is MatQueuedMesh queued ? queued.MSTActualMesh : mesh; + + public ushort* GetIndexData() => IndexData; + public void InvalidateAuxMeshSet() => CanSetAuxMeshes = false; + public int GetVertexSize() => VertexSize; + + public IMesh? MST_DetachActualMesh() { + IMesh? mesh = MSTActualMesh; + MSTActualMesh = null; + return mesh; + } + public IMesh? MST_GetActualMesh() => MSTActualMesh; + public int MST_GetActualVertexOffsetInBytes() => MSTActualVertexOffsetInBytes; + + static void MST_CopyDynamicVB(in MeshDesc desc, byte* vertexData, int sizeInBytes) { + byte* dest; + if (desc.Vertex.PositionSize != 0) + dest = (byte*)desc.Vertex.Position; + else { + dest = (byte*)nuint.MaxValue; + if (desc.Vertex.BoneWeightSize != 0 && (byte*)desc.Vertex.BoneWeight < dest) dest = (byte*)desc.Vertex.BoneWeight; + if (desc.Vertex.BoneMatrixIndexSize != 0 && desc.Vertex.BoneMatrixIndex < dest) dest = desc.Vertex.BoneMatrixIndex; + if (desc.Vertex.NormalSize != 0 && (byte*)desc.Vertex.Normal < dest) dest = (byte*)desc.Vertex.Normal; + if (desc.Vertex.ColorSize != 0 && desc.Vertex.Color < dest) dest = desc.Vertex.Color; + if (desc.Vertex.SpecularSize != 0 && desc.Vertex.Specular < dest) dest = desc.Vertex.Specular; + if (desc.Vertex.TangentSSize != 0 && (byte*)desc.Vertex.TangentS < dest) dest = (byte*)desc.Vertex.TangentS; + if (desc.Vertex.TangentTSize != 0 && (byte*)desc.Vertex.TangentT < dest) dest = (byte*)desc.Vertex.TangentT; + if (desc.Vertex.WrinkleSize != 0 && (byte*)desc.Vertex.Wrinkle < dest) dest = (byte*)desc.Vertex.Wrinkle; + if (desc.Vertex.UserDataSize != 0 && (byte*)desc.Vertex.UserData < dest) dest = (byte*)desc.Vertex.UserData; + + for (int i = 0; i < IMesh.VERTEX_MAX_TEXTURE_COORDINATES; i++) { + byte* texCoord = i switch { + 0 => (byte*)desc.Vertex.TexCoord0, + 1 => (byte*)desc.Vertex.TexCoord1, + 2 => (byte*)desc.Vertex.TexCoord2, + 3 => (byte*)desc.Vertex.TexCoord3, + 4 => (byte*)desc.Vertex.TexCoord4, + 5 => (byte*)desc.Vertex.TexCoord5, + 6 => (byte*)desc.Vertex.TexCoord6, + _ => (byte*)desc.Vertex.TexCoord7, + }; + if (desc.Vertex.TexCoordSize[i] != 0 && texCoord < dest) + dest = texCoord; + } + + if (dest == (byte*)nuint.MaxValue) + dest = null; + } + + Assert(dest != null); + if (dest != null) + Buffer.MemoryCopy(vertexData, dest, sizeInBytes, sizeInBytes); + } + + static void MST_CopyDynamicIB(in MeshDesc desc, ushort* indexData, int indexCount) { + if (desc.Vertex.FirstVertex == 0) { + Buffer.MemoryCopy(indexData, desc.Index.Indices, indexCount * sizeof(ushort), indexCount * sizeof(ushort)); + return; + } + + int firstVertex = desc.Vertex.FirstVertex; + ushort* dst = desc.Index.Indices; + for (int i = 0; i < indexCount; i++) + dst[i] = (ushort)(indexData[i] + firstVertex); + } + + IMesh MST_SetupDynamicMesh(in MST_MeshInfo info) { + int verticesToLock = info.VertexCount; + int indicesToLock = info.IndexCount; + + IMesh dynamicMesh; + if (!FlexMesh) + dynamicMesh = HardwareContext.GetDynamicMeshEx(info.VertexFormat, (info.Flags & MQM_BUFFERED) != 0, info.VertexOverride, info.IndexOverride, info.Material)!; + else + dynamicMesh = HardwareContext.GetFlexMesh()!; + + if (verticesToLock == 0 && indicesToLock == 0) + return dynamicMesh; + + dynamicMesh.SetPrimitiveType(info.PrimitiveType); + + MeshDesc desc = default; + dynamicMesh.LockMesh(verticesToLock, indicesToLock, ref desc); + MSTActualVertexOffsetInBytes = desc.Vertex.FirstVertex * desc.Vertex.ActualVertexSize; + if (verticesToLock != 0 && desc.Vertex.ActualVertexSize != 0) + MST_CopyDynamicVB(in desc, info.VertexData, info.VertexCount * info.VertexSizeInBytes); + + if (indicesToLock != 0 && info.IndexData != ScratchIndexBuffer && desc.Index.IndexSize != 0) + MST_CopyDynamicIB(in desc, info.IndexData, info.IndexCount); + + dynamicMesh.UnlockMesh(verticesToLock, indicesToLock, ref desc); + return dynamicMesh; + } + + void MST_BuildDynamicBuffers(in MST_MeshInfo info) { + Assert(info.VertexOverride != null || info.VertexCount != 0 || info.IndexOverride != null || info.IndexCount != 0); + MSTActualMesh = MST_SetupDynamicMesh(in info); + } + + void MST_Draw(in MST_DrawInfo info) { + MSTActualMesh!.SetPrimitiveType(info.Type); + MSTActualMesh.Draw(info.FirstIndex, info.IndexCount); + } + + public bool OnGetDynamicMesh(VertexFormat vertexFormat, uint flags, IMesh? vertexOverride, IMesh? indexOverride, IMaterialInternal? material, int hwSkinBoneCount) { + FreeBuffers(); + + VertexOverride = vertexOverride; + IndexOverride = indexOverride; + Flags = flags; + + if (!FlexMesh) { + if (vertexOverride != null) + Format = vertexOverride.GetVertexFormat(); + else { + VertexFormat materialFormat = material!.GetVertexFormat(); + Format = (vertexFormat != 0) ? vertexFormat : materialFormat; + + if (vertexFormat != 0) { + int vertexFormatBoneWeights = vertexFormat.GetBoneWeightsSize(); + if (hwSkinBoneCount < vertexFormatBoneWeights) + hwSkinBoneCount = vertexFormatBoneWeights; + } + + Format &= ~(VertexFormat.BoneWeights1 | VertexFormat.BoneWeights2 | VertexFormat.BoneWeights3 | VertexFormat.BoneWeights4); + if (hwSkinBoneCount > 0) { + Format |= VertexExts.GetBoneWeight(2); + Format |= VertexFormat.BoneIndex; + } + } + } + else + Format = VertexFormat.Position | VertexFormat.Normal | VertexFormat.Wrinkle; + + MeshDesc temp = default; + Owner.ShaderAPI.ComputeVertexDescription(default, Format, ref temp); + VertexSize = temp.Vertex.ActualVertexSize; + return true; + } + + public int VertexCount() => VertexSize != 0 ? Verts : 0; + public int IndexCount() => Indices; + public VertexFormat GetVertexFormat() => Format; + public bool IsDynamic() => true; + public MaterialIndexFormat IndexFormat() { + MatQueuedRenderContext.CannotSupport(); + return MaterialIndexFormat.x16; + } + + public void BeginCastBuffer(VertexFormat format) => MatQueuedRenderContext.CannotSupport(); + public void BeginCastBuffer(MaterialIndexFormat format) => MatQueuedRenderContext.CannotSupport(); + public void EndCastBuffer() => MatQueuedRenderContext.CannotSupport(); + public int GetRoomRemaining() { + MatQueuedRenderContext.CannotSupport(); + return 0; + } + + public bool Lock(int vertexCount, bool append, ref VertexDesc desc) { + MatQueuedRenderContext.CannotSupport(); + return false; + } + + public bool Unlock(int vertexCount, ref VertexDesc desc) { + MatQueuedRenderContext.CannotSupport(); + return false; + } + + public int Lock(bool readOnly, int firstIndex, int indexCount, ref IndexDesc desc) { + MatQueuedRenderContext.CannotSupport(); + return 0; + } + + public bool Unlock(int writtenIndexCount, ref IndexDesc desc) { + MatQueuedRenderContext.CannotSupport(); + return false; + } + + public void ModifyBegin(int firstVertex, int vertexCount, int firstIndex, int indexCount, ref MeshDesc desc) => MatQueuedRenderContext.CannotSupport(); + public void ModifyEnd(ref MeshDesc desc) => MatQueuedRenderContext.CannotSupport(); + public void Draw(ReadOnlySpan lists, int numLists) => MatQueuedRenderContext.CannotSupport(); + + public void SetPrimitiveType(MaterialPrimitiveType type) { + Type = type; + } + + public void LockMesh(int vertexCount, int indexCount, ref MeshDesc desc) { + if (VertexOverride != null) + vertexCount = 0; + if (IndexOverride != null) + indexCount = 0; + + Verts = vertexCount; + Indices = indexCount; + + desc.Vertex.FirstVertex = 0; + desc.Vertex.OffsetVertex = 0; + if (vertexCount > 0) { + Assert(VertexSize != 0); + Assert(VertexData == null); + VertexData = Owner.AllocVertices(vertexCount, VertexSize); + Owner.ShaderAPI.ComputeVertexDescription(new Span(VertexData, vertexCount * VertexSize), Format, ref desc); + } + else + Owner.ShaderAPI.ComputeVertexDescription(default, 0, ref desc); + + if (Type != MaterialPrimitiveType.Points && indexCount > 0) { + Assert(IndexData == null); + IndexData = Owner.AllocIndices(indexCount); + desc.Index.Indices = IndexData; + desc.Index.IndexSize = 1; + desc.Index.FirstIndex = 0; + desc.Index.OffsetIndex = 0; + } + else { + desc.Index.Indices = ScratchIndexBuffer; + desc.Index.IndexSize = 0; + desc.Index.FirstIndex = 0; + desc.Index.OffsetIndex = 0; + } + } + + public void UnlockMesh(int vertexCount, int indexCount, ref MeshDesc desc) { + if (VertexData != null && vertexCount < Verts) { + Owner.ReallocVertices(VertexData, Verts, vertexCount, VertexSize); + Verts = vertexCount; + } + + if (IndexData != null && indexCount < Indices) { + Owner.ReallocIndices(IndexData, Indices, indexCount); + Indices = indexCount; + } + + QueueBuild(); + } + + public void QueueBuild() { + if (VertexOverride == null && Verts == 0 && IndexOverride == null && Indices == 0) + return; + + MST_MeshInfo info = new() { + Material = Owner.GetCurrentMaterialInternal(), + Flags = 0, + VertexData = VertexData, + VertexCount = Verts, + VertexFormat = Format, + VertexSizeInBytes = VertexSize, + VertexOverride = VertexOverride, + IndexData = IndexData, + IndexCount = Indices, + IndexOverride = IndexOverride, + PrimitiveType = Type + }; + Assert(info.IndexData != null || Indices == 0); + + CallQueue.QueueCall(static s => s.Mesh.MST_BuildDynamicBuffers(in s.Info), (Mesh: this, Info: info)); + CanSetAuxMeshes = true; + } + + public void SetColorMesh(IMesh colorMesh, int vertexOffset) { + Assert(!FlexMesh); + if (!CanSetAuxMeshes) + QueueBuild(); + CallQueue.QueueCall(static s => s.Mesh.MSTActualMesh?.SetColorMesh(ResolveMesh(s.ColorMesh)!, s.Offset), (Mesh: this, ColorMesh: colorMesh, Offset: vertexOffset)); + } + + public void SetFlexMesh(IMesh? mesh, int vertexOffset) { + Assert(!FlexMesh); + if (!CanSetAuxMeshes) + QueueBuild(); + CallQueue.QueueCall(static s => s.Mesh.MSTActualMesh?.SetFlexMesh(ResolveMesh(s.FlexMesh), s.Offset), (Mesh: this, FlexMesh: mesh, Offset: vertexOffset)); + } + + public void DisableFlexMesh() { + Assert(!FlexMesh); + if (!CanSetAuxMeshes) + QueueBuild(); + CallQueue.QueueCall(static mesh => mesh.MSTActualMesh?.DisableFlexMesh(), this); + } + + public void Draw(int firstIndex = -1, int indexCount = 0) { + if ((Verts == 0 && VertexOverride == null) && (Indices == 0 && IndexOverride == null)) { + MarkAsDrawn(); + return; + } + + if (Type == MaterialPrimitiveType.InstancedQuads || Type == MaterialPrimitiveType.Points) { + if (Verts == 0 && VertexOverride == null) { + MarkAsDrawn(); + return; + } + } + + if (!CanSetAuxMeshes) + QueueBuild(); + + MST_DrawInfo info = new() { + Type = Type, + FirstIndex = firstIndex, + IndexCount = indexCount + }; + CallQueue.QueueCall(static s => s.Mesh.MST_Draw(in s.Info), (Mesh: this, Info: info)); + } + + public void MarkAsDrawn() { + if (CanSetAuxMeshes) + CallQueue.QueueCall(static mesh => mesh.MSTActualMesh?.MarkAsDrawn(), this); + FreeBuffers(); + } + + public void FreeBuffers() { + Indices = 0; + IndexData = null; + Verts = 0; + Format = 0; + VertexSize = 0; + VertexData = null; + CanSetAuxMeshes = false; + Flags = 0; + } +} diff --git a/Source.MaterialSystem/MatQueuedRenderContext.cs b/Source.MaterialSystem/MatQueuedRenderContext.cs new file mode 100644 index 000000000..4802e488e --- /dev/null +++ b/Source.MaterialSystem/MatQueuedRenderContext.cs @@ -0,0 +1,874 @@ +using Source.Common; +using Source.Common.Bitmap; +using Source.Common.Commands; +using Source.Common.MaterialSystem; +using Source.Common.Mathematics; +using Source.Common.ShaderAPI; + +using System.Drawing; +using System.Numerics; +using System.Runtime.CompilerServices; + +using Color = Source.Color; + +namespace Source.MaterialSystem; + +public sealed unsafe class MatQueuedRenderContext : MatRenderContextBase, IMatRenderContextInternal +{ + static readonly ConVar mat_report_queue_status = new("mat_report_queue_status", "0", FCvar.MaterialSystemThread); + + static readonly HashSet ReportedUnsupported = []; + public static void CannotSupport([CallerMemberName] string name = "") { + lock (ReportedUnsupported) { + if (ReportedUnsupported.Add(name)) + Msg($"Called function that cannot be supported ({name})\n"); + } + } + + readonly MatCallQueue Queue = new(); + MatQueuedMesh? QueuedMesh; + MatQueuedMesh? QueuedFlexMesh; + + MatRenderContext? HardwareContext; + MatRenderContext H => HardwareContext!; + + int RenderDepth; + int WidthBackBuffer, HeightBackBuffer; + int BoneCount; + MaterialFogMode FogModeValue; + float FogStartValue; + float FogEndValue; + float FogZ; + float FogMaxDensityValue = 1.0f; + bool FogRadialValue; + byte FogColorR, FogColorG, FogColorB; + + readonly MatMemoryStack Vertices = new(1024 * 1024, 16); + readonly MatMemoryStack Indices = new(256 * 1024, 16); + PrimList[] PrimLists = new PrimList[256]; + int PrimListCount; + + public MatQueuedRenderContext(MaterialSystem materials) : base(materials) { } + + public IShaderAPI ShaderAPI => materials.ShaderAPI; + + public void Init(MatRenderContext hardwareContext) { + HardwareContext = hardwareContext; + QueuedMesh = new MatQueuedMesh(this, hardwareContext, false); + QueuedFlexMesh = new MatQueuedMesh(this, hardwareContext, true); + } + + public void Shutdown() { + if (HardwareContext == null) + return; + + Assert(currentMaterial == null); + + QueuedMesh = null; + QueuedFlexMesh = null; + HardwareContext = null; + + Vertices.Term(); + Indices.Term(); + Assert(Queue.Count() == 0); + } + + public void CompactMemory() { + Vertices.Term(); + Indices.Term(); + } + + public bool IsInitialized() => HardwareContext != null; + + public void BeginQueue(MatRenderContextBase? initialState = null) { + initialState ??= HardwareContext; + + InitializeFrom(initialState!); + ShaderAPI.GetBackBufferDimensions(out WidthBackBuffer, out HeightBackBuffer); + IMatRenderContext initial = (IMatRenderContext)initialState!; + FogModeValue = initial.GetFogMode(); + BoneCount = initial.GetCurrentNumBones(); + initial.GetFogDistances(out FogStartValue, out FogEndValue, out FogZ); + } + + public void EndQueue(bool callQueued = false) { + if (callQueued) + CallQueued(); + + currentMaterial = null; + LocalCubemapTexture = null; + + for (int i = 0; i < MAX_FB_TEXTURES; i++) + CurrentFrameBufferCopyTexture[i] = null; + + for (int i = 0; i < RenderTargetStack.Count; i++) { + ref RenderTargetStackElement element = ref RenderTargetStack[i]; + for (int j = 0; j < RenderTargetStackElement.NUM_RENDER_TARGET_BINDS; j++) + element[j] = null; + element.DepthTexture = null; + } + + while (RenderTargetStack.Count > 0) + RenderTargetStack.Pop(); + } + + public void CallQueued(bool termAfterCall = false) { + if (mat_report_queue_status.GetBool()) + Msg($"{Queue.Count()} calls queued, {Vertices.GetUsed()} bytes verts, {Indices.GetUsed()} bytes indices\n"); + + Queue.CallQueued(); + + Vertices.FreeAll(); + Indices.FreeAll(); + Array.Clear(PrimLists, 0, PrimListCount); + PrimListCount = 0; + + if (termAfterCall) + Shutdown(); + } + + public void FlushQueued() => Queue.Flush(); + + public ICallQueue? GetCallQueue() => Queue; + public MatCallQueue GetCallQueueInternal() => Queue; + MatCallQueue? IMatRenderContextInternal.GetCallQueueInternal() => Queue; + + public byte* AllocVertices(int verts, int vertexSize) { + byte* memory = Vertices.Alloc(verts * vertexSize); + if (memory == null) + Warning("CMatQueuedRenderContext::AllocVertices failed. Something is trying to render far too many things!\n"); + return memory; + } + + public ushort* AllocIndices(int indices) { + ushort* memory = (ushort*)Indices.Alloc(indices * sizeof(ushort)); + if (memory == null) + Warning("CMatQueuedRenderContext::AllocIndices failed. Something is trying to render far too many things!\n"); + return memory; + } + + public byte* ReallocVertices(byte* verts, int vertsOld, int vertsNew, int vertexSize) { + Assert(vertsNew <= vertsOld); + if (vertsNew < vertsOld) + Vertices.ShrinkLast(verts, vertsOld * vertexSize, vertsNew * vertexSize); + return verts; + } + + public ushort* ReallocIndices(ushort* indices, int indicesOld, int indicesNew) { + Assert(indicesNew <= indicesOld); + if (indicesNew < indicesOld) + Indices.ShrinkLast((byte*)indices, indicesOld * sizeof(ushort), indicesNew * sizeof(ushort)); + return indices; + } + + void QueueMatrixSync() { + Queue.QueueCall(static s => s.H.LoadMatrix(in s.Matrix), (H, CurMatrixItem.Matrix)); + } + + public void BeginRender() { + if (++RenderDepth == 1) + Queue.QueueCall(static h => h.BeginRender(), H); + } + + public void EndRender() { + Assert(HardwareContext != null); + if (--RenderDepth == 0) + Queue.QueueCall(static h => h.EndRender(), H); + } + + public override void Flush(bool flushHardware = false) => Queue.QueueCall(static s => s.H.Flush(s.FlushHardware), (H, FlushHardware: flushHardware)); + public void SwapBuffers() => Queue.QueueCall(static h => h.SwapBuffers(), H); + + public override void SetRenderTargetEx(int renderTargetID, ITexture? newTarget) { + base.SetRenderTargetEx(renderTargetID, newTarget); + Queue.QueueCall(static s => s.H.SetRenderTargetEx(s.ID, s.Target), (H, ID: renderTargetID, Target: newTarget)); + } + + public void GetRenderTargetDimensions(out int width, out int height) { + ITexture? tos = RenderTargetStack.Top().RenderTarget0; + if (tos != null) { + width = tos.GetActualWidth(); + height = tos.GetActualHeight(); + } + else { + width = WidthBackBuffer; + height = HeightBackBuffer; + } + } + + public override void GetViewport(out int x, out int y, out int width, out int height) { + Assert(RenderTargetStack.Count > 0); + ref RenderTargetStackElement element = ref RenderTargetStack.Top(); + + if (element.ViewW < 0 || element.ViewH < 0) { + x = y = 0; + ITexture? renderTarget = element.RenderTarget0; + if (renderTarget == null) { + width = WidthBackBuffer; + height = HeightBackBuffer; + } + else { + width = renderTarget.GetActualWidth(); + height = renderTarget.GetActualHeight(); + } + } + else { + x = element.ViewX; + y = element.ViewY; + width = element.ViewW; + height = element.ViewH; + } + } + + public override void Bind(IMaterial iMaterial, object? proxyData) { + IMaterialInternal? material = (IMaterialInternal?)iMaterial; + if (material == null) { + if (materials.errorMaterial == null) + return; + material = materials.errorMaterial; + } + else + material = material.GetRealTimeVersion(); + + currentMaterial = material; + CurrentProxyData = proxyData; + + material.CallBindProxy(proxyData); + + Queue.QueueCall(static s => s.H.Bind(s.Material, s.Proxy), (H, Material: material, Proxy: proxyData)); + } + + public override void BindLocalCubemap(ITexture? texture) { + base.BindLocalCubemap(texture); + Queue.QueueCall(static s => s.H.BindLocalCubemap(s.Texture), (H, Texture: texture)); + } + + public override void BindLightmapPage(int lightmapPageID) { + base.BindLightmapPage(lightmapPageID); + Queue.QueueCall(static s => s.H.BindLightmapPage(s.Page), (H, Page: lightmapPageID)); + } + + public void DepthRange(float near, float far) => Queue.QueueCall(static s => s.H.DepthRange(s.Near, s.Far), (H, Near: near, Far: far)); + public void ClearBuffers(bool clearColor, bool clearDepth, bool clearStencil = false) => Queue.QueueCall(static s => s.H.ClearBuffers(s.Color, s.Depth, s.Stencil), (H, Color: clearColor, Depth: clearDepth, Stencil: clearStencil)); + + public void ReadPixels(int x, int y, int width, int height, Span data, ImageFormat dstFormat) { + MaterialLock hLock = materials.Lock(); + CallQueued(); + ShaderAPI.ReadPixels(x, y, width, height, data, dstFormat); + materials.Unlock(hLock); + } + + public void ReadPixelsAndStretch(ref Rectangle srcRect, ref Rectangle dstRect, Span buffer, ImageFormat dstFormat, int dstStride) { + MaterialLock hLock = materials.Lock(); + CallQueued(); + H.ReadPixelsAndStretch(ref srcRect, ref dstRect, buffer, dstFormat, dstStride); + materials.Unlock(hLock); + } + + public void SetAmbientLight(float r, float g, float b) => Queue.QueueCall(static s => s.H.SetAmbientLight(s.R, s.G, s.B), (H, R: r, G: g, B: b)); + public void SetLight(int lightNum, in LightDesc desc) => Queue.QueueCall(static s => s.H.SetLight(s.Num, in s.Desc), (H, Num: lightNum, Desc: desc)); + public void SetLightingOrigin(Vector3 lightingOrigin) => Queue.QueueCall(static s => s.H.SetLightingOrigin(s.Origin), (H, Origin: lightingOrigin)); + + public void SetAmbientLightCube(Span cube) { + InlineArray6 copy = default; + cube[..6].CopyTo(copy); + Queue.QueueCall(static s => { + InlineArray6 c = s.Cube; + s.H.SetAmbientLightCube(c); + }, (H, Cube: copy)); + } + + public void CopyRenderTargetToTexture(ITexture? texture) => Queue.QueueCall(static s => s.H.CopyRenderTargetToTexture(s.Texture!), (H, Texture: texture)); + + public override void SetFrameBufferCopyTexture(ITexture? texture, int textureIndex = 0) { + base.SetFrameBufferCopyTexture(texture, textureIndex); + Queue.QueueCall(static s => s.H.SetFrameBufferCopyTexture(s.Texture, s.Index), (H, Texture: texture, Index: textureIndex)); + } + + public override void MatrixMode(MaterialMatrixMode mode) { + base.MatrixMode(mode); + Queue.QueueCall(static s => s.H.MatrixMode(s.Mode), (H, Mode: mode)); + } + + public override void PushMatrix() { + base.PushMatrix(); + Queue.QueueCall(static h => h.PushMatrix(), H); + } + + public override void PopMatrix() { + base.PopMatrix(); + Queue.QueueCall(static h => h.PopMatrix(), H); + } + + public override void LoadMatrix(in Matrix4x4 matrix) { + base.LoadMatrix(in matrix); + QueueMatrixSync(); + } + + public override void LoadMatrix(Matrix3x4 matrix) { + base.LoadMatrix(matrix); + QueueMatrixSync(); + } + + public override void MultMatrix(in Matrix4x4 matrix) { + base.MultMatrix(in matrix); + QueueMatrixSync(); + } + + public override void MultMatrix(in Matrix3x4 matrix) { + base.MultMatrix(in matrix); + QueueMatrixSync(); + } + + public override void MultMatrixLocal(in Matrix4x4 matrix) { + base.MultMatrixLocal(in matrix); + QueueMatrixSync(); + } + + public override void MultMatrixLocal(in Matrix3x4 matrix) { + base.MultMatrixLocal(in matrix); + QueueMatrixSync(); + } + + public override void LoadIdentity() { + base.LoadIdentity(); + Queue.QueueCall(static h => h.LoadIdentity(), H); + } + + public override void Ortho(double left, double top, double right, double bottom, double near, double far) { + base.Ortho(left, top, right, bottom, near, far); + QueueMatrixSync(); + } + + public override void PerspectiveX(double fovx, double aspect, double zNear, double zFar) { + base.PerspectiveX(fovx, aspect, zNear, zFar); + QueueMatrixSync(); + } + + public override void PerspectiveOffCenterX(double fovx, double aspect, double zNear, double zFar, double bottom, double top, double left, double right) { + base.PerspectiveOffCenterX(fovx, aspect, zNear, zFar, bottom, top, left, right); + QueueMatrixSync(); + } + + public override void PickMatrix(int x, int y, int width, int height) { + base.PickMatrix(x, y, width, height); + QueueMatrixSync(); + } + + public override void Rotate(float angle, float x, float y, float z) { + base.Rotate(angle, x, y, z); + QueueMatrixSync(); + } + + public override void Viewport(int x, int y, int width, int height) { + base.Viewport(x, y, width, height); + Queue.QueueCall(static s => s.H.Viewport(s.X, s.Y, s.W, s.Height), (H, X: x, Y: y, W: width, Height: height)); + } + + public void CullMode(MaterialCullMode cullMode) => Queue.QueueCall(static s => s.H.CullMode(s.Mode), (H, Mode: cullMode)); + + public void FogMode(MaterialFogMode fogMode) { + FogModeValue = fogMode; + Queue.QueueCall(static s => s.H.FogMode(s.Mode), (H, Mode: fogMode)); + } + + public void FogStart(float start) { + FogStartValue = start; + Queue.QueueCall(static s => s.H.FogStart(s.Start), (H, Start: start)); + } + + public void FogEnd(float end) { + FogEndValue = end; + Queue.QueueCall(static s => s.H.FogEnd(s.End), (H, End: end)); + } + + public void FogMaxDensity(float maxDensity) { + FogMaxDensityValue = maxDensity; + Queue.QueueCall(static s => s.H.FogMaxDensity(s.Density), (H, Density: maxDensity)); + } + + public void SetFogZ(float fogZ) { + FogZ = fogZ; + Queue.QueueCall(static s => s.H.SetFogZ(s.FogZ), (H, FogZ: fogZ)); + } + + public void FogRadial(bool radial) { + FogRadialValue = radial; + Queue.QueueCall(static s => s.H.FogRadial(s.Radial), (H, Radial: radial)); + } + + public bool GetFogRadial() => FogRadialValue; + + public MaterialFogMode GetFogMode() => FogModeValue; + + public void FogColor3f(float r, float g, float b) => FogColor3ub((byte)Math.Clamp((int)(r * 255.0f), 0, 255), (byte)Math.Clamp((int)(g * 255.0f), 0, 255), (byte)Math.Clamp((int)(b * 255.0f), 0, 255)); + public void FogColor3fv(ReadOnlySpan rgb) => FogColor3ub((byte)Math.Clamp((int)(rgb[0] * 255.0f), 0, 255), (byte)Math.Clamp((int)(rgb[1] * 255.0f), 0, 255), (byte)Math.Clamp((int)(rgb[2] * 255.0f), 0, 255)); + + public void FogColor3ub(byte r, byte g, byte b) { + FogColorR = r; + FogColorG = g; + FogColorB = b; + Queue.QueueCall(static s => s.H.FogColor3ub(s.R, s.G, s.B), (H, R: r, G: g, B: b)); + } + + public void FogColor3ubv(ReadOnlySpan rgb) => FogColor3ub(rgb[0], rgb[1], rgb[2]); + + public void GetFogColor(out Color rgb) => rgb = new Color(FogColorR, FogColorG, FogColorB); + + public void GetFogDistances(out float start, out float end, out float fogZ) { + start = FogStartValue; + end = FogEndValue; + fogZ = FogZ; + } + + public int GetCurrentNumBones() => BoneCount; + + public void SetNumBoneWeights(int numBones) { + BoneCount = numBones; + Queue.QueueCall(static s => s.H.SetNumBoneWeights(s.Bones), (H, Bones: numBones)); + } + + public IMesh? CreateStaticMesh(VertexFormat fmt, ReadOnlySpan textureBudgetGroup, IMaterial? material = null) => H.CreateStaticMesh(fmt, textureBudgetGroup, material); + public void DestroyStaticMesh(IMesh? mesh) => Queue.QueueCall(static s => s.H.DestroyStaticMesh(s.Mesh!), (H, Mesh: mesh)); + + public IMesh? GetDynamicMesh(bool buffered = true, IMesh? vertexOverride = null, IMesh? indexOverride = null, IMaterial? autoBind = null) => GetDynamicMeshEx(0, buffered, vertexOverride, indexOverride, autoBind); + + public IMesh? GetDynamicMeshEx(VertexFormat vertexFormat, bool buffered = true, IMesh? vertexOverride = null, IMesh? indexOverride = null, IMaterial? autoBind = null) { + if (autoBind != null) + Bind(autoBind, null); + + int currentBoneCount = GetCurrentNumBones(); + Assert(currentBoneCount <= 4); + if (currentBoneCount > 1) + --currentBoneCount; + + QueuedMesh!.OnGetDynamicMesh(vertexFormat, buffered ? MatQueuedMesh.MQM_BUFFERED : 0, vertexOverride, indexOverride, GetCurrentMaterialInternal(), currentBoneCount); + return QueuedMesh; + } + + public IMesh? GetFlexMesh() { + QueuedFlexMesh!.OnGetDynamicMesh(0, 0, null, null, null, 0); + return QueuedFlexMesh; + } + + public int GetMaxVerticesToRender(IMaterial? material) { + material = ((IMaterialInternal)material!).GetRealTimeVersion(); + + MeshDesc temp = default; + ShaderAPI.ComputeVertexDescription(default, material.GetVertexFormat(), ref temp); + int vertexSize = temp.Vertex.ActualVertexSize; + if (vertexSize == 0) { + Warning($"bad vertex size for material {material.GetName()}\n"); + return 0; + } + + int maxVerts = ShaderAPI.GetCurrentDynamicVBSize() / vertexSize; + return Math.Min(maxVerts, 32767); + } + + public int GetMaxIndicesToRender() => IMesh.INDEX_BUFFER_SIZE; + + public void GetMaxToRender(IMesh? mesh, bool maxUntilFlush, Span maxVerts, Span maxIndices) { + Assert(!maxUntilFlush); + int vertexSize = QueuedMesh!.GetVertexSize(); + maxVerts[0] = vertexSize != 0 ? Math.Min(ShaderAPI.GetCurrentDynamicVBSize() / vertexSize, 32767) : 0; + maxIndices[0] = IMesh.INDEX_BUFFER_SIZE; + } + + public int SelectionMode(bool selectionMode) { + CannotSupport(); + return 0; + } + + public void SelectionBuffer(Span buffer) => CannotSupport(); + public void ClearSelectionNames() => Queue.QueueCall(static h => h.ClearSelectionNames(), H); + public void LoadSelectionName(int name) => Queue.QueueCall(static s => s.H.LoadSelectionName(s.Name), (H, Name: name)); + public void PushSelectionName(int name) => Queue.QueueCall(static s => s.H.PushSelectionName(s.Name), (H, Name: name)); + public void PopSelectionName() => Queue.QueueCall(static h => h.PopSelectionName(), H); + + public void ClearColor3ub(byte r, byte g, byte b) => Queue.QueueCall(static s => s.H.ClearColor3ub(s.R, s.G, s.B), (H, R: r, G: g, B: b)); + public void ClearColor4ub(byte r, byte g, byte b, byte a) => Queue.QueueCall(static s => s.H.ClearColor4ub(s.R, s.G, s.B, s.A), (H, R: r, G: g, B: b, A: a)); + + public void OverrideDepthEnable(bool enable, bool depthEnable) => Queue.QueueCall(static s => s.H.OverrideDepthEnable(s.Enable, s.Depth), (H, Enable: enable, Depth: depthEnable)); + public void OverrideAlphaWriteEnable(bool enable, bool alphaWriteEnable) => Queue.QueueCall(static s => s.H.OverrideAlphaWriteEnable(s.Enable, s.Alpha), (H, Enable: enable, Alpha: alphaWriteEnable)); + public void OverrideColorWriteEnable(bool overrideEnable, bool colorWriteEnable) => Queue.QueueCall(static s => s.H.OverrideColorWriteEnable(s.Enable, s.Color), (H, Enable: overrideEnable, Color: colorWriteEnable)); + + public void DrawScreenSpaceQuad(IMaterial? material) => Queue.QueueCall(static s => s.H.DrawScreenSpaceQuad(s.Material), (H, Material: material)); + + public void SyncToken(ReadOnlySpan token) => Queue.QueueCall(static s => s.H.SyncToken(s.Token), (H, Token: new string(token))); + + public OcclusionQueryObjectHandle_t CreateOcclusionQueryObject() { + OcclusionQueryObjectHandle_t h = materials.OcclusionQueryMgr.CreateOcclusionQueryObject(); + Queue.QueueCall(static s => s.Mgr.OnCreateOcclusionQueryObject(s.Handle), (Mgr: materials.OcclusionQueryMgr, Handle: h)); + return h; + } + + public void DestroyOcclusionQueryObject(OcclusionQueryObjectHandle_t handle) => Queue.QueueCall(static s => s.H.DestroyOcclusionQueryObject(s.Handle), (H, Handle: handle)); + public void BeginOcclusionQueryDrawing(OcclusionQueryObjectHandle_t handle) => Queue.QueueCall(static s => s.H.BeginOcclusionQueryDrawing(s.Handle), (H, Handle: handle)); + public void EndOcclusionQueryDrawing(OcclusionQueryObjectHandle_t handle) => Queue.QueueCall(static s => s.H.EndOcclusionQueryDrawing(s.Handle), (H, Handle: handle)); + public void ResetOcclusionQueryObject(OcclusionQueryObjectHandle_t handle) => Queue.QueueCall(static s => s.H.ResetOcclusionQueryObject(s.Handle), (H, Handle: handle)); + + public int OcclusionQuery_GetNumPixelsRendered(OcclusionQueryObjectHandle_t handle) { + Queue.QueueCall(static s => s.Mgr.OcclusionQuery_IssueNumPixelsRenderedQuery(s.Handle), (Mgr: materials.OcclusionQueryMgr, Handle: handle)); + return materials.OcclusionQueryMgr.OcclusionQuery_GetNumPixelsRendered(handle, false); + } + + public void SetFlashlightMode(bool enable) { + FlashlightEnable = enable; + Queue.QueueCall(static s => s.H.SetFlashlightMode(s.Enable), (H, Enable: enable)); + } + + public bool GetFlashlightMode() => FlashlightEnable; + + public void SetFlashlightState(in FlashlightState state, in Matrix4x4 worldToTexture) => Queue.QueueCall(static s => s.H.SetFlashlightState(in s.State, in s.WorldToTexture), (H, State: state, WorldToTexture: worldToTexture)); + public void SetFlashlightStateEx(in FlashlightState state, in Matrix4x4 worldToTexture, ITexture? flashlightDepthTexture) => Queue.QueueCall(static s => s.H.SetFlashlightStateEx(in s.State, in s.WorldToTexture, s.Depth), (H, State: state, WorldToTexture: worldToTexture, Depth: flashlightDepthTexture)); + + public override void SetHeightClipMode(MaterialHeightClipMode heightClipMode) { + base.SetHeightClipMode(heightClipMode); + Queue.QueueCall(static s => s.H.SetHeightClipMode(s.Mode), (H, Mode: heightClipMode)); + } + + public override void SetHeightClipZ(float z) { + base.SetHeightClipZ(z); + Queue.QueueCall(static s => s.H.SetHeightClipZ(s.Z), (H, Z: z)); + } + + public override bool EnableClipping(bool enable) { + Queue.QueueCall(static s => s.H.EnableClipping(s.Enable), (H, Enable: enable)); + return base.EnableClipping(enable); + } + + public void EnableUserClipTransformOverride(bool enable) => Queue.QueueCall(static s => s.H.EnableUserClipTransformOverride(s.Enable), (H, Enable: enable)); + public void UserClipTransform(in Matrix4x4 worldToView) => Queue.QueueCall(static s => s.H.UserClipTransform(in s.Matrix), (H, Matrix: worldToView)); + + public IMorph CreateMorph(MorphFormatFlags format, ReadOnlySpan debugName) { + CannotSupport(); + return null!; + } + + public void DestroyMorph(IMorph morph) => Queue.QueueCall(static s => s.H.DestroyMorph(s.Morph), (H, Morph: morph)); + public void BindMorph(IMorph morph) => Queue.QueueCall(static s => s.H.BindMorph(s.Morph), (H, Morph: morph)); + public void SetFlexWeights(int firstWeight, ReadOnlySpan weights) => Assert(false); + + public void GetWindowSize(out int width, out int height) { + width = WidthBackBuffer; + height = HeightBackBuffer; + } + + public void DrawScreenSpaceRectangle(IMaterial? material, int destX, int destY, int width, int height, float srcTextureX0, float srcTextureY0, float srcTextureX1, float srcTextureY1, int srcTextureWidth, int srcTextureHeight, IClientRenderable? clientRenderable = null, int xDice = 1, int yDice = 1) { + IMaterialInternal realTimeVersion = ((IMaterialInternal)material!).GetRealTimeVersion(); + realTimeVersion.CallBindProxy(clientRenderable); + Queue.QueueCall(static s => s.H.DrawScreenSpaceRectangle(s.Material, s.DestX, s.DestY, s.Width, s.Height, s.Src.X, s.Src.Y, s.Src.Z, s.Src.W, s.SrcWidth, s.SrcHeight, s.Renderable, s.XDice, s.YDice), + (H, Material: material, DestX: destX, DestY: destY, Width: width, Height: height, Src: new Vector4(srcTextureX0, srcTextureY0, srcTextureX1, srcTextureY1), SrcWidth: srcTextureWidth, SrcHeight: srcTextureHeight, Renderable: clientRenderable, XDice: xDice, YDice: yDice)); + } + + public void LoadBoneMatrix(int boneIndex, in Matrix3x4 matrix) => Queue.QueueCall(static s => s.H.LoadBoneMatrix(s.Index, in s.Matrix), (H, Index: boneIndex, Matrix: matrix)); + + public override void PushRenderTargetAndViewport() { + base.PushRenderTargetAndViewport(); + Queue.QueueCall(static h => h.PushRenderTargetAndViewport(), H); + } + + public override void PushRenderTargetAndViewport(ITexture? texture) { + base.PushRenderTargetAndViewport(texture); + Queue.QueueCall(static s => s.H.PushRenderTargetAndViewport(s.Texture), (H, Texture: texture)); + } + + public override void PushRenderTargetAndViewport(ITexture? texture, int viewX, int viewY, int viewW, int viewH) { + base.PushRenderTargetAndViewport(texture, viewX, viewY, viewW, viewH); + Queue.QueueCall(static s => s.H.PushRenderTargetAndViewport(s.Texture, s.X, s.Y, s.W, s.Height), (H, Texture: texture, X: viewX, Y: viewY, W: viewW, Height: viewH)); + } + + public override void PushRenderTargetAndViewport(ITexture? texture, ITexture? depthTexture, int viewX, int viewY, int viewW, int viewH) { + base.PushRenderTargetAndViewport(texture, depthTexture, viewX, viewY, viewW, viewH); + Queue.QueueCall(static s => s.H.PushRenderTargetAndViewport(s.Texture, s.Depth, s.X, s.Y, s.W, s.Height), (H, Texture: texture, Depth: depthTexture, X: viewX, Y: viewY, W: viewW, Height: viewH)); + } + + public override void PopRenderTargetAndViewport() { + base.PopRenderTargetAndViewport(); + Queue.QueueCall(static h => h.PopRenderTargetAndViewport(), H); + } + + public void BindLightmatexture(ITexture? lightmapTexture) => Queue.QueueCall(static s => s.H.BindLightmatexture(s.Texture), (H, Texture: lightmapTexture)); + + public void CopyRenderTargetToTextureEx(ITexture? texture, int renderTargetID, ref Rectangle srcRect, ref Rectangle dstRect) { + Rectangle? src = Unsafe.IsNullRef(ref srcRect) ? null : srcRect; + Rectangle? dst = Unsafe.IsNullRef(ref dstRect) ? null : dstRect; + Queue.QueueCall(static s => { + Rectangle src = s.Src.GetValueOrDefault(), dst = s.Dst.GetValueOrDefault(); + s.H.CopyRenderTargetToTextureEx(s.Texture, s.ID, ref s.Src.HasValue ? ref src : ref Unsafe.NullRef(), ref s.Dst.HasValue ? ref dst : ref Unsafe.NullRef()); + }, (H, Texture: texture, ID: renderTargetID, Src: src, Dst: dst)); + } + + public void CopyTextureToRenderTargetEx(int renderTargetID, ITexture? texture, ref Rectangle srcRect, ref Rectangle dstRect) { + Rectangle? src = Unsafe.IsNullRef(ref srcRect) ? null : srcRect; + Rectangle? dst = Unsafe.IsNullRef(ref dstRect) ? null : dstRect; + Queue.QueueCall(static s => { + Rectangle src = s.Src.GetValueOrDefault(), dst = s.Dst.GetValueOrDefault(); + s.H.CopyTextureToRenderTargetEx(s.ID, s.Texture, ref s.Src.HasValue ? ref src : ref Unsafe.NullRef(), ref s.Dst.HasValue ? ref dst : ref Unsafe.NullRef()); + }, (H, Texture: texture, ID: renderTargetID, Src: src, Dst: dst)); + } + + public void SetFloatRenderingParameter(int parm_number, float value) => Queue.QueueCall(static s => s.H.SetFloatRenderingParameter(s.Parm, s.Value), (H, Parm: parm_number, Value: value)); + public void SetIntRenderingParameter(int parm_number, int value) => Queue.QueueCall(static s => s.H.SetIntRenderingParameter(s.Parm, s.Value), (H, Parm: parm_number, Value: value)); + public void SetVectorRenderingParameter(int parm_number, in Vector3 value) => Queue.QueueCall(static s => s.H.SetVectorRenderingParameter(s.Parm, in s.Value), (H, Parm: parm_number, Value: value)); + + public float GetFloatRenderingParameter(int parmNumber) { + CannotSupport(); + return 0; + } + + public int GetIntRenderingParameter(int parmNumber) { + CannotSupport(); + return 0; + } + + public Vector3 GetVectorRenderingParameter(int parmNumber) { + CannotSupport(); + return default; + } + + public void SetStencilEnable(bool onoff) => Queue.QueueCall(static s => s.H.SetStencilEnable(s.On), (H, On: onoff)); + public void SetStencilFailOperation(StencilOperation op) => Queue.QueueCall(static s => s.H.SetStencilFailOperation(s.Op), (H, Op: op)); + public void SetStencilZFailOperation(StencilOperation op) => Queue.QueueCall(static s => s.H.SetStencilZFailOperation(s.Op), (H, Op: op)); + public void SetStencilPassOperation(StencilOperation op) => Queue.QueueCall(static s => s.H.SetStencilPassOperation(s.Op), (H, Op: op)); + public void SetStencilCompareFunction(StencilComparisonFunction cmpfn) => Queue.QueueCall(static s => s.H.SetStencilCompareFunction(s.Fn), (H, Fn: cmpfn)); + public void SetStencilReferenceValue(int reference) => Queue.QueueCall(static s => s.H.SetStencilReferenceValue(s.Ref), (H, Ref: reference)); + public void SetStencilTestMask(uint msk) => Queue.QueueCall(static s => s.H.SetStencilTestMask(s.Mask), (H, Mask: msk)); + public void SetStencilWriteMask(uint msk) => Queue.QueueCall(static s => s.H.SetStencilWriteMask(s.Mask), (H, Mask: msk)); + public void ClearStencilBufferRectangle(int xmin, int ymin, int xmax, int ymax, int value) => Queue.QueueCall(static s => s.H.ClearStencilBufferRectangle(s.XMin, s.YMin, s.XMax, s.YMax, s.Value), (H, XMin: xmin, YMin: ymin, XMax: xmax, YMax: ymax, Value: value)); + + public void PushCustomClipPlane(ReadOnlySpan plane) { + InlineArray4 copy = default; + plane[..4].CopyTo(copy); + Queue.QueueCall(static s => { + InlineArray4 p = s.Plane; + s.H.PushCustomClipPlane(p); + }, (H, Plane: copy)); + } + + public void PopCustomClipPlane() => Queue.QueueCall(static h => h.PopCustomClipPlane(), H); + + public void DisableAllLocalLights() => Queue.QueueCall(static h => h.DisableAllLocalLights(), H); + + public int CompareMaterialCombos(IMaterial? material1, IMaterial? material2, int lightMapID1, int lightMapID2) { + CannotSupport(); + return 0; + } + + public ITexture? GetLocalCubemap() => LocalCubemapTexture; + + public void ClearBuffersObeyStencil(bool clearColor, bool clearDepth) => Queue.QueueCall(static s => s.H.ClearBuffersObeyStencil(s.Color, s.Depth), (H, Color: clearColor, Depth: clearDepth)); + public void ClearBuffersObeyStencilEx(bool clearColor, bool clearAlpha, bool clearDepth) => Queue.QueueCall(static s => s.H.ClearBuffersObeyStencilEx(s.Color, s.Alpha, s.Depth), (H, Color: clearColor, Alpha: clearAlpha, Depth: clearDepth)); + public void PerformFullScreenStencilOperation() => Queue.QueueCall(static h => h.PerformFullScreenStencilOperation(), H); + + public void BeginPIXEvent(Color color, ReadOnlySpan name) => Queue.QueueCall(static s => s.H.BeginPIXEvent(s.Color, s.Name), (H, Color: color, Name: new string(name))); + public void EndPIXEvent() => Queue.QueueCall(static h => h.EndPIXEvent(), H); + public void SetPIXMarker(Color color, ReadOnlySpan name) => Queue.QueueCall(static s => s.H.SetPIXMarker(s.Color, s.Name), (H, Color: color, Name: new string(name))); + + public void BeginBatch(IMesh? indices) { + Assert(indices == QueuedMesh); + QueuedMesh!.InvalidateAuxMeshSet(); + Queue.QueueCall(static ctx => ctx.H.BeginBatch(ctx.QueuedMesh!.MST_DetachActualMesh()!), this); + } + + public void BindBatch(IMesh? vertices, IMaterial? autoBind = null) { + Assert(vertices != QueuedMesh); + Queue.QueueCall(static s => s.H.BindBatch(s.Vertices!, s.AutoBind), (H, Vertices: vertices, AutoBind: autoBind)); + } + + public void DrawBatch(int firstIndex, int numIndices) => Queue.QueueCall(static s => s.H.DrawBatch(s.First, s.Num), (H, First: firstIndex, Num: numIndices)); + public void EndBatch() => Queue.QueueCall(static h => h.EndBatch(), H); + + public void ResetToneMappingScale(float monoscale) { + LastSetToneMapScale = new(monoscale, monoscale, monoscale); + Queue.QueueCall(static s => s.H.ResetToneMappingScale(s.Scale), (H, Scale: monoscale)); + } + + public void SetGoalToneMappingScale(float monoscale) => Queue.QueueCall(static s => s.H.SetGoalToneMappingScale(s.Scale), (H, Scale: monoscale)); + public void TurnOnToneMapping() => Queue.QueueCall(static h => h.TurnOnToneMapping(), H); + + public void SetToneMappingScaleLinear(in Vector3 scale) { + LastSetToneMapScale = scale; + Queue.QueueCall(static s => s.H.SetToneMappingScaleLinear(in s.Scale), (H, Scale: scale)); + } + + public override Vector3 GetToneMappingScaleLinear() => LastSetToneMapScale; + + public void SetShadowDepthBiasFactors(float slopeScaleDepthBias, float depthBias) => Queue.QueueCall(static s => s.H.SetShadowDepthBiasFactors(s.Slope, s.Bias), (H, Slope: slopeScaleDepthBias, Bias: depthBias)); + public void SetScissorRect(int left, int top, int right, int bottom, bool enableScissor) => Queue.QueueCall(static s => s.H.SetScissorRect(s.Left, s.Top, s.Right, s.Bottom, s.Enable), (H, Left: left, Top: top, Right: right, Bottom: bottom, Enable: enableScissor)); + + public void BeginMorphAccumulation() => Queue.QueueCall(static h => h.BeginMorphAccumulation(), H); + public void EndMorphAccumulation() => Queue.QueueCall(static h => h.EndMorphAccumulation(), H); + public void AccumulateMorph(IMorph morph, ReadOnlySpan weights) => Assert(false); + + public void PushDeformation(ref readonly DeformationBase deformation) => CannotSupport(); + public void PopDeformation() => CannotSupport(); + public int GetNumActiveDeformations() => 0; + + public bool GetMorphAccumulatorTexCoord(out Vector2 texCoord, IMorph morph, int vertex) { + Assert(false); + texCoord = default; + return false; + } + + public void EnableColorCorrection(bool enable) => Queue.QueueCall(static s => s.H.EnableColorCorrection(s.Enable), (H, Enable: enable)); + + public ColorCorrectionHandle_t AddLookup(ReadOnlySpan name) { + MaterialLock hLock = materials.Lock(); + ColorCorrectionHandle_t handle = H.AddLookup(name); + materials.Unlock(hLock); + return handle; + } + + public bool RemoveLookup(ColorCorrectionHandle_t handle) { + MaterialLock hLock = materials.Lock(); + bool removed = H.RemoveLookup(handle); + materials.Unlock(hLock); + return removed; + } + + public void LockLookup(ColorCorrectionHandle_t handle) { + MaterialLock hLock = materials.Lock(); + H.LockLookup(handle); + materials.Unlock(hLock); + } + + public void LoadLookup(ColorCorrectionHandle_t handle, ReadOnlySpan lookuname) { + MaterialLock hLock = materials.Lock(); + H.LoadLookup(handle, lookuname); + materials.Unlock(hLock); + } + + public void UnlockLookup(ColorCorrectionHandle_t handle) { + MaterialLock hLock = materials.Lock(); + H.UnlockLookup(handle); + materials.Unlock(hLock); + } + + public void SetLookupWeight(ColorCorrectionHandle_t handle, float weight) => Queue.QueueCall(static s => s.H.SetLookupWeight(s.Handle, s.Weight), (H, Handle: handle, Weight: weight)); + public void ResetLookupWeights() => Queue.QueueCall(static h => h.ResetLookupWeights(), H); + public void SetResetable(ColorCorrectionHandle_t handle, bool resetable) => Queue.QueueCall(static s => s.H.SetResetable(s.Handle, s.Resetable), (H, Handle: handle, Resetable: resetable)); + + public void SetFullScreenDepthTextureValidityFlag(bool isValid) => Queue.QueueCall(static s => s.H.SetFullScreenDepthTextureValidityFlag(s.Valid), (H, Valid: isValid)); + + public void SetNonInteractivePacifierTexture(ITexture? texture, float normalizedX, float normalizedY, float normalizedSize) => Queue.QueueCall(static s => s.H.SetNonInteractivePacifierTexture(s.Texture, s.X, s.Y, s.Size), (H, Texture: texture, X: normalizedX, Y: normalizedY, Size: normalizedSize)); + public void SetNonInteractiveTempFullscreenBuffer(ITexture? texture, MaterialNonInteractiveMode mode) => Queue.QueueCall(static s => s.H.SetNonInteractiveTempFullscreenBuffer(s.Texture, s.Mode), (H, Texture: texture, Mode: mode)); + public void EnableNonInteractiveMode(MaterialNonInteractiveMode mode) => Queue.QueueCall(static s => s.H.EnableNonInteractiveMode(s.Mode), (H, Mode: mode)); + public void RefreshFrontBufferNonInteractive() => Queue.QueueCall(static h => h.RefreshFrontBufferNonInteractive(), H); + + public E? LockRenderDataTyped(int count, E? srcData = null) where E : class { + throw new NotImplementedException("Incomplete port of IMatRenderContext"); + } + + public override float Knob(Span knobname, Span setvalue = default) => 0.0f; + + public void GMOD_ForceFilterMode(bool min, int mode) => Queue.QueueCall(static s => s.H.GMOD_ForceFilterMode(s.Min, s.Mode), (H, Min: min, Mode: mode)); + public void GMOD_FlushQueue() { } + public void OverrideBlend(bool unk1, bool unk2, int unk3, int unk4, int unk5) => Queue.QueueCall(static s => s.H.OverrideBlend(s.A, s.B, s.C, s.D, s.E), (H, A: unk1, B: unk2, C: unk3, D: unk4, E: unk5)); + public void OverrideBlendSeparateAlpha(bool unk1, bool unk2, int unk3, int unk4, int unk5) => Queue.QueueCall(static s => s.H.OverrideBlendSeparateAlpha(s.A, s.B, s.C, s.D, s.E), (H, A: unk1, B: unk2, C: unk3, D: unk4, E: unk5)); + + public override void SetCurrentMaterialInternal(IMaterialInternal? mat) { + base.SetCurrentMaterialInternal(mat); + Queue.QueueCall(static s => s.H.SetCurrentMaterialInternal(s.Material), (H, Material: mat)); + } + + public override void SetCurrentProxy(object? proxyData) { + base.SetCurrentProxy(proxyData); + Queue.QueueCall(static s => s.H.SetCurrentProxy(s.Proxy), (H, Proxy: proxyData)); + } + + public override int GetLightmapPage() { + CannotSupport(); + return 0; + } + + public void ForceDepthFuncEquals(bool force) => CannotSupport(); + + public bool InFlashlightMode() { + CannotSupport(); + return false; + } + + public void BindStandardTexture(Sampler sampler, StandardTextureId id) => CannotSupport(); + + public void GetLightmapDimensions(out int w, out int h) { + CannotSupport(); + w = h = 0; + } + + public MorphFormatFlags GetBoundMorphFormat() { + CannotSupport(); + return 0; + } + + public void DrawClearBufferQuad(byte r, byte g, byte b, byte a, bool clearColor, bool clearAlpha, bool clearDepth) => CannotSupport(); + + public bool OnDrawMesh(IMesh mesh, int firstIndex, int numIndices) { + Assert(mesh != QueuedMesh); + Queue.QueueCall(static s => s.Mesh.Draw(s.First, s.Num), (Mesh: mesh, First: firstIndex, Num: numIndices)); + return false; + } + + public bool OnDrawMesh(IMesh mesh, PrimList* lists, int numLists) { + if (PrimListCount + numLists > PrimLists.Length) + Array.Resize(ref PrimLists, Math.Max(PrimLists.Length * 2, PrimListCount + numLists)); + + int offset = PrimListCount; + new ReadOnlySpan(lists, numLists).CopyTo(PrimLists.AsSpan(offset)); + PrimListCount += numLists; + + Queue.QueueCall(static s => s.Ctx.DeferredDrawPrimList(s.Mesh, s.Offset, s.Count), (Ctx: this, Mesh: mesh, Offset: offset, Count: numLists)); + return false; + } + + void DeferredDrawPrimList(IMesh mesh, int offset, int count) { + Assert(mesh != QueuedMesh); + mesh.Draw(new ReadOnlySpan(PrimLists, offset, count), count); + } + + void DeferredSetFlexMesh(IMesh staticMesh) => staticMesh.SetFlexMesh(QueuedFlexMesh!.MST_GetActualMesh(), QueuedFlexMesh.MST_GetActualVertexOffsetInBytes()); + + public bool OnSetFlexMesh(IMesh staticMesh, IMesh? mesh, int nVertexOffsetInBytes) { + Assert(mesh == QueuedFlexMesh || mesh == null); + if (mesh != null) + Queue.QueueCall(static s => s.Ctx.DeferredSetFlexMesh(s.Mesh), (Ctx: this, Mesh: staticMesh)); + else + Queue.QueueCall(static mesh => mesh.SetFlexMesh(null, 0), staticMesh); + return false; + } + + public bool OnSetColorMesh(IMesh staticMesh, IMesh? mesh, int nVertexOffsetInBytes) { + Assert(staticMesh != QueuedMesh && staticMesh != QueuedFlexMesh); + Queue.QueueCall(static s => s.Static.SetColorMesh(MatQueuedMesh.ResolveMesh(s.Mesh)!, s.Offset), (Static: staticMesh, Mesh: mesh, Offset: nVertexOffsetInBytes)); + return false; + } + + public bool OnSetPrimitiveType(IMesh mesh, MaterialPrimitiveType type) { + Assert(mesh != QueuedMesh && mesh != QueuedFlexMesh); + Queue.QueueCall(static s => s.Mesh.SetPrimitiveType(s.Type), (Mesh: mesh, Type: type)); + return false; + } + + public bool OnFlushBufferedPrimitives() { + Queue.QueueCall(static shaderAPI => shaderAPI.FlushBufferedPrimitives(), ShaderAPI); + return false; + } + + public void ForceHardwareSync() => Queue.QueueCall(static h => h.ForceHardwareSync(), H); + public void BeginFrame() => Queue.QueueCall(static h => h.BeginFrame(), H); + public void EndFrame() => Queue.QueueCall(static h => h.EndFrame(), H); + + public override void SetFrameTime(double frameTime) { + Queue.QueueCall(static s => s.H.SetFrameTime(s.Time), (H, Time: frameTime)); + base.SetFrameTime(frameTime); + } + + public int AddRef() { + throw new NotImplementedException("Incomplete port of IRefCounted"); + } + + public int Release() { + throw new NotImplementedException("Incomplete port of IRefCounted"); + } +} diff --git a/Source.MaterialSystem/MatStub.cs b/Source.MaterialSystem/MatStub.cs index f80169feb..791ab7864 100644 --- a/Source.MaterialSystem/MatStub.cs +++ b/Source.MaterialSystem/MatStub.cs @@ -499,6 +499,9 @@ public void AsyncFindTexture(ReadOnlySpan pFilename, ReadOnlySpan public bool HasShaderAPI() => true; public bool InFlashlightMode() => false; public bool OnDrawMesh(IMesh mesh, int firstIndex, int indexCount) => false; + public unsafe bool OnDrawMesh(IMesh mesh, PrimList* lists, int numLists) => false; + public bool OnSetFlexMesh(IMesh staticMesh, IMesh? mesh, int vertexOffsetInBytes) => false; + public bool OnSetColorMesh(IMesh staticMesh, IMesh? mesh, int vertexOffsetInBytes) => false; public bool OnSetPrimitiveType(IMesh mesh, MaterialPrimitiveType type) => false; public bool OnFlushBufferedPrimitives() => false; public void SyncMatrices() { } diff --git a/Source.MaterialSystem/Material.cs b/Source.MaterialSystem/Material.cs index 0b2a3e67b..dff33b0d4 100644 --- a/Source.MaterialSystem/Material.cs +++ b/Source.MaterialSystem/Material.cs @@ -103,16 +103,22 @@ public void Precache() { if (IsPrecached()) return; - if (!PrecacheVars()) - return; + MaterialLock hMaterialLock = materials.Lock(); + try { + if (!PrecacheVars()) + return; - flags |= MaterialFlags.IsPrecached; - if (Shader != null) - materials.ShaderSystem.InitShaderInstance(Shader, ShaderParams, GetName(), GetTextureGroupName()); + flags |= MaterialFlags.IsPrecached; + if (Shader != null) + materials.ShaderSystem.InitShaderInstance(Shader, ShaderParams, GetName(), GetTextureGroupName()); - RecomputeStateSnapshots(); - FindRepresentativeTexture(); - PrecacheMappingDimensions(); + RecomputeStateSnapshots(); + FindRepresentativeTexture(); + PrecacheMappingDimensions(); + } + finally { + materials.Unlock(hMaterialLock); + } } public void ColorModulate(float r, float g, float b) { @@ -146,6 +152,12 @@ public float GetAlphaModulation() { } public void RecomputeStateSnapshots() { + MatCallQueue? callQueue = materials.GetRenderCallQueue(); + if (callQueue != null) { + callQueue.QueueCall(static material => material.RecomputeStateSnapshots(), this); + return; + } + bool ok = InitializeRenderState(); if (!ok) SetupErrorShader(); @@ -1129,6 +1141,8 @@ public void RefreshPreservingMaterialVars() { } public void Uncache(bool preserveVars = false) { + MaterialLock hMaterialLock = materials.Lock(); + if (IsPrecached()) { CleanUpStateSnapshots(); flags &= ~MaterialFlags.ValidRenderState; @@ -1145,6 +1159,8 @@ public void Uncache(bool preserveVars = false) { flags &= ~MaterialFlags.VarsIsPrecached; } } + + materials.Unlock(hMaterialLock); } private void CleanUpMaterialProxy() { @@ -1326,9 +1342,16 @@ public bool HasProxy() { public void CallBindProxy(object? proxydata) { if (ProxyInfo.Count != 0) { + bool threaded = materials.GetRenderCallQueue() != null; + if (threaded) + MaterialVar.EnableThreadedMaterialVarAccess(true); + foreach (var proxy in ProxyInfo) { proxy.OnBind(proxydata); } + + if (threaded) + MaterialVar.EnableThreadedMaterialVarAccess(false); } } @@ -1344,6 +1367,12 @@ public void SetMaterialVarFlags(MaterialVarFlags flags, bool on) { } public void SetMaterialVarFlag(MaterialVarFlags flag, bool on) { + MatCallQueue? callQueue = materials.GetRenderCallQueue(); + if (callQueue != null) { + callQueue.QueueCall(static s => s.Material.SetMaterialVarFlag(s.Flag, s.On), (Material: this, Flag: flag, On: on)); + return; + } + bool oldOn = (GetMaterialVarFlags() & flag) != 0; if (oldOn != on) { SetMaterialVarFlags(flag, on); diff --git a/Source.MaterialSystem/MaterialSystem.cs b/Source.MaterialSystem/MaterialSystem.cs index e6d929fd9..bb9db95ef 100644 --- a/Source.MaterialSystem/MaterialSystem.cs +++ b/Source.MaterialSystem/MaterialSystem.cs @@ -126,6 +126,8 @@ public MaterialSystem(IServiceProvider services) { Config = services.GetRequiredService()!; CommandLine = services.GetRequiredService()!; HardwareRenderContext = new(this); + QueuedRenderContexts = [new(this), new(this)]; + ExecuteQueuedContextJobs = [new(MatQueueJob.JobKind.ExecuteQueuedContext, QueuedRenderContexts[0]), new(MatQueueJob.JobKind.ExecuteQueuedContext, QueuedRenderContexts[1])]; // Link up ShaderDevice.PreInit(this, services); @@ -300,7 +302,7 @@ public bool OverrideConfig(MaterialSystem_Config config, bool forceUpdate) { if (resetAnisotropy || recomputeSnapshots || redownloadLightmaps || redownloadTextures || resetAnisotropy || videoModeChange || setStandardVertexShaderConstants || resetTextureFilter) { - // ForceSingleThreaded(); + ForceSingleThreaded(); } if (reloadMaterials) @@ -338,8 +340,8 @@ public bool OverrideConfig(MaterialSystem_Config config, bool forceUpdate) { TextureSystem.RestoreRenderTargets(); } - // if (videoModeChange) - // ForceSingleThreaded(); + if (videoModeChange) + ForceSingleThreaded(); return redownloadLightmaps; } @@ -463,10 +465,208 @@ public void EndFrame() { GetRenderContextInternal().EndFrame(); + int convarThreadMode = mat_queue_mode.GetInt(); + if (convarThreadMode == -2) + convarThreadMode = (int)MaterialThreadMode.QueuedThreaded; + + if (convarThreadMode == (int)MaterialThreadMode.QueuedSingleThreaded) + convarThreadMode = (int)MaterialThreadMode.SingleThreaded; + + MaterialThreadMode nextThreadMode = (convarThreadMode >= 0) ? (MaterialThreadMode)convarThreadMode : IdealThreadMode; + bool deviceReady = ShaderAPI.CanDownloadTextures(); + if (!deviceReady || !AllowQueuedRendering) + nextThreadMode = MaterialThreadMode.SingleThreaded; + + if (ForcedSingleThreaded) { + nextThreadMode = MaterialThreadMode.SingleThreaded; + ForcedSingleThreaded = false; + } + + switch (ThreadMode) { + case MaterialThreadMode.SingleThreaded: + break; + + case MaterialThreadMode.QueuedThreaded: { + if (!ThreadHasOwnership) + ThreadAcquire(true); + + if (ActiveAsyncJob != null && !ActiveAsyncJob.IsFinished()) + ActiveAsyncJob.WaitForFinish(); + ActiveAsyncJob = null; + + MatQueuedRenderContext prevContext = QueuedRenderContexts[CurQueuedContext]; + + CurQueuedContext = (CurQueuedContext + 1) % QueuedRenderContexts.Length; + QueuedRenderContexts[CurQueuedContext].BeginQueue(prevContext); + CurrentThreadRenderContext = QueuedRenderContexts[CurQueuedContext]; + + ActiveAsyncJob = ExecuteQueuedContextJobs[prevContext == QueuedRenderContexts[0] ? 0 : 1]; + ActiveAsyncJob.Reset(); + MatQueueThread.AddJob(ActiveAsyncJob); + break; + } + } + + if (!deviceReady && nextThreadMode != MaterialThreadMode.SingleThreaded) + nextThreadMode = MaterialThreadMode.SingleThreaded; + + if (ThreadMode != nextThreadMode) { + switch (ThreadMode) { + case MaterialThreadMode.SingleThreaded: + break; + + case MaterialThreadMode.QueuedThreaded: { + if (ActiveAsyncJob != null) { + ActiveAsyncJob.WaitForFinish(); + ActiveAsyncJob = null; + } + + HardwareRenderContext.InitializeFrom(QueuedRenderContexts[CurQueuedContext]); + CurrentThreadRenderContext = HardwareRenderContext; + QueuedRenderContexts[CurQueuedContext].EndQueue(true); + ThreadRelease(); + } + break; + } + + ThreadMode = nextThreadMode; + + switch (ThreadMode) { + case MaterialThreadMode.SingleThreaded: + CurrentThreadRenderContext = HardwareRenderContext; + for (int i = 0; i < QueuedRenderContexts.Length; i++) + QueuedRenderContexts[i].EndQueue(true); + break; + + case MaterialThreadMode.QueuedThreaded: { + for (int i = 0; i < QueuedRenderContexts.Length; i++) { + if (!QueuedRenderContexts[i].IsInitialized()) + QueuedRenderContexts[i].Init(HardwareRenderContext); + } + + CurQueuedContext = 0; + QueuedRenderContexts[CurQueuedContext].BeginQueue(HardwareRenderContext); + CurrentThreadRenderContext = QueuedRenderContexts[CurQueuedContext]; + + ShaderAPI.ReleaseThreadOwnership(); + MatQueueThread.AddJob(new MatQueueJob(MatQueueJob.JobKind.AcquireThreadOwnership)); + + ThreadHasOwnership = true; + ThreadOwnershipID = ThreadGetCurrentId(); + } + break; + } + } + Assert(InFrame); InFrame = false; } + static readonly ConVar mat_queue_mode = new("mat_queue_mode", "-1", FCvar.Archive, "The queue/thread mode the material system should use: -1=default, 0=synchronous single thread, 2=queued multithreaded"); + static readonly ConVar mat_queue_report = new("mat_queue_report", "0", FCvar.Archive, "Report thread stalls. Positive number will filter by stalls >= time in ms. -1 reports all locks."); + MaterialThreadMode IdealThreadMode = MaterialThreadMode.SingleThreaded; + int ServiceThread = -1; + bool AllowQueuedRendering; + bool ForcedSingleThreaded; + bool ThreadHasOwnership; + int ThreadOwnershipID; + + readonly object MatSysMutex = new(); + readonly MatQueuedRenderContext[] QueuedRenderContexts; + readonly MatQueueJob[] ExecuteQueuedContextJobs; + int CurQueuedContext; + MatQueueJob? ActiveAsyncJob; + MatQueueThread? matQueueThread; + MatQueueThread MatQueueThread => matQueueThread ??= new(this); + + public void ForceSingleThreaded() { + if (!ThreadInMainThread()) + Error("Can't force single thread from within thread!\n"); + + if (GetThreadMode() != MaterialThreadMode.SingleThreaded) { + if (ActiveAsyncJob != null && !ActiveAsyncJob.IsFinished()) + ActiveAsyncJob.WaitForFinish(); + ActiveAsyncJob = null; + + ThreadRelease(); + + HardwareRenderContext.InitializeFrom(QueuedRenderContexts[CurQueuedContext]); + CurrentThreadRenderContext = HardwareRenderContext; + for (int i = 0; i < QueuedRenderContexts.Length; i++) + QueuedRenderContexts[i].EndQueue(true); + + ThreadMode = MaterialThreadMode.SingleThreaded; + ForcedSingleThreaded = true; + } + } + + internal void ExecuteJob(MatQueueJob job) { + switch (job.Kind) { + case MatQueueJob.JobKind.AcquireThreadOwnership: + ShaderAPI.AcquireThreadOwnership(); + break; + case MatQueueJob.JobKind.ReleaseThreadOwnership: + ShaderAPI.ReleaseThreadOwnership(); + break; + case MatQueueJob.JobKind.ExecuteQueuedContext: + ThreadExecuteQueuedContext(job.Context!); + break; + } + } + + void ThreadExecuteQueuedContext(MatQueuedRenderContext context) { + RenderThreadID = (uint)ThreadGetCurrentId(); + IMatRenderContextInternal? savedRenderContext = CurrentThreadRenderContext; + CurrentThreadRenderContext = HardwareRenderContext; + context.EndQueue(true); + CurrentThreadRenderContext = savedRenderContext; + RenderThreadID = 0xFFFFFFFF; + } + + void ThreadRelease() { + if (!ThreadHasOwnership) + return; + + double startTime = mat_queue_report.GetInt() != 0 ? Platform.Time : 0; + + MatQueueJob job = new(MatQueueJob.JobKind.ReleaseThreadOwnership); + MatQueueThread.AddJob(job); + job.WaitForFinish(); + + double endThreadRelease = mat_queue_report.GetInt() != 0 ? Platform.Time : 0; + + ShaderAPI.AcquireThreadOwnership(); + + ThreadHasOwnership = false; + ThreadOwnershipID = 0; + + if (mat_queue_report.GetInt() != 0) { + double endTime = Platform.Time; + double result = (endTime - startTime) * 1000.0; + if (mat_queue_report.GetInt() == -1 || result > mat_queue_report.GetFloat()) + ConColorMsg(new Color(200, 20, 20, 255), $"CMaterialSystem::ThreadRelease: {result:0.00}ms = Release:{(endThreadRelease - startTime) * 1000.0:0.00}ms + Acquire:{(endTime - endThreadRelease) * 1000.0:0.00}ms\n"); + } + } + + void ThreadAcquire(bool force) { + if (!force) + return; + + double startTime = mat_queue_report.GetInt() != 0 ? Platform.Time : 0; + + ShaderAPI.ReleaseThreadOwnership(); + MatQueueThread.AddJob(new MatQueueJob(MatQueueJob.JobKind.AcquireThreadOwnership)); + + ThreadHasOwnership = true; + ThreadOwnershipID = ThreadGetCurrentId(); + + if (mat_queue_report.GetInt() != 0) { + double result = (Platform.Time - startTime) * 1000.0; + if (mat_queue_report.GetInt() == -1 || result > mat_queue_report.GetFloat()) + ConColorMsg(new Color(200, 20, 20, 255), $"CMaterialSystem::ThreadAcquire: {result:0.00}ms\n"); + } + } + ulong FrameNum; public void SwapBuffers() { @@ -476,15 +676,14 @@ public void SwapBuffers() { public MaterialSystem_Config GetCurrentConfigForVideoCard() => Config; - readonly ThreadLocal RenderContext = new(); + readonly IMatRenderContextInternal?[] RenderContexts = new IMatRenderContextInternal?[2]; readonly MatRenderContext HardwareRenderContext; + ref IMatRenderContextInternal? CurrentThreadRenderContext => ref RenderContexts[ThreadInMainThread() ? 1 : 0]; + public IMatRenderContext GetRenderContext() { - IMatRenderContext? result = RenderContext.Value; - if (result == null) { - result = HardwareRenderContext; - RenderContext.Value = HardwareRenderContext; - } + ref IMatRenderContextInternal? result = ref CurrentThreadRenderContext; + result ??= HardwareRenderContext; return result; } @@ -753,9 +952,11 @@ public IMaterialInternal CreateMaterial(ReadOnlySpan materialName, KeyValu => CreateMaterial(materialName, MaterialDefines.TEXTURE_GROUP_OTHER, keyValues); public IMaterialInternal CreateMaterial(ReadOnlySpan materialName, ReadOnlySpan textureGroup, KeyValues? keyValues) { IMaterialInternal material; + MaterialLock hMaterialLock = Lock(); lock (this) { material = new Material(this, materialName, textureGroup, keyValues); } + Unlock(hMaterialLock); MaterialDict.AddMaterialToMaterialList(material); return material; @@ -780,7 +981,7 @@ public void RemoveMaterialSubRect(IMaterialInternal? material) { public void ForceDepthFuncEquals(bool enable) => GetRenderContextInternal().ForceDepthFuncEquals(enable); public MaterialHeightClipMode GetHeightClipMode() => GetRenderContextInternal().GetHeightClipMode(); - public MatCallQueue GetRenderCallQueue() => GetRenderContextInternal().GetCallQueueInternal(); + public MatCallQueue? GetRenderCallQueue() => CurrentThreadRenderContext?.GetCallQueueInternal(); public void UnbindMaterial(IMaterial? material) { Assert(material == null || ((IMaterialInternal)material).IsRealTimeVersion()); @@ -805,7 +1006,7 @@ public bool OnDrawMesh(IMesh mesh, int firstIndex, int indexCount) { } public IMatRenderContextInternal GetRenderContextInternal() { - IMatRenderContextInternal? renderContext = RenderContext.Value; + IMatRenderContextInternal? renderContext = CurrentThreadRenderContext; return renderContext ?? HardwareRenderContext; } @@ -817,10 +1018,18 @@ public bool OnSetPrimitiveType(IMesh mesh, MaterialPrimitiveType type) { return GetRenderContextInternal().OnSetPrimitiveType(mesh, type); } - public bool OnFlushBufferedPrimitives() { - throw new NotImplementedException(); + public bool OnFlushBufferedPrimitives() => GetRenderContextInternal().OnFlushBufferedPrimitives(); + + public unsafe bool OnDrawMesh(IMesh mesh, PrimList* lists, int numLists) { + if (IsInStubMode()) + return false; + + return GetRenderContextInternal().OnDrawMesh(mesh, lists, numLists); } + public bool OnSetFlexMesh(IMesh staticMesh, IMesh? mesh, int vertexOffsetInBytes) => GetRenderContextInternal().OnSetFlexMesh(staticMesh, mesh, vertexOffsetInBytes); + public bool OnSetColorMesh(IMesh staticMesh, IMesh? mesh, int vertexOffsetInBytes) => GetRenderContextInternal().OnSetColorMesh(staticMesh, mesh, vertexOffsetInBytes); + public void SyncMatrices() => GetRenderContextInternal().SyncMatrices(); public void SyncMatrix(MaterialMatrixMode mode) => GetRenderContextInternal().SyncMatrix(mode); @@ -1394,7 +1603,30 @@ public int AllocateDynamicLightmap(Span lightmapSize, Span outOffsetIn } public bool AllowThreading(bool allow, int serviceThread) { - throw new NotImplementedException("Incomplete port of IMaterialSystem"); + if (CommandLine.ParmValue("-threads", 2) < 2) + allow = false; + + bool oldAllow = AllowQueuedRendering; + + if (Environment.ProcessorCount >= 2) { + AllowQueuedRendering = allow; + bool queued = IdealThreadMode != MaterialThreadMode.SingleThreaded; + if (allow && !queued) { + DevMsg("Queued Material System: ENABLED!\n"); + SetThreadMode(MaterialThreadMode.QueuedThreaded, serviceThread); + } + else if (!allow && queued) { + DevMsg("Queued Material System: DISABLED!\n"); + ForceSingleThreaded(); + MaterialLock hMaterialLock = Lock(); + SetThreadMode(MaterialThreadMode.SingleThreaded, -1); + Unlock(hMaterialLock); + } + } + else + AllowQueuedRendering = false; + + return oldAllow; } public void AsyncFindTexture(ReadOnlySpan pFilename, ReadOnlySpan textureGroupName, IAsyncTextureOperationReceiver recipient, ref T extraArgs, bool complain = true, CreateTextureFlags additionalCreationFlags = 0) { @@ -1414,7 +1646,8 @@ public void ClearBuffers(bool clearColor, bool clearDepth, bool clearStencil = f } public void CompactMemory() { - throw new NotImplementedException("Incomplete port of IMaterialSystem"); + for (int i = 0; i < QueuedRenderContexts.Length; i++) + QueuedRenderContexts[i].CompactMemory(); } public ITexture? CreateNamedRenderTargetTexture(ReadOnlySpan rtName, int w, int h, RenderTargetSizeMode sizeMode, ImageFormat format, MaterialRenderTargetDepth depth = MaterialRenderTargetDepth.Shared, bool clampTexCoords = true, bool autoMipMap = false) { @@ -1461,24 +1694,26 @@ public void EvictManagedResources() { throw new NotImplementedException("Incomplete port of IMaterialSystem"); } - public void ExecuteQueued() { - throw new NotImplementedException("Incomplete port of IMaterialSystem"); - } + public void ExecuteQueued() { } public MaterialHandle_t FirstMaterial() { throw new NotImplementedException("Incomplete port of IMaterialSystem"); } - public void Flush(bool flushHardware = false) { - throw new NotImplementedException("Incomplete port of IMaterialSystem"); - } + public void Flush(bool flushHardware = false) => GetRenderContextInternal().Flush(flushHardware); public void GMOD_ClearMissing(bool unknown) { throw new NotImplementedException("Incomplete port of IMaterialSystem"); } public void GMOD_FlushQueue() { - throw new NotImplementedException("Incomplete port of IMaterialSystem"); + IMatRenderContextInternal renderContext = GetRenderContextInternal(); + if (ThreadMode != MaterialThreadMode.SingleThreaded && renderContext.GetCallQueueInternal() != null && renderContext.GMOD_IsLowOnMemory()) { + DevWarning("Render queue too large, flushing! (This is super slow you shouldn't render so much stuff.)\n"); + ActiveAsyncJob?.WaitForFinish(); + ActiveAsyncJob = null; + renderContext.GMOD_FlushQueue(); + } } public IMaterial? GMOD_GetErrorMaterial() { @@ -1569,11 +1804,33 @@ public bool IsMaterialLoaded(ReadOnlySpan materialName) { } public bool IsRenderThreadSafe() { - throw new NotImplementedException("Incomplete port of IMaterialSystem"); + return (ThreadMode != MaterialThreadMode.QueuedThreaded && ThreadInMainThread()) || + (ThreadMode == MaterialThreadMode.QueuedThreaded && RenderThreadID == (uint)ThreadGetCurrentId()); } + [ThreadStatic] static List? LockedContexts; + public MaterialLock Lock() { - throw new NotImplementedException("Incomplete port of IMaterialSystem"); + IMatRenderContextInternal curContext = GetRenderContextInternal(); + + if (curContext != HardwareRenderContext && ActiveAsyncJob != null) { + ActiveAsyncJob.WaitForFinish(); + ActiveAsyncJob = null; + } + + Monitor.Enter(MatSysMutex); + + LockedContexts ??= []; + LockedContexts.Add(curContext); + MaterialLock hMaterialLock = new(LockedContexts.Count); + CurrentThreadRenderContext = HardwareRenderContext; + + if (ThreadMode != MaterialThreadMode.SingleThreaded) { + if (curContext.GetCallQueueInternal() != null) + ThreadRelease(); + } + + return hMaterialLock; } public ITextureCompositor? NewTextureCompositor(int w, int h, ReadOnlySpan compositeName, int teamNum, ulong randomSeed, KeyValues stageDesc, CreateTextureFlags texCompositeCreateFlags = 0) { @@ -1634,7 +1891,8 @@ public void SetShaderAPI(IServiceProvider shaderAPIFactory) { } public void SetThreadMode(MaterialThreadMode mode, int nServiceThread = -1) { - throw new NotImplementedException("Incomplete port of IMaterialSystem"); + IdealThreadMode = mode; + ServiceThread = nServiceThread; } public void SetView(IWindow hwnd) { @@ -1690,7 +1948,11 @@ public void UncacheUnusedMaterials(bool bRecomputeStateSnapshots = false) { } public void Unlock(MaterialLock l) { - throw new NotImplementedException("Incomplete port of IMaterialSystem"); + Assert(LockedContexts != null && LockedContexts.Count == (int)l.Value); + IMatRenderContextInternal renderContext = LockedContexts![^1]; + LockedContexts.RemoveAt(LockedContexts.Count - 1); + CurrentThreadRenderContext = renderContext; + Monitor.Exit(MatSysMutex); } public void UpdateExcludedTextures() { diff --git a/Source.MaterialSystem/MaterialVar.cs b/Source.MaterialSystem/MaterialVar.cs index d675c5ebc..1cb7e1f72 100644 --- a/Source.MaterialSystem/MaterialVar.cs +++ b/Source.MaterialSystem/MaterialVar.cs @@ -9,24 +9,77 @@ namespace Source.MaterialSystem; public sealed class MaterialVar : IMaterialVar { IMaterialInternal owningMaterial; + MaterialSystem? materials; Matrix4x4 MatrixVal = Matrix4x4.Identity; void VarChanged() => owningMaterial?.ReportVarChanged(this); void Init() { + TempIndex = NO_TEMP_INDEX; + } + + const byte NO_TEMP_INDEX = 0xFF; + const int MAX_TEMP_VARS = 254; + static readonly MaterialVar[] TempMaterialVars = CreateTempMaterialVars(); + static readonly List TempIndexOwners = []; + static bool EnableThreadedAccess; + static int TempVarsUsed; + + static MaterialVar[] CreateTempMaterialVars() { + MaterialVar[] vars = new MaterialVar[MAX_TEMP_VARS]; + for (int i = 0; i < vars.Length; i++) + vars[i] = new MaterialVar(null!, "") { FakeMaterialVar = true }; + return vars; + } + + public static void EnableThreadedMaterialVarAccess(bool enable) { + if (EnableThreadedAccess == enable) + return; + + EnableThreadedAccess = enable; + if (!EnableThreadedAccess) { + for (int i = 0; i < TempVarsUsed; ++i) + TempMaterialVars[i].SetUndefined(); + for (int i = 0; i < TempIndexOwners.Count; ++i) + TempIndexOwners[i].TempIndex = NO_TEMP_INDEX; + TempIndexOwners.Clear(); + TempVarsUsed = 0; + } + } + + MatCallQueue? GetCallQueue() => FakeMaterialVar ? null : materials?.GetRenderCallQueue(); + + MaterialVar? GetThreadedTempVar() { + if (TempIndex == NO_TEMP_INDEX || GetCallQueue() == null) + return null; + return TempMaterialVars[TempIndex]; + } + + MaterialVar? AllocThreadedTempVar() { + if (!EnableThreadedAccess) + return null; + if (TempIndex == NO_TEMP_INDEX) { + if (TempVarsUsed >= MAX_TEMP_VARS) + return null; + TempIndex = (byte)TempVarsUsed++; + TempIndexOwners.Add(this); + } + return TempMaterialVars[TempIndex]; } public MaterialVar(IMaterial material, ReadOnlySpan key) { Init(); owningMaterial = (IMaterialInternal)material!; + materials = (material as Material)?.materials; Name = new(GetSymbol(key)); Type = MaterialVarType.Undefined; } public MaterialVar(IMaterial material, ReadOnlySpan key, int val) { Init(); owningMaterial = (IMaterialInternal)material!; + materials = (material as Material)?.materials; Name = new(GetSymbol(key)); Type = MaterialVarType.Int; VecVal[0] = VecVal[1] = VecVal[2] = VecVal[3] = (float)val; @@ -35,6 +88,7 @@ public MaterialVar(IMaterial material, ReadOnlySpan key, int val) { public MaterialVar(IMaterial material, ReadOnlySpan key, float val) { Init(); owningMaterial = (IMaterialInternal)material!; + materials = (material as Material)?.materials; Name = new(GetSymbol(key)); Type = MaterialVarType.Float; VecVal[0] = VecVal[1] = VecVal[2] = VecVal[3] = val; @@ -43,6 +97,7 @@ public MaterialVar(IMaterial material, ReadOnlySpan key, float val) { public MaterialVar(IMaterial material, ReadOnlySpan key, Span val) { Init(); owningMaterial = (IMaterialInternal)material!; + materials = (material as Material)?.materials; Name = new(GetSymbol(key)); Type = MaterialVarType.Vector; NumVectorComps = (byte)Math.Min(val.Length, 4); @@ -54,6 +109,7 @@ public MaterialVar(IMaterial material, ReadOnlySpan key, Span val) public MaterialVar(IMaterial material, ReadOnlySpan key, ReadOnlySpan val) { Init(); owningMaterial = (IMaterialInternal)material!; + materials = (material as Material)?.materials; Name = new(GetSymbol(key)); StringVal = new(val); Type = MaterialVarType.String; @@ -94,6 +150,9 @@ public override IMaterial GetOwningMaterial() { } public override string GetStringValue() { + MaterialVar? temp = GetThreadedTempVar(); + if (temp != null) + return temp.GetStringValue(); return StringVal; } @@ -121,6 +180,12 @@ public override string GetStringValue() { } public override unsafe void GetVecValue(Span val) { + MaterialVar? temp = GetThreadedTempVar(); + if (temp != null) { + temp.GetVecValue(val); + return; + } + fixed (Vector4* v4 = &VecVal) new Span(v4, val.Length > 4 ? 4 : val.Length).CopyTo(val); } @@ -143,6 +208,13 @@ void FlushIfCurrentMaterial() { } public override void SetFloatValue(float val) { + MatCallQueue? callQueue = GetCallQueue(); + if (callQueue != null) { + AllocThreadedTempVar()?.SetFloatValue(val); + callQueue.QueueCall(static s => s.Var.SetFloatValue(s.Val), (Var: this, Val: val)); + return; + } + // Suppress all this if we're not actually changing anything if (Type == MaterialVarType.Float && VecVal[0] == val) return; @@ -160,6 +232,13 @@ public override void SetFourCCValue(ulong type, object? data) { } public override void SetIntValue(int val) { + MatCallQueue? callQueue = GetCallQueue(); + if (callQueue != null) { + AllocThreadedTempVar()?.SetIntValue(val); + callQueue.QueueCall(static s => s.Var.SetIntValue(s.Val), (Var: this, Val: val)); + return; + } + // Suppress all this if we're not actually changing anything if (Type == MaterialVarType.Int && IntVal == val) return; @@ -177,6 +256,13 @@ public override void SetMaterialValue(IMaterial? material) { } public override void SetMatrixValue(in Matrix4x4 matrix) { + MatCallQueue? callQueue = GetCallQueue(); + if (callQueue != null) { + AllocThreadedTempVar()?.SetMatrixValue(in matrix); + callQueue.QueueCall(static s => s.Var.SetMatrixValue(in s.Val), (Var: this, Val: matrix)); + return; + } + FlushIfCurrentMaterial(); Matrix.Matrix = matrix; @@ -188,6 +274,14 @@ public override void SetMatrixValue(in Matrix4x4 matrix) { } public override void SetStringValue(ReadOnlySpan val) { + MatCallQueue? callQueue = GetCallQueue(); + if (callQueue != null) { + string copy = new(val.SliceNullTerminatedString()); + AllocThreadedTempVar()?.SetStringValue(copy); + callQueue.QueueCall(static s => s.Var.SetStringValue(s.Val), (Var: this, Val: copy)); + return; + } + FlushIfCurrentMaterial(); StringVal = new(val.SliceNullTerminatedString()); @@ -196,6 +290,13 @@ public override void SetStringValue(ReadOnlySpan val) { } public override void SetTextureValue(ITexture? texture) { + MatCallQueue? callQueue = GetCallQueue(); + if (callQueue != null) { + AllocThreadedTempVar()?.SetTextureValue(texture); + callQueue.QueueCall(static s => s.Var.SetTextureValue(s.Val), (Var: this, Val: texture)); + return; + } + // Suppress all this if we're not actually changing anything if (Type == MaterialVarType.Texture && TextureValue == texture) return; @@ -208,6 +309,13 @@ public override void SetTextureValue(ITexture? texture) { } public override void SetUndefined() { + MatCallQueue? callQueue = GetCallQueue(); + if (callQueue != null) { + AllocThreadedTempVar()?.SetUndefined(); + callQueue.QueueCall(static var => var.SetUndefined(), this); + return; + } + if (Type == MaterialVarType.Undefined) return; @@ -222,6 +330,13 @@ public override void SetValueAutodetectType(ReadOnlySpan val) { } public override void SetVecComponentValue(float val, int component) { + MatCallQueue? callQueue = GetCallQueue(); + if (callQueue != null) { + AllocThreadedTempVar()?.SetVecComponentValue(val, component); + callQueue.QueueCall(static s => s.Var.SetVecComponentValue(s.Val, s.Component), (Var: this, Val: val, Component: component)); + return; + } + if (Type == MaterialVarType.Vector && VecVal[component] == val) return; @@ -243,6 +358,13 @@ public override void SetVecComponentValue(float val, int component) { } void SetVecValueInternal(in Vector4 vec, int comps) { + MatCallQueue? callQueue = GetCallQueue(); + if (callQueue != null) { + AllocThreadedTempVar()?.SetVecValueInternal(in vec, comps); + callQueue.QueueCall(static s => s.Var.SetVecValueInternal(in s.Val, s.Comps), (Var: this, Val: vec, Comps: comps)); + return; + } + // Suppress all this if we're not actually changing anything if (Type == MaterialVarType.Vector && VecVal == vec) return; @@ -270,11 +392,13 @@ public override void SetVecValue(ReadOnlySpan val) { public override void SetVecValue(float x, float y, float z, float w) => SetVecValueInternal(new(x, y, z, w), 4); protected override float GetFloatValueInternal() { - throw new NotImplementedException(); + MaterialVar? temp = GetThreadedTempVar(); + return temp != null ? temp.VecVal.X : VecVal.X; } protected override int GetIntValueInternal() { - throw new NotImplementedException(); + MaterialVar? temp = GetThreadedTempVar(); + return temp != null ? temp.IntVal : IntVal; } protected override Span GetVecValueInternal() { @@ -286,8 +410,14 @@ protected override void GetVecValueInternal(Span val) { } protected override int VectorSizeInternal() { - return NumVectorComps; + MaterialVar? temp = GetThreadedTempVar(); + return temp != null ? temp.NumVectorComps : NumVectorComps; } - public override Span GetVecValue() => new Span(ref VecVal).Cast(); + public override Span GetVecValue() { + MaterialVar? temp = GetThreadedTempVar(); + if (temp != null) + return temp.GetVecValue(); + return new Span(ref VecVal).Cast(); + } } diff --git a/Source.MaterialSystem/Texture.cs b/Source.MaterialSystem/Texture.cs index 04c8a2450..80987b0fe 100644 --- a/Source.MaterialSystem/Texture.cs +++ b/Source.MaterialSystem/Texture.cs @@ -301,8 +301,10 @@ public void Bind(Sampler sampler, int frame) { public void Download(Rectangle rect, int additionalCreationFlags) { if (materials.ShaderAPI.CanDownloadTextures()) { + MaterialLock hLock = materials.Lock(); Flags |= (uint)additionalCreationFlags; DownloadTexture(rect); + materials.Unlock(hLock); } } diff --git a/Source.SDLManager/SDL3_LauncherManager.cs b/Source.SDLManager/SDL3_LauncherManager.cs index 42f682c0c..d7fc38aed 100644 --- a/Source.SDLManager/SDL3_LauncherManager.cs +++ b/Source.SDLManager/SDL3_LauncherManager.cs @@ -76,6 +76,11 @@ public void MakeCurrent() { Warning($"Could not make GL context current: {SDL3.SDL_GetError()}\n"); } + public void ReleaseCurrent() { + if (!SDL3.SDL_GL_MakeCurrent((SDL_Window*)window, null)) + Warning($"Could not release GL context: {SDL3.SDL_GetError()}\n"); + } + public void SetSwapInterval(float swapInterval) { SDL3.SDL_GL_MakeCurrent((SDL_Window*)window, (SDL_GLContextState*)ctx); if (!SDL3.SDL_GL_SetSwapInterval((int)swapInterval)) { diff --git a/Source.ShaderAPI.Gl46/MeshGl46.cs b/Source.ShaderAPI.Gl46/MeshGl46.cs index bd2059631..89578a931 100644 --- a/Source.ShaderAPI.Gl46/MeshGl46.cs +++ b/Source.ShaderAPI.Gl46/MeshGl46.cs @@ -121,13 +121,14 @@ public virtual void Draw(ReadOnlySpan lis if (VertexBuffer == null) return; - if (!ShaderUtil.OnDrawMesh(this, -1, 0)) { - MarkAsDrawn(); - return; - } + fixed (Source.Common.MaterialSystem.PrimList* lp = lists) { + if (!ShaderUtil.OnDrawMesh(this, lp, numLists)) { + MarkAsDrawn(); + return; + } - fixed (PrimList* p = MemoryMarshal.Cast(lists)) - DrawInternal(p, numLists); + DrawInternal((PrimList*)lp, numLists); + } } private unsafe void DrawInternal(PrimList* primList, int lists) { @@ -289,6 +290,9 @@ public virtual void ModifyEnd(ref MeshDesc desc) { int ColorMeshVertOffsetInBytes = 0; public virtual void SetColorMesh(IMesh colorMesh, int vertexOffset) { + if (!ShaderUtil.OnSetColorMesh(this, colorMesh, vertexOffset)) + return; + ColorMesh = colorMesh; ColorMeshVertOffsetInBytes = vertexOffset; } @@ -446,6 +450,9 @@ internal bool HasFlexMesh() { bool HasFlexVerts; public virtual void SetFlexMesh(IMesh? mesh, int vertexOffsetInBytes) { + if (!ShaderUtil.OnSetFlexMesh(this, mesh, vertexOffsetInBytes)) + return; + FlexVertOffsetInBytes = vertexOffsetInBytes; if (mesh != null) { diff --git a/Source.ShaderAPI.Gl46/ShaderAPIGl46.cs b/Source.ShaderAPI.Gl46/ShaderAPIGl46.cs index dca5a8f11..7d081558c 100644 --- a/Source.ShaderAPI.Gl46/ShaderAPIGl46.cs +++ b/Source.ShaderAPI.Gl46/ShaderAPIGl46.cs @@ -1057,7 +1057,12 @@ private bool MatrixIsChanging(TransformType type = TransformType.IsGeneral) { return true; } - public void FlushBufferedPrimitives() => FlushBufferedPrimitivesInternal(); + public void FlushBufferedPrimitives() { + if (ShaderUtil != null && !ShaderUtil.OnFlushBufferedPrimitives()) + return; + + FlushBufferedPrimitivesInternal(); + } private void FlushBufferedPrimitivesInternal() { Assert(RenderMesh == null); #if DEBUG @@ -1689,6 +1694,7 @@ public bool InitDevice(IWindow window, in ShaderDeviceInfo deviceInfo) { Driver = deviceInfo.Driver; Device!.MakeCurrent(); + OwnerThreadID = ThreadGetCurrentId(); unsafe { if (0 != (deviceInfo.Driver & GraphicsDriver.OpenGL)) GL_LoadExtensions(graphics.GL_LoadExtensionsPtr()); @@ -1726,13 +1732,6 @@ public void InvokeModeChangeCallbacks() { public void Present() { FlushBufferedPrimitives(); - bool validPresent = true; - bool inMainThread = ThreadInMainThread(); - - if (!inMainThread) { - throw new Exception(); - } - Device!.SwapBuffers(); MeshMgr.DiscardVertexBuffers(); } @@ -3315,8 +3314,15 @@ public float GetAmbientLightCubeLuminance() { return fLuminance / 6.0f; } + int OwnerThreadID; + internal bool IsOwnerThread() => OwnerThreadID == ThreadGetCurrentId(); + public void AcquireThreadOwnership() { - throw new NotImplementedException("Incomplete port of IShaderAPI"); + if (OwnerThreadID == ThreadGetCurrentId()) + return; + + Device?.MakeCurrent(); + OwnerThreadID = ThreadGetCurrentId(); } public void BeginOcclusionQueryDrawing(ShaderAPIOcclusionQuery_t query) { @@ -3356,8 +3362,9 @@ public void ClearVertexAndPixelShaderRefCounts() { throw new NotImplementedException("Incomplete port of IShaderAPI"); } - public void ComputeVertexDescription(Span buffer, VertexFormat vertexFormat, ref MeshDesc desc) { - throw new NotImplementedException("Incomplete port of IShaderAPI"); + public unsafe void ComputeVertexDescription(Span buffer, VertexFormat vertexFormat, ref MeshDesc desc) { + fixed (byte* memory = buffer) + VertexBufferGl46.ComputeVertexDescription(memory, vertexFormat, ref desc.Vertex); } public void CopyRenderTargetToTexture(ShaderAPITextureHandle_t textureHandle) { @@ -3429,9 +3436,7 @@ public void EnableHWMorphing(bool enable) { throw new NotImplementedException("Incomplete port of IShaderAPI"); } - public void EnableShaderShaderMutex(bool enable) { - throw new NotImplementedException("Incomplete port of IShaderAPI"); - } + public void EnableShaderShaderMutex(bool enable) { } public void EnableUserClipTransformOverride(bool enable) { throw new NotImplementedException("Incomplete port of IShaderAPI"); @@ -3647,7 +3652,11 @@ public void ReadPixels(ref System.Drawing.Rectangle srcRect, ref System.Drawing. } public void ReleaseThreadOwnership() { - throw new NotImplementedException("Incomplete port of IShaderAPI"); + if (OwnerThreadID != ThreadGetCurrentId()) + return; + + Device?.ReleaseCurrent(); + OwnerThreadID = 0; } public void SceneFogColor3ub(byte r, byte g, byte b) { @@ -3700,9 +3709,7 @@ public void SetClipPlane(int index, ReadOnlySpan plane) { } } - public void SetDisallowAccess(bool access) { - throw new NotImplementedException("Incomplete port of IShaderAPI"); - } + public void SetDisallowAccess(bool access) { } public void SetFastClipPlane(ReadOnlySpan plane) { throw new NotImplementedException("Incomplete port of IShaderAPI"); @@ -3756,13 +3763,9 @@ public void SetStandardVertexShaderConstants(float fOverbright) { throw new NotImplementedException("Incomplete port of IShaderAPI"); } - public void ShaderLock() { - throw new NotImplementedException("Incomplete port of IShaderAPI"); - } + public void ShaderLock() { } - public void ShaderUnlock() { - throw new NotImplementedException("Incomplete port of IShaderAPI"); - } + public void ShaderUnlock() { } public bool SupportsCSAAMode(int numSamples, int qualityLevel) { throw new NotImplementedException("Incomplete port of IShaderAPI"); diff --git a/Source.ShaderAPI.Gl46/ShaderShadow.cs b/Source.ShaderAPI.Gl46/ShaderShadow.cs index c52df59ae..09e71ab7b 100644 --- a/Source.ShaderAPI.Gl46/ShaderShadow.cs +++ b/Source.ShaderAPI.Gl46/ShaderShadow.cs @@ -115,7 +115,7 @@ private unsafe void CreateShaderObjects() { internal VertexFormat VertexFormat; public unsafe void Dispose() { - if (!ThreadInMainThread()) { + if (!ShaderAPI.IsOwnerThread()) { Warning("NOT IN MAIN THREAD - CANNOT DELETE UBO - GRAPHICS MEMORY LEAK\n"); return; } diff --git a/Source.StudioRender/StudioRenderContext.cs b/Source.StudioRender/StudioRenderContext.cs index 5b1d72da7..d7acc7017 100644 --- a/Source.StudioRender/StudioRenderContext.cs +++ b/Source.StudioRender/StudioRenderContext.cs @@ -185,12 +185,14 @@ public bool LoadModel(StudioHeader studioHdr, Memory vtxBuffer, StudioHWDa int nColorMeshID = 0; int nLodID; + MaterialLock hLock = materialSystem.Lock(); for (nLodID = studioHWData.RootLOD; nLodID < studioHWData.NumLODs; nLodID++) { LoadMaterials(studioHdr, vertexHdr, studioHWData.LODs[nLodID], nLodID); R_StudioCreateStaticMeshes(studioHdr, vertexHdr, studioHWData, nLodID, ref nColorMeshID); ComputeHWMorphDecalBoneRemap(studioHdr, vertexHdr, studioHWData, nLodID); studioHWData.LODs[nLodID].SwitchPoint = vertexHdr.BodyPart(0).Model(0).LOD(nLodID).SwitchPoint; } + materialSystem.Unlock(hLock); return true; } From 7b38900e55c38c3bfa020c8c699f69fd04eb3db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=B6eh=20Matt?= <5415177+ZehMatt@users.noreply.github.com> Date: Mon, 19 Oct 2026 04:58:27 +0300 Subject: [PATCH 5/8] Queue dynamic lightmap builds while rendering is queued --- Source.Engine/Render.cs | 36 +++++++++++++++++++++++++++ Source.MaterialSystem/MatLightmaps.cs | 12 +++++++++ 2 files changed, 48 insertions(+) diff --git a/Source.Engine/Render.cs b/Source.Engine/Render.cs index 34b0f3058..1d7af64ae 100644 --- a/Source.Engine/Render.cs +++ b/Source.Engine/Render.cs @@ -600,6 +600,25 @@ public void EndUpdateLightmaps() { if (--LightmapUpdateDepth == 0) { if (g_LightmapUpdateList.Count != 0 && r_dynamiclighting.GetBool() && !ModelLoader.r_unloadlightmaps.GetBool()) { DLight[] lights = CL.DLights; + if (CL.ActiveDlights && materials.GetRenderContext().GetCallQueue() != null) { + lights = ThreadDLights[ThreadDLightsFrame]; + for (int i = 0; i < lights.Length; i++) { + DLight src = CL.DLights[i], dst = lights[i]; + dst.Flags = src.Flags; + dst.Origin = src.Origin; + dst.Radius = src.Radius; + dst.Color = src.Color; + dst.Die = src.Die; + dst.Decay = src.Decay; + dst.MinLight = src.MinLight; + dst.Key = src.Key; + dst.Style = src.Style; + dst.Direction = src.Direction; + dst.InnerAngle = src.InnerAngle; + dst.OuterAngle = src.OuterAngle; + } + ThreadDLightsFrame = (ThreadDLightsFrame + 1) & 1; + } g_LightmapUpdateList.Sort(LightmapPageComparison ??= LightmapPageCompareFunc); for (int i = g_LightmapUpdateList.Count - 1; i >= 0; --i) { @@ -842,6 +861,23 @@ private void BuildLightMap(DLight[] lights, ref BSPMSurface2 surfID, in Matrix3x if (onlyUseLightStyles) dlightMask = 0; + ICallQueue? callQueue = materials.GetRenderContext().GetCallQueue(); + if (callQueue != null) { + Span surfaces = host_state.WorldBrush!.Surfaces2; + int surfaceIndex = (int)(Unsafe.ByteOffset(ref surfaces[0], ref surfID) / Unsafe.SizeOf()); + callQueue.QueueCall(static s => s.Render.BuildLightMapGutsQueued(s.Lights, s.SurfaceIndex, in s.XForm, s.Mask, s.Bumpmap, s.Lightmap), + (Render: this, Lights: lights, SurfaceIndex: surfaceIndex, XForm: entityToWorld, Mask: dlightMask, Bumpmap: needsBumpmap, Lightmap: needsLightmap)); + return; + } + + BuildLightMapGuts(lights, ref surfID, in entityToWorld, dlightMask, needsBumpmap, needsLightmap); + } + + static readonly DLight[][] ThreadDLights = [new DLight[Constants.MAX_DLIGHTS].InstantiateArray(), new DLight[Constants.MAX_DLIGHTS].InstantiateArray()]; + static int ThreadDLightsFrame; + + void BuildLightMapGutsQueued(DLight[] lights, int surfaceIndex, in Matrix3x4 entityToWorld, uint dlightMask, bool needsBumpmap, bool needsLightmap) { + ref BSPMSurface2 surfID = ref host_state.WorldBrush!.Surfaces2[surfaceIndex]; BuildLightMapGuts(lights, ref surfID, in entityToWorld, dlightMask, needsBumpmap, needsLightmap); } diff --git a/Source.MaterialSystem/MatLightmaps.cs b/Source.MaterialSystem/MatLightmaps.cs index 3b7fe7b83..fdb00f57f 100644 --- a/Source.MaterialSystem/MatLightmaps.cs +++ b/Source.MaterialSystem/MatLightmaps.cs @@ -410,10 +410,22 @@ public void EnableLightmapFiltering(bool enabled) { public MaterialSystem GetMaterialSystem() => materialSystem; public void BeginUpdateLightmaps() { + MatCallQueue? callQueue = materialSystem.GetRenderContextInternal().GetCallQueueInternal(); + if (callQueue != null) { + callQueue.QueueCall(static lightmaps => lightmaps.BeginUpdateLightmaps(), this); + return; + } + UpdatingLightmapsStackDepth++; } public void EndUpdateLightmaps() { + MatCallQueue? callQueue = materialSystem.GetRenderContextInternal().GetCallQueueInternal(); + if (callQueue != null) { + callQueue.QueueCall(static lightmaps => lightmaps.EndUpdateLightmaps(), this); + return; + } + UpdatingLightmapsStackDepth--; Assert(UpdatingLightmapsStackDepth >= 0); if (UpdatingLightmapsStackDepth <= 0 && LockedLightmap != -1) { From b60cee6045ca1edb591342a43f32fb4407e9d927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=B6eh=20Matt?= <5415177+ZehMatt@users.noreply.github.com> Date: Mon, 19 Oct 2026 06:08:28 +0300 Subject: [PATCH 6/8] Fix queued tone mapping by running it in the base render context --- .../MatQueuedRenderContext.cs | 10 +--- Source.MaterialSystem/MatRenderContext.cs | 57 +----------------- Source.MaterialSystem/MatRenderContextBase.cs | 60 +++++++++++++++++++ 3 files changed, 62 insertions(+), 65 deletions(-) diff --git a/Source.MaterialSystem/MatQueuedRenderContext.cs b/Source.MaterialSystem/MatQueuedRenderContext.cs index 4802e488e..74297c610 100644 --- a/Source.MaterialSystem/MatQueuedRenderContext.cs +++ b/Source.MaterialSystem/MatQueuedRenderContext.cs @@ -680,15 +680,7 @@ public void BindBatch(IMesh? vertices, IMaterial? autoBind = null) { public void DrawBatch(int firstIndex, int numIndices) => Queue.QueueCall(static s => s.H.DrawBatch(s.First, s.Num), (H, First: firstIndex, Num: numIndices)); public void EndBatch() => Queue.QueueCall(static h => h.EndBatch(), H); - public void ResetToneMappingScale(float monoscale) { - LastSetToneMapScale = new(monoscale, monoscale, monoscale); - Queue.QueueCall(static s => s.H.ResetToneMappingScale(s.Scale), (H, Scale: monoscale)); - } - - public void SetGoalToneMappingScale(float monoscale) => Queue.QueueCall(static s => s.H.SetGoalToneMappingScale(s.Scale), (H, Scale: monoscale)); - public void TurnOnToneMapping() => Queue.QueueCall(static h => h.TurnOnToneMapping(), H); - - public void SetToneMappingScaleLinear(in Vector3 scale) { + public override void SetToneMappingScaleLinear(in Vector3 scale) { LastSetToneMapScale = scale; Queue.QueueCall(static s => s.H.SetToneMappingScaleLinear(in s.Scale), (H, Scale: scale)); } diff --git a/Source.MaterialSystem/MatRenderContext.cs b/Source.MaterialSystem/MatRenderContext.cs index 74a1e54d0..a7726fa2b 100644 --- a/Source.MaterialSystem/MatRenderContext.cs +++ b/Source.MaterialSystem/MatRenderContext.cs @@ -320,49 +320,7 @@ public bool InFlashlightMode() { return FlashlightEnable; } - float CurToneMapScale = 1.0f; - float GoalToneMapScale = 1.0f; - - static readonly ConVar mat_accelerate_adjust_exposure_down = new("mat_accelerate_adjust_exposure_down", "3.0", FCvar.Cheat); - static readonly ConVar mat_hdr_manual_tonemap_rate = new("mat_hdr_manual_tonemap_rate", "1.0"); - static readonly ConVar mat_tonemap_algorithm = new("mat_tonemap_algorithm", "1", FCvar.Cheat, "0 = Original Algorithm 1 = New Algorithm"); - - public void TurnOnToneMapping() { - if ((materials.HardwareConfig.GetHDRType() != HDRType.None) && (FrameTime > 0.0)) { - TimeUnit_t elapsedTime = FrameTime; - float goalScale = GoalToneMapScale; - float rate = mat_hdr_manual_tonemap_rate.GetFloat(); - - if (mat_tonemap_algorithm.GetInt() == 1) - rate *= 2.0f; - - if (rate == 0.0f) - CurToneMapScale = goalScale; - else { - if (goalScale < CurToneMapScale) { - float accExposureAdjust = mat_accelerate_adjust_exposure_down.GetFloat(); - rate = Math.Min(accExposureAdjust * rate, MathLib.Lerp(rate, accExposureAdjust * rate, 0.0f, 1.5f, CurToneMapScale - goalScale)); - } - - float rateTimesTime = (float)(rate * elapsedTime); - if (mat_tonemap_algorithm.GetInt() == 1) - rateTimesTime = Math.Min(rateTimesTime, (1.0f / 16.0f) * 0.25f); - - float alpha = Math.Max(0.0f, Math.Min(1.0f, rateTimesTime)); - CurToneMapScale = (goalScale * alpha) + (CurToneMapScale * (1.0f - alpha)); - - if (!float.IsFinite(CurToneMapScale)) { - Assert(false); - CurToneMapScale = goalScale; - } - } - - SetToneMappingScaleLinear(new(CurToneMapScale, CurToneMapScale, CurToneMapScale)); - LastSetToneMapScale = new(CurToneMapScale, CurToneMapScale, CurToneMapScale); - } - } - - public void SetToneMappingScaleLinear(in Vector3 scale) => shaderAPI.SetToneMappingScaleLinear(in scale); + public override void SetToneMappingScaleLinear(in Vector3 scale) => shaderAPI.SetToneMappingScaleLinear(in scale); public void BeginFrame() => shaderAPI.BeginFrame(); public void EndFrame() => shaderAPI.EndFrame(); @@ -855,13 +813,6 @@ public void ResetLookupWeights() { public void ResetOcclusionQueryObject(OcclusionQueryObjectHandle_t handle) => materials.OcclusionQueryMgr.ResetOcclusionQueryObject(handle); - public void ResetToneMappingScale(float monoscale) { - CurToneMapScale = monoscale; - SetToneMappingScaleLinear(new(CurToneMapScale, CurToneMapScale, CurToneMapScale)); - LastSetToneMapScale = new(CurToneMapScale, CurToneMapScale, CurToneMapScale); - GoalToneMapScale = 1; - } - public void SelectionBuffer(Span buffer) { throw new NotImplementedException("Incomplete port of IMatRenderContext"); } @@ -882,12 +833,6 @@ public void SetFullScreenDepthTextureValidityFlag(bool isValid) { throw new NotImplementedException("Incomplete port of IMatRenderContext"); } - public void SetGoalToneMappingScale(float monoscale) { - Assert(float.IsFinite(monoscale)); - if (float.IsFinite(monoscale)) - GoalToneMapScale = monoscale; - } - public void SetIntRenderingParameter(int parm_number, int value) => shaderAPI.SetIntRenderingParameter(parm_number, value); public void SetLookupWeight(ColorCorrectionHandle_t handle, float weight) { diff --git a/Source.MaterialSystem/MatRenderContextBase.cs b/Source.MaterialSystem/MatRenderContextBase.cs index 2bc1fbc20..ec60b66a1 100644 --- a/Source.MaterialSystem/MatRenderContextBase.cs +++ b/Source.MaterialSystem/MatRenderContextBase.cs @@ -1,4 +1,5 @@ using Source.Common; +using Source.Common.Commands; using Source.Common.MaterialSystem; using Source.Common.Mathematics; using Source.Common.ShaderAPI; @@ -60,6 +61,8 @@ protected MatRenderContextBase(MaterialSystem materials) { protected Vector3 LastSetToneMapScale = new(1, 1, 1); protected TimeUnit_t FrameTime; + protected float CurToneMapScale = 1.0f; + protected float GoalToneMapScale = 1.0f; protected Vector3 VecViewOrigin; protected Vector3 VecViewForward; @@ -97,6 +100,9 @@ public virtual void InitializeFrom(MatRenderContextBase initialState) { } FlashlightEnable = initialState.FlashlightEnable; + FrameTime = initialState.FrameTime; + GoalToneMapScale = initialState.GoalToneMapScale; + CurToneMapScale = initialState.CurToneMapScale; LastSetToneMapScale = initialState.LastSetToneMapScale; } @@ -412,6 +418,60 @@ public virtual Vector3 GetToneMappingScaleLinear() { return LastSetToneMapScale; } + static readonly ConVar mat_accelerate_adjust_exposure_down = new("mat_accelerate_adjust_exposure_down", "3.0", FCvar.Cheat); + static readonly ConVar mat_hdr_manual_tonemap_rate = new("mat_hdr_manual_tonemap_rate", "1.0"); + static readonly ConVar mat_tonemap_algorithm = new("mat_tonemap_algorithm", "1", FCvar.Cheat, "0 = Original Algorithm 1 = New Algorithm"); + + public abstract void SetToneMappingScaleLinear(in Vector3 scale); + + public void TurnOnToneMapping() { + if ((materials.HardwareConfig.GetHDRType() != HDRType.None) && (FrameTime > 0.0)) { + TimeUnit_t elapsedTime = FrameTime; + float goalScale = GoalToneMapScale; + float rate = mat_hdr_manual_tonemap_rate.GetFloat(); + + if (mat_tonemap_algorithm.GetInt() == 1) + rate *= 2.0f; + + if (rate == 0.0f) + CurToneMapScale = goalScale; + else { + if (goalScale < CurToneMapScale) { + float accExposureAdjust = mat_accelerate_adjust_exposure_down.GetFloat(); + rate = Math.Min(accExposureAdjust * rate, MathLib.Lerp(rate, accExposureAdjust * rate, 0.0f, 1.5f, CurToneMapScale - goalScale)); + } + + float rateTimesTime = (float)(rate * elapsedTime); + if (mat_tonemap_algorithm.GetInt() == 1) + rateTimesTime = Math.Min(rateTimesTime, (1.0f / 16.0f) * 0.25f); + + float alpha = Math.Max(0.0f, Math.Min(1.0f, rateTimesTime)); + CurToneMapScale = (goalScale * alpha) + (CurToneMapScale * (1.0f - alpha)); + + if (!float.IsFinite(CurToneMapScale)) { + Assert(false); + CurToneMapScale = goalScale; + } + } + + SetToneMappingScaleLinear(new(CurToneMapScale, CurToneMapScale, CurToneMapScale)); + LastSetToneMapScale = new(CurToneMapScale, CurToneMapScale, CurToneMapScale); + } + } + + public void ResetToneMappingScale(float monoscale) { + CurToneMapScale = monoscale; + SetToneMappingScaleLinear(new(CurToneMapScale, CurToneMapScale, CurToneMapScale)); + LastSetToneMapScale = new(CurToneMapScale, CurToneMapScale, CurToneMapScale); + GoalToneMapScale = 1; + } + + public void SetGoalToneMappingScale(float monoscale) { + Assert(float.IsFinite(monoscale)); + if (float.IsFinite(monoscale)) + GoalToneMapScale = monoscale; + } + public virtual int GetLightmapPage() { throw new NotImplementedException("Incomplete port of IMatRenderContextInternal"); } From 653b638c9024b86ebb9e63595d26d87fc2e95573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=B6eh=20Matt?= <5415177+ZehMatt@users.noreply.github.com> Date: Mon, 19 Oct 2026 06:36:33 +0300 Subject: [PATCH 7/8] Fix render target allocation reloading every texture after the first --- Source.MaterialSystem/MaterialSystem.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Source.MaterialSystem/MaterialSystem.cs b/Source.MaterialSystem/MaterialSystem.cs index bb9db95ef..6225a1532 100644 --- a/Source.MaterialSystem/MaterialSystem.cs +++ b/Source.MaterialSystem/MaterialSystem.cs @@ -1195,12 +1195,18 @@ public void BeginRenderTargetAllocation() { AllocatingRenderTargets = true; } + static readonly ConVar mat_requires_rt_alloc_first = new("mat_requires_rt_alloc_first", "0", FCvar.Hidden); + static bool FirstRenderTargetAllocation = true; + public void EndRenderTargetAllocation() { + bool requiresRenderTargetAllocationFirst = mat_requires_rt_alloc_first.GetBool() || FirstRenderTargetAllocation; + FirstRenderTargetAllocation = false; + ShaderAPI.FlushBufferedPrimitives(); AllocatingRenderTargets = false; // I believe this step is unnecessary (and breaks how textures work rn) - if (ShaderAPI.CanDownloadTextures()) { + if (requiresRenderTargetAllocationFirst && ShaderAPI.CanDownloadTextures()) { ShaderDevice.ReleaseResources(); ShaderDevice.ReacquireResources(); } From 000aa7fe4a6601da71fd04377b988743edf4976c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=B6eh=20Matt?= <5415177+ZehMatt@users.noreply.github.com> Date: Mon, 19 Oct 2026 07:12:47 +0300 Subject: [PATCH 8/8] Fix render context storage by making it thread static --- Source.MaterialSystem/MaterialSystem.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Source.MaterialSystem/MaterialSystem.cs b/Source.MaterialSystem/MaterialSystem.cs index 6225a1532..1c2374e33 100644 --- a/Source.MaterialSystem/MaterialSystem.cs +++ b/Source.MaterialSystem/MaterialSystem.cs @@ -676,11 +676,9 @@ public void SwapBuffers() { public MaterialSystem_Config GetCurrentConfigForVideoCard() => Config; - readonly IMatRenderContextInternal?[] RenderContexts = new IMatRenderContextInternal?[2]; + [ThreadStatic] static IMatRenderContextInternal? CurrentThreadRenderContext; readonly MatRenderContext HardwareRenderContext; - ref IMatRenderContextInternal? CurrentThreadRenderContext => ref RenderContexts[ThreadInMainThread() ? 1 : 0]; - public IMatRenderContext GetRenderContext() { ref IMatRenderContextInternal? result = ref CurrentThreadRenderContext; result ??= HardwareRenderContext;