diff --git a/Client/Client.Commands.cs b/Client/Client.Commands.cs index ddf6da33d..a0dba9809 100644 --- a/Client/Client.Commands.cs +++ b/Client/Client.Commands.cs @@ -657,6 +657,21 @@ private void Use(ConsoleCommandEventArgs args) => private static void GarbageCollect(ConsoleCommandEventArgs args) => GCUtil.ForceGarbageCollection(); + [ConsoleCommand("drawcounts", "Logs the draw counts of the renderer.")] + private void DrawCounts(ConsoleCommandEventArgs args) + { + ref var counts = ref m_window.Renderer.LastDrawCounts; + HelionLog.Info($"GeometryStatic: {counts.GeometryStatic}"); + HelionLog.Info($"GeometryDynamic: {counts.GeometryDynamic}"); + HelionLog.Info($"FloodFillStatic: {counts.FloodFillStatic}"); + HelionLog.Info($"FloodFillDynamic: {counts.FloodFillDynamic}"); + HelionLog.Info($"Skies: {counts.Skies}"); + HelionLog.Info($"Sprites: {counts.Sprites}"); + HelionLog.Info($"HudElements: {counts.HudElements}"); + HelionLog.Info($"Level Total: {counts.LevelTotal()}"); + HelionLog.Info($"Total: {counts.Total()}"); + } + private void DoUseCommand(ConsoleCommandEventArgs args) { if (m_layerManager.WorldLayer == null || args.Args.Count == 0) diff --git a/Client/Client.cs b/Client/Client.cs index 3c62599e4..7b9442fc7 100755 --- a/Client/Client.cs +++ b/Client/Client.cs @@ -38,7 +38,6 @@ using System.IO; using System.Reflection; using System.Runtime.InteropServices; -using System.Threading; using System.Threading.Tasks; using static Helion.Util.Assertion.Assert; diff --git a/Core/Graphics/Image.cs b/Core/Graphics/Image.cs index 90908ed01..6df56a2ae 100644 --- a/Core/Graphics/Image.cs +++ b/Core/Graphics/Image.cs @@ -2,6 +2,7 @@ using Helion.Geometry.Vectors; using Helion.Graphics.Palettes; using Helion.Resources; +using Helion.Util.Assertion; using Helion.Util.Extensions; using SixLabors.ImageSharp.PixelFormats; using System; @@ -30,7 +31,7 @@ public class Image public Dimension Dimension; public ImageType ImageType; public ImageType UploadType; - public readonly Vec2I Offset; + public Vec2I Offset; public readonly ResourceNamespace Namespace; public uint[] m_pixels; // Stored as argb with a = high byte, b = low byte public readonly byte[] m_indices; @@ -407,6 +408,24 @@ public void SetPixel(int x, int y, Color color, Palette palette) } } + public void CopyPixelsFrom(Image image) + { + Precondition(!(image.Indices.Length > 0 && Indices.Length == 0), "Image type mismatch"); + + var width = Math.Min(image.Dimension.Width, Dimension.Width); + var height = Math.Min(image.Dimension.Height, Dimension.Height); + + for (int x = 0; x < width; x++) + { + for (int y = 0; y < height; y++) + { + SetPixel(x, y, image.GetPixel(x, y)); + if (image.ImageType == ImageType.PaletteWithArgb) + SetIndex(x, y, image.GetIndex(x, y)); + } + } + } + public void ConvertToGrayscale(bool normalize) { if (ImageType == ImageType.Palette) diff --git a/Core/Render/OpenGL/Context/GLInfo.cs b/Core/Render/OpenGL/Context/GLInfo.cs index 351b47db7..35ae03557 100644 --- a/Core/Render/OpenGL/Context/GLInfo.cs +++ b/Core/Render/OpenGL/Context/GLInfo.cs @@ -32,11 +32,18 @@ public static class GLInfo public static bool MapPersistentBitSupported = true; public static bool MemoryBarrierSupported = true; public static bool DebugLabel; + public static int MaxArrayLayers; static GLInfo() { Renderer = GL.GetString(StringName.Renderer); ShadingVersion = GL.GetString(StringName.ShadingLanguageVersion); Vendor = GL.GetString(StringName.Vendor); + MaxArrayLayers = GetMaxArrayLayers(); + } + public static int GetMaxArrayLayers() + { + GL.GetInteger(GetPName.MaxArrayTextureLayers, out var maxLayers); + return maxLayers; } } diff --git a/Core/Render/OpenGL/Renderers/Legacy/Hud/LegacyHudRenderer.cs b/Core/Render/OpenGL/Renderers/Legacy/Hud/LegacyHudRenderer.cs index 39023bf9e..355f36565 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/Hud/LegacyHudRenderer.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/Hud/LegacyHudRenderer.cs @@ -13,6 +13,7 @@ using Helion.Resources.Definitions.Zdoom; using Helion.Util; using Helion.Util.Configs; +using Helion.Util.Profiling.Timers; using OpenTK.Graphics.OpenGL; using static Helion.Util.Assertion.Assert; @@ -25,12 +26,14 @@ public class LegacyHudRenderer : HudRenderer private readonly LegacyHudShader m_program; private readonly HudDrawBuffer m_drawBuffer; private readonly IConfig m_config; + private readonly RenderProfiler m_renderProfiler; private float DrawDepth = 1.0f; - public LegacyHudRenderer(IConfig config, LegacyGLTextureManager textureManager, DataCache dataCache) + public LegacyHudRenderer(IConfig config, LegacyGLTextureManager textureManager, DataCache dataCache, RenderProfiler renderProfiler) { m_config = config; m_textureManager = textureManager; + m_renderProfiler = renderProfiler; m_program = new(); m_pipeline = new(m_program, new StreamVertexBuffer("Hud"), "Hud"); m_drawBuffer = new(dataCache); @@ -175,9 +178,10 @@ public override void Render(Rectangle viewport, Dimension windowDimension, Dimen if (data.BrightmapTexture != null) data.BrightmapTexture.Bind(); else - GL.BindTexture(TextureTarget.Texture2D, 0); + GL.BindTexture(data.Texture.Target, 0); m_pipeline.DrawArrays(); data.Texture.Unbind(); + m_renderProfiler.DrawCounts.HudElements++; } m_pipeline.Unbind(); diff --git a/Core/Render/OpenGL/Renderers/Legacy/Hud/LegacyHudShader.cs b/Core/Render/OpenGL/Renderers/Legacy/Hud/LegacyHudShader.cs index 1140b08cd..ce2294f5c 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/Hud/LegacyHudShader.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/Hud/LegacyHudShader.cs @@ -112,7 +112,7 @@ private static string GetBrightmapColorBlend() return @" if (useBrightmaps == 1) - fragColor.rgb *= mix(vec3(1.0), min(vec3(1.0), texture(brightmapTexture, uvFrag.st).rgb + rgbMultiplierFrag.rgb), rgbMultiplierFrag.w); + fragColor.rgb *= mix(vec3(1.0), min(vec3(1.0), texture(brightmapTexture, vec3(uvFrag.st, 0)).rgb + rgbMultiplierFrag.rgb), rgbMultiplierFrag.w); else fragColor.xyz *= mix(vec3(1.0), rgbMultiplierFrag.xyz, rgbMultiplierFrag.w);"; } @@ -129,9 +129,9 @@ private static string GetBrightmapColorBlend() out vec4 fragColor; - uniform sampler2D boundTexture; + uniform sampler2DArray boundTexture; uniform sampler2D opaqueTexture; - uniform sampler2D brightmapTexture; + uniform sampler2DArray brightmapTexture; uniform samplerBuffer colormapTexture; uniform float fuzzFrac; uniform float fuzzDiv; @@ -145,10 +145,12 @@ private static string GetBrightmapColorBlend() uniform vec2 fuzzSampleOffset; uniform int fuzzRefraction; + const int boundTextureIndex = 0; + ${FuzzFunction} void main() { - fragColor = texture(boundTexture, uvFrag.st); + fragColor = texture(boundTexture, vec3(uvFrag.st, 0)); ${ColorMapFetch} ${AlphaFlag} fragColor.w *= alphaFrag; diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderData.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderData.cs index b5cc49afb..86516d6cb 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderData.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderData.cs @@ -45,10 +45,10 @@ public void Clear() Pipeline.Clear(); } - public void Draw() + public bool Draw() { if (Pipeline.Empty) - return; + return false; GL.ActiveTexture(BindTextures.BoundTexture); Texture.Bind(); @@ -56,13 +56,14 @@ public void Draw() if (BrightMapTexture != null) BrightMapTexture.Bind(); else - GL.BindTexture(TextureTarget.Texture2D, 0); + GL.BindTexture(Texture.Target, 0); Pipeline.Bind(true); Pipeline.Vbo.Upload(); Pipeline.Vbo.DrawArraysInstanced(PrimitiveType.TriangleStrip, 0, 4); Texture.Unbind(); + return true; } protected virtual void Dispose(bool disposing) diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderDataCollection.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderDataCollection.cs index 7ec2a7159..5e7f5ebe3 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderDataCollection.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderDataCollection.cs @@ -18,13 +18,15 @@ public class RenderDataCollection<[DynamicallyAccessedMembers(DynamicallyAccesse private readonly DynamicArray> m_dataToRender = new(2048); private readonly RenderProgram m_program; private readonly RenderDataPool m_renderDataPool; + private readonly Action? m_onDraw; private int m_renderCount; private bool m_disposed; - public RenderDataCollection(RenderProgram program, RenderDataPool renderDataPool) + public RenderDataCollection(RenderProgram program, RenderDataPool renderDataPool, Action? onDraw = null) { m_program = program; m_renderDataPool = renderDataPool; + m_onDraw = onDraw; } ~RenderDataCollection() @@ -72,7 +74,10 @@ public void Render() return; for (int i = 0; i < m_dataToRender.Length; i++) - m_dataToRender[i].Draw(); + { + if (m_dataToRender[i].Draw()) + m_onDraw?.Invoke(); + } } protected virtual void Dispose(bool disposing) diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderDataManager.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderDataManager.cs index 9ed807eaa..c34b5f337 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderDataManager.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderDataManager.cs @@ -23,16 +23,18 @@ public class RenderDataManager<[DynamicallyAccessedMembers(DynamicallyAccessedMe private readonly RenderDataCollection[] m_renderDataStyles; private readonly RenderData m_healthBarData; + private readonly Action? m_onDraw; private bool m_disposed; - public RenderDataManager(RenderProgram program, GLLegacyTexture healthBarTexture, RenderDataPool renderDataPool) + public RenderDataManager(RenderProgram program, GLLegacyTexture healthBarTexture, RenderDataPool renderDataPool, Action? onDraw = null) { Assert.Precondition(RenderStyleLookup.Length == (int)RenderStyle.Count, "Render style lookup size mismatch"); m_renderDataStyles = new RenderDataCollection[(int)RenderDataStyle.Count]; for (int i = 0; i < m_renderDataStyles.Length; i++) - m_renderDataStyles[i] = new(program, renderDataPool); + m_renderDataStyles[i] = new(program, renderDataPool, onDraw); m_healthBarData = new(program, healthBarTexture); + m_onDraw = onDraw; } ~RenderDataManager() diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderWorldData.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderWorldData.cs index 705d3953a..8b5d11a4b 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderWorldData.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderWorldData.cs @@ -42,10 +42,10 @@ public void Clear() Pipeline.Clear(); } - public void Draw() + public bool Draw() { if (Pipeline.Empty) - return; + return false; // We are doing binding manually since apparently these are all // coming up in the memory profiler as a bunch of new 'actions'. @@ -57,13 +57,14 @@ public void Draw() if (BrightmapTexture != null) BrightmapTexture.Bind(); else - GL.BindTexture(TextureTarget.Texture2D, 0); + GL.BindTexture(Texture.Target, 0); Pipeline.Bind(true); Pipeline.Vbo.Upload(); Pipeline.DrawArrays(); Pipeline.Unbind(); Texture.Unbind(); + return true; } public void Dispose() diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderWorldDataList.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderWorldDataList.cs index 47db2f961..4656a3beb 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderWorldDataList.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderWorldDataList.cs @@ -1,17 +1,17 @@ -using Helion.Render.OpenGL.Shader; -using Helion.Render.OpenGL.Texture.Legacy; +using Helion.Render.OpenGL.Texture.Legacy; using Helion.Util.Container; using System; using System.Collections.Generic; namespace Helion.Render.OpenGL.Renderers.Legacy.World.Data; -public class RenderWorldDataList(RenderWorldDataPool pool) +public class RenderWorldDataList(RenderWorldDataPool pool, Action? onDraw) { public List RenderData = []; private RenderWorldData?[] m_allRenderData = new RenderWorldData?[1024]; private readonly DynamicArray m_dataToRender = new(1024); private readonly RenderWorldDataPool m_pool = pool; + private readonly Action? m_onDraw = onDraw; private int m_renderCount; public void Reset() @@ -60,7 +60,10 @@ public RenderWorldData Add(GLLegacyTexture texture, GLLegacyTexture? brightmapTe public void Draw() { for (int i = 0; i < m_dataToRender.Length; i++) - m_dataToRender[i].Draw(); + { + if (m_dataToRender[i].Draw()) + m_onDraw?.Invoke(); + } } public void Clear() diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderWorldDataManager.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderWorldDataManager.cs index b41f2034d..2976b0262 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderWorldDataManager.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Data/RenderWorldDataManager.cs @@ -12,19 +12,21 @@ public sealed class RenderWorldDataManager : StyleRendererBase, IDisposable { private readonly RenderWorldDataPool m_pool; private readonly GeometryTypeLookup m_lookup; + private readonly Action? m_onDraw; private RenderWorldData? m_coverWalls; public bool BufferCoverWalls = true; - public RenderWorldDataManager(RenderProgram program) + public RenderWorldDataManager(RenderProgram program, Action? onDraw) { m_pool = new(program, 1024); + m_onDraw = onDraw; m_lookup = new(AllocateDataList); } private RenderWorldDataList AllocateDataList() { - return new RenderWorldDataList(m_pool); + return new RenderWorldDataList(m_pool, m_onDraw); } public void Reset() diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Entities/EntityProgram.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Entities/EntityProgram.cs index 84cdc0da8..1e4badc56 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Entities/EntityProgram.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Entities/EntityProgram.cs @@ -69,6 +69,7 @@ protected override string VertexShader() => @" layout(location = 2) in vec3 prevPos; layout(location = 3) in float offsetXYZ; layout(location = 4) in float renderOptions; + layout(location = 5) in float textureInfo; flat out float lightLevelFrag; flat out float alphaFrag; @@ -76,6 +77,7 @@ protected override string VertexShader() => @" flat out float colorMapTranslationFrag; flat out float zPosFrag; flat out float textureWidthFrag; + flat out float boundTextureIndex; flat out vec3 centerPosFrag; flat out vec3 minPosFrag; flat out vec3 maxPosFrag; @@ -96,7 +98,7 @@ protected override string VertexShader() => @" uniform float timeFrac; uniform int useSectorColor; uniform int useSectorFog; - uniform sampler2D boundTexture; + uniform sampler2DArray boundTexture; uniform samplerBuffer sectorColormapTexture; uniform samplerBuffer sectorFogTexture; @@ -120,13 +122,15 @@ void main() intOptions = floatBitsToInt(offsetXYZ); float offsetXYOption = float((intOptions >> 16) & 0x3FFF); - float offsetZ = float(intOptions & 0x3FFF); float offsetXYSign = float(((intOptions >> 31) & 1) > 0); float offsetZSign = float(((intOptions >> 30) & 1) > 0); offsetXYOption = mix(offsetXYOption, -offsetXYOption, offsetXYSign); offsetZ = mix(offsetZ, -offsetZ, offsetZSign); - ivec2 textureDim = textureSize(boundTexture, 0); + + intOptions = floatBitsToInt(textureInfo); + boundTextureIndex = (intOptions >> 20) & 0xFFF; + ivec2 textureDim = ivec2((intOptions >> 10) & 0x3FF, intOptions & 0x3FF); textureWidthFrag = textureDim.x; ${SectorColorMapVertexFunction} @@ -149,12 +153,11 @@ void main() float z = mix(minPos.z, maxPos.z, ySelect); vec3 cornerPos = vec3(x, y, z); - - float leftU = clamp(flipU, 0, 1); - float rightU = 1 - clamp(flipU, 0, 1); + float leftU = mix(0.0, calcU, float(flipU)); + float rightU = mix(calcU, 0.0, float(flipU)); float u = mix(leftU, rightU, xSelect); - float v = mix(1.0, 0.0, ySelect); + float v = mix(calcV, 0.0, ySelect); uvFrag = vec2(u, v); @@ -184,6 +187,8 @@ private string GetMinMaxPos() if (this is EntityHealthBarProgram) { return @" + float calcU = 1; + float calcV = 1; zPosFrag = interpolatedPos.z; interpolatedPos.z += offsetZ; vec3 minPos = interpolatedPos; @@ -193,10 +198,16 @@ private string GetMinMaxPos() } return @" + ivec2 boundTextureSize = textureSize(boundTexture, 0).xy; + float calcU = textureDim.x / float(boundTextureSize.x); + float calcV = textureDim.y / float(boundTextureSize.y); + + float worldWidth = calcU * boundTextureSize.x; + float worldHeight = calcV * boundTextureSize.y; zPosFrag = interpolatedPos.z; interpolatedPos.z += offsetZ; vec3 minPos = interpolatedPos - offsetXY; - vec3 maxPos = interpolatedPos + (posMoveDir * textureDim.x) + (vec3(0, 0, 1) * textureDim.y) - offsetXY;"; + vec3 maxPos = interpolatedPos + (posMoveDir * worldWidth) + (vec3(0, 0, 1) * worldHeight) - offsetXY;"; } private static string AdjustSpriteVertexClip() @@ -222,6 +233,7 @@ private static string AdjustSpriteVertexClip() flat in float colorMapTranslationFrag; flat in float zPosFrag; flat in float textureWidthFrag; + flat in float boundTextureIndex; flat in vec3 centerPosFrag; flat in vec3 minPosFrag; flat in vec3 maxPosFrag; @@ -232,8 +244,8 @@ private static string AdjustSpriteVertexClip() uniform int hasInvulnerability; uniform float fuzzFrac; - uniform sampler2D boundTexture; - uniform sampler2D brightmapTexture; + uniform sampler2DArray boundTexture; + uniform sampler2DArray brightmapTexture; uniform samplerBuffer colormapTexture; uniform float lightLevelMix; uniform int extraLight; diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Entities/EntityRenderer.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Entities/EntityRenderer.cs index d24dbd979..7a30141dc 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Entities/EntityRenderer.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Entities/EntityRenderer.cs @@ -10,6 +10,7 @@ using Helion.Util; using Helion.Util.Configs; using Helion.Util.Container; +using Helion.Util.Profiling.Timers; using Helion.World; using Helion.World.Entities; using Helion.World.Entities.Definition; @@ -40,6 +41,7 @@ public sealed class EntityRenderer : StyleRendererBase, IDisposable private readonly SpriteRotation m_nullSpriteRotation; private readonly ArchiveCollection m_archiveCollection; private readonly RenderDataPool m_renderDataPool; + private readonly RenderProfiler m_renderProfiler; private readonly bool m_vanillaRender; private Vec2F m_viewRightNormal; private Vec2F m_prevViewRightNormal; @@ -54,19 +56,25 @@ public sealed class EntityRenderer : StyleRendererBase, IDisposable private bool m_disposed; private int m_lastViewerEntityId; - public EntityRenderer(IConfig config, LegacyGLTextureManager textureManager, ArchiveCollection archiveCollection) + public EntityRenderer(IConfig config, LegacyGLTextureManager textureManager, ArchiveCollection archiveCollection, RenderProfiler renderProfiler) { m_config = config; m_textureManager = textureManager; m_archiveCollection = archiveCollection; m_nullSpriteRotation = m_textureManager.NullSpriteRotation; m_renderDataPool = new(m_program, RenderPoolSize); - m_dataManager = new(m_program, textureManager.BlackTexture, m_renderDataPool); + m_dataManager = new(m_program, textureManager.BlackTexture, m_renderDataPool, OnDraw); m_spriteAlpha = m_config.Render.SpriteTransparency; m_spriteClip = m_config.Render.SpriteClip; m_spriteClipMin = m_config.Render.SpriteClipMin; m_vanillaRender = m_config.Render.VanillaRender; m_spriteClipFactorMax = (float)m_config.Render.SpriteClipFactorMax.Value; + m_renderProfiler = renderProfiler; + } + + private void OnDraw() + { + m_renderProfiler.DrawCounts.Sprites++; } ~EntityRenderer() @@ -128,9 +136,9 @@ private static uint CalculateRotation(uint viewAngle, uint entityAngle) return unchecked((viewAngle - entityAngle + SpriteFrameRotationAngle) >> 29); } - private int GetOffsetZ(Entity entity, GLLegacyTexture texture) + private int GetOffsetZ(Entity entity, GLLegacyTexture texture, int textureHeight) { - int offsetAmount = texture.Offset.Y - texture.Height; + int offsetAmount = texture.Offset.Y - textureHeight; if (m_vanillaRender) return offsetAmount; @@ -140,13 +148,13 @@ private int GetOffsetZ(Entity entity, GLLegacyTexture texture) if (entity.Sector.Flood || entity.Sector.Floor.NoRender) return offsetAmount; - if (!m_spriteClip || texture.Height < m_spriteClipMin || entity.Definition.IsInventory) + if (!m_spriteClip || textureHeight < m_spriteClipMin || entity.Definition.IsInventory) return MathHelper.Max(offsetAmount, -texture.BlankRowsFromBottom); if (entity.Position.Z - entity.HighestFloorSector.Floor.Z < texture.Offset.Y) { // Truncate to integer pixel amount. This helps the jumpiness for the stock large torches. - int maxHeight = (int)((texture.Height - texture.BlankRowsFromBottom) * m_spriteClipFactorMax); + int maxHeight = (int)((textureHeight - texture.BlankRowsFromBottom) * m_spriteClipFactorMax); if (-offsetAmount > maxHeight) offsetAmount = -maxHeight - texture.BlankRowsFromBottom; return offsetAmount; @@ -168,7 +176,7 @@ private SpriteRotation GetSpriteRotation(SpriteDefinition spriteDefinition, int } public void RenderEntity(Entity entity, in Vec2D position, int renderIndex) - { + { Vec3D centerBottom = entity.Position; Vec2D nudgeAmount = default; @@ -237,8 +245,7 @@ public void RenderEntity(Entity entity, in Vec2D position, int renderIndex) var disableFullbright = spriteRotation.BrightmapNoFullbright; var isFullBright = (entity.Flags.Bright() || entity.FrameState.Frame.Properties.Bright) && !disableFullbright; - var offsetZ = GetOffsetZ(entity, texture); - + var offsetZ = GetOffsetZ(entity, texture, spriteRotation.TextureDimension.Height); int fuzz; RenderStyle renderStyle; @@ -249,7 +256,7 @@ public void RenderEntity(Entity entity, in Vec2D position, int renderIndex) } else { - renderStyle = m_spriteAlpha ? entity.RenderStyle: RenderStyle.Normal; + renderStyle = m_spriteAlpha ? entity.RenderStyle : RenderStyle.Normal; fuzz = 0; } @@ -269,7 +276,8 @@ public void RenderEntity(Entity entity, in Vec2D position, int renderIndex) if (renderStyle == RenderStyle.ColorAdd) entityAlpha = 1.0f; - var renderData = m_dataManager.GetByRenderStyle(renderStyle, texture, brightmapTexture); + var arrayTexture = texture.ParentArrayTexture ?? texture; + var renderData = m_dataManager.GetByRenderStyle(renderStyle, arrayTexture, brightmapTexture); var alpha = m_spriteAlpha && renderStyle != RenderStyle.Normal ? entityAlpha : 1.0f; var arrayData = renderData.ArrayData; @@ -291,6 +299,7 @@ public void RenderEntity(Entity entity, in Vec2D position, int renderIndex) vertex.SurfaceOptions = VertexOptions.EntityPackSurface(alpha, fuzz, flipU, colorMapIndex, lightLevel); vertex.RenderOptions = VertexOptions.EntityPackRender( Renderer.GetLightBufferIndex(sector, WorldStatic.Sector3D && sector.Sectors3D.Length > 0 ? LightBufferType.Wall : LightBufferType.Floor), renderIndex); + vertex.TextureInfo = VertexOptions.EntityPackTextureInfo(texture.ArrayIndex, spriteRotation.TextureDimension.Width, spriteRotation.TextureDimension.Height); if (entity.Definition.Flags.SpawnCeiling() && m_vanillaRender) { @@ -301,7 +310,7 @@ public void RenderEntity(Entity entity, in Vec2D position, int renderIndex) vertex.Pos.Z = ceilingZ + diff; vertex.PrevPos.Z = entity.PrevPosition.Z != entity.Position.Z ? (float)entity.Sector.Ceiling.PrevZ : ceilingZ; } - + vertex.OffsetXYZ = VertexOptions.EntityPackXYZ(offsetX, offsetZ); arrayData.Length = length + 1; @@ -334,6 +343,7 @@ private void RenderHealthBar(Entity entity, GLLegacyTexture texture, float offse vertex.Pos = entityVertex.Pos; vertex.PrevPos = entityVertex.PrevPos; vertex.OffsetXYZ = VertexOptions.EntityPackXYZ(0, offset); + vertex.TextureInfo = 0; array.SetLength(array.Length + 1); } diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Entities/EntityVertex.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Entities/EntityVertex.cs index 1dfdf3608..d97ae2682 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Entities/EntityVertex.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Entities/EntityVertex.cs @@ -22,4 +22,7 @@ public struct EntityVertex [VertexAttribute(divisor: 1)] public float RenderOptions; + + [VertexAttribute(divisor: 1)] + public float TextureInfo; } diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/GeometryRenderer.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/GeometryRenderer.cs index 2f00a75a7..54587a5f8 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/GeometryRenderer.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/GeometryRenderer.cs @@ -13,10 +13,13 @@ using Helion.Render.OpenGL.Texture.Legacy; using Helion.Resources; using Helion.Resources.Archives.Collection; +using Helion.Resources.Definitions.Animdefs.Textures; +using Helion.Resources.IWad; using Helion.Util; using Helion.Util.Configs; using Helion.Util.Configs.Components; using Helion.Util.Container; +using Helion.Util.Profiling.Timers; using Helion.World; using Helion.World.Geometry.Lines; using Helion.World.Geometry.Sectors; @@ -24,6 +27,7 @@ using Helion.World.Geometry.Subsectors; using Helion.World.Geometry.Walls; using Helion.World.Physics; +using Helion.World.Special.Switches; using Helion.World.Static; using System; using System.Collections; @@ -35,6 +39,8 @@ namespace Helion.Render.OpenGL.Renderers.Legacy.World.Geometry; public partial class GeometryRenderer : IDisposable { + sealed record class LoadTextureCounts(HashSet FlatTextures, HashSet WallTexturesRepeat, HashSet WallTexturesClamp); + public delegate void RenderCoverWallAction(Side side, Span vertices, WallLocation location, bool oneSided); private const double MaxSky = 16384; @@ -103,13 +109,14 @@ public partial class GeometryRenderer : IDisposable private BitArray m_hitLines = new(0); public GeometryRenderer(IConfig config, ArchiveCollection archiveCollection, LegacyGLTextureManager glTextureManager, - RenderProgram program, RenderProgram staticProgram, RenderWorldDataManager worldDataManager, ViewClipper viewClipper, ViewClipper viewClipperPrev, bool unitTest = false) + RenderProgram program, RenderProgram staticProgram, RenderWorldDataManager worldDataManager, + ViewClipper viewClipper, ViewClipper viewClipperPrev, RenderProfiler renderProfiler, bool unitTest = false) { m_config = config; m_program = program; m_glTextureManager = glTextureManager; m_worldDataManager = worldDataManager; - m_skyRenderer = new LegacySkyRenderer(archiveCollection, glTextureManager); + m_skyRenderer = new LegacySkyRenderer(archiveCollection, glTextureManager, renderProfiler); m_archiveCollection = archiveCollection; m_fakeSideScrollData = new(); m_fakeSide = new(0, default, m_fakeWall, m_fakeWall, m_fakeWall, m_sliceSector) @@ -135,8 +142,8 @@ public GeometryRenderer(IConfig config, ArchiveCollection archiveCollection, Leg } else { - Portals = new(archiveCollection, glTextureManager); - m_staticCacheGeometryRenderer = new(archiveCollection, glTextureManager, staticProgram, this); + Portals = new(archiveCollection, glTextureManager, renderProfiler); + m_staticCacheGeometryRenderer = new(archiveCollection, glTextureManager, staticProgram, renderProfiler, this); m_renderCoverWallAction = m_worldDataManager.AddCoverWallVertices; } @@ -146,7 +153,7 @@ public GeometryRenderer(IConfig config, ArchiveCollection archiveCollection, Leg m_renderTwoSidedMiddleSliceFunc = RenderTwoSidedMiddleSlice; m_renderSectorSliceFunc3D = RenderSectorSlice3D; - var options = VertexOptions.PackSurface(1, 1, 0, 0, 0, 0); + var options = VertexOptions.PackSurface(1, 1, 0, 0, 0, 0, 0); for (int i = 0; i < m_wallVertices.Length; i++) m_wallVertices[i].SurfaceOptions = options; @@ -174,8 +181,6 @@ public void UpdateTo(IWorld world, bool unitTest = false) m_vanillaRender = world.Config.Render.VanillaRender; m_pixelGapCorrection = world.Config.Render.PixelGapCorrection; - PreloadAllTextures(world); - int sideCount = world.Geometry.GetSideCount(); int sectorCount = world.Geometry.GetSectorCount(); bool freeData = !world.SameAsPreviousMap; @@ -193,8 +198,11 @@ public void UpdateTo(IWorld world, bool unitTest = false) m_invalidatedCounter = 1; + LoadTextureCounts? loadTextureCounts = null; if (!world.SameAsPreviousMap) { + loadTextureCounts = PreloadAllTextures(world); + for (int i = 0; i < m_subsectors.Length; i++) { m_subsectors[i].FlushReferences(); @@ -227,6 +235,12 @@ public void UpdateTo(IWorld world, bool unitTest = false) if (!unitTest) { + if (!world.SameAsPreviousMap && loadTextureCounts != null) + { + var arrayBuilder = new GLTextureArrayBuilder(m_archiveCollection.TextureManager, m_glTextureManager); + arrayBuilder.BuildLevel(loadTextureCounts.FlatTextures, loadTextureCounts.WallTexturesRepeat, loadTextureCounts.WallTexturesClamp); + } + Portals.UpdateTo(world); m_staticCacheGeometryRenderer.UpdateTo(world); m_worldDataManager?.InitCoverWallRenderData(m_glTextureManager.WhiteTexture, m_program); @@ -604,49 +618,95 @@ public void Dispose() GC.SuppressFinalize(this); } - private void PreloadAllTextures(IWorld world) + private LoadTextureCounts? PreloadAllTextures(IWorld world) { - if (world.SameAsPreviousMap) - return; + var wallTexturesRepeat = new HashSet(128); + var wallTexturesClamp = new HashSet(128); + var flatTextures = new HashSet(128); - HashSet textures = []; for (int i = 0; i < world.Lines.Count; i++) { var line = world.Lines[i]; - AddSideTextures(textures, line.Front); + AddSideTextures(wallTexturesRepeat, wallTexturesClamp, line.Front, line.Flags.TwoSided); if (line.Back == null) continue; - AddSideTextures(textures, line.Back); + AddSideTextures(wallTexturesRepeat, wallTexturesClamp, line.Back, line.Flags.TwoSided); } for (int i = 0; i < world.Sectors.Count; i++) { var sector = world.Sectors[i]; - textures.Add(sector.Floor.TextureHandle); - textures.Add(sector.Ceiling.TextureHandle); + flatTextures.Add(sector.Floor.TextureHandle); + flatTextures.Add(sector.Ceiling.TextureHandle); if (sector.FloorSkyTextureHandle.HasValue) - textures.Add(sector.FloorSkyTextureHandle.Value); + flatTextures.Add(sector.FloorSkyTextureHandle.Value); if (sector.CeilingSkyTextureHandle.HasValue) - textures.Add(sector.CeilingSkyTextureHandle.Value); + flatTextures.Add(sector.CeilingSkyTextureHandle.Value); } foreach (var textureName in world.GetPreCacheTextureNames()) { var texture = TextureManager.GetTexture(textureName, ResourceNamespace.Global, ResourceNamespace.Textures); if (texture.Index > 0) - textures.Add(texture.Index); + { + wallTexturesClamp.Add(texture.Index); + wallTexturesRepeat.Add(texture.Index); + } } - TextureManager.LoadTextureImages(textures); + AddLineSwitchTextures(wallTexturesRepeat, wallTexturesClamp); + + TextureManager.LoadTextureImages(flatTextures); + TextureManager.LoadTextureImages(wallTexturesClamp); + TextureManager.LoadTextureImages(wallTexturesRepeat); + return new(flatTextures, wallTexturesRepeat, wallTexturesClamp); } - private static void AddSideTextures(HashSet textures, Side side) + private void AddLineSwitchTextures(HashSet wallTexturesRepeat, HashSet wallTexturesClamp) { - textures.Add(side.Lower.TextureHandle); - textures.Add(side.Middle.TextureHandle); - textures.Add(side.Upper.TextureHandle); + var switches = new List(m_archiveCollection.Definitions.Animdefs.AnimatedSwitches.Count); + for (int i = 0; i < m_archiveCollection.Definitions.Animdefs.AnimatedSwitches.Count; i++) + { + var animSwitch = m_archiveCollection.Definitions.Animdefs.AnimatedSwitches[i]; + if (animSwitch.IWad != IWadBaseType.None && animSwitch.IWad != m_archiveCollection.IWadType) + continue; + switches.Add(animSwitch); + } + + AddLineSwitchTextures(wallTexturesRepeat, switches); + AddLineSwitchTextures(wallTexturesClamp, switches); + } + + private static void AddLineSwitchTextures(HashSet wallTextures, List switches) + { + var addTextures = new HashSet(); + foreach (var textureIndex in wallTextures) + { + foreach (var animSwitch in switches) + { + if (animSwitch.IsMatch(textureIndex)) + { + addTextures.Add(animSwitch.GetOpposingTexture(textureIndex)); + break; + } + } + } + + foreach (var textureIndex in addTextures) + wallTextures.Add(textureIndex); + } + + private static void AddSideTextures(HashSet wallTexturesRepeat, HashSet wallTexturesClamp, Side side, bool twoSided) + { + wallTexturesRepeat.Add(side.Lower.TextureHandle); + wallTexturesRepeat.Add(side.Upper.TextureHandle); + + if (twoSided) + wallTexturesClamp.Add(side.Middle.TextureHandle); + else + wallTexturesRepeat.Add(side.Middle.TextureHandle); } private void RenderSectorWalls(Sector sector, Vec2D pos2D, Vec2D prevPos2D) @@ -849,9 +909,9 @@ public void RenderOneSided(Side side, bool isFront, out DynamicVertex[]? vertice int addAlpha = allowAlpha ? 0 : 1; WorldTriangulator.HandleOneSided(side, offsetSide ?? side, floor, ceiling, texture.UVInverse, ref wall, isFront: isFront); if (data == null) - data = GetWallVertices(wall, GetLightLevelAdd(side), lightIndex, overrideLightIndex, GetWallLightLevel(side, side.Middle), side.Line.Id, WallLocation.Middle, addAlpha: addAlpha, alpha: side.Alpha); + data = GetWallVertices(wall, GetLightLevelAdd(side), lightIndex, overrideLightIndex, GetWallLightLevel(side, side.Middle), side.Line.Id, WallLocation.Middle, texture.ArrayIndex, addAlpha: addAlpha, alpha: side.Alpha); else - SetWallVertices(data, wall, GetLightLevelAdd(side), lightIndex, overrideLightIndex, GetWallLightLevel(side, side.Middle), side.Line.Id, WallLocation.Middle, addAlpha: addAlpha, alpha: side.Alpha); + SetWallVertices(data, wall, GetLightLevelAdd(side), lightIndex, overrideLightIndex, GetWallLightLevel(side, side.Middle), side.Line.Id, WallLocation.Middle, texture.ArrayIndex, addAlpha: addAlpha, alpha: side.Alpha); SetCachedSide(m_vertexLookup, side, data); } @@ -859,7 +919,8 @@ public void RenderOneSided(Side side, bool isFront, out DynamicVertex[]? vertice if (m_buffer) { var geometryType = GetGeometryType(style, baseType); - var renderData = m_worldDataManager.GetRenderData(texture, geometryType, brightmapTexture); + var arrayTexture = m_glTextureManager?.GetParentArrayTexture(side.Middle.TextureHandle) ?? TestTexture; + var renderData = m_worldDataManager.GetRenderData(arrayTexture, geometryType, brightmapTexture); renderData.Pipeline.Vbo.AddMemoryCopy(data); if (m_vanillaRender && baseType == GeometryType.Wall && style == RenderDataStyle.Normal) m_worldDataManager.AddCoverWallVertices(side, data, side.Middle.Location, true); @@ -1207,16 +1268,17 @@ public void RenderTwoSidedLower(Side facingSide, Side otherSide, Sector facingSe WorldTriangulator.HandleTwoSidedLower(facingSide, top, bottom, texture.UVInverse, isFrontSide, ref wall); if (data == null) - data = GetWallVertices(wall, GetLightLevelAdd(facingSide), lightIndex, overrideLightIndex, GetWallLightLevel(facingSide, facingSide.Lower), facingSide.Line.Id, WallLocation.Lower); + data = GetWallVertices(wall, GetLightLevelAdd(facingSide), lightIndex, overrideLightIndex, GetWallLightLevel(facingSide, facingSide.Lower), facingSide.Line.Id, WallLocation.Lower, texture.ArrayIndex); else - SetWallVertices(data, wall, GetLightLevelAdd(facingSide), lightIndex, overrideLightIndex, GetWallLightLevel(facingSide, facingSide.Lower), facingSide.Line.Id, WallLocation.Lower); + SetWallVertices(data, wall, GetLightLevelAdd(facingSide), lightIndex, overrideLightIndex, GetWallLightLevel(facingSide, facingSide.Lower), facingSide.Line.Id, WallLocation.Lower, texture.ArrayIndex); SetCachedSide(m_vertexLowerLookup, facingSide, data); } if (m_buffer) { - var renderData = m_worldDataManager.GetRenderData(texture, GeometryType.Wall, brightmapTexture); + var arrayTexture = m_glTextureManager.GetParentArrayTexture(lowerWall.TextureHandle); + var renderData = m_worldDataManager.GetRenderData(arrayTexture, GeometryType.Wall, brightmapTexture); renderData.Pipeline.Vbo.AddMemoryCopy(data); } vertices = data; @@ -1328,16 +1390,17 @@ public void RenderTwoSidedUpper(Side facingSide, Side otherSide, Sector facingSe int lightIndex = Renderer.GetLightBufferIndex(facingSide, facingSide.Upper, lightLevelSector, out var overrideLightIndex); WorldTriangulator.HandleTwoSidedUpper(facingSide, top, bottom, texture.UVInverse, isFrontSide, ref wall); if (data == null) - data = GetWallVertices(wall, GetLightLevelAdd(facingSide), lightIndex, overrideLightIndex, GetWallLightLevel(facingSide, facingSide.Upper), facingSide.Line.Id, WallLocation.Upper); + data = GetWallVertices(wall, GetLightLevelAdd(facingSide), lightIndex, overrideLightIndex, GetWallLightLevel(facingSide, facingSide.Upper), facingSide.Line.Id, WallLocation.Upper, texture.ArrayIndex); else - SetWallVertices(data, wall, GetLightLevelAdd(facingSide), lightIndex, overrideLightIndex, GetWallLightLevel(facingSide, facingSide.Upper), facingSide.Line.Id, WallLocation.Upper); + SetWallVertices(data, wall, GetLightLevelAdd(facingSide), lightIndex, overrideLightIndex, GetWallLightLevel(facingSide, facingSide.Upper), facingSide.Line.Id, WallLocation.Upper, texture.ArrayIndex); SetCachedSide(m_vertexUpperLookup, facingSide, data); } if (m_buffer) { - var renderData = m_worldDataManager.GetRenderData(texture, GeometryType.Wall, brightmapTexture); + var arrayTexture = m_glTextureManager.GetParentArrayTexture(upperWall.TextureHandle); + var renderData = m_worldDataManager.GetRenderData(arrayTexture, GeometryType.Wall, brightmapTexture); renderData.Pipeline.Vbo.AddMemoryCopy(data); } vertices = data; @@ -1383,7 +1446,7 @@ public DynamicVertex[] RenderTwoSidedUpperOrLowerRaw(WallLocation location, Side WorldTriangulator.HandleTwoSidedUpper(facingSide, facingSector.Ceiling, otherSector.Ceiling, texture.UVInverse, isFrontSide, ref wall); else WorldTriangulator.HandleTwoSidedLower(facingSide, otherSector.Floor, facingSector.Floor, texture.UVInverse, isFrontSide, ref wall); - SetWallVertices(m_wallVertices, wall, GetLightLevelAdd(facingSide), lightIndex, lightIndex, 0, facingSide.Line.Id, location); + SetWallVertices(m_wallVertices, wall, GetLightLevelAdd(facingSide), lightIndex, lightIndex, 0, facingSide.Line.Id, location, 0); return m_wallVertices; } @@ -1511,9 +1574,9 @@ public void RenderTwoSidedMiddle(Side facingSide, Side otherSide, Sector facingS clipPlanes: GetTwoSidedMiddleClipPlanes(facingSide, otherSide, facingSector, otherSector), restrictSpan: restrictSpan); if (data == null) - data = GetWallVertices(wall, GetLightLevelAdd(facingSide), lightIndex, overrideLightIndex, GetWallLightLevel(facingSide, facingSide.Middle), line.Id, WallLocation.None, alpha, addAlpha: 0); + data = GetWallVertices(wall, GetLightLevelAdd(facingSide), lightIndex, overrideLightIndex, GetWallLightLevel(facingSide, facingSide.Middle), line.Id, WallLocation.None, texture.ArrayIndex, alpha, addAlpha: 0); else - SetWallVertices(data, wall, GetLightLevelAdd(facingSide), lightIndex, overrideLightIndex, GetWallLightLevel(facingSide, facingSide.Middle), line.Id, WallLocation.None, alpha, addAlpha: 0); + SetWallVertices(data, wall, GetLightLevelAdd(facingSide), lightIndex, overrideLightIndex, GetWallLightLevel(facingSide, facingSide.Middle), line.Id, WallLocation.None, texture.ArrayIndex, alpha, addAlpha: 0); SetCachedSide(m_vertexLookup, facingSide, data); line.RenderSegStart = saveStart; @@ -1523,7 +1586,8 @@ public void RenderTwoSidedMiddle(Side facingSide, Side otherSide, Sector facingS if (m_buffer) { var geometryType = facingSide == m_fogSide ? GeometryType.FogBarrier : GetWallType(facingSide, facingSide.Middle, null); - var renderData = m_worldDataManager.GetRenderData(texture, geometryType, brightmapTexture); + var arrayTexture = m_glTextureManager.GetParentArrayTexture(middleWall.TextureHandle, repeatY: facingSide.Flags.WrapMidTex); + var renderData = m_worldDataManager.GetRenderData(arrayTexture, geometryType, brightmapTexture); renderData.Pipeline.Vbo.AddMemoryCopy(data); } vertices = data; @@ -1805,7 +1869,7 @@ private bool RenderFlat(DynamicArray subsectors, SectorPlane renderPl { ref var second = ref m_subsectorVertices.Data[i]; ref var third = ref m_subsectorVertices.Data[i + 1]; - GetFlatVertices(lookupData, indexStart, ref root, ref second, ref third, lightIndex, overrideLightIndex, flatLightLevel, upper, lower, addAlpha, alpha); + GetFlatVertices(lookupData, indexStart, ref root, ref second, ref third, lightIndex, overrideLightIndex, flatLightLevel, upper, lower, addAlpha, alpha, texture.ArrayIndex); indexStart += 3; } } @@ -1815,7 +1879,8 @@ private bool RenderFlat(DynamicArray subsectors, SectorPlane renderPl vertices = lookupData; if (m_buffer) { - var renderData = m_worldDataManager.GetRenderData(texture, geometryType, brightmapTexture); + var arrayTexture = m_glTextureManager.GetParentArrayTexture(textureHandle); + var renderData = m_worldDataManager.GetRenderData(arrayTexture, geometryType, brightmapTexture); renderData.Pipeline.Vbo.AddMemoryCopy(lookupData); // Don't need to clip floor on lower view and ceiling on upper view if (sector.TransferHeights != null @@ -2015,7 +2080,7 @@ private static unsafe void CreateSkyFlatVertices(SkyGeometryVertex[] vertices, i } private static unsafe void SetWallVertices(DynamicVertex[] data, in WallVertices wv, int lightLevelAdd, int lightIndex, int overrideLightIndex, byte wallLightLevel, - int mapId, WallLocation location, float alpha = 1.0f, int addAlpha = 1) + int mapId, WallLocation location, int textureIndex, float alpha = 1.0f, int addAlpha = 1) { var uvFlags = UvFlags.Normal; if (wv.TopLeft.U > wv.BottomRight.U) @@ -2039,7 +2104,7 @@ private static unsafe void SetWallVertices(DynamicVertex[] data, in WallVertices vertex->V = wv.TopLeft.V; vertex->PrevU = wv.TopLeft.PrevU; vertex->PrevV = wv.TopLeft.PrevV; - vertex->SurfaceOptions = VertexOptions.PackSurface(1, alpha, addAlpha, upper, lower, overrideLightIndex); + vertex->SurfaceOptions = VertexOptions.PackSurface(1, alpha, addAlpha, upper, lower, overrideLightIndex, textureIndex); vertex->LightLevelAdd = lightLevelAddAndMapId; vertex->RenderOptions = renderOptions; @@ -2054,7 +2119,7 @@ private static unsafe void SetWallVertices(DynamicVertex[] data, in WallVertices vertex->V = wv.BottomRight.V; vertex->PrevU = wv.TopLeft.PrevU; vertex->PrevV = wv.BottomRight.PrevV; - vertex->SurfaceOptions = VertexOptions.PackSurface(1, alpha, addAlpha, upper, lower, overrideLightIndex); + vertex->SurfaceOptions = VertexOptions.PackSurface(1, alpha, addAlpha, upper, lower, overrideLightIndex, textureIndex); vertex->LightLevelAdd = lightLevelAddAndMapId; vertex->RenderOptions = renderOptions; @@ -2069,7 +2134,7 @@ private static unsafe void SetWallVertices(DynamicVertex[] data, in WallVertices vertex->V = wv.TopLeft.V; vertex->PrevU = wv.BottomRight.PrevU; vertex->PrevV = wv.TopLeft.PrevV; - vertex->SurfaceOptions = VertexOptions.PackSurface(1, alpha, addAlpha, upper, lower, overrideLightIndex); + vertex->SurfaceOptions = VertexOptions.PackSurface(1, alpha, addAlpha, upper, lower, overrideLightIndex, textureIndex); vertex->LightLevelAdd = lightLevelAddAndMapId; vertex->RenderOptions = renderOptions; @@ -2084,7 +2149,7 @@ private static unsafe void SetWallVertices(DynamicVertex[] data, in WallVertices vertex->V = wv.BottomRight.V; vertex->PrevU = wv.BottomRight.PrevU; vertex->PrevV = wv.BottomRight.PrevV; - vertex->SurfaceOptions = VertexOptions.PackSurface(0, alpha, addAlpha, upper, lower, overrideLightIndex); + vertex->SurfaceOptions = VertexOptions.PackSurface(0, alpha, addAlpha, upper, lower, overrideLightIndex, textureIndex); vertex->LightLevelAdd = lightLevelAddAndMapId; vertex->RenderOptions = renderOptions; @@ -2099,7 +2164,7 @@ private static unsafe void SetWallVertices(DynamicVertex[] data, in WallVertices vertex->V = wv.TopLeft.V; vertex->PrevU = wv.BottomRight.PrevU; vertex->PrevV = wv.TopLeft.PrevV; - vertex->SurfaceOptions = VertexOptions.PackSurface(0, alpha, addAlpha, upper, lower, overrideLightIndex); + vertex->SurfaceOptions = VertexOptions.PackSurface(0, alpha, addAlpha, upper, lower, overrideLightIndex, textureIndex); vertex->LightLevelAdd = lightLevelAddAndMapId; vertex->RenderOptions = renderOptions; @@ -2114,14 +2179,14 @@ private static unsafe void SetWallVertices(DynamicVertex[] data, in WallVertices vertex->V = wv.BottomRight.V; vertex->PrevU = wv.TopLeft.PrevU; vertex->PrevV = wv.BottomRight.PrevV; - vertex->SurfaceOptions = VertexOptions.PackSurface(0, alpha, addAlpha, upper, lower, overrideLightIndex); + vertex->SurfaceOptions = VertexOptions.PackSurface(0, alpha, addAlpha, upper, lower, overrideLightIndex, textureIndex); vertex->LightLevelAdd = lightLevelAddAndMapId; vertex->RenderOptions = renderOptions; } } private static unsafe DynamicVertex[] GetWallVertices(in WallVertices wv, int lightLevelAdd, int lightIndex, int overrideLightIndex, byte wallLightLevel, - int mapId, WallLocation location, float alpha = 1.0f, int addAlpha = 1) + int mapId, WallLocation location, int textureIndex, float alpha = 1.0f, int addAlpha = 1) { var uvFlags = UvFlags.Normal; if (wv.TopLeft.U > wv.BottomRight.U) @@ -2155,7 +2220,7 @@ private static unsafe DynamicVertex[] GetWallVertices(in WallVertices wv, int li vertex->V = wv.TopLeft.V; vertex->PrevU = wv.TopLeft.PrevU; vertex->PrevV = wv.TopLeft.PrevV; - vertex->SurfaceOptions = VertexOptions.PackSurface(1, alpha, addAlpha, upper, lower, overrideLightIndex); + vertex->SurfaceOptions = VertexOptions.PackSurface(1, alpha, addAlpha, upper, lower, overrideLightIndex, textureIndex); vertex->LightLevelAdd = lightLevelAddAndMapId; vertex->RenderOptions = renderOptions; @@ -2171,7 +2236,7 @@ private static unsafe DynamicVertex[] GetWallVertices(in WallVertices wv, int li vertex->V = wv.BottomRight.V; vertex->PrevU = wv.TopLeft.PrevU; vertex->PrevV = wv.BottomRight.PrevV; - vertex->SurfaceOptions = VertexOptions.PackSurface(1, alpha, addAlpha, upper, lower, overrideLightIndex); + vertex->SurfaceOptions = VertexOptions.PackSurface(1, alpha, addAlpha, upper, lower, overrideLightIndex, textureIndex); vertex->LightLevelAdd = lightLevelAddAndMapId; vertex->RenderOptions = renderOptions; @@ -2187,7 +2252,7 @@ private static unsafe DynamicVertex[] GetWallVertices(in WallVertices wv, int li vertex->V = wv.TopLeft.V; vertex->PrevU = wv.BottomRight.PrevU; vertex->PrevV = wv.TopLeft.PrevV; - vertex->SurfaceOptions = VertexOptions.PackSurface(1, alpha, addAlpha, upper, lower, overrideLightIndex); + vertex->SurfaceOptions = VertexOptions.PackSurface(1, alpha, addAlpha, upper, lower, overrideLightIndex, textureIndex); vertex->LightLevelAdd = lightLevelAddAndMapId; vertex->RenderOptions = renderOptions; @@ -2203,7 +2268,7 @@ private static unsafe DynamicVertex[] GetWallVertices(in WallVertices wv, int li vertex->V = wv.BottomRight.V; vertex->PrevU = wv.BottomRight.PrevU; vertex->PrevV = wv.BottomRight.PrevV; - vertex->SurfaceOptions = VertexOptions.PackSurface(0, alpha, addAlpha, upper, lower, overrideLightIndex); + vertex->SurfaceOptions = VertexOptions.PackSurface(0, alpha, addAlpha, upper, lower, overrideLightIndex, textureIndex); vertex->LightLevelAdd = lightLevelAddAndMapId; vertex->RenderOptions = renderOptions; @@ -2219,7 +2284,7 @@ private static unsafe DynamicVertex[] GetWallVertices(in WallVertices wv, int li vertex->V = wv.TopLeft.V; vertex->PrevU = wv.BottomRight.PrevU; vertex->PrevV = wv.TopLeft.PrevV; - vertex->SurfaceOptions = VertexOptions.PackSurface(0, alpha, addAlpha, upper, lower, overrideLightIndex); + vertex->SurfaceOptions = VertexOptions.PackSurface(0, alpha, addAlpha, upper, lower, overrideLightIndex, textureIndex); vertex->LightLevelAdd = lightLevelAddAndMapId; vertex->RenderOptions = renderOptions; @@ -2235,7 +2300,7 @@ private static unsafe DynamicVertex[] GetWallVertices(in WallVertices wv, int li vertex->V = wv.BottomRight.V; vertex->PrevU = wv.TopLeft.PrevU; vertex->PrevV = wv.BottomRight.PrevV; - vertex->SurfaceOptions = VertexOptions.PackSurface(0, alpha, addAlpha, upper, lower, overrideLightIndex); + vertex->SurfaceOptions = VertexOptions.PackSurface(0, alpha, addAlpha, upper, lower, overrideLightIndex, textureIndex); vertex->LightLevelAdd = lightLevelAddAndMapId; vertex->RenderOptions = renderOptions; } @@ -2244,9 +2309,9 @@ private static unsafe DynamicVertex[] GetWallVertices(in WallVertices wv, int li } private static unsafe void GetFlatVertices(DynamicVertex[] vertices, int startIndex, ref TriangulatedWorldVertex root, ref TriangulatedWorldVertex second, ref TriangulatedWorldVertex third, - int lightIndex, int overrideLightIndex, int flatLightLevel, int upper, int lower, int addAlpha, float alpha) + int lightIndex, int overrideLightIndex, int flatLightLevel, int upper, int lower, int addAlpha, float alpha, int textureIndex) { - var surfaceOptions = VertexOptions.PackSurface(0, alpha, addAlpha, upper, lower, overrideLightIndex); + var surfaceOptions = VertexOptions.PackSurface(0, alpha, addAlpha, upper, lower, overrideLightIndex, textureIndex); var renderOptions = VertexOptions.PackRender(lightIndex, flatLightLevel); fixed (DynamicVertex* startVertex = &vertices[startIndex]) { diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Portals/FloodFill/FloodFillInfo.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Portals/FloodFill/FloodFillInfo.cs index df3bc7965..c5a1ce850 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Portals/FloodFill/FloodFillInfo.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Portals/FloodFill/FloodFillInfo.cs @@ -1,8 +1,9 @@ -using System; +using Helion.Render.OpenGL.Texture.Legacy; +using System; namespace Helion.Render.OpenGL.Renderers.Legacy.World.Geometry.Portals.FloodFill; -public record FloodFillInfo(int TextureHandle, double Z, VertexPipeline Pipeline) : IDisposable +public record FloodFillInfo(int TextureHandle, GLLegacyTexture Texture, double Z, VertexPipeline Pipeline) : IDisposable { private bool m_disposed; diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Portals/FloodFill/FloodFillProgram.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Portals/FloodFill/FloodFillProgram.cs index 146b9a661..7fdad609c 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Portals/FloodFill/FloodFillProgram.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Portals/FloodFill/FloodFillProgram.cs @@ -41,6 +41,7 @@ protected override string VertexShader() => @" flat out float mapIdFrag; flat out float upperFrag; flat out float lowerFrag; + flat out float boundTextureIndex; out float depthFrag; ${SectorColorMapVertexFragVariables} @@ -101,13 +102,14 @@ protected override string FragmentShader() flat in float mapIdFrag; flat in float upperFrag; flat in float lowerFrag; + flat in float boundTextureIndex; in vec3 vertexPosFrag; in float dist2D; out vec4 fragColor; - uniform sampler2D boundTexture; - uniform sampler2D brightmapTexture; + uniform sampler2DArray boundTexture; + uniform sampler2DArray brightmapTexture; uniform vec3 camera; uniform mat4 mvpNoPitch; uniform mat4 mvp; @@ -128,7 +130,7 @@ void main() vec3 lookDir = normalize(vertexPosFrag - camera); float planeDot = dot(pointOnPlane - camera, planeNormal) / dot(lookDir, planeNormal); vec3 planePos = camera + (lookDir * planeDot); - vec2 texDim = textureSize(boundTexture, 0); + vec2 texDim = textureSize(boundTexture, 0).xy; vec2 uvFrag = vec2(planePos.x / texDim.x, planePos.y / texDim.y); uvFrag.y = -uvFrag.y; // Vanilla textures are drawn top-down. diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Portals/FloodFill/FloodFillRenderer.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Portals/FloodFill/FloodFillRenderer.cs index 649fe43dd..983e49c30 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Portals/FloodFill/FloodFillRenderer.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Portals/FloodFill/FloodFillRenderer.cs @@ -10,7 +10,9 @@ using Helion.Render.OpenGL.Texture.Legacy; using Helion.Resources; using Helion.Util; +using Helion.Util.Assertion; using Helion.Util.Container; +using Helion.Util.Profiling.Timers; using Helion.World; using Helion.World.Geometry.Sectors; using Helion.World.Static; @@ -18,7 +20,7 @@ namespace Helion.Render.OpenGL.Renderers.Legacy.World.Geometry.Portals.FloodFill; -public class FloodFillRenderer(LegacyGLTextureManager glTextureManager, FloodFillRenderMode renderMode) : IDisposable +public class FloodFillRenderer(LegacyGLTextureManager glTextureManager, FloodFillRenderMode renderMode, RenderProfiler renderProfiler) : IDisposable { const int FloodPlaneAddCount = 2; const int VerticesPerWall = 6; @@ -32,6 +34,7 @@ public class FloodFillRenderer(LegacyGLTextureManager glTextureManager, FloodFil private readonly DynamicArray m_floodGeometry = []; private readonly LinkedList m_freeData = []; private readonly DynamicArray> m_freeNodes = []; + private readonly RenderProfiler m_renderProfiler = renderProfiler; private TextureManager? m_textureManager; private bool m_disposed; @@ -52,16 +55,19 @@ private FloodFillInfo CreateFloodFillInfo(SectorPlane plane) { var label = GetLabel(plane); var pipeline = new VertexPipeline(m_program, new StaticVertexBuffer(label), label); - return new(plane.TextureHandle, plane.Z, pipeline); + + var texture = m_glTextureManager.GetParentArrayTexture(plane.TextureHandle); + return new(plane.TextureHandle, texture, plane.Z, pipeline); } - private unsafe FloodFillInfo GetOrCreateFloodFillInfo(SectorPlane plane) + private FloodFillInfo GetOrCreateFloodFillInfo(SectorPlane plane) { - if (m_textureHandleToFloodFillInfoIndex.TryGetValue(plane.TextureHandle, out int index)) + var textureHandle = m_glTextureManager.GetWorldArrayTextureHandle(plane.TextureHandle, true); + if (m_textureHandleToFloodFillInfoIndex.TryGetValue(textureHandle, out int index)) return m_floodFillInfos[index]; - FloodFillInfo floodInfo = CreateFloodFillInfo(plane); - m_textureHandleToFloodFillInfoIndex[plane.TextureHandle] = m_floodFillInfos.Count; + var floodInfo = CreateFloodFillInfo(plane); + m_textureHandleToFloodFillInfoIndex[textureHandle] = m_floodFillInfos.Count; m_floodFillInfos.Add(floodInfo); return floodInfo; } @@ -79,6 +85,14 @@ private ref FloodGeometry TryGetFloodGeometry(int floodKey, out bool success) return ref m_floodGeometry.Data[floodKey]; } + private bool TryGetFloodFillInfoIndex(int textureHandle, out int index) + { + textureHandle = m_glTextureManager.GetWorldArrayTextureHandle(textureHandle, true); + var success = m_textureHandleToFloodFillInfoIndex.TryGetValue(textureHandle, out index); + Assert.Precondition(success, $"Failed to find flood fill info for {textureHandle}"); + return success; + } + public void UpdateStaticWall(int floodKey, SectorPlane floodPlane, WallVertices vertices, double minPlaneZ, double maxPlaneZ, SideTexture sideTexture, int mapId, bool isFloodFillPlane = false) { @@ -95,7 +109,7 @@ public void UpdateStaticWall(int floodKey, SectorPlane floodPlane, WallVertices return; } - if (!m_textureHandleToFloodFillInfoIndex.TryGetValue(data.TextureHandle, out int index)) + if (!TryGetFloodFillInfoIndex(data.TextureHandle, out int index)) return; FloodFillInfo floodInfo = m_floodFillInfos[index]; @@ -111,7 +125,8 @@ public void UpdateStaticWall(int floodKey, SectorPlane floodPlane, WallVertices var upper = sideTexture == SideTexture.Upper ? 1 : 0; var lower = sideTexture == SideTexture.Lower ? 1 : 0; - var options = VertexOptions.PackSurface(0, 0, 0, upper, lower, data.OverrideLightIndex); + var textureIndex = m_glTextureManager.GetTexture(floodPlane.TextureHandle).ArrayIndex; + var options = VertexOptions.PackSurface(0, 0, 0, upper, lower, data.OverrideLightIndex, textureIndex); FloodFillVertex topLeft = new((vertices.TopLeft.X, vertices.TopLeft.Y, topZ), prevTopZ, planeZ, prevPlaneZ, minZ, maxZ, options, data.RenderOptions, mapId); @@ -132,7 +147,7 @@ public void UpdateStaticWall(int floodKey, SectorPlane floodPlane, WallVertices if (isFloodFillPlane) ProjectFloodPlane(vbo, data.VboOffset + VerticesPerWall, vertices, minZ, maxZ, planeZ, prevPlaneZ, data.OverrideLightIndex, - maxPlaneZ > Constants.MaxTextureHeight ? -Constants.MaxTextureHeight : Constants.MaxTextureHeight, false, data.RenderOptions, mapId); + maxPlaneZ > Constants.MaxTextureHeight ? -Constants.MaxTextureHeight : Constants.MaxTextureHeight, false, data.RenderOptions, mapId, textureIndex); vbo.Bind(); vbo.UploadSubData(data.VboOffset, data.Vertices); @@ -183,7 +198,8 @@ public int AddStaticWall(SectorPlane sectorPlane, in WallVertices vertices, doub var upper = sideTexture == SideTexture.Upper ? 1 : 0; var lower = sideTexture == SideTexture.Lower ? 1 : 0; - var options = VertexOptions.PackSurface(0, 0, 0, upper, lower, overrideLightIndex); + var textureIndex = m_glTextureManager.GetTexture(sectorPlane.TextureHandle).ArrayIndex; + var options = VertexOptions.PackSurface(0, 0, 0, upper, lower, overrideLightIndex, textureIndex); FloodFillVertex topLeft = new((vertices.TopLeft.X, vertices.TopLeft.Y, vertices.TopLeft.Z), vertices.TopLeft.Z, planeZ, prevPlaneZ, minZ, maxZ, options, lightBufferIndex, mapId); @@ -208,7 +224,7 @@ public int AddStaticWall(SectorPlane sectorPlane, in WallVertices vertices, doub if (isFloodFillPlane) ProjectFloodPlane(vbo, vbo.Data.Length, vertices, minZ, maxZ, planeZ, prevPlaneZ, lightIndex, - maxPlaneZ > Constants.MaxTextureHeight ? -Constants.MaxTextureHeight : Constants.MaxTextureHeight, true, lightIndex, mapId); + maxPlaneZ > Constants.MaxTextureHeight ? -Constants.MaxTextureHeight : Constants.MaxTextureHeight, true, lightIndex, mapId, textureIndex); if (m_renderMode == FloodFillRenderMode.Dynamic) return 0; @@ -217,7 +233,7 @@ public int AddStaticWall(SectorPlane sectorPlane, in WallVertices vertices, doub } private static void ProjectFloodPlane(VertexBufferObject vbo, int startIndex, - WallVertices vertices, float minZ, float maxZ, float planeZ, float prevPlaneZ, int overrideLightIndex, int addHeight, bool add, float renderOptions, int mapId) + WallVertices vertices, float minZ, float maxZ, float planeZ, float prevPlaneZ, int overrideLightIndex, int addHeight, bool add, float renderOptions, int mapId, int textureIndex) { int newLength = startIndex + FloodPlaneAddCount * VerticesPerWall; vbo.Data.EnsureCapacity(newLength); @@ -231,7 +247,7 @@ private static void ProjectFloodPlane(VertexBufferObject vbo, i float prevTopZ = vertices.PrevTopZ + currentAddHeight; float prevBottomZ = vertices.PrevBottomZ + currentAddHeight; - var surfaceOptions = VertexOptions.PackSurface(0, 0, 0, 0, 0, overrideLightIndex); + var surfaceOptions = VertexOptions.PackSurface(0, 0, 0, 0, 0, overrideLightIndex, textureIndex); FloodFillVertex topLeft = new((vertices.TopLeft.X, vertices.TopLeft.Y, topLeftZ), prevTopZ, planeZ, prevPlaneZ, minZ, maxZ, surfaceOptions, renderOptions, mapId); FloodFillVertex topRight = new((vertices.BottomRight.X, vertices.BottomRight.Y, topLeftZ), @@ -262,7 +278,9 @@ public void ClearStaticWall(int floodKey) ref var data = ref TryGetFloodGeometry(floodKey, out var success); if (success) { - int listIndex = m_textureHandleToFloodFillInfoIndex[data.TextureHandle]; + if (!TryGetFloodFillInfoIndex(data.TextureHandle, out var listIndex)) + return; + FloodFillInfo info = m_floodFillInfos[listIndex]; OverwriteAndSubUploadVboWithZero(info.Pipeline.Vbo, data.VboOffset, data.Vertices); @@ -320,12 +338,17 @@ private void Render(FloodFillProgram program, RenderInfo renderInfo, bool wallCl if (brightmapTexture != null) brightmapTexture.Bind(); else - GL.BindTexture(TextureTarget.Texture2D, 0); + GL.BindTexture(texture.Target, 0); } info.Pipeline.Vbo.UploadIfNeeded(); info.Pipeline.Bind(); info.Pipeline.DrawArrays(); + + if (m_renderMode == FloodFillRenderMode.Dynamic) + m_renderProfiler.DrawCounts.FloodFillDynamic++; + else + m_renderProfiler.DrawCounts.FloodFillStatic++; } } diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Portals/PortalRenderer.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Portals/PortalRenderer.cs index 6675924d3..5fd1e8233 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Portals/PortalRenderer.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Portals/PortalRenderer.cs @@ -14,6 +14,7 @@ using Helion.World.Geometry.Lines; using Helion.Resources; using Helion.World.Geometry.Walls; +using Helion.Util.Profiling.Timers; namespace Helion.Render.OpenGL.Renderers.Legacy.World.Geometry.Portals; @@ -39,11 +40,11 @@ public class PortalRenderer : IDisposable private bool m_disposed; - public PortalRenderer(ArchiveCollection archiveCollection, LegacyGLTextureManager glTextureManager) + public PortalRenderer(ArchiveCollection archiveCollection, LegacyGLTextureManager glTextureManager, RenderProfiler renderProfiler) { m_archiveCollection = archiveCollection; - m_floodFillStatic = new(glTextureManager, FloodFillRenderMode.Static); - m_floodFillDynamic = new(glTextureManager, FloodFillRenderMode.Dynamic); + m_floodFillStatic = new(glTextureManager, FloodFillRenderMode.Static, renderProfiler); + m_floodFillDynamic = new(glTextureManager, FloodFillRenderMode.Dynamic, renderProfiler); m_floodFillRenderer = m_floodFillStatic; m_transferHeightView = TransferHeightView.Middle; } diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Static/FreeGeometryManager.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Static/FreeGeometryManager.cs index a174e119a..ba637955d 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Static/FreeGeometryManager.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Static/FreeGeometryManager.cs @@ -1,4 +1,5 @@ -using Helion.Util.Assertion; +using Helion.Render.OpenGL.Texture; +using Helion.Util.Assertion; using Helion.World.Static; using System; using System.Collections.Generic; @@ -29,8 +30,9 @@ public override bool Equals(object? obj) } } -public class FreeGeometryManager +public class FreeGeometryManager(IArrayTextureLookup arrayTextureLookup) { + private readonly IArrayTextureLookup m_arrayTextureLookup = arrayTextureLookup; private readonly Dictionary m_data = new(128); public void Add(in StaticGeometryData geometryData, GeometryType type, bool repeatY) @@ -38,7 +40,7 @@ public void Add(in StaticGeometryData geometryData, GeometryType type, bool repe if (geometryData.GeometryData == null) return; - var textureHandle = geometryData.GeometryData.TextureHandle; + var textureHandle = m_arrayTextureLookup.GetWorldArrayTextureHandle(geometryData.GeometryData.TextureHandle, repeatY); var key = new GeometryKey(textureHandle, type, repeatY); if (!m_data.TryGetValue(key, out var list)) @@ -86,6 +88,7 @@ public bool GetAndRemove(int textureHandle, GeometryType type, bool repeatY, int { int minLength = int.MaxValue; int minIndex = -1; + textureHandle = m_arrayTextureLookup.GetWorldArrayTextureHandle(textureHandle, repeatY); var key = new GeometryKey(textureHandle, type, repeatY); if (!m_data.TryGetValue(key, out var list)) diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Static/GeometryData.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Static/GeometryData.cs index 43d1ce8d0..10873770c 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Static/GeometryData.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Static/GeometryData.cs @@ -1,6 +1,4 @@ -using Helion.Render.OpenGL.Buffer.Array.Vertex; -using Helion.Render.OpenGL.Texture.Legacy; -using Helion.Render.OpenGL.Vertex; +using Helion.Render.OpenGL.Texture.Legacy; namespace Helion.Render.OpenGL.Renderers.Legacy.World.Geometry.Static; @@ -23,4 +21,9 @@ public void Dispose() { Pipeline.Dispose(); } + + public override string ToString() + { + return Texture.ToString(); + } } diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Static/GeometryTextureLookup.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Static/GeometryTextureLookup.cs index 51c7ff289..74ef0a5d1 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Static/GeometryTextureLookup.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Static/GeometryTextureLookup.cs @@ -1,10 +1,12 @@ -using System.Diagnostics.CodeAnalysis; +using Helion.Render.OpenGL.Texture; +using System.Diagnostics.CodeAnalysis; namespace Helion.Render.OpenGL.Renderers.Legacy.World.Geometry.Static; -public class GeometryTextureLookup +public class GeometryTextureLookup(IArrayTextureLookup arrayTextureLookup) { private readonly GeometryTypeLookup m_lookup = new(() => new TextureGeometryLookup()); + private readonly IArrayTextureLookup m_arrayTextureLookup = arrayTextureLookup; public void Clear() { @@ -15,11 +17,13 @@ public void Clear() public bool TryGetValue(GeometryType type, int textureHandle, bool repeatY, [NotNullWhen(true)] out GeometryData? value) { + textureHandle = m_arrayTextureLookup.GetWorldArrayTextureHandle(textureHandle, repeatY); return m_lookup.Get(type).TryGetValue(textureHandle, repeatY, out value); } public void Add(GeometryType type, int textureHandle, bool repeatY, GeometryData data) { + textureHandle = m_arrayTextureLookup.GetWorldArrayTextureHandle(textureHandle, repeatY); m_lookup.Get(type).Add(textureHandle, repeatY, data); } } diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Static/RenderGeometry.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Static/RenderGeometry.cs index df81389d6..1f9f5e2c5 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Static/RenderGeometry.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Static/RenderGeometry.cs @@ -1,5 +1,4 @@ using Helion.Util.Container; -using Helion.World.Geometry; using System.Collections.Generic; namespace Helion.Render.OpenGL.Renderers.Legacy.World.Geometry.Static; diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Static/StaticCacheGeometryRenderer.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Static/StaticCacheGeometryRenderer.cs index 82ae4bde2..e3699da72 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Static/StaticCacheGeometryRenderer.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/Static/StaticCacheGeometryRenderer.cs @@ -13,6 +13,7 @@ using Helion.Util; using Helion.Util.Assertion; using Helion.Util.Container; +using Helion.Util.Profiling.Timers; using Helion.World; using Helion.World.Geometry.Lines; using Helion.World.Geometry.Sectors; @@ -40,10 +41,11 @@ public partial class StaticCacheGeometryRenderer : StyleRendererBase, IDisposabl private readonly FloodFillRenderer m_floodFillRenderer; private readonly RenderProgram m_program; private readonly RenderGeometry m_geometry = new(); + private readonly RenderProfiler m_renderProfiler; - private readonly GeometryTextureLookup m_textureToGeometryLookup = new(); + private readonly GeometryTextureLookup m_textureToGeometryLookup; - private readonly FreeGeometryManager m_freeManager = new(); + private readonly FreeGeometryManager m_freeManager; private readonly LegacySkyRenderer m_skyRenderer; private readonly LookupArray?> m_transferHeightsLookup = new(); @@ -64,14 +66,17 @@ public partial class StaticCacheGeometryRenderer : StyleRendererBase, IDisposabl private readonly bool m_vanillaRender; public StaticCacheGeometryRenderer(ArchiveCollection archiveCollection, LegacyGLTextureManager textureManager, - RenderProgram program, GeometryRenderer geometryRenderer) + RenderProgram program, RenderProfiler renderProfiler, GeometryRenderer geometryRenderer) { m_archiveCollection = archiveCollection; m_textureManager = textureManager; m_geometryRenderer = geometryRenderer; m_floodFillRenderer = geometryRenderer.Portals.GetStaticFloodFillRenderer(); m_program = program; - m_skyRenderer = new(archiveCollection, textureManager); + m_skyRenderer = new(archiveCollection, textureManager, renderProfiler); + m_textureToGeometryLookup = new(textureManager); + m_freeManager = new(textureManager); + m_renderProfiler = renderProfiler; m_renderCoverWallAction = AddOrUpdateCoverWall; m_renderOneSidedSliceFunc = m_geometryRenderer.RenderOneSidedSlice; @@ -611,7 +616,7 @@ private static void AddVertices(DynamicArray staticVertices, Span< v.SurfaceOptions, v.LightLevelAdd, v.RenderOptions); } - staticVertices.SetLength(staticVertices.Length + vertices.Length); + staticVertices.SetLength(staticVertices.Length + vertices.Length); } private static void CopyVertices(StaticVertex[] staticVertices, Span vertices, int index) @@ -671,7 +676,13 @@ private GeometryData AllocateGeometryData(GeometryType type, int textureHandle, { vboSize = Math.Max(vboSize, 32); label ??= GetGeometryLabel(type, textureHandle, repeat); - var texture = overrideTexture ?? m_textureManager.GetTexture(textureHandle, repeat); + + GLLegacyTexture? texture; + if (overrideTexture != null) + texture = overrideTexture.ParentArrayTexture ?? overrideTexture; + else + texture = m_textureManager.GetParentArrayTexture(textureHandle, repeat); + var brightmapTexture = m_textureManager.GetBrightmapTexture(textureHandle, repeat); var vbo = new StaticVertexBuffer(label, vboSize); var pipeline = new VertexPipeline(m_program, vbo, label); @@ -738,12 +749,6 @@ private static void ClearVbo(VertexBufferObject? vbo) where T : struct vbo.Data.Clear(); } - private static void ClearBufferData(DynamicArray?> bufferData) - { - for (int i = 0; i < bufferData.Capacity; i++) - bufferData.Data[i]?.FlushStruct(); - } - private void AddSectorPlane(Sector sectorForSubsectors, SectorPlaneFace face, bool floor, bool update = false, Sector? renderSector = null, Sector? lightLevelSector = null, SectorPlane? geometryPlane = null, bool allowAlpha = false, Sector3D? sector3D = null) { @@ -838,7 +843,7 @@ private static void RenderCoverInternal(GeometryData? data) GL.ActiveTexture(BindTextures.BoundTexture); texture.Bind(); GL.ActiveTexture(BindTextures.BrightmapTexture); - GL.BindTexture(TextureTarget.Texture2D, 0); + GL.BindTexture(data.Texture.Target, 0); data.Pipeline.Vbo.UploadCapacity(); data.Pipeline.Bind(); @@ -856,13 +861,14 @@ private void RenderGeometry(List geometry) if (data.Pipeline.Vbo.Count == 0) continue; - GL.ActiveTexture(BindTextures.BoundTexture); bool isNullCompatTex = data.TextureHandle <= Constants.NullCompatibilityTextureIndex; bool repeatY = (data.Texture.Flags & TextureFlags.ClampY) == 0; // Special case for one-sided walls with no texture. Uses black texture to block rendering so use directly. var texture = isNullCompatTex ? data.Texture : m_textureManager.GetTexture(data.TextureHandle, repeatY); + + GL.ActiveTexture(BindTextures.BoundTexture); texture.Bind(); var brightmapTexture = isNullCompatTex @@ -872,12 +878,14 @@ private void RenderGeometry(List geometry) if (brightmapTexture != null) brightmapTexture.Bind(); else - GL.BindTexture(TextureTarget.Texture2D, 0); + GL.BindTexture(texture.Target, 0); data.Pipeline.Vbo.UploadIfNeeded(); data.Pipeline.Bind(); data.Pipeline.DrawArrays(); + + m_renderProfiler.DrawCounts.GeometryStatic++; } } @@ -1227,6 +1235,7 @@ private void World_SideTextureChanged(object? sender, SideTextureEvent e) CheckForFloodFill(e.Side, e.Side.PartnerSide, e.Side.Sector, e.Side.PartnerSide.Sector, e.Side.IsFront); } + m_geometryRenderer.SetBuffer(false); m_geometryRenderer.SetRenderMode(GeometryRenderMode.Dynamic, TransferHeightView.Middle); AddLine(e.Side.Line, update: true); @@ -1268,6 +1277,7 @@ private void World_PlaneTextureChanged(object? sender, PlaneTextureEvent e) e.Plane.Static.GeometryData = null; + m_geometryRenderer.SetBuffer(false); m_geometryRenderer.SetRenderMode(GeometryRenderMode.Dynamic, TransferHeightView.Middle); if (WorldStatic.Sector3D && e.Plane.Sector.TaggedSectors3D.Length > 0) @@ -1313,6 +1323,10 @@ private void UpdateVertices(ref StaticGeometryData staticGeometry, int textureHa if (textureHandle <= Constants.NullCompatibilityTextureIndex && !isOneSidedLine) return; + // This doesn't practically matter but makes debugging nicer + if (texture != null) + texture = texture.ParentArrayTexture ?? texture; + // If this surface generated more vertices than previously cached, release so a new one can be requested. (happens with 3D sectors) if (staticGeometry.GeometryData != null && staticGeometry.Length < vertices.Length) { @@ -1526,7 +1540,7 @@ private void SetRuntimeGeometryData(SectorPlane? plane, Side? side, Wall? wall, } } - private static unsafe void ClearGeometryVertices(GeometryData geometryData, int startIndex, int length) + private static void ClearGeometryVertices(GeometryData geometryData, int startIndex, int length) { ref var reference = ref geometryData.Pipeline.Vbo.Data.Data[startIndex]; Unsafe.InitBlockUnaligned(ref Unsafe.As(ref reference), 0, (uint)(Marshal.SizeOf() * length)); diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/InterpolationShader.cs b/Core/Render/OpenGL/Renderers/Legacy/World/InterpolationShader.cs index 138d87fcb..cd5309f7f 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/InterpolationShader.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/InterpolationShader.cs @@ -1,9 +1,4 @@ -using GlmSharp; -using Helion.Geometry.Vectors; using Helion.Render.OpenGL.Renderers.Legacy.World.Shader; -using Helion.Render.OpenGL.Shader; -using Helion.Util.Configs.Components; -using OpenTK.Graphics.OpenGL; namespace Helion.Render.OpenGL.Renderers.Legacy.World; @@ -38,6 +33,7 @@ protected static string VertexShaderInternal(bool planeClip) => @" flat out float mapIdFrag; flat out float upperFrag; flat out float lowerFrag; + flat out float boundTextureIndex; out float depthFrag; ${VertexGapVariables} ${VertexDistVar3D} @@ -50,7 +46,7 @@ protected static string VertexShaderInternal(bool planeClip) => @" uniform mat4 mvp; uniform float timeFrac; uniform int vertexGapClampUV; - uniform sampler2D boundTexture; + uniform sampler2DArray boundTexture; uniform int useSectorColor; uniform int useSectorFog; @@ -118,6 +114,7 @@ protected override string FragmentShader() flat in float distFrag; flat in float upperFrag; flat in float lowerFrag; + flat in float boundTextureIndex; in float depthFrag; ${VertexGapVariables} ${VertexDistVar3D} @@ -125,8 +122,8 @@ protected override string FragmentShader() ${OutTargets} uniform int hasInvulnerability; - uniform sampler2D boundTexture; - uniform sampler2D brightmapTexture; + uniform sampler2DArray boundTexture; + uniform sampler2DArray brightmapTexture; uniform vec3 colorMix; uniform int paletteIndex; uniform int colormapIndex; diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs index 234ce3955..ce42007bb 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs @@ -17,7 +17,6 @@ using Helion.Util.Configs; using Helion.Util.Configs.Components; using Helion.Util.Profiling.Timers; -using Helion.Util.Timing; using Helion.World; using Helion.World.Entities; using Helion.World.Geometry.Sectors; @@ -30,6 +29,8 @@ namespace Helion.Render.OpenGL.Renderers.Legacy.World; public partial class LegacyWorldRenderer : WorldRenderer { + public override TextureBuckets SpriteTextureBuckets { get; protected set; } + const int EntityRenderIndexMax = 100; private static readonly Logger Log = LogManager.GetCurrentClassLogger(); private readonly IConfig m_config; @@ -67,6 +68,7 @@ public partial class LegacyWorldRenderer : WorldRenderer private bool m_downscaleVanillaBuffer; private bool m_postProcessingEffects; private bool m_lastUseBsp; + private bool m_spritesCached; private int m_lastTicker = -1; private Entity? m_viewerEntity; private IWorld? m_previousWorld; @@ -79,15 +81,20 @@ public LegacyWorldRenderer(IConfig config, ArchiveCollection archiveCollection, { m_config = config; m_renderProfiler = renderProfiler; - m_entityRenderer = new(config, textureManager, archiveCollection); + m_entityRenderer = new(config, textureManager, archiveCollection, renderProfiler); m_primitiveRenderer = new(); - m_worldDataManager = new(m_interpolationProgram); - m_geometryRenderer = new(config, archiveCollection, textureManager, m_interpolationProgram, m_staticProgram, m_worldDataManager, m_viewClipper, m_viewClipperPrev); + m_worldDataManager = new(m_interpolationProgram, OnDynamicDraw); + m_geometryRenderer = new(config, archiveCollection, textureManager, m_interpolationProgram, m_staticProgram, m_worldDataManager, m_viewClipper, m_viewClipperPrev, renderProfiler); m_archiveCollection = archiveCollection; m_textureManager = textureManager; m_vanillaRender = config.Render.VanillaRender; } + private void OnDynamicDraw() + { + m_renderProfiler.DrawCounts.GeometryDynamic++; + } + ~LegacyWorldRenderer() { ReleaseUnmanagedResources(); @@ -117,14 +124,11 @@ public override void UpdateToNewWorld(IWorld world) m_previousWorld?.OnResetInterpolation -= World_OnResetInterpolation; - var spriteDefinitions = m_archiveCollection.TextureManager.SpriteDefinitions; - for (int i = 0; i < spriteDefinitions.Length; i++) + if (!m_spritesCached) { - var spriteDefinition = spriteDefinitions[i]; - if (spriteDefinition == null) - continue; - - m_textureManager.CacheSpriteRotations(spriteDefinition); + var textureArrayBuilder = new GLTextureArrayBuilder(m_archiveCollection.TextureManager, m_textureManager); + SpriteTextureBuckets = textureArrayBuilder.BuildSprites(m_archiveCollection.TextureManager.SpriteDefinitions); + m_spritesCached = true; } m_geometryRenderer.UpdateTo(world); diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Shader/FragFunction.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Shader/FragFunction.cs index 8c52bdf80..9942a295b 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Shader/FragFunction.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Shader/FragFunction.cs @@ -165,7 +165,7 @@ public static string ColorMapFetch(bool lightLevel, ColorMapFetchContext ctx) static string BrightMapLightColorIndexFetch(string uvVar) => @$" - vec3 brightColor = texture(brightmapTexture, {uvVar}).rgb; + vec3 brightColor = texture(brightmapTexture, vec3({uvVar}, boundTextureIndex)).rgb; float hasBrightColor = float(brightColor.r != 0 && brightColor.r == brightColor.g && brightColor.g == brightColor.b); lightColorIndex = int(mix(lightColorIndex, min(int((1 - brightColor.r) * 31), lightColorIndex), hasBrightColor)); "; @@ -186,7 +186,7 @@ public static string FragColorFunction(FragColorFunctionOptions options, ColorMa var fragColor = @$" {textureMappingClamp} - {declareFragColor} = texture(boundTexture, texUV);"; + {declareFragColor} = texture(boundTexture, vec3(texUV, boundTextureIndex));"; return fragColor + @@ -232,7 +232,7 @@ private static string GetBrightMapBlend(FragColorFunctionOptions options) return @" if (useBrightmaps == 1) - fragColor.rgb *= min(vec3(1.0), texture(brightmapTexture, texUV).rgb + vec3(lightLevel)); + fragColor.rgb *= min(vec3(1.0), texture(brightmapTexture, vec3(texUV, boundTextureIndex)).rgb + vec3(lightLevel)); else fragColor.rgb *= lightLevel;"; } diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Shader/PlaneClip.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Shader/PlaneClip.cs index 8d6c78eef..c0540e577 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Shader/PlaneClip.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Shader/PlaneClip.cs @@ -1,6 +1,4 @@ -using System; - -namespace Helion.Render.OpenGL.Renderers.Legacy.World.Shader; +namespace Helion.Render.OpenGL.Renderers.Legacy.World.Shader; public enum WallClipFragOptions { @@ -27,10 +25,11 @@ public static string WritePlaneFragFunction(PlaneClipFragOptions options) flat in float zPos; flat in float upperFrag; flat in float lowerFrag; + flat in float boundTextureIndex; in float depthFrag; ${{InVars}} - uniform sampler2D boundTexture; + uniform sampler2DArray boundTexture; layout (location = 0) out vec3 outPlane; @@ -53,10 +52,11 @@ public static string WriteWallFragFunction(WallClipFragOptions options) flat in float mapIdFrag; flat in float upperFrag; flat in float lowerFrag; + flat in float boundTextureIndex; in float depthFrag; ${InVars} - uniform sampler2D boundTexture; + uniform sampler2DArray boundTexture; layout (location = 0) out vec4 outPlane; @@ -109,7 +109,7 @@ private static string GetAlphaSample(bool alphaSample) if (!alphaSample) return ""; - return @"float alpha = texture(boundTexture, uvFrag.xy).a; + return @"float alpha = texture(boundTexture, vec3(uvFrag.xy, boundTextureIndex)).a; if (alpha <= 0) discard;"; } diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Shader/VertexFunction.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Shader/VertexFunction.cs index 0983b268e..ac4bf4a32 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Shader/VertexFunction.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Shader/VertexFunction.cs @@ -18,7 +18,7 @@ public static class VertexFunction uvClampMaxFrag = vec2(MaxValue, MaxValue); if (vertexGapClampUV == 1) { - ivec2 texSize = textureSize(boundTexture, 0); + ivec2 texSize = textureSize(boundTexture, 0).xy; const float VertexGapX = 0.1; float pixelSize = uvFrag.y * texSize.y; // Push y further since it's more likely to show t-junction issue with subsector flat splits @@ -45,7 +45,8 @@ public static class VertexFunction addAlphaFrag = float((intOptions >> 9) & 1); upperFrag = float((intOptions >> 10) & 1); lowerFrag = float((intOptions >> 11) & 1); - float overrideLightIndex = float(float(intOptions >> 12));"; + float overrideLightIndex = float((intOptions >> 12) & 1); + boundTextureIndex = float((intOptions >> 13) & 0xFFFFF);"; public static string ColorMapAndLightLevelSet => @" diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Sky/LegacySkyRenderer.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Sky/LegacySkyRenderer.cs index b1e990f0a..ac907dd38 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Sky/LegacySkyRenderer.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Sky/LegacySkyRenderer.cs @@ -8,12 +8,13 @@ using Helion.Render.OpenGL.Texture.Legacy; using Helion.Resources.Archives.Collection; using Helion.Resources.Definitions; +using Helion.Util.Profiling.Timers; using OpenTK.Graphics.OpenGL; using static Helion.Util.Assertion.Assert; namespace Helion.Render.OpenGL.Renderers.Legacy.World.Sky; -public class LegacySkyRenderer(ArchiveCollection archiveCollection, LegacyGLTextureManager glTextureManager) : IDisposable +public class LegacySkyRenderer(ArchiveCollection archiveCollection, LegacyGLTextureManager glTextureManager, RenderProfiler renderProfiler) : IDisposable { private const int MaxSkyTextures = 255; @@ -23,6 +24,7 @@ public class LegacySkyRenderer(ArchiveCollection archiveCollection, LegacyGLText private readonly LegacyGLTextureManager m_glTextureManager = glTextureManager; private readonly Dictionary m_skyComponents = []; private readonly List m_skyComponentsList = []; + private readonly RenderProfiler m_renderProfiler = renderProfiler; ~LegacySkyRenderer() { @@ -108,6 +110,7 @@ public void Render(RenderInfo renderInfo) sky.RenderSky(renderInfo); GL.Enable(EnableCap.DepthTest); + m_renderProfiler.DrawCounts.Skies++; } GL.Disable(EnableCap.StencilTest); diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Sky/Sphere/SkySphereForegroundShader.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Sky/Sphere/SkySphereForegroundShader.cs index 97955510c..369c82b08 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Sky/Sphere/SkySphereForegroundShader.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Sky/Sphere/SkySphereForegroundShader.cs @@ -36,7 +36,7 @@ protected override string FragmentShader() => @" out vec4 fragColor; uniform vec2 scale; - uniform sampler2D boundTexture; + uniform sampler2DArray boundTexture; uniform samplerBuffer colormapTexture; uniform int hasInvulnerability; uniform int paletteIndex; @@ -78,7 +78,7 @@ void main() { else { vec2 textureUV = uvFrag - skyMin; vec2 offset = mix(prevScrollOffset, scrollOffset, timeFrac); - fragColor = texture(boundTexture, textureUV / scale + offset); + fragColor = texture(boundTexture, vec3(textureUV / scale + offset, 0)); } if (fragColor.a == 0) diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Sky/Sphere/SkySphereShader.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Sky/Sphere/SkySphereShader.cs index c88cce514..13addb6ce 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Sky/Sphere/SkySphereShader.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Sky/Sphere/SkySphereShader.cs @@ -108,7 +108,7 @@ protected override string FragmentShader() => @" out vec4 fragColor; uniform vec2 scale; - uniform sampler2D boundTexture; + uniform sampler2DArray boundTexture; uniform samplerBuffer colormapTexture; uniform int hasInvulnerability; uniform int paletteIndex; @@ -144,7 +144,7 @@ void main() { else { vec2 textureUV = uvFrag - skyMin; vec2 offset = mix(prevScrollOffset, scrollOffset, timeFrac); - fragColor = texture(boundTexture, textureUV / scale + offset); + fragColor = texture(boundTexture, vec3(textureUV / scale + offset, 0)); } ${ColorMapFetch} diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/StaticShader.cs b/Core/Render/OpenGL/Renderers/Legacy/World/StaticShader.cs index f2af69166..deef0b54a 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/StaticShader.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/StaticShader.cs @@ -31,6 +31,7 @@ private static string VertexShaderInternal(bool planeClip) => @" flat out float mapIdFrag; flat out float upperFrag; flat out float lowerFrag; + flat out float boundTextureIndex; out float depthFrag; ${VertexGapVariables} ${VertexDistVar3D} @@ -43,7 +44,7 @@ private static string VertexShaderInternal(bool planeClip) => @" uniform mat4 mvp; uniform float timeFrac; uniform int vertexGapClampUV; - uniform sampler2D boundTexture; + uniform sampler2DArray boundTexture; uniform int useSectorColor; uniform int useSectorFog; @@ -109,6 +110,7 @@ protected override string FragmentShader() flat in float mapIdFrag; flat in float upperFrag; flat in float lowerFrag; + flat in float boundTextureIndex; in float depthFrag; ${VertexGapVariables} ${VertexDistVar3D} @@ -116,8 +118,8 @@ protected override string FragmentShader() ${OutTargets} uniform int hasInvulnerability; - uniform sampler2D boundTexture; - uniform sampler2D brightmapTexture; + uniform sampler2DArray boundTexture; + uniform sampler2DArray brightmapTexture; uniform vec3 colorMix; uniform int paletteIndex; uniform int colormapIndex; diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/VertexOptions.cs b/Core/Render/OpenGL/Renderers/Legacy/World/VertexOptions.cs index 3f83bfef1..59a3f18ab 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/VertexOptions.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/VertexOptions.cs @@ -7,10 +7,10 @@ public static class VertexOptions { // When overrideLightIndex is non-zero then lighting uses index overrideLightIndex - 1 [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe float PackSurface(int topLeft, float alpha, int addAlpha, int upper, int lower, int overrideLightIndex) + public static unsafe float PackSurface(int topLeft, float alpha, int addAlpha, int upper, int lower, int overrideLightIndex, int textureIndex) { int alphaByte = (int)(alpha * 255.0f); - int packed = (alphaByte & 0xFF) | (topLeft << 8) | (addAlpha << 9) | (upper << 10) | (lower << 11) | (overrideLightIndex << 12); + int packed = (alphaByte & 0xFF) | (topLeft << 8) | (addAlpha << 9) | (upper << 10) | (lower << 11) | (overrideLightIndex << 12) | ((textureIndex & 0xFFFFF) << 13); return *(float*)&packed; } @@ -63,4 +63,12 @@ public static unsafe float EntityPackXYZ(int offsetXY, int offsetZ) int packed = (offsetXYSign << 31) | (offsetZSign << 30) | (offsetXY << 16) | offsetZ; return *(float*)&packed; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe float EntityPackTextureInfo(int arrayIndex, int paddedWidth, int paddedHeight) + { + // 12, 10, and 10 bits + int packed = ((arrayIndex & 0xFFF) << 20) | ((paddedWidth & 0x3FF) << 10) | ((paddedHeight & 0x3FF)); + return *(float*)&packed; + } } diff --git a/Core/Render/OpenGL/Renderers/WorldRenderer.cs b/Core/Render/OpenGL/Renderers/WorldRenderer.cs index 98667db2d..d9864c123 100644 --- a/Core/Render/OpenGL/Renderers/WorldRenderer.cs +++ b/Core/Render/OpenGL/Renderers/WorldRenderer.cs @@ -1,7 +1,8 @@ -using System; using Helion.Render.OpenGL.Framebuffer; using Helion.Render.OpenGL.Shared; +using Helion.Render.OpenGL.Texture.Legacy; using Helion.World; +using System; namespace Helion.Render.OpenGL.Renderers; @@ -10,6 +11,8 @@ namespace Helion.Render.OpenGL.Renderers; /// public abstract class WorldRenderer : IDisposable { + public abstract TextureBuckets SpriteTextureBuckets { get; protected set; } + protected readonly WeakReference m_lastRenderedWorld = new WeakReference(null); /// diff --git a/Core/Render/OpenGL/Texture/ArrayTextureData.cs b/Core/Render/OpenGL/Texture/ArrayTextureData.cs new file mode 100644 index 000000000..e262d5055 --- /dev/null +++ b/Core/Render/OpenGL/Texture/ArrayTextureData.cs @@ -0,0 +1,90 @@ +using Helion.Util.Assertion; +using Helion.Util.Container; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Helion.Render.OpenGL.Texture; + +internal readonly struct LookupKey(TextureFlags textureFlags, int textureHandle) : IEquatable +{ + public readonly int Key1 = (int)textureFlags; + public readonly int Key2 = textureHandle; + + public override int GetHashCode() + { + return Key1 + Key2 * 131072; + } + + public override bool Equals([NotNullWhen(true)] object? obj) + { + if (obj is LookupKey key) + return key.Key1 == Key1 && key.Key2 == Key2; + return false; + } + + public bool Equals(LookupKey other) + { + return other.Key1 == Key1 && other.Key2 == Key2; + } +} + +internal sealed class ArrayTextureData(TextureContext context) where GLTextureType : GLTexture +{ + public readonly TextureContext Context = context; + + private readonly DynamicArray m_arraySubTextures = new(256); + private readonly DynamicArray m_arrayTextures = new(256); + private readonly DynamicArray m_texturesForArrays = new(256); + private readonly Dictionary m_arrayTextureLookup = []; + + public void Add(GLTextureType arrayTexture, GLTextureType[] arraySubTextures, Span textures, TextureFlags textureFlags) + { + Assert.Precondition(arraySubTextures.Length == textures.Length, "arraySubTextures != textures length"); + // Flip high bit to ensure no collisions + var arrayTextureId = arrayTexture.TextureId | (1 << 30 - (int)Context); + m_arrayTextures.Add(arrayTexture); + m_arraySubTextures.Add(arraySubTextures); + m_texturesForArrays.Add(textures); + for (int i = 0; i < textures.Length; i++) + m_arrayTextureLookup[new(textureFlags, textures[i].Index)] = arrayTextureId; + } + + public int GetArrayTextureHandle(int textureHandle, TextureFlags textureFlags) + { + if (m_arrayTextureLookup.TryGetValue(new(textureFlags, textureHandle), out var handle)) + return handle; + return textureHandle; + } + + public bool TryGetTexture(int textureIndex, [NotNullWhen(true)] out Resources.Texture? texture) + { + for (int i = 0; i < m_texturesForArrays.Length; i++) + { + var check = m_texturesForArrays.Data[i]; + if (check.Index == textureIndex) + { + texture = check; + return true; + } + } + + texture = null; + return false; + } + + public void Destroy() + { + for (int i = 0; i < m_arrayTextures.Length; i++) + m_arrayTextures[i].Dispose(); + + for (int i = 0; i < m_texturesForArrays.Length; i++) + m_texturesForArrays.Data[i].ClearGLTexture(); + + m_arrayTextures.Clear(); + m_arraySubTextures.Clear(); + m_arrayTextureLookup.Clear(); + m_texturesForArrays.Clear(); + } +} diff --git a/Core/Render/OpenGL/Texture/GLTexture.cs b/Core/Render/OpenGL/Texture/GLTexture.cs index b2e8e168d..f95ec9568 100644 --- a/Core/Render/OpenGL/Texture/GLTexture.cs +++ b/Core/Render/OpenGL/Texture/GLTexture.cs @@ -8,6 +8,13 @@ namespace Helion.Render.OpenGL.Texture; +public enum TextureContext +{ + Default, + WorldArray, + WorldSprites +} + public abstract class GLTexture : IRenderableTextureHandle, IDisposable { public int Index => TextureId; @@ -16,11 +23,13 @@ public abstract class GLTexture : IRenderableTextureHandle, IDisposable public Dimension Dimension { get; } public Vec2I Offset { get; } public readonly int TextureId; - public readonly string Name; + public string Name; public readonly Vec2F UVInverse; public readonly ResourceNamespace Namespace; public readonly TextureTarget Target; public readonly int TransparentPixelCount; + public readonly TextureContext Context; + private readonly bool m_ownsTexture; private bool m_disposed; public int Width => Dimension.Width; @@ -30,7 +39,7 @@ public abstract class GLTexture : IRenderableTextureHandle, IDisposable public int BlankRowsFromBottom; protected GLTexture(int textureId, string name, Dimension dimension, Vec2I offset, ResourceNamespace ns, TextureTarget target, - int transparentPixelCount, int blankRowsFromTop, int blankRowsFromBottom) + int transparentPixelCount, int blankRowsFromTop, int blankRowsFromBottom, bool ownsTexture = true, TextureContext textureContext = TextureContext.Default) { TextureId = textureId; Name = name; @@ -42,6 +51,8 @@ protected GLTexture(int textureId, string name, Dimension dimension, Vec2I offse TransparentPixelCount = transparentPixelCount; BlankRowsFromTop = blankRowsFromTop; BlankRowsFromBottom = blankRowsFromBottom; + m_ownsTexture = ownsTexture; + Context = textureContext; } ~GLTexture() @@ -57,7 +68,7 @@ public void Dispose() protected virtual void ReleaseUnmanagedResources() { - if (m_disposed) + if (m_disposed || !m_ownsTexture) return; GL.DeleteTexture(TextureId); diff --git a/Core/Render/OpenGL/Texture/GLTextureManager.cs b/Core/Render/OpenGL/Texture/GLTextureManager.cs index c52980d7d..0066ad4bb 100644 --- a/Core/Render/OpenGL/Texture/GLTextureManager.cs +++ b/Core/Render/OpenGL/Texture/GLTextureManager.cs @@ -9,9 +9,12 @@ using Helion.Resources.Definitions.Zdoom; using Helion.Resources.Definitions.ZDoom; using Helion.Util; +using Helion.Util.Assertion; using Helion.Util.Configs; +using Helion.Util.Container; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Font = Helion.Graphics.Fonts.Font; using Image = Helion.Graphics.Image; @@ -26,7 +29,7 @@ public enum TextureFlags ClampY = 2, } -public abstract class GLTextureManager : IRendererTextureManager +public abstract class GLTextureManager : IRendererTextureManager, IArrayTextureLookup where GLTextureType : GLTexture { protected readonly IConfig Config; @@ -34,6 +37,7 @@ public abstract class GLTextureManager : IRendererTextureManager protected readonly ResourceTracker TextureTracker = new(); protected readonly ResourceTracker TextureTrackerClamp = new(); private readonly Dictionary> m_fonts = new(StringComparer.OrdinalIgnoreCase); + private readonly LookupArray?> m_arrayTextures = new(); private bool m_disposed; private TextureManager TextureManager => ArchiveCollection.TextureManager; @@ -89,6 +93,15 @@ private SpriteRotation CreateNullSpriteRotation() Dispose(); } + public void DestroyTextureArrays(TextureContext textureContext) + { + if (!m_arrayTextures.TryGetValue((int)textureContext, out var arrayTextureData)) + return; + + arrayTextureData.Destroy(); + m_arrayTextures.Set((int)textureContext, null); + } + public bool TryGet(string name, [NotNullWhen(true)] out IRenderableTextureHandle? handle, ResourceNamespace? specificNamespace = null, int upscalingFactor = 1, BrightmapDefinition? brightmap = null) { if (TryGet(name, specificNamespace ?? ResourceNamespace.Undefined, out GLTextureType texture, upscalingFactor, brightmap)) @@ -208,6 +221,79 @@ public GLTextureType GetTexture(int index, bool repeatY = true) return (GLTextureType)renderTexture; } + public GLTextureType? CreateTextureArray(Span textures, TextureContext textureContext, TextureFlags textureFlags, Dimension dimension) + { + if (textures.Length == 0) + return null; + + var images = new Image[textures.Length]; + for (int i = 0; i < textures.Length; i++) + { + var texture = textures[i]; + TextureManager.EnsureTextureImageLoaded(texture.Index); + Assert.Precondition(texture.Image != null, "Texture must have an image"); + + var image = texture.Image ?? new Image(default, ImageType.Argb); + images[i] = image; + textures[i] = texture; + } + + var fitImage = new Image(dimension, images[0].ImageType); + + var arraySubTextures = GenerateTextureArray(imageIndex => + { + var image = images[imageIndex]; + if (image.Width == dimension.Width && image.Height == dimension.Height) + return image; + + fitImage.CopyPixelsFrom(image); + fitImage.Offset = image.Offset; + return fitImage; + }, images.Length, dimension, ResourceNamespace.Textures, textureFlags, textureContext, out var arrayTexture); + + for (int i = 0; i < images.Length; i++) + { + var glTexture = arraySubTextures[i]; + var texture = textures[i]; + SetDebugName(glTexture, texture); + texture.SetGLTexture(glTexture, (textureFlags & TextureFlags.ClampY) == 0); + } + + if (!m_arrayTextures.TryGetValue((int)textureContext, out var arrayTextureData)) + { + arrayTextureData = new(textureContext); + m_arrayTextures.Set((int)textureContext, arrayTextureData); + } + + arrayTextureData.Add(arrayTexture, arraySubTextures, textures, textureFlags); + return arrayTexture; + } + + [Conditional("DEBUG")] + private static void SetDebugName(GLTextureType glTexture, Resources.Texture texture) + { + glTexture.Name = $"{glTexture.Name} {texture.Index}:{texture.Name}"; + } + + public int GetWorldArrayTextureHandle(int textureHandle, bool repeatY) + { + if (m_arrayTextures.TryGetValue((int)TextureContext.WorldArray, out var arrayTextureData)) + return arrayTextureData.GetArrayTextureHandle(textureHandle, repeatY ? TextureFlags.Default : TextureFlags.ClampY); + + return textureHandle; + } + + public bool TryGetTexture(int textureIndex, TextureContext textureContext, [NotNullWhen(true)] out Resources.Texture? texture) + { + if (!m_arrayTextures.TryGetValue((int)textureContext, out var arrayTextureData)) + { + texture = null; + return false; + } + + return arrayTextureData.TryGetTexture(textureIndex, out texture); + } + public GLTextureType? GetBrightmapTexture(int index, bool repeatY = true) { var texture = TextureManager.GetTexture(index); @@ -275,25 +361,6 @@ public SpriteRotation GetSpriteRotation(SpriteDefinition spriteDefinition, int f return spriteRotation; } - public void CacheSpriteRotations(SpriteDefinition spriteDef) - { - for (int i = 0; i < SpriteDefinition.MaxFrames; i++) - { - for (int j = 0; j < SpriteDefinition.MaxRotations; j++) - { - var rotation = spriteDef.Rotations[i, j]; - if (rotation == null) - continue; - - if (rotation.Texture.RenderStore != null) - continue; - - rotation.Texture.RenderStore = CreateTexture(rotation.Texture.Image, rotation.Texture.Name, ResourceNamespace.Sprites); - rotation.Texture.BrightmapRenderStore = CreateBrightMapTexture(rotation.Texture.BrightmapImage, rotation.Texture.Name, ResourceNamespace.Brightmaps); - } - } - } - public SpriteDefinition? GetSpriteDefinition(int spriteIndex) { SpriteDefinition? spriteDef = TextureManager.GetSpriteDefinition(spriteIndex); @@ -367,7 +434,7 @@ protected static int CalculateMipmapLevels(Dimension dimension) return (int)Math.Floor(Math.Log(smallerAxis, 2)); } - protected GLTextureType CreateBrightMapTexture(Image? image, string? name, ResourceNamespace resourceNamespace, bool repeatY = true) + public GLTextureType CreateBrightMapTexture(Image? image, string? name, ResourceNamespace resourceNamespace, bool repeatY = true) { var texture = CreateTexture(image, name, resourceNamespace, repeatY); // Ensure that the brightmap texture is the null transparent texture. The debug option creates red/black checker texture for NullTexture. @@ -376,10 +443,11 @@ protected GLTextureType CreateBrightMapTexture(Image? image, string? name, Resou return texture; } - protected GLTextureType CreateTexture(Image? image, bool repeatY) => CreateTexture(image, null, ResourceNamespace.Global, repeatY); + public GLTextureType CreateTexture(Image? image, bool repeatY) => CreateTexture(image, null, ResourceNamespace.Global, repeatY); - protected GLTextureType CreateTexture(Image? image, string? name, ResourceNamespace resourceNamespace, bool repeatY = true) + public GLTextureType CreateTexture(Image? image, string? name, ResourceNamespace resourceNamespace, bool repeatY = true) { + repeatY = resourceNamespace != ResourceNamespace.Sprites && resourceNamespace != ResourceNamespace.Brightmaps && resourceNamespace != ResourceNamespace.Brightmaps && repeatY; var textureTracker = GetTextureTracker(repeatY); GLTextureType? texture; if (name != null) @@ -432,6 +500,7 @@ private void ReleaseUnmanagedResources() } protected abstract GLTextureType GenerateTexture(Image image, string name, ResourceNamespace resourceNamespace, TextureFlags flags = TextureFlags.Default); + protected abstract GLTextureType[] GenerateTextureArray(Func getImage, int imageLength, Dimension dimension, ResourceNamespace resourceNamespace, TextureFlags flags, TextureContext textureContext, out GLTextureType arrayTexture); public abstract void ReUpload(GLTextureType texture, Image image, uint[] imagePixels); diff --git a/Core/Render/OpenGL/Texture/IArrayTextureLookup.cs b/Core/Render/OpenGL/Texture/IArrayTextureLookup.cs new file mode 100644 index 000000000..ead917743 --- /dev/null +++ b/Core/Render/OpenGL/Texture/IArrayTextureLookup.cs @@ -0,0 +1,8 @@ +namespace Helion.Render.OpenGL.Texture; + +public interface IArrayTextureLookup +{ + // Returns unique base array texture handle given a texture handle from the TextureManager + // If the texture does not have a parent array texture then the textureHandle parameter is returned + int GetWorldArrayTextureHandle(int textureHandle, bool repeatY); +} diff --git a/Core/Render/OpenGL/Texture/Legacy/GLLegacyTexture.cs b/Core/Render/OpenGL/Texture/Legacy/GLLegacyTexture.cs index 7e8299f46..b19edd3cc 100644 --- a/Core/Render/OpenGL/Texture/Legacy/GLLegacyTexture.cs +++ b/Core/Render/OpenGL/Texture/Legacy/GLLegacyTexture.cs @@ -8,10 +8,13 @@ namespace Helion.Render.OpenGL.Texture.Legacy; public class GLLegacyTexture : GLTexture { public TextureFlags Flags; + public int ArrayIndex; + public bool IsArray => ParentArrayTexture != null; + public GLLegacyTexture? ParentArrayTexture; public GLLegacyTexture(int textureId, string name, Dimension dimension, Vec2I offset, ResourceNamespace ns, TextureTarget target, - int transparentPixelCount, int blankRowsFromTop = 0, int blankRowsFromBottom = 0) - : base(textureId, name, dimension, offset, ns, target, transparentPixelCount, blankRowsFromTop, blankRowsFromBottom) + int transparentPixelCount, int blankRowsFromTop = 0, int blankRowsFromBottom = 0, bool ownsTexture = true, TextureContext textureContext = TextureContext.Default) + : base(textureId, name, dimension, offset, ns, TextureTarget.Texture2DArray, transparentPixelCount, blankRowsFromTop, blankRowsFromBottom, ownsTexture, textureContext) { } @@ -25,5 +28,5 @@ public void Unbind() GL.BindTexture(Target, 0); } - public override string ToString() => $"{TextureId}:{Name}"; + public override string ToString() => $"{TextureId}:{Name} [{Dimension}]"; } diff --git a/Core/Render/OpenGL/Texture/Legacy/GLTextureArrayBuilder.cs b/Core/Render/OpenGL/Texture/Legacy/GLTextureArrayBuilder.cs new file mode 100644 index 000000000..e57b65b40 --- /dev/null +++ b/Core/Render/OpenGL/Texture/Legacy/GLTextureArrayBuilder.cs @@ -0,0 +1,266 @@ +using Helion.Geometry; +using Helion.Render.OpenGL.Context; +using Helion.Resources; +using Helion.Util; +using Helion.Util.Assertion; +using Helion.Util.Container; +using Helion.Util.Loggers; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Helion.Render.OpenGL.Texture.Legacy; + +public record struct TextureBuckets(int MaxTextureIndex, TextureBucket[] Buckets); +public record struct TextureBucket(Dimension Dimension, DynamicArray Textures); + +public class GLTextureArrayBuilder(TextureManager textureManager, LegacyGLTextureManager glTextureManager) +{ + + private readonly TextureManager m_textureManager = textureManager; + private readonly LegacyGLTextureManager m_glTextureManager = glTextureManager; + + public void BuildLevel(IEnumerable flatTextures, IEnumerable wallTexturesRepeat, IEnumerable wallTexturesClamp) + { + m_glTextureManager.DestroyTextureArrays(TextureContext.WorldArray); + + // Animations are excluded. This leaves them reliant on their previous behavior of swapping textures. + int buildTextures = 0; + int arrayTextures = 0; + var textures = new DynamicArray(1024); + foreach (var index in flatTextures.Where(x => !m_textureManager.IsTextureAnimated(x))) + AddTexture(textures, index); + + foreach (var index in wallTexturesRepeat.Where(x => !m_textureManager.IsTextureAnimated(x))) + AddTexture(textures, index); + + buildTextures += textures.Count; + arrayTextures += BuildTextureArrayFromTextures(textures, TextureContext.WorldArray, TextureFlags.Default); + + textures.Clear(); + foreach (var index in wallTexturesClamp.Where(x => !m_textureManager.IsTextureAnimated(x))) + AddTexture(textures, index); + + buildTextures += textures.Count; + arrayTextures += BuildTextureArrayFromTextures(textures, TextureContext.WorldArray, TextureFlags.ClampY); + + var totalTextures = flatTextures.Count() + wallTexturesRepeat.Count() + wallTexturesClamp.Count(); + var animated = totalTextures - buildTextures; + DebugLog(totalTextures, arrayTextures + animated); + } + + public TextureBuckets BuildSprites(DynamicArray spriteDefinitions) + { + var maxIndex = 0; + var spriteTextures = new DynamicArray(); + var spriteTextureHandles = new HashSet(spriteDefinitions.Length); + foreach (var spriteDefinition in spriteDefinitions) + { + if (spriteDefinition == null) + continue; + + for (int i = 0; i < SpriteDefinition.MaxFrames; i++) + { + for (int j = 0; j < SpriteDefinition.MaxRotations; j++) + { + var rotation = spriteDefinition.Rotations[i, j]; + if (rotation == null) + continue; + + if (spriteTextureHandles.Add(rotation.Texture.Index)) + { + spriteTextures.Add(rotation.Texture); + maxIndex = MathHelper.Max(rotation.Texture.Index, maxIndex); + } + } + } + } + + const int BaseSize = 32; + const int BucketCount = 5; + var buckets = CreateTextureBuckets(spriteTextures, BaseSize, BucketCount); + for (int i = 0; i < buckets.Length - 1; i++) + { + var bucket = buckets[i]; + BuildTextureArray(bucket.Textures.Data.AsSpan(0, bucket.Textures.Length), TextureContext.WorldSprites, TextureFlags.ClampX | TextureFlags.ClampY, bucket.Dimension); + } + + foreach (var spriteDefinition in spriteDefinitions) + { + if (spriteDefinition == null) + continue; + + for (int i = 0; i < SpriteDefinition.MaxFrames; i++) + { + for (int j = 0; j < SpriteDefinition.MaxRotations; j++) + { + var rotation = spriteDefinition.Rotations[i, j]; + if (rotation == null) + continue; + + if (rotation.RenderStore != null) + continue; + + rotation.BrightmapRenderStore = m_glTextureManager.CreateBrightMapTexture(rotation.Texture.BrightmapImage, rotation.Texture.Name, ResourceNamespace.Brightmaps); + + if (!CanTextureArray(rotation.Texture)) + { + rotation.RenderStore = m_glTextureManager.CreateTexture(rotation.Texture.Image, rotation.Texture.Name, ResourceNamespace.Sprites); + continue; + } + + if (m_glTextureManager.TryGetTexture(rotation.Texture.Index, TextureContext.WorldSprites, out var texture)) + rotation.RenderStore = texture.RenderStoreClamp ?? texture.RenderStore; + + Assert.Postcondition(rotation.RenderStore != null, $"Failed to find generated array image for sprite"); + } + } + } + + return new(maxIndex, buckets); + } + + [Conditional("DEBUG")] + private static void DebugLog(int totalTextures, int compressed) + { + HelionLog.Info($"Compressed textures {totalTextures} -> {compressed}"); + } + + private int BuildTextureArrayFromTextures(DynamicArray textures, TextureContext textureContext, TextureFlags textureFlags) + { + textures.Sort(SortTexturesByDimensions); + + int textureCount = 0; + var arrayTextures = new DynamicArray(); + var dimension = new Dimension(0, 0); + foreach (var texture in textures) + { + if (texture.Image == null) + continue; + + if (dimension != texture.Image.Dimension) + { + if (arrayTextures.Count > 0) + textureCount += BuildTextureArray(arrayTextures.Data.AsSpan(0, arrayTextures.Length), textureContext, textureFlags, dimension); + arrayTextures.Clear(); + dimension = texture.Image.Dimension; + } + + arrayTextures.Add(texture); + } + + if (arrayTextures.Count > 0) + textureCount += BuildTextureArray(arrayTextures.Data.AsSpan(0, arrayTextures.Length), textureContext, textureFlags, dimension); + + return textureCount; + } + + private void AddTexture(DynamicArray textures, int index) + { + var texture = m_textureManager.GetTexture(index); + if (CanTextureArray(texture)) + textures.Add(texture); + } + + private static bool CanTextureArray(Resources.Texture texture) + { + // Exclude brightmaps for now. They probably could be made into arrays but like animations the complication may not be worth it. + return texture.Image != null && texture.BrightmapImage == null; + } + + private static int SortTexturesByDimensions(Resources.Texture x, Resources.Texture y) + { + if (x.Image == null || y.Image == null) + throw new NullReferenceException("Texture image must not be null"); + + if (x.Image.Height == y.Image.Height) + return x.Image.Width.CompareTo(y.Image.Width); + + return x.Image.Height.CompareTo(y.Image.Height); + } + + private int BuildTextureArray(Span textures, TextureContext textureContext, TextureFlags textureFlags, Dimension dimension) + { + var arrayTexture = m_glTextureManager.CreateTextureArray(textures, textureContext, textureFlags, dimension); + return arrayTexture == null ? 0 : 1; + } + + private static TextureBucket[] CreateTextureBuckets(DynamicArray textures, int baseSize, int bucketCount) + { + if (baseSize <= 0) + throw new ArgumentException($"Invalid baseSize {baseSize}"); + if (bucketCount <= 0) + throw new ArgumentException($"Invalid bucketCount {bucketCount}"); + + var buckets = new TextureBucket[bucketCount + 1]; + + for (int i = 0; i < bucketCount; i++) + { + var size = baseSize * (int)Math.Pow(2, i); + buckets[i] = new TextureBucket(new Dimension(size, size), new(128)); + } + + var overflowBucketIndex = buckets.Length - 1; + buckets[overflowBucketIndex] = new TextureBucket(new Dimension(0, 0), []); + + foreach (var texture in textures) + { + var index = CanTextureArray(texture) ? GetBucketIndex(texture.Image!.Dimension, baseSize, bucketCount) : overflowBucketIndex; + buckets[index].Textures.Add(texture); + } + + // Split buckets that overflow GLInfo.MaxArrayLayers + return FinalizeTextureBuckets(buckets); + } + + private static TextureBucket[] FinalizeTextureBuckets(TextureBucket[] buckets) + { + var finalizedBuckets = new List(); + foreach (var bucket in buckets) + { + if (bucket.Textures.Length <= GLInfo.MaxArrayLayers) + { + finalizedBuckets.Add(bucket); + continue; + } + + var remaining = bucket.Textures.Length; + var offset = 0; + + while (remaining > 0) + { + var count = MathHelper.Min(remaining, GLInfo.MaxArrayLayers); + var newBucket = new TextureBucket(bucket.Dimension, new(count)); + for (int i = 0; i < count; i++) + newBucket.Textures.Add(bucket.Textures[offset + i]); + + finalizedBuckets.Add(newBucket); + + offset += count; + remaining -= count; + } + } + + return [.. finalizedBuckets]; + } + + public static int GetBucketIndex(Dimension dimension, int baseSize, int bucketCount) + { + int size = MathHelper.Max(dimension.Width, dimension.Height); + // Fits in the smallest bucket + if (size <= baseSize) + return 0; + + int index = 0; + int current = baseSize; + + while (index < bucketCount - 1 && size > current) + { + current *= 2; + index++; + } + + return index; + } +} diff --git a/Core/Render/OpenGL/Texture/Legacy/LegacyGLTextureManager.cs b/Core/Render/OpenGL/Texture/Legacy/LegacyGLTextureManager.cs index a61a53bae..49e380a21 100644 --- a/Core/Render/OpenGL/Texture/Legacy/LegacyGLTextureManager.cs +++ b/Core/Render/OpenGL/Texture/Legacy/LegacyGLTextureManager.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using Helion.Geometry; using Helion.Graphics; using Helion.Graphics.Fonts; using Helion.Render.Common.Textures; @@ -12,6 +11,8 @@ using Helion.Util.Configs; using Helion.Util.Extensions; using OpenTK.Graphics.OpenGL; +using System; +using System.Collections.Generic; using static Helion.Util.Assertion.Assert; namespace Helion.Render.OpenGL.Texture.Legacy; @@ -21,7 +22,7 @@ public class LegacyGLTextureManager : GLTextureManager public override IImageDrawInfoProvider ImageDrawInfoProvider { get; } private bool m_disposed; - private List m_registeredTextures = new(); + private readonly List m_registeredTextures = []; public LegacyGLTextureManager(IConfig config, ArchiveCollection archiveCollection) : base(config, archiveCollection) @@ -43,6 +44,12 @@ public void UnRegisterTexture(GLLegacyTexture texture) m_registeredTextures.Remove(texture); } + public GLLegacyTexture GetParentArrayTexture(int index, bool repeatY = true) + { + var texture = GetTexture(index, repeatY); + return texture.ParentArrayTexture ?? texture; + } + private void HandleFilterChange(object? sender, FilterType e) { UpdateTextureTrackerFilter(TextureTracker); @@ -99,12 +106,48 @@ private void HandleAnisotropyChange(object? sender, int e) Dispose(); } - public unsafe void UploadAndSetParameters(GLLegacyTexture texture, Image image, string name, ResourceNamespace resourceNamespace, TextureFlags flags) + private unsafe void UploadAndSetParameters3D(GLLegacyTexture texture, Func getImage, int imageLength, string name, ResourceNamespace resourceNamespace, TextureFlags flags, + Dimension dimensions, Action onUploadImage) { GL.BindTexture(texture.Target, texture.TextureId); + flags = GetFlagsForNamespace(resourceNamespace, flags); + + GL.TexImage3D(TextureTarget.Texture2DArray, 0, PixelInternalFormat.Rgba8, dimensions.Width, dimensions.Height, imageLength, 0, + PixelFormat.Bgra, PixelType.UnsignedInt8888Reversed, IntPtr.Zero); + + if (GLInfo.DebugLabel) + GLHelper.ObjectLabel(ObjectLabelIdentifier.Texture, texture.TextureId, $"Texture: {name} ({flags})"); - if (resourceNamespace == ResourceNamespace.Sprites || resourceNamespace == ResourceNamespace.Undefined) + for (int i = 0; i < imageLength; i++) + { + var image = getImage(i); + fixed (uint* pixelPtr = image.GetGlTexturePixels(ShaderVars.PaletteColorMode)) + { + IntPtr ptr = new(pixelPtr); + GL.TexSubImage3D(texture.Target, 0, 0, 0, zoffset: i, image.Width, image.Height, + depth: 1, PixelFormat.Bgra, PixelType.UnsignedInt8888Reversed, ptr); + } + onUploadImage(i, image); + } + + GL.GenerateMipmap(GenerateMipmapTarget.Texture2DArray); + SetTextureParameters(texture.Target, flags); + + GL.BindTexture(texture.Target, 0); + texture.Flags = flags; + } + + private static TextureFlags GetFlagsForNamespace(ResourceNamespace resourceNamespace, TextureFlags flags) + { + if (resourceNamespace == ResourceNamespace.Sprites || resourceNamespace == ResourceNamespace.Brightmaps || resourceNamespace == ResourceNamespace.Undefined) flags = TextureFlags.ClampX | TextureFlags.ClampY; + return flags; + } + + public unsafe void UploadAndSetParameters(GLLegacyTexture texture, Image image, string name, ResourceNamespace resourceNamespace, TextureFlags flags) + { + GL.BindTexture(texture.Target, texture.TextureId); + flags = GetFlagsForNamespace(resourceNamespace, flags); if (GLInfo.DebugLabel) GLHelper.ObjectLabel(ObjectLabelIdentifier.Texture, texture.TextureId, $"Texture: {name} ({flags})"); @@ -112,14 +155,25 @@ public unsafe void UploadAndSetParameters(GLLegacyTexture texture, Image image, fixed (uint* pixelPtr = image.GetGlTexturePixels(ShaderVars.PaletteColorMode)) { IntPtr ptr = new(pixelPtr); - // Because the C# image format is 'ARGB', we can get it into the - // RGBA format by doing a BGRA format and then reversing it. - GL.TexImage2D(texture.Target, 0, PixelInternalFormat.Rgba8, image.Width, image.Height, 0, - PixelFormat.Bgra, PixelType.UnsignedInt8888Reversed, ptr); + if (texture.Target == TextureTarget.Texture2DArray) + { + GL.TexImage3D(texture.Target, 0, PixelInternalFormat.Rgba8, image.Width, image.Height, 1, 0, + PixelFormat.Bgra, PixelType.UnsignedInt8888Reversed, IntPtr.Zero); + + GL.TexSubImage3D(texture.Target, 0, 0, 0, 0, image.Width, image.Height, depth: 1, + PixelFormat.Bgra, PixelType.UnsignedInt8888Reversed, ptr); + } + else + { + // Because the C# image format is 'ARGB', we can get it into the + // RGBA format by doing a BGRA format and then reversing it. + GL.TexImage2D(texture.Target, 0, PixelInternalFormat.Rgba8, image.Width, image.Height, 0, + PixelFormat.Bgra, PixelType.UnsignedInt8888Reversed, ptr); + } } - GL.GenerateMipmap(GenerateMipmapTarget.Texture2D); - SetTextureParameters(TextureTarget.Texture2D, resourceNamespace, flags); + GL.GenerateMipmap(texture.Target == TextureTarget.Texture2DArray ? GenerateMipmapTarget.Texture2DArray : GenerateMipmapTarget.Texture2D); + SetTextureParameters(texture.Target, flags); GL.BindTexture(texture.Target, 0); texture.Flags = flags; @@ -132,8 +186,16 @@ public unsafe override void ReUpload(GLLegacyTexture texture, Image image, uint[ fixed (uint* pixelPtr = imagePixels) { IntPtr ptr = new(pixelPtr); - GL.TexSubImage2D(texture.Target, 0, 0, 0, image.Width, image.Height, - PixelFormat.Bgra, PixelType.UnsignedInt8888Reversed, ptr); + if (texture.Target == TextureTarget.Texture2DArray) + { + GL.TexSubImage3D(texture.Target, 0, 0, 0, 0, image.Width, image.Height, depth: 1, + PixelFormat.Bgra, PixelType.UnsignedInt8888Reversed, ptr); + } + else + { + GL.TexSubImage2D(texture.Target, 0, 0, 0, image.Width, image.Height, + PixelFormat.Bgra, PixelType.UnsignedInt8888Reversed, ptr); + } } GL.BindTexture(texture.Target, 0); @@ -158,6 +220,29 @@ protected override GLLegacyTexture GenerateTexture(Image image, string name, return texture; } + protected override GLLegacyTexture[] GenerateTextureArray(Func getImage, int imageLength, Dimension dimension, ResourceNamespace resourceNamespace, TextureFlags flags, + TextureContext textureContext, out GLLegacyTexture arrayTexture) + { + var textures = new GLLegacyTexture[imageLength]; + int textureId = GL.GenTexture(); + arrayTexture = new GLLegacyTexture(textureId, $"Texture Array Length={imageLength} {flags}", dimension, default, resourceNamespace, TextureTarget.Texture2DArray, 0, 0, 0, + textureContext: textureContext); + var parentArrayTexture = arrayTexture; + UploadAndSetParameters3D(arrayTexture, getImage, imageLength, arrayTexture.Name, resourceNamespace, flags, dimension, (int imageIndex, Image image) => + { + var texture = new GLLegacyTexture(textureId, $"Sub Image {imageIndex}", dimension, image.Offset, resourceNamespace, TextureTarget.Texture2DArray, + image.TransparentPixelCount(), image.BlankRowsFromTop, image.BlankRowsFromBottom, ownsTexture: false) + { + ArrayIndex = imageIndex, + ParentArrayTexture = parentArrayTexture + }; + + textures[imageIndex] = texture; + }); + + return textures; + } + /// /// Creates a new font. The caller is responsible for disposing it. /// @@ -172,7 +257,7 @@ protected override GLFontTexture GenerateFont(Font font, string return fontTexture; } - private void SetTextureParameters(TextureTarget targetType, ResourceNamespace resourceNamespace, TextureFlags flags) + private void SetTextureParameters(TextureTarget targetType, TextureFlags flags) { TextureWrapMode textureWrapS = (flags & TextureFlags.ClampX) != 0 ? TextureWrapMode.ClampToEdge : TextureWrapMode.Repeat; TextureWrapMode textureWrapT = (flags & TextureFlags.ClampY) != 0 ? TextureWrapMode.ClampToEdge : TextureWrapMode.Repeat; diff --git a/Core/Render/OpenGL/Vertex/Attributes.cs b/Core/Render/OpenGL/Vertex/Attributes.cs index 5962bfd99..00205ce36 100644 --- a/Core/Render/OpenGL/Vertex/Attributes.cs +++ b/Core/Render/OpenGL/Vertex/Attributes.cs @@ -162,8 +162,8 @@ private static int GetNextIndex(VertexAttributeAttribute codeAttribute, HashSet< foreach (VaoAttribute attr in ReadStructAttributes()) { ProgramAttribute? progAttr = FindShaderAttribute(shaderAttribs, attr.Name); - if (progAttr == null && attr.Required) - throw new($"Cannot find shader attribute named {attr.Name}, was it optimized out?"); + //if (progAttr == null && attr.Required) + // throw new($"Cannot find shader attribute named {attr.Name}, was it optimized out?"); // TODO: This doesn't work because Size = 1 for Vec2, but we supply Size = 2 for Float. // This means we need to be able to find out if different types are the same size, since diff --git a/Core/Render/Renderer.cs b/Core/Render/Renderer.cs index 5dad134cf..87e19efe5 100644 --- a/Core/Render/Renderer.cs +++ b/Core/Render/Renderer.cs @@ -51,6 +51,8 @@ public partial class Renderer : IDisposable public readonly IWindow Window; public readonly GLSurface Default; + public readonly RenderProfiler RenderProfiler; + public DrawCounts LastDrawCounts; private GLFramebuffer m_mainFramebuffer; private GLFramebuffer? m_virtualFramebuffer; private GLFramebuffer m_worldFramebuffer; @@ -86,6 +88,7 @@ public partial class Renderer : IDisposable public Renderer(IWindow window, IConfig config, ArchiveCollection archiveCollection, FpsTracker fpsTracker, RenderProfiler renderProfiler) { Window = window; + RenderProfiler = renderProfiler; m_config = config; m_archiveCollection = archiveCollection; m_fpsTracker = fpsTracker; @@ -95,7 +98,7 @@ public Renderer(IWindow window, IConfig config, ArchiveCollection archiveCollect Textures = new LegacyGLTextureManager(config, archiveCollection); m_worldRenderer = new LegacyWorldRenderer(config, archiveCollection, Textures, renderProfiler); - m_hudRenderer = new LegacyHudRenderer(config, Textures, archiveCollection.DataCache); + m_hudRenderer = new LegacyHudRenderer(config, Textures, archiveCollection.DataCache, renderProfiler); m_automapRenderer = new LegacyAutomapRenderer(archiveCollection); m_transitionRenderer = new TransitionRenderer(window); Default = new(window, this); @@ -608,6 +611,8 @@ public void Render(RenderCommands renderCommands) if (!virtualFrameBufferDraw) DrawVirtualFramebufferToMain(); + + LastDrawCounts = RenderProfiler.DrawCounts; } private static void HandleScissorCommand(ScissorCommand cmd) @@ -677,6 +682,7 @@ private static void PrintGLInfo() Log.Info("GL_ARB_clip_control {0}", GLInfo.ClipControlSupported); Log.Info("GL_ARB_shader_image_load_store {0}", GLInfo.MemoryBarrierSupported); Log.Info("GL_ARB_buffer_storage {0}", GLInfo.MapPersistentBitSupported); + Log.Info("MaxArrayLayers {0}", GLInfo.MaxArrayLayers); InfoPrinted = true; } diff --git a/Core/Resources/SpriteDefinition.cs b/Core/Resources/SpriteDefinition.cs index 6dd9ef214..33ed57f68 100644 --- a/Core/Resources/SpriteDefinition.cs +++ b/Core/Resources/SpriteDefinition.cs @@ -14,7 +14,9 @@ public class SpriteDefinition public const int MaxRotationIndex = MaxRotations - 1; public SpriteRotation?[,] Rotations = new SpriteRotation[MaxFrames, MaxRotations]; public bool HasRotations; + public bool Cached; + private static int StaticId; private static readonly Dictionary SpriteTextureLookup = []; public SpriteDefinition(IList entries, IImageRetriever imageRetriever, ArchiveCollection archiveCollection) @@ -52,7 +54,7 @@ private void CreateRotations(Entry entry, IImageRetriever imageRetriever, Archiv if (!SpriteTextureLookup.TryGetValue(entry.Path.Name, out var texture)) { - texture = new(entry.Path.Name, ResourceNamespace.Sprites, 0); + texture = new(entry.Path.Name, ResourceNamespace.Sprites, ++StaticId); texture.Image = imageRetriever.GetOnly(entry.Path.Name, ResourceNamespace.Sprites); SpriteTextureLookup[entry.Path.Name] = texture; } diff --git a/Core/Resources/SpriteRotation.cs b/Core/Resources/SpriteRotation.cs index 2cf479a30..7b42a2f72 100644 --- a/Core/Resources/SpriteRotation.cs +++ b/Core/Resources/SpriteRotation.cs @@ -1,3 +1,4 @@ +using Helion.Geometry; using Helion.Util.Container; using System.Diagnostics.CodeAnalysis; @@ -8,9 +9,11 @@ public class SpriteRotation(Texture texture, bool mirror, bool brightmapNoFullbr public Texture Texture = texture; public bool Mirror = mirror; public int FlipU = mirror ? 1 : 0; + public int TextureIndex = texture.Index; public object? RenderStore; public object? BrightmapRenderStore; public bool BrightmapNoFullbright = brightmapNoFullbright; + public Dimension TextureDimension = texture.Image?.Dimension ?? default; private LookupArray? m_translationRotations; public bool TryGetTranslationRotation(int index, [NotNullWhen(true)] out SpriteRotation? rotation) diff --git a/Core/Resources/Texture.cs b/Core/Resources/Texture.cs index 139f692df..279a6c1f8 100644 --- a/Core/Resources/Texture.cs +++ b/Core/Resources/Texture.cs @@ -27,6 +27,12 @@ public void SetBrightmapGLTexture(object glTexture, bool repeatY) BrightmapRenderStoreClamp = glTexture; } + public void ClearGLTexture() + { + RenderStore = null; + RenderStoreClamp = null; + } + /// /// Name of the texture. /// @@ -62,4 +68,9 @@ public void SetBrightmapGLTexture(object glTexture, bool repeatY) /// public object? BrightmapRenderStore; public object? BrightmapRenderStoreClamp; + + public override string ToString() + { + return $"{Name} {Image?.Dimension}"; + } } diff --git a/Core/Util/Profiling/Timers/RenderProfiler.cs b/Core/Util/Profiling/Timers/RenderProfiler.cs index 07228a753..61be99907 100644 --- a/Core/Util/Profiling/Timers/RenderProfiler.cs +++ b/Core/Util/Profiling/Timers/RenderProfiler.cs @@ -2,7 +2,21 @@ namespace Helion.Util.Profiling.Timers; -public class RenderProfiler: ProfileComponent +public struct DrawCounts +{ + public int Sprites; + public int GeometryStatic; + public int GeometryDynamic; + public int FloodFillStatic; + public int FloodFillDynamic; + public int Skies; + public int HudElements; + + public readonly int Total() => GeometryStatic + GeometryDynamic + FloodFillStatic + FloodFillDynamic + Skies + Sprites + HudElements; + public readonly int LevelTotal() => Total() - HudElements; +} + +public class RenderProfiler : ProfileComponent { public readonly ProfilerStopwatch FlushPipeline = new(); public readonly ProfilerStopwatch Hud = new(); @@ -17,6 +31,8 @@ public class RenderProfiler: ProfileComponent public readonly ProfilerStopwatch WorldFloodFill = new(); public readonly ProfilerStopwatch Automap = new(); + public DrawCounts DrawCounts; + public override List Profilers { get; } = []; public RenderProfiler() @@ -47,5 +63,6 @@ internal void ResetAll() WorldTransparent.Reset(); WorldFloodFill.Reset(); Automap.Reset(); + DrawCounts = default; } } diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 19bcbb6c1..ed828aa42 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,7 @@ - Automatic blood color and fuzz blood options. - Initial support for MacOS ARM64 builds. - Add weapon fire bob option. +- Pack textures into 2D arrays for faster rendering speeds, especially on older iGPUs. ## Bug Fixes: - Do not clear player velocity when slide movement fails. Matches vanilla doom behavior where players can move out of lines with enough momentum to pass clip checks. (Fixes Hellevator MAP06 start) diff --git a/Tests/Unit/GameAction/3DSector/Sector3D_Walls.cs b/Tests/Unit/GameAction/3DSector/Sector3D_Walls.cs index ff6f7b0e5..7428cd1c9 100644 --- a/Tests/Unit/GameAction/3DSector/Sector3D_Walls.cs +++ b/Tests/Unit/GameAction/3DSector/Sector3D_Walls.cs @@ -30,7 +30,7 @@ record struct WallSlice(double TopZ, double BottomZ, short LightLevel, Vec2D Off public Sector3D_Walls() { World = WorldAllocator.LoadMap("Resources/sector3d-map.zip", "sector3d-map.wad", "MAP01", GetType().Name, (world) => { }, IWadType.Doom2); - GeometryRenderer = new GeometryRenderer(World.Config, World.ArchiveCollection, null!, null!, null!, null!, null!, null!, unitTest: true); + GeometryRenderer = new GeometryRenderer(World.Config, World.ArchiveCollection, null!, null!, null!, null!, null!, null!, null!, unitTest: true); GeometryRenderer.UpdateTo(World, unitTest: true); GeometryRenderer.SetBuffer(false); }