refactor(app): compose world rendering startup
Move Region/environment, mandatory modern rendering, terrain, WB, texture, and sampler construction behind the typed Phase-4 composition boundary. Give every fallible GL constructor prefix retryable ownership so partial startup failure cannot leak or replay resource deletion while preserving the accepted render path and DAT inputs. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
cd7b519f78
commit
6a2fe98cc4
22 changed files with 1983 additions and 634 deletions
534
src/AcDream.App/Composition/WorldRenderComposition.cs
Normal file
534
src/AcDream.App/Composition/WorldRenderComposition.cs
Normal file
|
|
@ -0,0 +1,534 @@
|
|||
using System.Collections.Concurrent;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Content;
|
||||
using AcDream.Core.Terrain;
|
||||
using AcDream.UI.Abstractions.Settings;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Silk.NET.Input;
|
||||
using Silk.NET.OpenGL;
|
||||
using Shader = AcDream.App.Rendering.Shader;
|
||||
|
||||
namespace AcDream.App.Composition;
|
||||
|
||||
internal sealed record WorldRegionData(Region Region, float[] HeightTable);
|
||||
|
||||
internal sealed record WorldTerrainBuildContext(
|
||||
uint InitialCenterLandblockId,
|
||||
int InitialCenterX,
|
||||
int InitialCenterY,
|
||||
float[] HeightTable,
|
||||
TerrainBlendingContext Blending,
|
||||
ConcurrentDictionary<uint, SurfaceInfo> SurfaceCache);
|
||||
|
||||
internal sealed record WorldRenderFoundation(
|
||||
string ShadersDirectory,
|
||||
BindlessSupport Bindless,
|
||||
TerrainAtlas TerrainAtlas,
|
||||
Shader TerrainShader,
|
||||
SceneLightingUboBinding SceneLighting,
|
||||
DebugLineRenderer DebugLines,
|
||||
BitmapFont? DebugFont,
|
||||
TextRenderer? TextRenderer,
|
||||
TerrainModernRenderer Terrain,
|
||||
Shader MeshShader,
|
||||
WbMeshAdapter MeshAdapter,
|
||||
TextureCache TextureCache,
|
||||
SamplerCache Samplers);
|
||||
|
||||
internal sealed record WorldRenderResult(
|
||||
WorldTerrainBuildContext TerrainBuild,
|
||||
WorldRenderFoundation Foundation);
|
||||
|
||||
internal sealed record WorldRenderDependencies(
|
||||
WorldEnvironmentController Environment,
|
||||
IGameRenderResourceLifetime RenderResources,
|
||||
IGpuResourceRetirementQueue ResourceRetirement,
|
||||
uint InitialCenterLandblockId,
|
||||
Action<string> Log);
|
||||
|
||||
internal interface IGameWindowWorldRenderPublication
|
||||
{
|
||||
void PublishBindlessSupport(BindlessSupport value);
|
||||
void PublishTerrainShader(Shader value);
|
||||
void PublishSceneLighting(SceneLightingUboBinding value);
|
||||
void PublishDebugLines(DebugLineRenderer value);
|
||||
void PublishHudResources(BitmapFont font, TextRenderer text);
|
||||
void PublishTerrain(TerrainModernRenderer value);
|
||||
void PublishTerrainBuildState(
|
||||
float[] heightTable,
|
||||
TerrainBlendingContext blending,
|
||||
ConcurrentDictionary<uint, SurfaceInfo> surfaceCache);
|
||||
void PublishMeshShader(Shader value);
|
||||
void PublishWbMeshAdapter(WbMeshAdapter value);
|
||||
void PublishTextureCache(TextureCache value);
|
||||
void PublishSamplerCache(SamplerCache value);
|
||||
}
|
||||
|
||||
internal interface IWorldRenderCompositionFactory
|
||||
{
|
||||
void InitializeGlState(GL gl);
|
||||
WorldRegionData LoadRegion(IDatReaderWriter dats);
|
||||
void InitializeEnvironment(WorldEnvironmentController environment, Region region);
|
||||
BindlessSupport RequireBindless(GL gl, Action<string> log);
|
||||
TerrainAtlas AcquireTerrainAtlas(
|
||||
IGameRenderResourceLifetime lifetime,
|
||||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
BindlessSupport bindless);
|
||||
void SetTerrainAnisotropic(TerrainAtlas atlas, int level);
|
||||
Shader CreateTerrainShader(GL gl, string shadersDirectory);
|
||||
SceneLightingUboBinding CreateSceneLighting(GL gl);
|
||||
DebugLineRenderer CreateDebugLines(GL gl, string shadersDirectory);
|
||||
byte[]? TryLoadDebugFont();
|
||||
BitmapFont CreateDebugFont(GL gl, byte[] bytes);
|
||||
TextRenderer CreateTextRenderer(GL gl, string shadersDirectory);
|
||||
TerrainModernRenderer CreateTerrain(
|
||||
GL gl,
|
||||
BindlessSupport bindless,
|
||||
Shader shader,
|
||||
TerrainAtlas atlas,
|
||||
IGpuResourceRetirementQueue retirement);
|
||||
WorldTerrainBuildContext CreateTerrainBuildContext(
|
||||
uint initialCenterLandblockId,
|
||||
float[] heightTable,
|
||||
TerrainAtlas atlas);
|
||||
Shader CreateMeshShader(GL gl, string shadersDirectory);
|
||||
WbMeshAdapter CreateMeshAdapter(
|
||||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
IGpuResourceRetirementQueue retirement);
|
||||
TextureCache CreateTextureCache(
|
||||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
BindlessSupport bindless,
|
||||
IGpuResourceRetirementQueue retirement);
|
||||
SamplerCache CreateSamplerCache(GL gl);
|
||||
void Release(IDisposable resource);
|
||||
}
|
||||
|
||||
internal sealed class RetailWorldRenderCompositionFactory
|
||||
: IWorldRenderCompositionFactory
|
||||
{
|
||||
public void InitializeGlState(GL gl)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
gl.ClearColor(0.05f, 0.10f, 0.18f, 1.0f);
|
||||
gl.Enable(EnableCap.DepthTest);
|
||||
}
|
||||
|
||||
public WorldRegionData LoadRegion(IDatReaderWriter dats)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dats);
|
||||
Region region = dats.Get<Region>(0x13000000u)
|
||||
?? throw new InvalidOperationException(
|
||||
"Region dat id 0x13000000 missing");
|
||||
float[]? heightTable = region.LandDefs.LandHeightTable;
|
||||
if (heightTable is null || heightTable.Length < 256)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Region.LandDefs.LandHeightTable missing or truncated");
|
||||
}
|
||||
return new WorldRegionData(region, heightTable);
|
||||
}
|
||||
|
||||
public void InitializeEnvironment(
|
||||
WorldEnvironmentController environment,
|
||||
Region region)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(environment);
|
||||
ArgumentNullException.ThrowIfNull(region);
|
||||
environment.Initialize(region);
|
||||
}
|
||||
|
||||
public BindlessSupport RequireBindless(GL gl, Action<string> log)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
ArgumentNullException.ThrowIfNull(log);
|
||||
if (BindlessSupport.TryCreate(gl, out BindlessSupport? bindless))
|
||||
{
|
||||
if (bindless!.HasShaderDrawParameters(gl))
|
||||
{
|
||||
log("[N.5] modern path capabilities present " +
|
||||
"(bindless + ARB_shader_draw_parameters)");
|
||||
return bindless;
|
||||
}
|
||||
log("[N.5] GL_ARB_shader_draw_parameters not present — " +
|
||||
"modern path not available");
|
||||
}
|
||||
else
|
||||
{
|
||||
log("[N.5] GL_ARB_bindless_texture not present — " +
|
||||
"modern path not available");
|
||||
}
|
||||
|
||||
throw new NotSupportedException(
|
||||
"acdream requires GL_ARB_bindless_texture + " +
|
||||
"GL_ARB_shader_draw_parameters (GL 4.3+ with bindless support). " +
|
||||
"Your GPU/driver does not expose these extensions. If this is " +
|
||||
"unexpected, please file a bug report with your GPU vendor + " +
|
||||
"driver version.");
|
||||
}
|
||||
|
||||
public TerrainAtlas AcquireTerrainAtlas(
|
||||
IGameRenderResourceLifetime lifetime,
|
||||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
BindlessSupport bindless)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(lifetime);
|
||||
return lifetime.AcquireTerrainAtlas(
|
||||
() => TerrainAtlas.Build(gl, dats, bindless));
|
||||
}
|
||||
|
||||
public void SetTerrainAnisotropic(TerrainAtlas atlas, int level) =>
|
||||
atlas.SetAnisotropic(level);
|
||||
|
||||
public Shader CreateTerrainShader(GL gl, string shadersDirectory) =>
|
||||
new(
|
||||
gl,
|
||||
Path.Combine(shadersDirectory, "terrain_modern.vert"),
|
||||
Path.Combine(shadersDirectory, "terrain_modern.frag"));
|
||||
|
||||
public SceneLightingUboBinding CreateSceneLighting(GL gl) => new(gl);
|
||||
|
||||
public DebugLineRenderer CreateDebugLines(
|
||||
GL gl,
|
||||
string shadersDirectory) =>
|
||||
new(gl, shadersDirectory);
|
||||
|
||||
public byte[]? TryLoadDebugFont() =>
|
||||
BitmapFont.TryLoadSystemMonospaceFont();
|
||||
|
||||
public BitmapFont CreateDebugFont(GL gl, byte[] bytes) =>
|
||||
new(gl, bytes, pixelHeight: 15f, atlasSize: 512);
|
||||
|
||||
public TextRenderer CreateTextRenderer(
|
||||
GL gl,
|
||||
string shadersDirectory) =>
|
||||
new(gl, shadersDirectory);
|
||||
|
||||
public TerrainModernRenderer CreateTerrain(
|
||||
GL gl,
|
||||
BindlessSupport bindless,
|
||||
Shader shader,
|
||||
TerrainAtlas atlas,
|
||||
IGpuResourceRetirementQueue retirement) =>
|
||||
new(gl, bindless, shader, atlas, retirement);
|
||||
|
||||
public WorldTerrainBuildContext CreateTerrainBuildContext(
|
||||
uint initialCenterLandblockId,
|
||||
float[] heightTable,
|
||||
TerrainAtlas atlas)
|
||||
{
|
||||
int centerX = (int)((initialCenterLandblockId >> 24) & 0xFFu);
|
||||
int centerY = (int)((initialCenterLandblockId >> 16) & 0xFFu);
|
||||
var layers = new Dictionary<uint, byte>(atlas.TerrainTypeToLayer.Count);
|
||||
foreach ((uint terrainType, uint layer) in atlas.TerrainTypeToLayer)
|
||||
layers[terrainType] = (byte)layer;
|
||||
|
||||
const uint RoadTypeEnumValue = 0x20;
|
||||
byte roadLayer = layers.TryGetValue(RoadTypeEnumValue, out byte road)
|
||||
? road
|
||||
: SurfaceInfo.None;
|
||||
var blending = new TerrainBlendingContext(
|
||||
TerrainTypeToLayer: layers,
|
||||
RoadLayer: roadLayer,
|
||||
CornerAlphaLayers: atlas.CornerAlphaLayers,
|
||||
SideAlphaLayers: atlas.SideAlphaLayers,
|
||||
RoadAlphaLayers: atlas.RoadAlphaLayers,
|
||||
CornerAlphaTCodes: atlas.CornerAlphaTCodes,
|
||||
SideAlphaTCodes: atlas.SideAlphaTCodes,
|
||||
RoadAlphaRCodes: atlas.RoadAlphaRCodes);
|
||||
return new WorldTerrainBuildContext(
|
||||
initialCenterLandblockId,
|
||||
centerX,
|
||||
centerY,
|
||||
heightTable,
|
||||
blending,
|
||||
new ConcurrentDictionary<uint, SurfaceInfo>());
|
||||
}
|
||||
|
||||
public Shader CreateMeshShader(GL gl, string shadersDirectory) =>
|
||||
new(
|
||||
gl,
|
||||
Path.Combine(shadersDirectory, "mesh_modern.vert"),
|
||||
Path.Combine(shadersDirectory, "mesh_modern.frag"));
|
||||
|
||||
public WbMeshAdapter CreateMeshAdapter(
|
||||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
IGpuResourceRetirementQueue retirement) =>
|
||||
new(
|
||||
gl,
|
||||
dats,
|
||||
NullLogger<WbMeshAdapter>.Instance,
|
||||
retirement);
|
||||
|
||||
public TextureCache CreateTextureCache(
|
||||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
BindlessSupport bindless,
|
||||
IGpuResourceRetirementQueue retirement) =>
|
||||
new(gl, dats, bindless, retirement);
|
||||
|
||||
public SamplerCache CreateSamplerCache(GL gl) => new(gl);
|
||||
|
||||
public void Release(IDisposable resource) => resource.Dispose();
|
||||
}
|
||||
|
||||
internal enum WorldRenderCompositionPoint
|
||||
{
|
||||
GlStateInitialized,
|
||||
RegionLoaded,
|
||||
EnvironmentInitialized,
|
||||
BindlessPublished,
|
||||
TerrainAtlasAcquired,
|
||||
TerrainShaderPublished,
|
||||
SceneLightingPublished,
|
||||
DebugLinesPublished,
|
||||
DebugFontCreated,
|
||||
TextRendererCreated,
|
||||
HudResourcesPublished,
|
||||
HudResourcesCompleted,
|
||||
TerrainPublished,
|
||||
TerrainBuildStatePublished,
|
||||
MeshShaderPublished,
|
||||
MeshAdapterPublished,
|
||||
TextureCachePublished,
|
||||
SamplerCachePublished,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Production Phase 4. It preserves the mandatory modern renderer and sole
|
||||
/// DAT path while moving construction into an ordered, failure-injectable
|
||||
/// ownership boundary.
|
||||
/// </summary>
|
||||
internal sealed class WorldRenderCompositionPhase
|
||||
: IWorldRenderCompositionPhase<
|
||||
GameWindowPlatformResult<GL, IInputContext>,
|
||||
ContentEffectsAudioResult,
|
||||
SettingsDevToolsResult,
|
||||
WorldRenderResult>
|
||||
{
|
||||
private readonly WorldRenderDependencies _dependencies;
|
||||
private readonly IGameWindowWorldRenderPublication _publication;
|
||||
private readonly IWorldRenderCompositionFactory _factory;
|
||||
private readonly Action<WorldRenderCompositionPoint>? _faultInjection;
|
||||
|
||||
public WorldRenderCompositionPhase(
|
||||
WorldRenderDependencies dependencies,
|
||||
IGameWindowWorldRenderPublication publication,
|
||||
IWorldRenderCompositionFactory? factory = null,
|
||||
Action<WorldRenderCompositionPoint>? faultInjection = null)
|
||||
{
|
||||
_dependencies = dependencies
|
||||
?? throw new ArgumentNullException(nameof(dependencies));
|
||||
_publication = publication
|
||||
?? throw new ArgumentNullException(nameof(publication));
|
||||
_factory = factory ?? new RetailWorldRenderCompositionFactory();
|
||||
_faultInjection = faultInjection;
|
||||
}
|
||||
|
||||
public WorldRenderResult Compose(
|
||||
GameWindowPlatformResult<GL, IInputContext> platform,
|
||||
ContentEffectsAudioResult content,
|
||||
SettingsDevToolsResult settings)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(platform);
|
||||
ArgumentNullException.ThrowIfNull(content);
|
||||
ArgumentNullException.ThrowIfNull(settings);
|
||||
|
||||
var scope = new CompositionAcquisitionScope();
|
||||
try
|
||||
{
|
||||
GL gl = platform.Graphics;
|
||||
_factory.InitializeGlState(gl);
|
||||
Fault(WorldRenderCompositionPoint.GlStateInitialized);
|
||||
|
||||
WorldRegionData region = _factory.LoadRegion(content.Dats);
|
||||
Fault(WorldRenderCompositionPoint.RegionLoaded);
|
||||
_factory.InitializeEnvironment(_dependencies.Environment, region.Region);
|
||||
Fault(WorldRenderCompositionPoint.EnvironmentInitialized);
|
||||
|
||||
BindlessSupport bindless = _factory.RequireBindless(gl, _dependencies.Log);
|
||||
_publication.PublishBindlessSupport(bindless);
|
||||
Fault(WorldRenderCompositionPoint.BindlessPublished);
|
||||
|
||||
TerrainAtlas terrainAtlas = _factory.AcquireTerrainAtlas(
|
||||
_dependencies.RenderResources,
|
||||
gl,
|
||||
content.Dats,
|
||||
bindless);
|
||||
_factory.SetTerrainAnisotropic(
|
||||
terrainAtlas,
|
||||
settings.ResolvedQuality.AnisotropicLevel);
|
||||
Fault(WorldRenderCompositionPoint.TerrainAtlasAcquired);
|
||||
|
||||
string shadersDirectory = Path.Combine(
|
||||
AppContext.BaseDirectory,
|
||||
"Rendering",
|
||||
"Shaders");
|
||||
Shader terrainShader = AcquireAndPublish(
|
||||
scope,
|
||||
"terrain shader",
|
||||
() => _factory.CreateTerrainShader(gl, shadersDirectory),
|
||||
_publication.PublishTerrainShader,
|
||||
WorldRenderCompositionPoint.TerrainShaderPublished);
|
||||
SceneLightingUboBinding sceneLighting = AcquireAndPublish(
|
||||
scope,
|
||||
"scene lighting",
|
||||
() => _factory.CreateSceneLighting(gl),
|
||||
_publication.PublishSceneLighting,
|
||||
WorldRenderCompositionPoint.SceneLightingPublished);
|
||||
DebugLineRenderer debugLines = AcquireAndPublish(
|
||||
scope,
|
||||
"debug lines",
|
||||
() => _factory.CreateDebugLines(gl, shadersDirectory),
|
||||
_publication.PublishDebugLines,
|
||||
WorldRenderCompositionPoint.DebugLinesPublished);
|
||||
|
||||
(BitmapFont? debugFont, TextRenderer? textRenderer) =
|
||||
ComposeOptionalHudResources(scope, gl, shadersDirectory);
|
||||
|
||||
TerrainModernRenderer terrain = AcquireAndPublish(
|
||||
scope,
|
||||
"terrain renderer",
|
||||
() => _factory.CreateTerrain(
|
||||
gl,
|
||||
bindless,
|
||||
terrainShader,
|
||||
terrainAtlas,
|
||||
_dependencies.ResourceRetirement),
|
||||
_publication.PublishTerrain,
|
||||
WorldRenderCompositionPoint.TerrainPublished);
|
||||
|
||||
WorldTerrainBuildContext terrainBuild =
|
||||
_factory.CreateTerrainBuildContext(
|
||||
_dependencies.InitialCenterLandblockId,
|
||||
region.HeightTable,
|
||||
terrainAtlas);
|
||||
_publication.PublishTerrainBuildState(
|
||||
terrainBuild.HeightTable,
|
||||
terrainBuild.Blending,
|
||||
terrainBuild.SurfaceCache);
|
||||
Fault(WorldRenderCompositionPoint.TerrainBuildStatePublished);
|
||||
|
||||
Shader meshShader = AcquireAndPublish(
|
||||
scope,
|
||||
"mesh shader",
|
||||
() => _factory.CreateMeshShader(gl, shadersDirectory),
|
||||
_publication.PublishMeshShader,
|
||||
WorldRenderCompositionPoint.MeshShaderPublished);
|
||||
_dependencies.Log("[N.5] mesh_modern shader loaded");
|
||||
WbMeshAdapter meshAdapter = AcquireAndPublish(
|
||||
scope,
|
||||
"WB mesh adapter",
|
||||
() => _factory.CreateMeshAdapter(
|
||||
gl,
|
||||
content.Dats,
|
||||
_dependencies.ResourceRetirement),
|
||||
_publication.PublishWbMeshAdapter,
|
||||
WorldRenderCompositionPoint.MeshAdapterPublished);
|
||||
TextureCache textureCache = AcquireAndPublish(
|
||||
scope,
|
||||
"texture cache",
|
||||
() => _factory.CreateTextureCache(
|
||||
gl,
|
||||
content.Dats,
|
||||
bindless,
|
||||
_dependencies.ResourceRetirement),
|
||||
_publication.PublishTextureCache,
|
||||
WorldRenderCompositionPoint.TextureCachePublished);
|
||||
SamplerCache samplers = AcquireAndPublish(
|
||||
scope,
|
||||
"sampler cache",
|
||||
() => _factory.CreateSamplerCache(gl),
|
||||
_publication.PublishSamplerCache,
|
||||
WorldRenderCompositionPoint.SamplerCachePublished);
|
||||
|
||||
scope.Complete();
|
||||
_dependencies.Log(
|
||||
"[N.4+N.5] WB foundation + modern path active — " +
|
||||
"routing all content through ObjectMeshManager.");
|
||||
return new WorldRenderResult(
|
||||
terrainBuild,
|
||||
new WorldRenderFoundation(
|
||||
shadersDirectory,
|
||||
bindless,
|
||||
terrainAtlas,
|
||||
terrainShader,
|
||||
sceneLighting,
|
||||
debugLines,
|
||||
debugFont,
|
||||
textRenderer,
|
||||
terrain,
|
||||
meshShader,
|
||||
meshAdapter,
|
||||
textureCache,
|
||||
samplers));
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
scope.RollbackAndThrow(failure);
|
||||
throw new System.Diagnostics.UnreachableException();
|
||||
}
|
||||
}
|
||||
|
||||
private (BitmapFont? Font, TextRenderer? Text) ComposeOptionalHudResources(
|
||||
CompositionAcquisitionScope scope,
|
||||
GL gl,
|
||||
string shadersDirectory)
|
||||
{
|
||||
byte[]? fontBytes = _factory.TryLoadDebugFont();
|
||||
if (fontBytes is null)
|
||||
{
|
||||
_dependencies.Log("world-hud font: no system monospace font found");
|
||||
Fault(WorldRenderCompositionPoint.HudResourcesCompleted);
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
var fontLease = scope.Acquire(
|
||||
"world HUD font",
|
||||
() => _factory.CreateDebugFont(gl, fontBytes),
|
||||
_factory.Release);
|
||||
BitmapFont font = fontLease.Resource;
|
||||
Fault(WorldRenderCompositionPoint.DebugFontCreated);
|
||||
var textLease = scope.Acquire(
|
||||
"world HUD text renderer",
|
||||
() => _factory.CreateTextRenderer(gl, shadersDirectory),
|
||||
_factory.Release);
|
||||
TextRenderer text = textLease.Resource;
|
||||
Fault(WorldRenderCompositionPoint.TextRendererCreated);
|
||||
|
||||
_publication.PublishHudResources(font, text);
|
||||
fontLease.Transfer();
|
||||
textLease.Transfer();
|
||||
Fault(WorldRenderCompositionPoint.HudResourcesPublished);
|
||||
_dependencies.Log(
|
||||
$"world-hud font: loaded {fontBytes.Length / 1024}KB, " +
|
||||
$"atlas {font.AtlasWidth}x{font.AtlasHeight}, " +
|
||||
$"lineHeight={font.LineHeight:F1}px (reserved for D.6 HUD)");
|
||||
Fault(WorldRenderCompositionPoint.HudResourcesCompleted);
|
||||
return (font, text);
|
||||
}
|
||||
|
||||
private T AcquireAndPublish<T>(
|
||||
CompositionAcquisitionScope scope,
|
||||
string name,
|
||||
Func<T> factory,
|
||||
Action<T> publish,
|
||||
WorldRenderCompositionPoint point)
|
||||
where T : class, IDisposable
|
||||
{
|
||||
T value = scope.Acquire(name, factory, _factory.Release).Publish(publish);
|
||||
Fault(point);
|
||||
return value;
|
||||
}
|
||||
|
||||
private void Fault(WorldRenderCompositionPoint point) =>
|
||||
_faultInjection?.Invoke(point);
|
||||
}
|
||||
|
|
@ -38,6 +38,7 @@ public sealed unsafe class BitmapFont : IDisposable
|
|||
private readonly Glyph[] _glyphs;
|
||||
private readonly int _firstChar;
|
||||
private readonly int _numChars;
|
||||
private readonly ResourceCleanupGroup _resources;
|
||||
|
||||
public uint TextureId { get; }
|
||||
public float PixelHeight { get; }
|
||||
|
|
@ -95,27 +96,65 @@ public sealed unsafe class BitmapFont : IDisposable
|
|||
adv: bc.xadvance);
|
||||
}
|
||||
|
||||
// Upload atlas as a single-channel GL texture (R8).
|
||||
TextureId = _gl.GenTexture();
|
||||
_gl.BindTexture(TextureTarget.Texture2D, TextureId);
|
||||
_gl.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
|
||||
fixed (byte* ptr = pixels)
|
||||
// Upload atlas as a single-channel GL texture (R8). Publish the GL
|
||||
// name into the construction ledger before any later upload/state
|
||||
// command can fail.
|
||||
var resources = new ResourceCleanupGroup();
|
||||
uint texture = 0;
|
||||
try
|
||||
{
|
||||
_gl.TexImage2D(TextureTarget.Texture2D, 0,
|
||||
(int)InternalFormat.R8,
|
||||
(uint)AtlasWidth, (uint)AtlasHeight, 0,
|
||||
PixelFormat.Red, PixelType.UnsignedByte, ptr);
|
||||
texture = GlResourceCommand.CreateTexture(_gl, "BitmapFont atlas");
|
||||
uint ownedTexture = texture;
|
||||
resources.Add(
|
||||
"bitmap-font atlas",
|
||||
() => GlResourceCommand.DeleteTexture(
|
||||
_gl,
|
||||
ownedTexture,
|
||||
$"delete BitmapFont atlas {ownedTexture}"));
|
||||
_gl.GetInteger(GetPName.TextureBinding2D, out int previousTexture);
|
||||
_gl.GetInteger(GetPName.UnpackAlignment, out int previousAlignment);
|
||||
GlResourceCommand.Execute(_gl, "initialize BitmapFont atlas", () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
_gl.BindTexture(TextureTarget.Texture2D, texture);
|
||||
_gl.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
|
||||
fixed (byte* ptr = pixels)
|
||||
{
|
||||
_gl.TexImage2D(TextureTarget.Texture2D, 0,
|
||||
(int)InternalFormat.R8,
|
||||
(uint)AtlasWidth, (uint)AtlasHeight, 0,
|
||||
PixelFormat.Red, PixelType.UnsignedByte, ptr);
|
||||
}
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter,
|
||||
(int)TextureMinFilter.Linear);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter,
|
||||
(int)TextureMagFilter.Linear);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS,
|
||||
(int)TextureWrapMode.ClampToEdge);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT,
|
||||
(int)TextureWrapMode.ClampToEdge);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gl.PixelStore(
|
||||
PixelStoreParameter.UnpackAlignment,
|
||||
previousAlignment);
|
||||
_gl.BindTexture(
|
||||
TextureTarget.Texture2D,
|
||||
unchecked((uint)previousTexture));
|
||||
}
|
||||
});
|
||||
}
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter,
|
||||
(int)TextureMinFilter.Linear);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter,
|
||||
(int)TextureMagFilter.Linear);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS,
|
||||
(int)TextureWrapMode.ClampToEdge);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT,
|
||||
(int)TextureWrapMode.ClampToEdge);
|
||||
_gl.PixelStore(PixelStoreParameter.UnpackAlignment, 4); // restore default
|
||||
_gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||
catch (Exception constructionFailure)
|
||||
{
|
||||
resources.RollbackConstructionAndThrow(
|
||||
"BitmapFont construction failed and its GL atlas did not cleanly roll back.",
|
||||
constructionFailure);
|
||||
}
|
||||
|
||||
TextureId = texture;
|
||||
_resources = resources;
|
||||
}
|
||||
|
||||
public bool TryGetGlyph(char c, out Glyph g)
|
||||
|
|
@ -144,7 +183,7 @@ public sealed unsafe class BitmapFont : IDisposable
|
|||
|
||||
public void Dispose()
|
||||
{
|
||||
_gl.DeleteTexture(TextureId);
|
||||
_resources.RetryCleanup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ public sealed unsafe class DebugLineRenderer : IDisposable
|
|||
private readonly Shader _shader;
|
||||
private readonly uint _vao;
|
||||
private readonly uint _vbo;
|
||||
private readonly ResourceCleanupGroup _resources;
|
||||
|
||||
private readonly List<float> _buffer = new(4096);
|
||||
private int _vertexCount;
|
||||
|
|
@ -27,25 +28,67 @@ public sealed unsafe class DebugLineRenderer : IDisposable
|
|||
|
||||
public DebugLineRenderer(GL gl, string shaderDir)
|
||||
{
|
||||
_gl = gl;
|
||||
_shader = new Shader(gl,
|
||||
Path.Combine(shaderDir, "debug_line.vert"),
|
||||
Path.Combine(shaderDir, "debug_line.frag"));
|
||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(shaderDir);
|
||||
var resources = new ResourceCleanupGroup();
|
||||
Shader? shader = null;
|
||||
uint vao = 0;
|
||||
uint vbo = 0;
|
||||
try
|
||||
{
|
||||
shader = new Shader(gl,
|
||||
Path.Combine(shaderDir, "debug_line.vert"),
|
||||
Path.Combine(shaderDir, "debug_line.frag"));
|
||||
resources.Add("debug-line shader", shader.Dispose);
|
||||
vao = GlResourceCommand.CreateName(
|
||||
gl,
|
||||
"debug-line VAO",
|
||||
gl.GenVertexArray,
|
||||
gl.DeleteVertexArray);
|
||||
uint ownedVao = vao;
|
||||
resources.Add(
|
||||
"debug-line VAO",
|
||||
() => GlResourceCommand.DeleteVertexArray(
|
||||
gl,
|
||||
ownedVao,
|
||||
$"delete debug-line VAO {ownedVao}"));
|
||||
vbo = GlResourceCommand.CreateName(
|
||||
gl,
|
||||
"debug-line VBO",
|
||||
gl.GenBuffer,
|
||||
gl.DeleteBuffer);
|
||||
uint ownedVbo = vbo;
|
||||
resources.Add(
|
||||
"debug-line VBO",
|
||||
() => GlResourceCommand.DeleteBuffer(
|
||||
gl,
|
||||
ownedVbo,
|
||||
$"delete debug-line VBO {ownedVbo}"));
|
||||
|
||||
_vao = _gl.GenVertexArray();
|
||||
_vbo = _gl.GenBuffer();
|
||||
GlResourceCommand.Execute(gl, "configure debug-line vertex state", () =>
|
||||
{
|
||||
gl.BindVertexArray(vao);
|
||||
gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo);
|
||||
// 24-byte stride: vec3 pos + vec3 color
|
||||
gl.EnableVertexAttribArray(0);
|
||||
gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 6 * sizeof(float), (void*)0);
|
||||
gl.EnableVertexAttribArray(1);
|
||||
gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, 6 * sizeof(float), (void*)(3 * sizeof(float)));
|
||||
gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
||||
gl.BindVertexArray(0);
|
||||
});
|
||||
}
|
||||
catch (Exception constructionFailure)
|
||||
{
|
||||
resources.RollbackConstructionAndThrow(
|
||||
"DebugLineRenderer construction failed and its GL prefix did not cleanly roll back.",
|
||||
constructionFailure);
|
||||
}
|
||||
|
||||
_gl.BindVertexArray(_vao);
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
|
||||
|
||||
// 24-byte stride: vec3 pos + vec3 color
|
||||
_gl.EnableVertexAttribArray(0);
|
||||
_gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 6 * sizeof(float), (void*)0);
|
||||
_gl.EnableVertexAttribArray(1);
|
||||
_gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, 6 * sizeof(float), (void*)(3 * sizeof(float)));
|
||||
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
||||
_gl.BindVertexArray(0);
|
||||
_resources = resources;
|
||||
_shader = shader!;
|
||||
_vao = vao;
|
||||
_vbo = vbo;
|
||||
}
|
||||
|
||||
/// <summary>Clear accumulated lines. Call at the start of each frame.</summary>
|
||||
|
|
@ -165,8 +208,6 @@ public sealed unsafe class DebugLineRenderer : IDisposable
|
|||
|
||||
public void Dispose()
|
||||
{
|
||||
_gl.DeleteVertexArray(_vao);
|
||||
_gl.DeleteBuffer(_vbo);
|
||||
_shader.Dispose();
|
||||
_resources.RetryCleanup();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal interface IGameRenderResourceLifetime
|
||||
{
|
||||
TerrainAtlas AcquireTerrainAtlas(Func<TerrainAtlas> factory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sole lifetime owner for render resources that are borrowed by, but not
|
||||
/// owned by, their renderers.
|
||||
/// </summary>
|
||||
internal sealed class GameRenderResourceLifetime
|
||||
internal sealed class GameRenderResourceLifetime : IGameRenderResourceLifetime
|
||||
{
|
||||
private readonly OwnedResourceSlot<TerrainAtlas> _terrainAtlas = new();
|
||||
private readonly OwnedResourceSlot<Shader> _skyShader = new();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using AcDream.Core.Plugins;
|
||||
using AcDream.App.Composition;
|
||||
using AcDream.App.Physics;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Settings;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Content;
|
||||
|
|
@ -17,7 +18,8 @@ public sealed class GameWindow :
|
|||
IGameWindowPlatformPublication<GL, IInputContext>,
|
||||
IGameWindowHostInputCameraPublication,
|
||||
IGameWindowContentEffectsAudioPublication,
|
||||
IGameWindowSettingsDevToolsPublication
|
||||
IGameWindowSettingsDevToolsPublication,
|
||||
IGameWindowWorldRenderPublication
|
||||
{
|
||||
private static double ClientTimerNow() =>
|
||||
System.Diagnostics.Stopwatch.GetTimestamp()
|
||||
|
|
@ -802,6 +804,84 @@ public sealed class GameWindow :
|
|||
_debugVm = value.Debug;
|
||||
}
|
||||
|
||||
void IGameWindowWorldRenderPublication.PublishBindlessSupport(
|
||||
BindlessSupport value) =>
|
||||
PublishCompositionOwner(
|
||||
ref _bindlessSupport,
|
||||
value,
|
||||
"bindless support");
|
||||
|
||||
void IGameWindowWorldRenderPublication.PublishTerrainShader(Shader value) =>
|
||||
PublishCompositionOwner(
|
||||
ref _terrainModernShader,
|
||||
value,
|
||||
"terrain shader");
|
||||
|
||||
void IGameWindowWorldRenderPublication.PublishSceneLighting(
|
||||
SceneLightingUboBinding value) =>
|
||||
PublishCompositionOwner(
|
||||
ref _sceneLightingUbo,
|
||||
value,
|
||||
"scene lighting");
|
||||
|
||||
void IGameWindowWorldRenderPublication.PublishDebugLines(
|
||||
DebugLineRenderer value) =>
|
||||
PublishCompositionOwner(ref _debugLines, value, "debug lines");
|
||||
|
||||
void IGameWindowWorldRenderPublication.PublishHudResources(
|
||||
BitmapFont font,
|
||||
TextRenderer text)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(font);
|
||||
ArgumentNullException.ThrowIfNull(text);
|
||||
if (_debugFont is not null || _textRenderer is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The GameWindow composition shell already owns world HUD resources.");
|
||||
}
|
||||
_debugFont = font;
|
||||
_textRenderer = text;
|
||||
}
|
||||
|
||||
void IGameWindowWorldRenderPublication.PublishTerrain(
|
||||
TerrainModernRenderer value) =>
|
||||
PublishCompositionOwner(ref _terrain, value, "terrain renderer");
|
||||
|
||||
void IGameWindowWorldRenderPublication.PublishTerrainBuildState(
|
||||
float[] heightTable,
|
||||
AcDream.Core.Terrain.TerrainBlendingContext blending,
|
||||
System.Collections.Concurrent.ConcurrentDictionary<
|
||||
uint,
|
||||
AcDream.Core.Terrain.SurfaceInfo> surfaceCache)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(heightTable);
|
||||
ArgumentNullException.ThrowIfNull(blending);
|
||||
ArgumentNullException.ThrowIfNull(surfaceCache);
|
||||
if (_heightTable is not null || _blendCtx is not null || _surfaceCache is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The GameWindow composition shell already owns terrain build state.");
|
||||
}
|
||||
_heightTable = heightTable;
|
||||
_blendCtx = blending;
|
||||
_surfaceCache = surfaceCache;
|
||||
}
|
||||
|
||||
void IGameWindowWorldRenderPublication.PublishMeshShader(Shader value) =>
|
||||
PublishCompositionOwner(ref _meshShader, value, "mesh shader");
|
||||
|
||||
void IGameWindowWorldRenderPublication.PublishWbMeshAdapter(
|
||||
WbMeshAdapter value) =>
|
||||
PublishCompositionOwner(ref _wbMeshAdapter, value, "WB mesh adapter");
|
||||
|
||||
void IGameWindowWorldRenderPublication.PublishTextureCache(
|
||||
TextureCache value) =>
|
||||
PublishCompositionOwner(ref _textureCache, value, "texture cache");
|
||||
|
||||
void IGameWindowWorldRenderPublication.PublishSamplerCache(
|
||||
SamplerCache value) =>
|
||||
PublishCompositionOwner(ref _samplerCache, value, "sampler cache");
|
||||
|
||||
private static void PublishCompositionOwner<T>(
|
||||
ref T? destination,
|
||||
T value,
|
||||
|
|
@ -932,168 +1012,22 @@ public sealed class GameWindow :
|
|||
Console.WriteLine),
|
||||
this).Compose(platform, hostInputCamera, contentEffectsAudio);
|
||||
|
||||
_gl.ClearColor(0.05f, 0.10f, 0.18f, 1.0f);
|
||||
_gl.Enable(EnableCap.DepthTest);
|
||||
|
||||
// Phase U.3: the 8 hardware clip planes (GL_CLIP_DISTANCE0..7) are NOT
|
||||
// enabled here. Only the mesh_modern / terrain_modern vertex shaders write
|
||||
// gl_ClipDistance[0..7]; the sky / particle / weather / UI / debug-line
|
||||
// shaders write gl_Position but no gl_ClipDistance. Enabling a clip plane
|
||||
// for a draw whose vertex shader doesn't write that plane's distance is
|
||||
// UNDEFINED per the GL/GLSL spec (a driver may read the unwritten value as
|
||||
// negative and clip the primitive away). So instead of a permanent global
|
||||
// enable, OnRender brackets glEnable/glDisable(GL_CLIP_DISTANCE0..7) around
|
||||
// ONLY the clip-writing world-geometry draws (terrain + entities, plus
|
||||
// U.4's EnvCellRenderer.Render); everything else draws with clipping off.
|
||||
|
||||
string shadersDir = Path.Combine(AppContext.BaseDirectory, "Rendering", "Shaders");
|
||||
|
||||
// Phase N.5b: terrain_modern shader pair — bindless texture handles +
|
||||
// glMultiDrawElementsIndirect dispatch path. The only terrain shader
|
||||
// since Task 9 retired the legacy terrain.vert/.frag program.
|
||||
_terrainModernShader = new Shader(_gl,
|
||||
Path.Combine(shadersDir, "terrain_modern.vert"),
|
||||
Path.Combine(shadersDir, "terrain_modern.frag"));
|
||||
|
||||
// Phase G.1/G.2: shared scene-lighting UBO. Stays bound at
|
||||
// binding=1 for the lifetime of the process — every shader that
|
||||
// declares `layout(std140, binding = 1) uniform SceneLighting`
|
||||
// reads from this without further intervention.
|
||||
_sceneLightingUbo = new SceneLightingUboBinding(_gl);
|
||||
|
||||
_debugLines = new DebugLineRenderer(_gl, shadersDir);
|
||||
|
||||
// Phase I.2: load a system monospace font + TextRenderer for the
|
||||
// future world-space HUD (D.6). The custom DebugOverlay is gone;
|
||||
// the ImGui DebugPanel handles all dev surfaces now. These fields
|
||||
// are reserved for future work — currently unused at the renderer
|
||||
// level. Skips silently if no font is available.
|
||||
var fontBytes = BitmapFont.TryLoadSystemMonospaceFont();
|
||||
if (fontBytes is not null)
|
||||
{
|
||||
_debugFont = new BitmapFont(_gl, fontBytes, pixelHeight: 15f, atlasSize: 512);
|
||||
_textRenderer = new TextRenderer(_gl, shadersDir);
|
||||
Console.WriteLine($"world-hud font: loaded {fontBytes.Length / 1024}KB, " +
|
||||
$"atlas {_debugFont.AtlasWidth}x{_debugFont.AtlasHeight}, " +
|
||||
$"lineHeight={_debugFont.LineHeight:F1}px (reserved for D.6 HUD)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("world-hud font: no system monospace font found");
|
||||
}
|
||||
|
||||
uint centerLandblockId = 0xA9B4FFFFu;
|
||||
Console.WriteLine($"loading world view centered on 0x{centerLandblockId:X8}");
|
||||
|
||||
var region = contentEffectsAudio.Dats.Get<DatReaderWriter.DBObjs.Region>(
|
||||
0x13000000u);
|
||||
var heightTable = region?.LandDefs.LandHeightTable;
|
||||
if (heightTable is null || heightTable.Length < 256)
|
||||
throw new InvalidOperationException("Region.LandDefs.LandHeightTable missing or truncated");
|
||||
|
||||
// Parse and install the DAT-backed sky, retail day-group state, and
|
||||
// offline clock seed through their single environment owner.
|
||||
_worldEnvironment.Initialize(region!);
|
||||
|
||||
// N.5: detect ARB_bindless_texture + ARB_shader_draw_parameters BEFORE
|
||||
// building the terrain atlas / renderer — both consume BindlessSupport
|
||||
// (atlas via Texture2DArray bindless handles, renderer for SSBO uploads).
|
||||
// The modern path (SSBO + glMultiDrawElementsIndirect + bindless textures)
|
||||
// is mandatory as of Phase N.5 — missing extensions throw at startup with
|
||||
// a clear error so users can file a real bug report rather than silently
|
||||
// falling back to a half-working renderer.
|
||||
if (AcDream.App.Rendering.Wb.BindlessSupport.TryCreate(_gl, out var bindless))
|
||||
{
|
||||
if (bindless!.HasShaderDrawParameters(_gl))
|
||||
{
|
||||
_bindlessSupport = bindless;
|
||||
Console.WriteLine("[N.5] modern path capabilities present (bindless + ARB_shader_draw_parameters)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("[N.5] GL_ARB_shader_draw_parameters not present — modern path not available");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("[N.5] GL_ARB_bindless_texture not present — modern path not available");
|
||||
}
|
||||
|
||||
if (_bindlessSupport is null)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"acdream requires GL_ARB_bindless_texture + GL_ARB_shader_draw_parameters " +
|
||||
"(GL 4.3+ with bindless support). Your GPU/driver does not expose these extensions. " +
|
||||
"If this is unexpected, please file a bug report with your GPU vendor + driver version.");
|
||||
}
|
||||
|
||||
// Build the terrain atlas once from the Region dat. Phase N.5b: the
|
||||
// atlas exposes bindless handles for the modern terrain path, so
|
||||
// BindlessSupport is threaded through.
|
||||
var terrainAtlas = _renderResourceLifetime.AcquireTerrainAtlas(
|
||||
() => AcDream.App.Rendering.TerrainAtlas.Build(
|
||||
_gl,
|
||||
contentEffectsAudio.Dats,
|
||||
_bindlessSupport));
|
||||
// A.5 T22.5: apply anisotropic level from quality preset. Build()
|
||||
// hard-codes 16x; override here to match the resolved quality so Low
|
||||
// (4x) and Medium (8x) actually take effect.
|
||||
terrainAtlas.SetAnisotropic(_runtimeSettings.ResolvedQuality.AnisotropicLevel);
|
||||
|
||||
_terrain = new TerrainModernRenderer(
|
||||
_gl,
|
||||
_bindlessSupport,
|
||||
_terrainModernShader!,
|
||||
terrainAtlas,
|
||||
_gpuFrameFlights!);
|
||||
|
||||
int centerX = (int)((centerLandblockId >> 24) & 0xFFu);
|
||||
int centerY = (int)((centerLandblockId >> 16) & 0xFFu);
|
||||
|
||||
// Build blending context from the terrain atlas. The worker-side
|
||||
// LandblockBuildFactory captures this immutable table for each build.
|
||||
var terrainTypeToLayerBytes = new Dictionary<uint, byte>(terrainAtlas.TerrainTypeToLayer.Count);
|
||||
foreach (var kvp in terrainAtlas.TerrainTypeToLayer)
|
||||
terrainTypeToLayerBytes[kvp.Key] = (byte)kvp.Value;
|
||||
|
||||
const uint RoadTypeEnumValue = 0x20; // TerrainTextureType.RoadType
|
||||
byte roadLayer = terrainTypeToLayerBytes.TryGetValue(RoadTypeEnumValue, out var rl)
|
||||
? rl
|
||||
: AcDream.Core.Terrain.SurfaceInfo.None;
|
||||
|
||||
_blendCtx = new AcDream.Core.Terrain.TerrainBlendingContext(
|
||||
TerrainTypeToLayer: terrainTypeToLayerBytes,
|
||||
RoadLayer: roadLayer,
|
||||
CornerAlphaLayers: terrainAtlas.CornerAlphaLayers,
|
||||
SideAlphaLayers: terrainAtlas.SideAlphaLayers,
|
||||
RoadAlphaLayers: terrainAtlas.RoadAlphaLayers,
|
||||
CornerAlphaTCodes: terrainAtlas.CornerAlphaTCodes,
|
||||
SideAlphaTCodes: terrainAtlas.SideAlphaTCodes,
|
||||
RoadAlphaRCodes: terrainAtlas.RoadAlphaRCodes);
|
||||
|
||||
_heightTable = heightTable;
|
||||
_surfaceCache = new System.Collections.Concurrent.ConcurrentDictionary<uint, AcDream.Core.Terrain.SurfaceInfo>();
|
||||
|
||||
// (Bindless detection moved above — must precede TerrainAtlas.Build /
|
||||
// TerrainModernRenderer ctor so they can consume BindlessSupport.)
|
||||
|
||||
// Mesh shader always loads (modern path is the only path).
|
||||
_meshShader = new Shader(_gl,
|
||||
Path.Combine(shadersDir, "mesh_modern.vert"),
|
||||
Path.Combine(shadersDir, "mesh_modern.frag"));
|
||||
Console.WriteLine("[N.5] mesh_modern shader loaded");
|
||||
|
||||
_textureCache = new TextureCache(
|
||||
_gl,
|
||||
contentEffectsAudio.Dats,
|
||||
_bindlessSupport,
|
||||
_gpuFrameFlights!);
|
||||
// Two persistent GL sampler objects (Repeat + ClampToEdge) so
|
||||
// the sky pass can pick wrap mode per submesh without mutating
|
||||
// shared per-texture wrap state. See SamplerCache + the
|
||||
// WorldBuilder reference at
|
||||
// references/WorldBuilder/Chorizite.OpenGLSDLBackend/OpenGLGraphicsDevice.cs:115-132.
|
||||
_samplerCache = new SamplerCache(_gl);
|
||||
const uint initialCenterLandblockId = 0xA9B4FFFFu;
|
||||
WorldRenderResult worldRender = new WorldRenderCompositionPhase(
|
||||
new WorldRenderDependencies(
|
||||
_worldEnvironment,
|
||||
_renderResourceLifetime,
|
||||
_gpuFrameFlights!,
|
||||
initialCenterLandblockId,
|
||||
Console.WriteLine),
|
||||
this).Compose(platform, contentEffectsAudio, settingsDevTools);
|
||||
string shadersDir = worldRender.Foundation.ShadersDirectory;
|
||||
TerrainAtlas terrainAtlas = worldRender.Foundation.TerrainAtlas;
|
||||
int centerX = worldRender.TerrainBuild.InitialCenterX;
|
||||
int centerY = worldRender.TerrainBuild.InitialCenterY;
|
||||
Console.WriteLine(
|
||||
$"loading world view centered on " +
|
||||
$"0x{worldRender.TerrainBuild.InitialCenterLandblockId:X8}");
|
||||
|
||||
// Retail ClientCombatSystem attack-request owner. This exists independently
|
||||
// of the retained UI so keyboard combat keeps the same press/hold/release
|
||||
|
|
@ -1456,22 +1390,6 @@ public sealed class GameWindow :
|
|||
|
||||
}
|
||||
|
||||
// Phase N.4+N.5 — WB rendering pipeline foundation. The modern path is
|
||||
// mandatory as of N.5 ship amendment: WbMeshAdapter + WbDrawDispatcher
|
||||
// always construct.
|
||||
// Phase O (2026-05-21): WbMeshAdapter now consumes our DatCollection
|
||||
// directly via DatCollectionAdapter. No second dat reader; index cache
|
||||
// is shared with the rest of the client.
|
||||
{
|
||||
var wbLogger = Microsoft.Extensions.Logging.Abstractions.NullLogger<AcDream.App.Rendering.Wb.WbMeshAdapter>.Instance;
|
||||
_wbMeshAdapter = new AcDream.App.Rendering.Wb.WbMeshAdapter(
|
||||
_gl,
|
||||
contentEffectsAudio.Dats,
|
||||
wbLogger,
|
||||
_gpuFrameFlights!);
|
||||
Console.WriteLine("[N.4+N.5] WB foundation + modern path active — routing all content through ObjectMeshManager.");
|
||||
}
|
||||
|
||||
// Phase N.4 Task 12: construct LandblockSpawnAdapter under the feature flag
|
||||
// and rebuild _worldState so it threads the adapter in. _worldState starts
|
||||
// as an unadorned GpuWorldState (field initializer); here we replace it with
|
||||
|
|
@ -1937,7 +1855,7 @@ public sealed class GameWindow :
|
|||
contentEffectsAudio.Dats,
|
||||
skyShader,
|
||||
_textureCache!,
|
||||
_samplerCache);
|
||||
_samplerCache!);
|
||||
|
||||
// Phase G.1 particle renderer — renders rain / snow / spell auras
|
||||
// spawned into the shared ParticleSystem. Mode-1/no-degrade GfxObjs
|
||||
|
|
|
|||
|
|
@ -221,35 +221,46 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
handle => _particleGfxInfoByEmitter.Remove(handle),
|
||||
handle => _textures?.ReleaseParticleTextureOwner(handle),
|
||||
error => Console.Error.WriteLine($"[particles] {error}"));
|
||||
_shader = new Shader(_gl,
|
||||
System.IO.Path.Combine(shadersDir, "particle.vert"),
|
||||
System.IO.Path.Combine(shadersDir, "particle.frag"));
|
||||
if (_meshAdapter?.MeshManager?.GlobalBuffer is not null)
|
||||
{
|
||||
_meshShader = new Shader(_gl,
|
||||
System.IO.Path.Combine(shadersDir, "particle_mesh.vert"),
|
||||
System.IO.Path.Combine(shadersDir, "particle_mesh.frag"));
|
||||
_meshTextureHandleLoc = _gl.GetUniformLocation(_meshShader.Program, "uTextureHandle");
|
||||
_meshTextureLayerLoc = _gl.GetUniformLocation(_meshShader.Program, "uTextureLayer");
|
||||
}
|
||||
|
||||
float[] quadVerts =
|
||||
{
|
||||
-0.5f, -0.5f, 0f, 0f,
|
||||
0.5f, -0.5f, 1f, 0f,
|
||||
0.5f, 0.5f, 1f, 1f,
|
||||
-0.5f, 0.5f, 0f, 1f,
|
||||
};
|
||||
uint[] quadIdx = { 0, 1, 2, 0, 2, 3 };
|
||||
|
||||
uint quadVbo = 0;
|
||||
uint quadEbo = 0;
|
||||
uint uploadVao = 0;
|
||||
bool vboAllocated = false;
|
||||
bool eboAllocated = false;
|
||||
var constructionResources = new ResourceCleanupGroup();
|
||||
try
|
||||
{
|
||||
quadVbo = TrackedGlResource.CreateBuffer(_gl, "creating particle quad VBO");
|
||||
_shader = new Shader(_gl,
|
||||
System.IO.Path.Combine(shadersDir, "particle.vert"),
|
||||
System.IO.Path.Combine(shadersDir, "particle.frag"));
|
||||
constructionResources.Add("particle shader", _shader.Dispose);
|
||||
if (_meshAdapter?.MeshManager?.GlobalBuffer is not null)
|
||||
{
|
||||
_meshShader = new Shader(_gl,
|
||||
System.IO.Path.Combine(shadersDir, "particle_mesh.vert"),
|
||||
System.IO.Path.Combine(shadersDir, "particle_mesh.frag"));
|
||||
constructionResources.Add(
|
||||
"particle mesh shader",
|
||||
_meshShader.Dispose);
|
||||
_meshTextureHandleLoc = _gl.GetUniformLocation(_meshShader.Program, "uTextureHandle");
|
||||
_meshTextureLayerLoc = _gl.GetUniformLocation(_meshShader.Program, "uTextureLayer");
|
||||
}
|
||||
|
||||
float[] quadVerts =
|
||||
{
|
||||
-0.5f, -0.5f, 0f, 0f,
|
||||
0.5f, -0.5f, 1f, 0f,
|
||||
0.5f, 0.5f, 1f, 1f,
|
||||
-0.5f, 0.5f, 0f, 1f,
|
||||
};
|
||||
uint[] quadIdx = { 0, 1, 2, 0, 2, 3 };
|
||||
|
||||
bool vboAllocated = false;
|
||||
bool eboAllocated = false;
|
||||
uint quadVbo = TrackedGlResource.CreateBuffer(
|
||||
_gl,
|
||||
"creating particle quad VBO");
|
||||
RetryableGpuResourceRelease quadVboRelease =
|
||||
TrackedGlResource.CreateRetryableBufferDeletion(
|
||||
_gl,
|
||||
quadVbo,
|
||||
() => vboAllocated ? quadVerts.Length * sizeof(float) : 0,
|
||||
"rolling back particle quad VBO");
|
||||
constructionResources.Add("particle quad VBO", quadVboRelease.Run);
|
||||
fixed (void* p = quadVerts)
|
||||
{
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
|
|
@ -264,9 +275,31 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
}
|
||||
vboAllocated = true;
|
||||
|
||||
quadEbo = TrackedGlResource.CreateBuffer(_gl, "creating particle quad EBO");
|
||||
uploadVao = TrackedGlResource.CreateVertexArray(_gl, "creating particle upload VAO");
|
||||
_gl.BindVertexArray(uploadVao);
|
||||
uint quadEbo = TrackedGlResource.CreateBuffer(
|
||||
_gl,
|
||||
"creating particle quad EBO");
|
||||
RetryableGpuResourceRelease quadEboRelease =
|
||||
TrackedGlResource.CreateRetryableBufferDeletion(
|
||||
_gl,
|
||||
quadEbo,
|
||||
() => eboAllocated ? quadIdx.Length * sizeof(uint) : 0,
|
||||
"rolling back particle quad EBO");
|
||||
constructionResources.Add("particle quad EBO", quadEboRelease.Run);
|
||||
|
||||
uint uploadVao = TrackedGlResource.CreateVertexArray(
|
||||
_gl,
|
||||
"creating particle upload VAO");
|
||||
RetryableGpuResourceRelease uploadVaoRelease =
|
||||
TrackedGlResource.CreateRetryableVertexArrayDeletion(
|
||||
_gl,
|
||||
uploadVao,
|
||||
"rolling back particle upload VAO");
|
||||
constructionResources.Add("particle upload VAO", uploadVaoRelease.Run);
|
||||
|
||||
GlResourceCommand.Execute(
|
||||
_gl,
|
||||
"bind particle upload VAO",
|
||||
() => _gl.BindVertexArray(uploadVao));
|
||||
fixed (void* p = quadIdx)
|
||||
{
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
|
|
@ -280,44 +313,26 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
"uploading particle quad EBO");
|
||||
}
|
||||
eboAllocated = true;
|
||||
_gl.BindVertexArray(0);
|
||||
TrackedGlResource.DeleteVertexArray(_gl, uploadVao, "deleting particle upload VAO");
|
||||
uploadVao = 0;
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
||||
GlResourceCommand.Execute(_gl, "finish particle static-buffer upload", () =>
|
||||
{
|
||||
_gl.BindVertexArray(0);
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
||||
});
|
||||
uploadVaoRelease.Run();
|
||||
|
||||
_quadVbo = quadVbo;
|
||||
_quadEbo = quadEbo;
|
||||
}
|
||||
catch (Exception creationFailure)
|
||||
{
|
||||
List<Exception>? cleanupFailures = null;
|
||||
void Attempt(Action action)
|
||||
{
|
||||
try { action(); }
|
||||
catch (Exception ex) { (cleanupFailures ??= []).Add(ex); }
|
||||
}
|
||||
if (uploadVao != 0)
|
||||
Attempt(() => TrackedGlResource.DeleteVertexArray(_gl, uploadVao, "rolling back particle upload VAO"));
|
||||
if (quadEbo != 0)
|
||||
Attempt(() => TrackedGlResource.DeleteBuffer(
|
||||
_gl,
|
||||
quadEbo,
|
||||
eboAllocated ? quadIdx.Length * sizeof(uint) : 0,
|
||||
"rolling back particle quad EBO"));
|
||||
if (quadVbo != 0)
|
||||
Attempt(() => TrackedGlResource.DeleteBuffer(
|
||||
_gl,
|
||||
quadVbo,
|
||||
vboAllocated ? quadVerts.Length * sizeof(float) : 0,
|
||||
"rolling back particle quad VBO"));
|
||||
if (cleanupFailures is not null)
|
||||
{
|
||||
cleanupFailures.Insert(0, creationFailure);
|
||||
throw new AggregateException("Particle static-buffer creation and rollback failed.", cleanupFailures);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
|
||||
_particles.EmitterDied += OnEmitterDied;
|
||||
_particles.EmitterDied += OnEmitterDied;
|
||||
constructionResources.TransferAll();
|
||||
}
|
||||
catch (Exception constructionFailure)
|
||||
{
|
||||
constructionResources.RollbackConstructionAndThrow(
|
||||
"ParticleRenderer construction failed and its shader prefix did not cleanly roll back.",
|
||||
constructionFailure);
|
||||
throw new System.Diagnostics.UnreachableException();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ void main() { } // depth-only: color writes are masked off by the caller state
|
|||
private readonly int _locForceFarZ;
|
||||
private readonly int _locDepthBias;
|
||||
private readonly int _locDepthBiasEyeCapN;
|
||||
private readonly ResourceCleanupGroup _resources;
|
||||
|
||||
private const int MaxFanVerts = 32;
|
||||
private readonly float[] _scratch = new float[MaxFanVerts * 3];
|
||||
|
|
@ -109,28 +110,52 @@ void main() { } // depth-only: color writes are masked off by the caller state
|
|||
public PortalDepthMaskRenderer(GL gl)
|
||||
{
|
||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
var resources = new ResourceCleanupGroup();
|
||||
try
|
||||
{
|
||||
_program = ShaderProgramConstruction.Build(
|
||||
new GlShaderProgramBuildApi(gl),
|
||||
VertSrc,
|
||||
FragSrc);
|
||||
resources.Add(
|
||||
"portal-depth program",
|
||||
() => GlResourceCommand.DeleteProgram(
|
||||
gl,
|
||||
_program,
|
||||
$"delete PortalDepthMask program {_program}"));
|
||||
|
||||
uint vs = Compile(ShaderType.VertexShader, VertSrc);
|
||||
uint fs = Compile(ShaderType.FragmentShader, FragSrc);
|
||||
_program = _gl.CreateProgram();
|
||||
_gl.AttachShader(_program, vs);
|
||||
_gl.AttachShader(_program, fs);
|
||||
_gl.LinkProgram(_program);
|
||||
_gl.GetProgram(_program, ProgramPropertyARB.LinkStatus, out int linked);
|
||||
if (linked == 0)
|
||||
throw new InvalidOperationException($"PortalDepthMask link failed: {_gl.GetProgramInfoLog(_program)}");
|
||||
_gl.DeleteShader(vs);
|
||||
_gl.DeleteShader(fs);
|
||||
(
|
||||
_locViewProjection,
|
||||
_locPlaneCount,
|
||||
_locPlanes,
|
||||
_locForceFarZ,
|
||||
_locDepthBias,
|
||||
_locDepthBiasEyeCapN) = GlResourceCommand.Execute(
|
||||
_gl,
|
||||
"resolve PortalDepthMask uniforms",
|
||||
() => (
|
||||
_gl.GetUniformLocation(_program, "uViewProjection"),
|
||||
_gl.GetUniformLocation(_program, "uPlaneCount"),
|
||||
_gl.GetUniformLocation(_program, "uPlanes"),
|
||||
_gl.GetUniformLocation(_program, "uForceFarZ"),
|
||||
_gl.GetUniformLocation(_program, "uDepthBias"),
|
||||
_gl.GetUniformLocation(_program, "uDepthBiasEyeCapN")));
|
||||
|
||||
_locViewProjection = _gl.GetUniformLocation(_program, "uViewProjection");
|
||||
_locPlaneCount = _gl.GetUniformLocation(_program, "uPlaneCount");
|
||||
_locPlanes = _gl.GetUniformLocation(_program, "uPlanes");
|
||||
_locForceFarZ = _gl.GetUniformLocation(_program, "uForceFarZ");
|
||||
_locDepthBias = _gl.GetUniformLocation(_program, "uDepthBias");
|
||||
_locDepthBiasEyeCapN = _gl.GetUniformLocation(_program, "uDepthBiasEyeCapN");
|
||||
for (int i = 0; i < _frameBuffers.Length; i++)
|
||||
{
|
||||
FrameBufferSet set = CreateFrameBufferSet(resources, i);
|
||||
_frameBuffers[i] = set;
|
||||
}
|
||||
}
|
||||
catch (Exception constructionFailure)
|
||||
{
|
||||
resources.RollbackConstructionAndThrow(
|
||||
"PortalDepthMaskRenderer construction failed and its GL prefix did not cleanly roll back.",
|
||||
constructionFailure);
|
||||
throw new System.Diagnostics.UnreachableException();
|
||||
}
|
||||
|
||||
for (int i = 0; i < _frameBuffers.Length; i++)
|
||||
_frameBuffers[i] = CreateFrameBufferSet();
|
||||
_resources = resources;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -146,19 +171,34 @@ void main() { } // depth-only: color writes are masked off by the caller state
|
|||
_activeFrameBuffer.UsedVertices = 0;
|
||||
}
|
||||
|
||||
private FrameBufferSet CreateFrameBufferSet()
|
||||
private FrameBufferSet CreateFrameBufferSet(
|
||||
ResourceCleanupGroup resources,
|
||||
int frameIndex)
|
||||
{
|
||||
uint vao = 0;
|
||||
uint vbo = 0;
|
||||
try
|
||||
uint vao = TrackedGlResource.CreateVertexArray(
|
||||
_gl,
|
||||
"PortalDepthMask frame VAO creation");
|
||||
RetryableGpuResourceRelease vaoRelease =
|
||||
TrackedGlResource.CreateRetryableVertexArrayDeletion(
|
||||
_gl,
|
||||
vao,
|
||||
"PortalDepthMask frame VAO disposal");
|
||||
resources.Add($"portal-depth frame {frameIndex} VAO", vaoRelease.Run);
|
||||
|
||||
uint vbo = TrackedGlResource.CreateBuffer(
|
||||
_gl,
|
||||
"PortalDepthMask frame VBO creation");
|
||||
var set = new FrameBufferSet { Vao = vao, Vbo = vbo };
|
||||
RetryableGpuResourceRelease vboRelease =
|
||||
TrackedGlResource.CreateRetryableBufferDeletion(
|
||||
_gl,
|
||||
vbo,
|
||||
() => set.CapacityBytes,
|
||||
"PortalDepthMask frame VBO disposal");
|
||||
resources.Add($"portal-depth frame {frameIndex} VBO", vboRelease.Run);
|
||||
|
||||
GlResourceCommand.Execute(_gl, "configure PortalDepthMask frame buffers", () =>
|
||||
{
|
||||
vao = TrackedGlResource.CreateVertexArray(
|
||||
_gl,
|
||||
"PortalDepthMask frame VAO creation");
|
||||
vbo = TrackedGlResource.CreateBuffer(
|
||||
_gl,
|
||||
"PortalDepthMask frame VBO creation");
|
||||
var set = new FrameBufferSet { Vao = vao, Vbo = vbo };
|
||||
_gl.BindVertexArray(set.Vao);
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, set.Vbo);
|
||||
unsafe
|
||||
|
|
@ -168,29 +208,8 @@ void main() { } // depth-only: color writes are masked off by the caller state
|
|||
_gl.EnableVertexAttribArray(0);
|
||||
_gl.BindVertexArray(0);
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
||||
return set;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (vbo != 0)
|
||||
TrackedGlResource.DeleteBuffer(
|
||||
_gl, vbo, 0, "PortalDepthMask frame VBO rollback");
|
||||
if (vao != 0)
|
||||
TrackedGlResource.DeleteVertexArray(
|
||||
_gl, vao, "PortalDepthMask frame VAO rollback");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private uint Compile(ShaderType type, string src)
|
||||
{
|
||||
uint s = _gl.CreateShader(type);
|
||||
_gl.ShaderSource(s, src);
|
||||
_gl.CompileShader(s);
|
||||
_gl.GetShader(s, ShaderParameterName.CompileStatus, out int ok);
|
||||
if (ok == 0)
|
||||
throw new InvalidOperationException($"PortalDepthMask {type} compile failed: {_gl.GetShaderInfoLog(s)}");
|
||||
return s;
|
||||
});
|
||||
return set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -387,18 +406,6 @@ void main() { } // depth-only: color writes are masked off by the caller state
|
|||
|
||||
public void Dispose()
|
||||
{
|
||||
_gl.DeleteProgram(_program);
|
||||
foreach (FrameBufferSet set in _frameBuffers)
|
||||
{
|
||||
TrackedGlResource.DeleteVertexArray(
|
||||
_gl,
|
||||
set.Vao,
|
||||
"PortalDepthMask frame VAO disposal");
|
||||
TrackedGlResource.DeleteBuffer(
|
||||
_gl,
|
||||
set.Vbo,
|
||||
set.CapacityBytes,
|
||||
"PortalDepthMask frame VBO disposal");
|
||||
}
|
||||
_resources.RetryCleanup();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
namespace AcDream.App.Rendering;
|
||||
|
||||
using System.Runtime.ExceptionServices;
|
||||
|
||||
/// <summary>
|
||||
/// Reverse-order, all-attempted cleanup owner used while a composite resource
|
||||
/// is still under construction and after it becomes the aggregate owner.
|
||||
|
|
@ -25,6 +27,15 @@ internal sealed class ResourceCleanupGroup : IRetryableResourceCleanup
|
|||
_entries.Add(new Entry(name, release));
|
||||
}
|
||||
|
||||
public void TransferAll()
|
||||
{
|
||||
if (_running)
|
||||
throw new InvalidOperationException(
|
||||
"The resource cleanup group is currently releasing resources.");
|
||||
foreach (Entry entry in _entries)
|
||||
entry.Complete = true;
|
||||
}
|
||||
|
||||
public void RetryCleanup()
|
||||
{
|
||||
if (_running || IsCleanupComplete)
|
||||
|
|
@ -60,4 +71,26 @@ internal sealed class ResourceCleanupGroup : IRetryableResourceCleanup
|
|||
if (failures is not null)
|
||||
throw new AggregateException("Composite resource cleanup remains incomplete.", failures);
|
||||
}
|
||||
|
||||
public void RollbackConstructionAndThrow(
|
||||
string message,
|
||||
Exception constructionFailure)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(message);
|
||||
ArgumentNullException.ThrowIfNull(constructionFailure);
|
||||
try
|
||||
{
|
||||
RetryCleanup();
|
||||
}
|
||||
catch (Exception cleanupFailure)
|
||||
{
|
||||
throw new GlResourceConstructionException(
|
||||
message,
|
||||
this,
|
||||
[constructionFailure, cleanupFailure]);
|
||||
}
|
||||
|
||||
ExceptionDispatchInfo.Capture(constructionFailure).Throw();
|
||||
throw new InvalidOperationException("Unreachable construction rollback path.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ namespace AcDream.App.Rendering;
|
|||
public sealed class SamplerCache : IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly ResourceCleanupGroup _resources;
|
||||
|
||||
/// <summary>Sampler with WrapS = WrapT = Repeat. The default for textures uploaded by <see cref="TextureCache"/>.</summary>
|
||||
public uint Wrap { get; }
|
||||
|
|
@ -41,23 +42,62 @@ public sealed class SamplerCache : IDisposable
|
|||
public SamplerCache(GL gl)
|
||||
{
|
||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
var resources = new ResourceCleanupGroup();
|
||||
uint wrap = 0;
|
||||
uint clamp = 0;
|
||||
try
|
||||
{
|
||||
wrap = GlResourceCommand.CreateName(
|
||||
_gl,
|
||||
"repeat sampler",
|
||||
_gl.GenSampler,
|
||||
_gl.DeleteSampler);
|
||||
uint ownedWrap = wrap;
|
||||
resources.Add(
|
||||
"repeat sampler",
|
||||
() => GlResourceCommand.Execute(
|
||||
_gl,
|
||||
$"delete repeat sampler {ownedWrap}",
|
||||
() => _gl.DeleteSampler(ownedWrap)));
|
||||
Configure(wrap, TextureWrapMode.Repeat, "repeat sampler");
|
||||
|
||||
Wrap = _gl.GenSampler();
|
||||
_gl.SamplerParameter(Wrap, SamplerParameterI.WrapS, (int)TextureWrapMode.Repeat);
|
||||
_gl.SamplerParameter(Wrap, SamplerParameterI.WrapT, (int)TextureWrapMode.Repeat);
|
||||
_gl.SamplerParameter(Wrap, SamplerParameterI.MinFilter, (int)TextureMinFilter.Linear);
|
||||
_gl.SamplerParameter(Wrap, SamplerParameterI.MagFilter, (int)TextureMagFilter.Linear);
|
||||
clamp = GlResourceCommand.CreateName(
|
||||
_gl,
|
||||
"clamp sampler",
|
||||
_gl.GenSampler,
|
||||
_gl.DeleteSampler);
|
||||
uint ownedClamp = clamp;
|
||||
resources.Add(
|
||||
"clamp sampler",
|
||||
() => GlResourceCommand.Execute(
|
||||
_gl,
|
||||
$"delete clamp sampler {ownedClamp}",
|
||||
() => _gl.DeleteSampler(ownedClamp)));
|
||||
Configure(clamp, TextureWrapMode.ClampToEdge, "clamp sampler");
|
||||
}
|
||||
catch (Exception constructionFailure)
|
||||
{
|
||||
resources.RollbackConstructionAndThrow(
|
||||
"SamplerCache construction failed and its sampler prefix did not cleanly roll back.",
|
||||
constructionFailure);
|
||||
}
|
||||
|
||||
Clamp = _gl.GenSampler();
|
||||
_gl.SamplerParameter(Clamp, SamplerParameterI.WrapS, (int)TextureWrapMode.ClampToEdge);
|
||||
_gl.SamplerParameter(Clamp, SamplerParameterI.WrapT, (int)TextureWrapMode.ClampToEdge);
|
||||
_gl.SamplerParameter(Clamp, SamplerParameterI.MinFilter, (int)TextureMinFilter.Linear);
|
||||
_gl.SamplerParameter(Clamp, SamplerParameterI.MagFilter, (int)TextureMagFilter.Linear);
|
||||
Wrap = wrap;
|
||||
Clamp = clamp;
|
||||
_resources = resources;
|
||||
}
|
||||
|
||||
private void Configure(uint sampler, TextureWrapMode wrap, string name) =>
|
||||
GlResourceCommand.Execute(_gl, $"configure {name}", () =>
|
||||
{
|
||||
_gl.SamplerParameter(sampler, SamplerParameterI.WrapS, (int)wrap);
|
||||
_gl.SamplerParameter(sampler, SamplerParameterI.WrapT, (int)wrap);
|
||||
_gl.SamplerParameter(sampler, SamplerParameterI.MinFilter, (int)TextureMinFilter.Linear);
|
||||
_gl.SamplerParameter(sampler, SamplerParameterI.MagFilter, (int)TextureMagFilter.Linear);
|
||||
});
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Wrap != 0) _gl.DeleteSampler(Wrap);
|
||||
if (Clamp != 0) _gl.DeleteSampler(Clamp);
|
||||
_resources.RetryCleanup();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,42 +144,56 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
|
|||
if (_uTexTilingLoc < 0)
|
||||
throw new InvalidOperationException("terrain_modern.frag is missing the required uTexTiling uniform.");
|
||||
|
||||
var constructionResources = new ResourceCleanupGroup();
|
||||
try
|
||||
{
|
||||
_globalVao = TrackedGlResource.CreateVertexArray(
|
||||
_gl,
|
||||
"creating terrain global VAO");
|
||||
RetryableGpuResourceRelease globalVaoRelease =
|
||||
TrackedGlResource.CreateRetryableVertexArrayDeletion(
|
||||
_gl,
|
||||
_globalVao,
|
||||
"rolling back terrain global VAO");
|
||||
constructionResources.Add(
|
||||
"terrain global VAO",
|
||||
globalVaoRelease.Run);
|
||||
_globalVbo = TrackedGlResource.CreateBuffer(
|
||||
_gl,
|
||||
"creating terrain global vertex buffer");
|
||||
RetryableGpuResourceRelease globalVboRelease =
|
||||
TrackedGlResource.CreateRetryableBufferDeletion(
|
||||
_gl,
|
||||
_globalVbo,
|
||||
() => _globalVboCapacityBytes,
|
||||
"rolling back terrain global vertex buffer");
|
||||
constructionResources.Add(
|
||||
"terrain global vertex buffer",
|
||||
globalVboRelease.Run);
|
||||
_globalEbo = TrackedGlResource.CreateBuffer(
|
||||
_gl,
|
||||
"creating terrain global index buffer");
|
||||
RetryableGpuResourceRelease globalEboRelease =
|
||||
TrackedGlResource.CreateRetryableBufferDeletion(
|
||||
_gl,
|
||||
_globalEbo,
|
||||
() => _globalEboCapacityBytes,
|
||||
"rolling back terrain global index buffer");
|
||||
constructionResources.Add(
|
||||
"terrain global index buffer",
|
||||
globalEboRelease.Run);
|
||||
AllocateGpuBuffers(initialSlotCapacity);
|
||||
ConfigureVao(_globalVao, _globalVbo, _globalEbo);
|
||||
GlResourceCommand.Execute(
|
||||
_gl,
|
||||
"configure terrain global vertex array",
|
||||
() => ConfigureVao(_globalVao, _globalVbo, _globalEbo));
|
||||
constructionResources.TransferAll();
|
||||
}
|
||||
catch
|
||||
catch (Exception constructionFailure)
|
||||
{
|
||||
TrackedGlResource.DeleteVertexArray(
|
||||
_gl,
|
||||
_globalVao,
|
||||
"rolling back terrain global VAO");
|
||||
TrackedGlResource.DeleteBuffer(
|
||||
_gl,
|
||||
_globalVbo,
|
||||
_globalVboCapacityBytes,
|
||||
"rolling back terrain global vertex buffer");
|
||||
TrackedGlResource.DeleteBuffer(
|
||||
_gl,
|
||||
_globalEbo,
|
||||
_globalEboCapacityBytes,
|
||||
"rolling back terrain global index buffer");
|
||||
_globalVao = 0;
|
||||
_globalVbo = 0;
|
||||
_globalEbo = 0;
|
||||
_globalVboCapacityBytes = 0;
|
||||
_globalEboCapacityBytes = 0;
|
||||
throw;
|
||||
constructionResources.RollbackConstructionAndThrow(
|
||||
"TerrainModernRenderer construction failed and its GL prefix did not cleanly roll back.",
|
||||
constructionFailure);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,10 +95,31 @@ public sealed unsafe class TextureCache : Wb.IEntityTextureLifetime, IDisposable
|
|||
ArgumentNullException.ThrowIfNull(retirementQueue);
|
||||
if (bindless is not null)
|
||||
{
|
||||
_compositeTextures = new CompositeTextureArrayCache(gl, bindless, retirementQueue);
|
||||
_particleTextures = new StandaloneBindlessTextureCache(
|
||||
new ParticleTextureBackend(this),
|
||||
retirementQueue);
|
||||
var resources = new ResourceCleanupGroup();
|
||||
CompositeTextureArrayCache? composite = null;
|
||||
StandaloneBindlessTextureCache? particles = null;
|
||||
try
|
||||
{
|
||||
composite = new CompositeTextureArrayCache(
|
||||
gl,
|
||||
bindless,
|
||||
retirementQueue);
|
||||
resources.Add("composite texture cache", composite.Dispose);
|
||||
particles = new StandaloneBindlessTextureCache(
|
||||
new ParticleTextureBackend(this),
|
||||
retirementQueue);
|
||||
resources.Add("particle texture cache", particles.Dispose);
|
||||
resources.TransferAll();
|
||||
}
|
||||
catch (Exception constructionFailure)
|
||||
{
|
||||
resources.RollbackConstructionAndThrow(
|
||||
"TextureCache construction failed and its child-cache prefix did not cleanly roll back.",
|
||||
constructionFailure);
|
||||
}
|
||||
|
||||
_compositeTextures = composite;
|
||||
_particleTextures = particles;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Chorizite.Core.Render;
|
||||
using AcDream.App.Rendering;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Silk.NET.OpenGL;
|
||||
using System;
|
||||
|
|
@ -161,7 +162,7 @@ namespace AcDream.App.Rendering.Wb {
|
|||
|
||||
if (string.IsNullOrWhiteSpace(vertShaderSource) || string.IsNullOrWhiteSpace(fragShaderSource)) {
|
||||
_log.LogError($"Shader {Name} has no source code!");
|
||||
return;
|
||||
throw new InvalidOperationException($"Shader {Name} has no source code.");
|
||||
}
|
||||
|
||||
if (_device.HasOpenGL43 && _device.HasBindless) {
|
||||
|
|
@ -170,41 +171,47 @@ namespace AcDream.App.Rendering.Wb {
|
|||
fragShaderSource = fragShaderSource.Replace("#version 330 core", replacement);
|
||||
}
|
||||
|
||||
uint vertexShader = CompileShader(ShaderType.VertexShader, Name, vertShaderSource);
|
||||
uint fragmentShader = CompileShader(ShaderType.FragmentShader, Name, fragShaderSource);
|
||||
var resources = new ResourceCleanupGroup();
|
||||
uint prog = 0;
|
||||
bool accountingPublished = false;
|
||||
try {
|
||||
prog = ShaderProgramConstruction.Build(
|
||||
new GlShaderProgramBuildApi(GL),
|
||||
vertShaderSource,
|
||||
fragShaderSource);
|
||||
uint ownedProgram = prog;
|
||||
var unpublishedProgramRelease = new RetryableGpuResourceRelease(
|
||||
() => GlResourceCommand.DeleteProgram(
|
||||
GL,
|
||||
ownedProgram,
|
||||
$"delete unpublished WB shader program {ownedProgram}"),
|
||||
() => {
|
||||
if (accountingPublished)
|
||||
{
|
||||
GpuMemoryTracker.TrackResourceDeallocation(
|
||||
GpuResourceType.Shader);
|
||||
}
|
||||
});
|
||||
resources.Add("WB shader program", unpublishedProgramRelease.Run);
|
||||
|
||||
var prog = GL.CreateProgram();
|
||||
GLHelpers.CheckErrors(GL, true);
|
||||
GL.AttachShader(prog, vertexShader);
|
||||
GLHelpers.CheckErrors(GL, true);
|
||||
GL.AttachShader(prog, fragmentShader);
|
||||
GLHelpers.CheckErrors(GL, true);
|
||||
GL.LinkProgram(prog);
|
||||
GLHelpers.CheckErrors(GL, true);
|
||||
// Bind SceneData uniform block to point 0 if it exists.
|
||||
GlResourceCommand.Execute(GL, $"configure shader {Name} SceneData binding", () => {
|
||||
uint sceneDataIndex = GL.GetUniformBlockIndex(prog, "SceneData");
|
||||
if (sceneDataIndex != uint.MaxValue)
|
||||
GL.UniformBlockBinding(prog, sceneDataIndex, 0);
|
||||
});
|
||||
|
||||
GL.GetProgram(prog, GLEnum.LinkStatus, out int success);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
if (success != 1) {
|
||||
var infoLog = GL.GetProgramInfoLog(prog);
|
||||
_log.LogError($"Error: shader {Name} link failed: {infoLog}");
|
||||
GL.DeleteProgram(prog);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
_log.LogTrace($"{(Program != 0 ? "Reloaded" : "Loaded")} shader: {Name}");
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Shader);
|
||||
accountingPublished = true;
|
||||
resources.TransferAll();
|
||||
} catch (Exception constructionFailure) {
|
||||
_log.LogError(constructionFailure, "Failed to construct shader {ShaderName}", Name);
|
||||
resources.RollbackConstructionAndThrow(
|
||||
$"Shader {Name} construction failed and its GL program did not cleanly roll back.",
|
||||
constructionFailure);
|
||||
}
|
||||
|
||||
// Bind SceneData uniform block to point 0 if it exists
|
||||
var sceneDataIndex = GL.GetUniformBlockIndex(prog, "SceneData");
|
||||
if (sceneDataIndex != uint.MaxValue) {
|
||||
GL.UniformBlockBinding(prog, sceneDataIndex, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
GL.DeleteShader(vertexShader);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GL.DeleteShader(fragmentShader);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
_log.LogTrace($"{(Program != 0 ? "Reloaded" : "Loaded")} shader: {Name}");
|
||||
|
||||
if (Program != 0) {
|
||||
Unload();
|
||||
|
|
@ -212,32 +219,12 @@ namespace AcDream.App.Rendering.Wb {
|
|||
_uniformLocations.Clear();
|
||||
_uniformValues.Clear();
|
||||
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Shader);
|
||||
Program = prog;
|
||||
ProgramId = prog;
|
||||
NeedsLoad = false;
|
||||
GLHelpers.CheckErrors(GL);
|
||||
}
|
||||
|
||||
private uint CompileShader(ShaderType shaderType, string name, string shaderSource) {
|
||||
uint shader = GL.CreateShader(shaderType);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
GL.ShaderSource(shader, shaderSource);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
GL.CompileShader(shader);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
GL.GetShader(shader, ShaderParameterName.CompileStatus, out int success);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
if (success != 1) {
|
||||
var infoLog = GL.GetShaderInfoLog(shader);
|
||||
_log.LogError($"Error: {name}:{shaderType} compilation failed: {infoLog}");
|
||||
}
|
||||
|
||||
return shader;
|
||||
}
|
||||
|
||||
public override void Bind() {
|
||||
lock (_lock) {
|
||||
SetActive();
|
||||
|
|
|
|||
|
|
@ -28,30 +28,44 @@ namespace AcDream.App.Rendering.Wb {
|
|||
/// <param name="usage">Buffer usage</param>
|
||||
/// <param name="size">The size of the buffer, in bytes</param>
|
||||
public unsafe ManagedGLUniformBuffer(OpenGLGraphicsDevice device, BufferUsage usage, int size) {
|
||||
_device = device;
|
||||
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(size, 1);
|
||||
Size = size;
|
||||
Usage = usage;
|
||||
|
||||
// Generate the buffer
|
||||
bufferId = GL.GenBuffer();
|
||||
if (bufferId == 0) {
|
||||
throw new Exception("Failed to generate uniform buffer.");
|
||||
var resources = new AcDream.App.Rendering.ResourceCleanupGroup();
|
||||
uint buffer = 0;
|
||||
bool allocated = false;
|
||||
try {
|
||||
buffer = TrackedGlResource.CreateBuffer(
|
||||
GL,
|
||||
"creating managed uniform buffer");
|
||||
uint ownedBuffer = buffer;
|
||||
RetryableGpuResourceRelease unpublishedBufferRelease =
|
||||
TrackedGlResource.CreateRetryableBufferDeletion(
|
||||
GL,
|
||||
ownedBuffer,
|
||||
() => allocated ? Size : 0,
|
||||
"rolling back managed uniform buffer");
|
||||
resources.Add(
|
||||
"managed uniform buffer",
|
||||
unpublishedBufferRelease.Run);
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
GL,
|
||||
GLEnum.UniformBuffer,
|
||||
buffer,
|
||||
0,
|
||||
Size,
|
||||
GLEnum.DynamicDraw,
|
||||
"allocating managed uniform buffer");
|
||||
allocated = true;
|
||||
resources.TransferAll();
|
||||
} catch (Exception constructionFailure) {
|
||||
resources.RollbackConstructionAndThrow(
|
||||
"ManagedGLUniformBuffer construction failed and its GL buffer did not cleanly roll back.",
|
||||
constructionFailure);
|
||||
}
|
||||
GpuMemoryTracker.TrackResourceAllocation(GpuResourceType.Buffer);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
// Allocate the buffer with the specified size
|
||||
GL.BindBuffer(GLEnum.UniformBuffer, bufferId);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
GL.BufferData(
|
||||
GLEnum.UniformBuffer,
|
||||
(uint)Size,
|
||||
(void*)0, // No initial data
|
||||
GLEnum.DynamicDraw);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
||||
GpuMemoryTracker.TrackAllocation(Size, GpuResourceType.Buffer);
|
||||
bufferId = buffer;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -139,5 +153,25 @@ namespace AcDream.App.Rendering.Wb {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases an unpublished constructor-owned buffer synchronously on
|
||||
/// the GL thread. This is deliberately separate from ordinary queued
|
||||
/// disposal so an enclosing constructor can prove rollback before it
|
||||
/// propagates its failure.
|
||||
/// </summary>
|
||||
internal void DisposeImmediately() {
|
||||
if (bufferId == 0)
|
||||
return;
|
||||
|
||||
RetryableGpuResourceRelease release =
|
||||
TrackedGlResource.CreateRetryableBufferDeletion(
|
||||
GL,
|
||||
bufferId,
|
||||
Size,
|
||||
"rolling back unpublished managed uniform buffer");
|
||||
release.Run();
|
||||
bufferId = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using Chorizite.Core.Render;
|
||||
using Chorizite.Core.Render.Enums;
|
||||
using Chorizite.Core.Render.Vertex;
|
||||
using AcDream.App.Rendering;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Silk.NET.OpenGL;
|
||||
// IUniformBuffer is in Chorizite.Core.dll but under the Chorizite.OpenGLSDLBackend namespace
|
||||
|
|
@ -149,38 +150,49 @@ namespace AcDream.App.Rendering.Wb {
|
|||
HasBindless = false;
|
||||
}
|
||||
|
||||
GL.GenBuffers(1, out uint instanceVbo);
|
||||
InstanceVBO = instanceVbo;
|
||||
var resources = new ResourceCleanupGroup();
|
||||
try {
|
||||
InstanceVBO = CreateConstructionBuffer(resources, "WB instance buffer");
|
||||
|
||||
// Query this immutable device limit once. Atlas construction can
|
||||
// happen hundreds of times during portal streaming; repeating a
|
||||
// driver GetFloat for every texture serialized the upload burst.
|
||||
if (renderSettings.EnableAnisotropicFiltering) {
|
||||
GL.GetFloat(GLEnum.MaxTextureMaxAnisotropy, out float maxAniso);
|
||||
MaxSupportedAnisotropy = Math.Max(0f, maxAniso);
|
||||
// Query this immutable device limit once. Atlas construction can
|
||||
// happen hundreds of times during portal streaming; repeating a
|
||||
// driver GetFloat for every texture serialized the upload burst.
|
||||
if (renderSettings.EnableAnisotropicFiltering) {
|
||||
MaxSupportedAnisotropy = GlResourceCommand.Execute(
|
||||
GL,
|
||||
"query maximum texture anisotropy",
|
||||
() => {
|
||||
GL.GetFloat(GLEnum.MaxTextureMaxAnisotropy, out float maxAniso);
|
||||
return Math.Max(0f, maxAniso);
|
||||
});
|
||||
}
|
||||
|
||||
WrapSampler = CreateConstructionSampler(
|
||||
resources,
|
||||
TextureWrapMode.Repeat,
|
||||
"WB repeat sampler");
|
||||
ClampSampler = CreateConstructionSampler(
|
||||
resources,
|
||||
TextureWrapMode.ClampToEdge,
|
||||
"WB clamp sampler");
|
||||
|
||||
_sceneDataBuffer = new ManagedGLUniformBuffer(
|
||||
this,
|
||||
BufferUsage.Dynamic,
|
||||
Marshal.SizeOf<SceneData>());
|
||||
ManagedGLUniformBuffer ownedSceneDataBuffer = _sceneDataBuffer;
|
||||
resources.Add(
|
||||
"WB scene-data uniform buffer",
|
||||
ownedSceneDataBuffer.DisposeImmediately);
|
||||
|
||||
InitializeSharedDebugResources(resources);
|
||||
resources.TransferAll();
|
||||
} catch (Exception constructionFailure) {
|
||||
resources.RollbackConstructionAndThrow(
|
||||
"OpenGLGraphicsDevice construction failed and its GL prefix did not cleanly roll back.",
|
||||
constructionFailure);
|
||||
}
|
||||
|
||||
// Create sampler objects for wrap vs clamp
|
||||
WrapSampler = GL.GenSampler();
|
||||
GL.SamplerParameter(WrapSampler, SamplerParameterI.WrapS, (int)TextureWrapMode.Repeat);
|
||||
GL.SamplerParameter(WrapSampler, SamplerParameterI.WrapT, (int)TextureWrapMode.Repeat);
|
||||
GL.SamplerParameter(WrapSampler, SamplerParameterI.MinFilter, (int)TextureMinFilter.LinearMipmapLinear);
|
||||
GL.SamplerParameter(WrapSampler, SamplerParameterI.MagFilter, (int)TextureMagFilter.Linear);
|
||||
if (MaxSupportedAnisotropy > 0)
|
||||
GL.SamplerParameter(WrapSampler, GLEnum.TextureMaxAnisotropy, MaxSupportedAnisotropy);
|
||||
|
||||
ClampSampler = GL.GenSampler();
|
||||
GL.SamplerParameter(ClampSampler, SamplerParameterI.WrapS, (int)TextureWrapMode.ClampToEdge);
|
||||
GL.SamplerParameter(ClampSampler, SamplerParameterI.WrapT, (int)TextureWrapMode.ClampToEdge);
|
||||
GL.SamplerParameter(ClampSampler, SamplerParameterI.MinFilter, (int)TextureMinFilter.LinearMipmapLinear);
|
||||
GL.SamplerParameter(ClampSampler, SamplerParameterI.MagFilter, (int)TextureMagFilter.Linear);
|
||||
if (MaxSupportedAnisotropy > 0)
|
||||
GL.SamplerParameter(ClampSampler, GLEnum.TextureMaxAnisotropy, MaxSupportedAnisotropy);
|
||||
|
||||
_sceneDataBuffer = new ManagedGLUniformBuffer(this, BufferUsage.Dynamic, Marshal.SizeOf<SceneData>());
|
||||
|
||||
InitializeSharedDebugResources();
|
||||
|
||||
// ParticleBatcher is constructed post-ctor by WbMeshAdapter (WbMeshAdapter.cs:78)
|
||||
// after the adapter has wired up all dependencies. The null! here is overridden
|
||||
// immediately after construction; it is not observable as null at runtime.
|
||||
|
|
@ -196,7 +208,49 @@ namespace AcDream.App.Rendering.Wb {
|
|||
internal AcDream.App.Rendering.IGpuResourceRetirementQueue ResourceRetirement =>
|
||||
_resourceRetirement;
|
||||
|
||||
private void InitializeSharedDebugResources() {
|
||||
private uint CreateConstructionBuffer(ResourceCleanupGroup resources, string name) {
|
||||
uint buffer = GlResourceCommand.CreateName(GL, name, GL.GenBuffer, GL.DeleteBuffer);
|
||||
resources.Add(
|
||||
name,
|
||||
() => GlResourceCommand.DeleteBuffer(
|
||||
GL,
|
||||
buffer,
|
||||
$"delete {name} {buffer}"));
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private uint CreateConstructionSampler(
|
||||
ResourceCleanupGroup resources,
|
||||
TextureWrapMode wrapMode,
|
||||
string name) {
|
||||
uint sampler = GlResourceCommand.CreateName(GL, name, GL.GenSampler, GL.DeleteSampler);
|
||||
resources.Add(
|
||||
name,
|
||||
() => GlResourceCommand.Execute(
|
||||
GL,
|
||||
$"delete {name} {sampler}",
|
||||
() => GL.DeleteSampler(sampler)));
|
||||
GlResourceCommand.Execute(GL, $"configure {name}", () => {
|
||||
GL.SamplerParameter(sampler, SamplerParameterI.WrapS, (int)wrapMode);
|
||||
GL.SamplerParameter(sampler, SamplerParameterI.WrapT, (int)wrapMode);
|
||||
GL.SamplerParameter(
|
||||
sampler,
|
||||
SamplerParameterI.MinFilter,
|
||||
(int)TextureMinFilter.LinearMipmapLinear);
|
||||
GL.SamplerParameter(
|
||||
sampler,
|
||||
SamplerParameterI.MagFilter,
|
||||
(int)TextureMagFilter.Linear);
|
||||
if (MaxSupportedAnisotropy > 0)
|
||||
GL.SamplerParameter(
|
||||
sampler,
|
||||
GLEnum.TextureMaxAnisotropy,
|
||||
MaxSupportedAnisotropy);
|
||||
});
|
||||
return sampler;
|
||||
}
|
||||
|
||||
private void InitializeSharedDebugResources(ResourceCleanupGroup resources) {
|
||||
// Unit quad vertices for two triangles (0 to 1 for length, -0.5 to 0.5 for thickness)
|
||||
float[] quadVertices = {
|
||||
0.0f, -0.5f,
|
||||
|
|
@ -207,53 +261,74 @@ namespace AcDream.App.Rendering.Wb {
|
|||
0.0f, 0.5f
|
||||
};
|
||||
|
||||
GL.GenBuffers(1, out uint quadVbo);
|
||||
SharedQuadVBO = quadVbo;
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, SharedQuadVBO);
|
||||
fixed (float* pQuad = quadVertices) {
|
||||
GL.BufferData(GLEnum.ArrayBuffer, (nuint)(quadVertices.Length * sizeof(float)), pQuad, GLEnum.StaticDraw);
|
||||
}
|
||||
SharedQuadVBO = CreateConstructionBuffer(resources, "WB shared debug quad buffer");
|
||||
SharedDebugInstanceVBO = CreateConstructionBuffer(
|
||||
resources,
|
||||
"WB shared debug instance buffer");
|
||||
SharedDebugVAO = GlResourceCommand.CreateName(
|
||||
GL,
|
||||
"WB shared debug vertex array",
|
||||
GL.GenVertexArray,
|
||||
GL.DeleteVertexArray);
|
||||
uint ownedDebugVao = SharedDebugVAO;
|
||||
resources.Add(
|
||||
"WB shared debug vertex array",
|
||||
() => GlResourceCommand.DeleteVertexArray(
|
||||
GL,
|
||||
ownedDebugVao,
|
||||
$"delete WB shared debug vertex array {ownedDebugVao}"));
|
||||
|
||||
GL.GenBuffers(1, out uint debugInstanceVbo);
|
||||
SharedDebugInstanceVBO = debugInstanceVbo;
|
||||
// Initial capacity for debug instances
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, SharedDebugInstanceVBO);
|
||||
GL.BufferData(GLEnum.ArrayBuffer, (nuint)(1024 * 44), (void*)0, GLEnum.StreamDraw); // 44 bytes is sizeof(LineInstance)
|
||||
GlResourceCommand.Execute(GL, "configure WB shared debug resources", () => {
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, SharedQuadVBO);
|
||||
fixed (float* pQuad = quadVertices) {
|
||||
GL.BufferData(
|
||||
GLEnum.ArrayBuffer,
|
||||
(nuint)(quadVertices.Length * sizeof(float)),
|
||||
pQuad,
|
||||
GLEnum.StaticDraw);
|
||||
}
|
||||
|
||||
GL.GenVertexArrays(1, out uint debugVao);
|
||||
SharedDebugVAO = debugVao;
|
||||
GL.BindVertexArray(SharedDebugVAO);
|
||||
// Initial capacity for debug instances.
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, SharedDebugInstanceVBO);
|
||||
GL.BufferData(
|
||||
GLEnum.ArrayBuffer,
|
||||
(nuint)(1024 * 44),
|
||||
(void*)0,
|
||||
GLEnum.StreamDraw); // 44 bytes is sizeof(LineInstance)
|
||||
|
||||
// Quad Pos attribute (location 0)
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, SharedQuadVBO);
|
||||
GL.EnableVertexAttribArray(0);
|
||||
GL.VertexAttribPointer(0, 2, GLEnum.Float, false, 2 * sizeof(float), (void*)0);
|
||||
GL.BindVertexArray(SharedDebugVAO);
|
||||
|
||||
// Instance attributes
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, SharedDebugInstanceVBO);
|
||||
uint lineInstanceSize = 44; // Marshal.SizeOf<LineInstance>() - we'll hardcode or use a constant later
|
||||
// Quad Pos attribute (location 0)
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, SharedQuadVBO);
|
||||
GL.EnableVertexAttribArray(0);
|
||||
GL.VertexAttribPointer(0, 2, GLEnum.Float, false, 2 * sizeof(float), (void*)0);
|
||||
|
||||
// aStart (location 1)
|
||||
GL.EnableVertexAttribArray(1);
|
||||
GL.VertexAttribPointer(1, 3, GLEnum.Float, false, lineInstanceSize, (void*)0);
|
||||
GL.VertexAttribDivisor(1, 1);
|
||||
// Instance attributes
|
||||
GL.BindBuffer(GLEnum.ArrayBuffer, SharedDebugInstanceVBO);
|
||||
uint lineInstanceSize = 44;
|
||||
|
||||
// aEnd (location 2)
|
||||
GL.EnableVertexAttribArray(2);
|
||||
GL.VertexAttribPointer(2, 3, GLEnum.Float, false, lineInstanceSize, (void*)12); // OffsetOf End
|
||||
GL.VertexAttribDivisor(2, 1);
|
||||
// aStart (location 1)
|
||||
GL.EnableVertexAttribArray(1);
|
||||
GL.VertexAttribPointer(1, 3, GLEnum.Float, false, lineInstanceSize, (void*)0);
|
||||
GL.VertexAttribDivisor(1, 1);
|
||||
|
||||
// aColor (location 3)
|
||||
GL.EnableVertexAttribArray(3);
|
||||
GL.VertexAttribPointer(3, 4, GLEnum.Float, false, lineInstanceSize, (void*)24); // OffsetOf Color
|
||||
GL.VertexAttribDivisor(3, 1);
|
||||
// aEnd (location 2)
|
||||
GL.EnableVertexAttribArray(2);
|
||||
GL.VertexAttribPointer(2, 3, GLEnum.Float, false, lineInstanceSize, (void*)12);
|
||||
GL.VertexAttribDivisor(2, 1);
|
||||
|
||||
// aThickness (location 4)
|
||||
GL.EnableVertexAttribArray(4);
|
||||
GL.VertexAttribPointer(4, 1, GLEnum.Float, false, lineInstanceSize, (void*)40); // OffsetOf Thickness
|
||||
GL.VertexAttribDivisor(4, 1);
|
||||
// aColor (location 3)
|
||||
GL.EnableVertexAttribArray(3);
|
||||
GL.VertexAttribPointer(3, 4, GLEnum.Float, false, lineInstanceSize, (void*)24);
|
||||
GL.VertexAttribDivisor(3, 1);
|
||||
|
||||
GL.BindVertexArray(0);
|
||||
// aThickness (location 4)
|
||||
GL.EnableVertexAttribArray(4);
|
||||
GL.VertexAttribPointer(4, 1, GLEnum.Float, false, lineInstanceSize, (void*)40);
|
||||
GL.VertexAttribDivisor(4, 1);
|
||||
|
||||
GL.BindVertexArray(0);
|
||||
});
|
||||
}
|
||||
|
||||
public void EnsureInstanceBufferCapacity(int count, int stride, bool forceOrphan = false) {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ namespace AcDream.App.Rendering.Wb {
|
|||
private readonly uint _ibo;
|
||||
private readonly uint _instanceVbo;
|
||||
private readonly IShader _shader;
|
||||
private readonly ResourceCleanupGroup _resources;
|
||||
private readonly ParticleInstance[] _instanceData = new ParticleInstance[MAX_PARTICLES_TOTAL];
|
||||
private readonly List<ParticleRenderData> _allParticles = new();
|
||||
private int _currentInstanceCount = 0;
|
||||
|
|
@ -43,91 +44,108 @@ namespace AcDream.App.Rendering.Wb {
|
|||
private Vector3 _cameraRight;
|
||||
|
||||
public ParticleBatcher(OpenGLGraphicsDevice graphicsDevice) {
|
||||
_graphicsDevice = graphicsDevice;
|
||||
_graphicsDevice = graphicsDevice ?? throw new ArgumentNullException(nameof(graphicsDevice));
|
||||
var gl = _graphicsDevice.GL;
|
||||
var resources = new ResourceCleanupGroup();
|
||||
IShader? shader = null;
|
||||
uint vao = 0;
|
||||
uint vbo = 0;
|
||||
uint ibo = 0;
|
||||
uint instanceVbo = 0;
|
||||
try {
|
||||
var vertSource = EmbeddedResourceReader.GetEmbeddedResource("Shaders.Particle.vert");
|
||||
var fragSource = EmbeddedResourceReader.GetEmbeddedResource("Shaders.Particle.frag");
|
||||
shader = _graphicsDevice.CreateShader("Particle", vertSource, fragSource);
|
||||
if (shader is IDisposable disposableShader)
|
||||
resources.Add("WB particle shader", disposableShader.Dispose);
|
||||
|
||||
var vertSource = EmbeddedResourceReader.GetEmbeddedResource("Shaders.Particle.vert");
|
||||
var fragSource = EmbeddedResourceReader.GetEmbeddedResource("Shaders.Particle.frag");
|
||||
_shader = _graphicsDevice.CreateShader("Particle", vertSource, fragSource);
|
||||
// Create quad vertices - centered to match ACViewer expansion logic
|
||||
float[] vertices = {
|
||||
-0.5f, 0.0f, -0.5f, 0.0f, 1.0f,
|
||||
0.5f, 0.0f, -0.5f, 1.0f, 1.0f,
|
||||
0.5f, 0.0f, 0.5f, 1.0f, 0.0f,
|
||||
-0.5f, 0.0f, 0.5f, 0.0f, 0.0f
|
||||
};
|
||||
ushort[] indices = { 0, 1, 2, 2, 3, 0 };
|
||||
|
||||
// Create quad vertices - centered to match ACViewer expansion logic
|
||||
float[] vertices = {
|
||||
// x, y, z, u, v
|
||||
-0.5f, 0.0f, -0.5f, 0.0f, 1.0f,
|
||||
0.5f, 0.0f, -0.5f, 1.0f, 1.0f,
|
||||
0.5f, 0.0f, 0.5f, 1.0f, 0.0f,
|
||||
-0.5f, 0.0f, 0.5f, 0.0f, 0.0f
|
||||
};
|
||||
vao = CreateVertexArray(resources, gl, "WB particle VAO");
|
||||
vbo = CreateBuffer(resources, gl, "WB particle vertex buffer");
|
||||
ibo = CreateBuffer(resources, gl, "WB particle index buffer");
|
||||
instanceVbo = CreateBuffer(resources, gl, "WB particle instance buffer");
|
||||
|
||||
ushort[] indices = { 0, 1, 2, 2, 3, 0 };
|
||||
GlResourceCommand.Execute(gl, "configure WB particle batcher", () => {
|
||||
gl.BindVertexArray(vao);
|
||||
gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo);
|
||||
fixed (float* p = vertices) {
|
||||
gl.BufferData(BufferTargetARB.ArrayBuffer, (uint)(vertices.Length * sizeof(float)), p, BufferUsageARB.StaticDraw);
|
||||
}
|
||||
gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, ibo);
|
||||
fixed (ushort* p = indices) {
|
||||
gl.BufferData(BufferTargetARB.ElementArrayBuffer, (uint)(indices.Length * sizeof(ushort)), p, BufferUsageARB.StaticDraw);
|
||||
}
|
||||
|
||||
_vao = gl.GenVertexArray();
|
||||
gl.BindVertexArray(_vao);
|
||||
gl.EnableVertexAttribArray(0);
|
||||
gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 5 * sizeof(float), (void*)0);
|
||||
gl.EnableVertexAttribArray(1);
|
||||
gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, 5 * sizeof(float), (void*)(3 * sizeof(float)));
|
||||
|
||||
_vbo = gl.GenBuffer();
|
||||
gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
|
||||
unsafe {
|
||||
fixed (float* p = vertices) {
|
||||
gl.BufferData(BufferTargetARB.ArrayBuffer, (uint)(vertices.Length * sizeof(float)), p, BufferUsageARB.StaticDraw);
|
||||
}
|
||||
gl.BindBuffer(BufferTargetARB.ArrayBuffer, instanceVbo);
|
||||
gl.BufferData(BufferTargetARB.ArrayBuffer, (uint)(MAX_PARTICLES_TOTAL * Marshal.SizeOf<ParticleInstance>()), (void*)0, BufferUsageARB.DynamicDraw);
|
||||
uint stride = (uint)Marshal.SizeOf<ParticleInstance>();
|
||||
gl.EnableVertexAttribArray(2);
|
||||
gl.VertexAttribPointer(2, 3, VertexAttribPointerType.Float, false, stride, (void*)0);
|
||||
gl.VertexAttribDivisor(2, 1);
|
||||
gl.EnableVertexAttribArray(3);
|
||||
gl.VertexAttribPointer(3, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
|
||||
gl.VertexAttribDivisor(3, 1);
|
||||
gl.EnableVertexAttribArray(4);
|
||||
gl.VertexAttribPointer(4, 1, VertexAttribPointerType.Float, false, stride, (void*)(6 * sizeof(float)));
|
||||
gl.VertexAttribDivisor(4, 1);
|
||||
gl.EnableVertexAttribArray(5);
|
||||
gl.VertexAttribPointer(5, 4, VertexAttribPointerType.Float, false, stride, (void*)(7 * sizeof(float)));
|
||||
gl.VertexAttribDivisor(5, 1);
|
||||
gl.EnableVertexAttribArray(6);
|
||||
gl.VertexAttribPointer(6, 2, VertexAttribPointerType.Float, false, stride, (void*)(11 * sizeof(float)));
|
||||
gl.VertexAttribDivisor(6, 1);
|
||||
gl.EnableVertexAttribArray(7);
|
||||
gl.VertexAttribPointer(7, 1, VertexAttribPointerType.Float, false, stride, (void*)(13 * sizeof(float)));
|
||||
gl.VertexAttribDivisor(7, 1);
|
||||
gl.BindVertexArray(0);
|
||||
});
|
||||
|
||||
shader.Bind();
|
||||
shader.SetUniform("uTextureArray", 0);
|
||||
shader.Unbind();
|
||||
} catch (Exception constructionFailure) {
|
||||
resources.RollbackConstructionAndThrow(
|
||||
"ParticleBatcher construction failed and its GL prefix did not cleanly roll back.",
|
||||
constructionFailure);
|
||||
}
|
||||
|
||||
_ibo = gl.GenBuffer();
|
||||
gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, _ibo);
|
||||
unsafe {
|
||||
fixed (ushort* p = indices) {
|
||||
gl.BufferData(BufferTargetARB.ElementArrayBuffer, (uint)(indices.Length * sizeof(ushort)), p, BufferUsageARB.StaticDraw);
|
||||
}
|
||||
}
|
||||
_resources = resources;
|
||||
_shader = shader!;
|
||||
_vao = vao;
|
||||
_vbo = vbo;
|
||||
_ibo = ibo;
|
||||
_instanceVbo = instanceVbo;
|
||||
}
|
||||
|
||||
// Quad attributes
|
||||
gl.EnableVertexAttribArray(0);
|
||||
gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 5 * sizeof(float), (void*)0);
|
||||
gl.EnableVertexAttribArray(1);
|
||||
gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, 5 * sizeof(float), (void*)(3 * sizeof(float)));
|
||||
private static uint CreateBuffer(
|
||||
ResourceCleanupGroup resources,
|
||||
GL gl,
|
||||
string name) {
|
||||
uint buffer = GlResourceCommand.CreateName(gl, name, gl.GenBuffer, gl.DeleteBuffer);
|
||||
resources.Add(name, () => GlResourceCommand.DeleteBuffer(gl, buffer, $"delete {name} {buffer}"));
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// Instance attributes
|
||||
_instanceVbo = gl.GenBuffer();
|
||||
gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo);
|
||||
gl.BufferData(BufferTargetARB.ArrayBuffer, (uint)(MAX_PARTICLES_TOTAL * Marshal.SizeOf<ParticleInstance>()), (void*)0, BufferUsageARB.DynamicDraw);
|
||||
|
||||
uint stride = (uint)Marshal.SizeOf<ParticleInstance>();
|
||||
|
||||
// iPosition
|
||||
gl.EnableVertexAttribArray(2);
|
||||
gl.VertexAttribPointer(2, 3, VertexAttribPointerType.Float, false, stride, (void*)0);
|
||||
gl.VertexAttribDivisor(2, 1);
|
||||
|
||||
// iScaleOpacityActive
|
||||
gl.EnableVertexAttribArray(3);
|
||||
gl.VertexAttribPointer(3, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
|
||||
gl.VertexAttribDivisor(3, 1);
|
||||
|
||||
// iTextureIndex
|
||||
gl.EnableVertexAttribArray(4);
|
||||
gl.VertexAttribPointer(4, 1, VertexAttribPointerType.Float, false, stride, (void*)(6 * sizeof(float)));
|
||||
gl.VertexAttribDivisor(4, 1);
|
||||
|
||||
// iRotation (Quaternion)
|
||||
gl.EnableVertexAttribArray(5);
|
||||
gl.VertexAttribPointer(5, 4, VertexAttribPointerType.Float, false, stride, (void*)(7 * sizeof(float)));
|
||||
gl.VertexAttribDivisor(5, 1);
|
||||
|
||||
// iSize
|
||||
gl.EnableVertexAttribArray(6);
|
||||
gl.VertexAttribPointer(6, 2, VertexAttribPointerType.Float, false, stride, (void*)(11 * sizeof(float)));
|
||||
gl.VertexAttribDivisor(6, 1);
|
||||
|
||||
// iIsBillboard
|
||||
gl.EnableVertexAttribArray(7);
|
||||
gl.VertexAttribPointer(7, 1, VertexAttribPointerType.Float, false, stride, (void*)(13 * sizeof(float)));
|
||||
gl.VertexAttribDivisor(7, 1);
|
||||
|
||||
gl.BindVertexArray(0);
|
||||
|
||||
_shader.Bind();
|
||||
_shader.SetUniform("uTextureArray", 0);
|
||||
_shader.Unbind();
|
||||
private static uint CreateVertexArray(
|
||||
ResourceCleanupGroup resources,
|
||||
GL gl,
|
||||
string name) {
|
||||
uint vao = GlResourceCommand.CreateName(gl, name, gl.GenVertexArray, gl.DeleteVertexArray);
|
||||
resources.Add(name, () => GlResourceCommand.DeleteVertexArray(gl, vao, $"delete {name} {vao}"));
|
||||
return vao;
|
||||
}
|
||||
|
||||
public void Begin(Matrix4x4 viewProjection, Vector3 cameraUp, Vector3 cameraRight) {
|
||||
|
|
@ -216,12 +234,7 @@ namespace AcDream.App.Rendering.Wb {
|
|||
}
|
||||
|
||||
public void Dispose() {
|
||||
var gl = _graphicsDevice.GL;
|
||||
gl.DeleteVertexArray(_vao);
|
||||
gl.DeleteBuffer(_vbo);
|
||||
gl.DeleteBuffer(_instanceVbo);
|
||||
gl.DeleteBuffer(_ibo);
|
||||
(_shader as IDisposable)?.Dispose();
|
||||
_resources.RetryCleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,11 +16,22 @@ internal static unsafe class TrackedGlResource
|
|||
GL gl,
|
||||
uint buffer,
|
||||
long capacityBytes,
|
||||
string context) =>
|
||||
CreateRetryableBufferDeletion(
|
||||
gl,
|
||||
buffer,
|
||||
() => capacityBytes,
|
||||
context);
|
||||
|
||||
public static RetryableGpuResourceRelease CreateRetryableBufferDeletion(
|
||||
GL gl,
|
||||
uint buffer,
|
||||
Func<long> capacityBytes,
|
||||
string context)
|
||||
{
|
||||
if (buffer == 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(buffer));
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(capacityBytes);
|
||||
ArgumentNullException.ThrowIfNull(capacityBytes);
|
||||
return new RetryableGpuResourceRelease(
|
||||
() => GLHelpers.ThrowOnResourceError(gl, $"{context} (precondition)"),
|
||||
() =>
|
||||
|
|
@ -34,8 +45,10 @@ internal static unsafe class TrackedGlResource
|
|||
},
|
||||
() =>
|
||||
{
|
||||
if (capacityBytes != 0)
|
||||
GpuMemoryTracker.TrackDeallocation(capacityBytes, GpuResourceType.Buffer);
|
||||
long bytes = capacityBytes();
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(bytes);
|
||||
if (bytes != 0)
|
||||
GpuMemoryTracker.TrackDeallocation(bytes, GpuResourceType.Buffer);
|
||||
},
|
||||
() => GpuMemoryTracker.TrackResourceDeallocation(GpuResourceType.Buffer));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,18 +122,48 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
|
||||
_dats = dats;
|
||||
_resourceRetirement = resourceRetirement;
|
||||
_graphicsDevice = new OpenGLGraphicsDevice(
|
||||
gl,
|
||||
logger,
|
||||
new DebugRenderSettings(),
|
||||
resourceRetirement);
|
||||
_graphicsDevice.ParticleBatcher = new ParticleBatcher(_graphicsDevice);
|
||||
// ConsoleErrorLogger surfaces WB's silently-caught exceptions
|
||||
// (ObjectMeshManager.PrepareMeshData try/catch at line ~589).
|
||||
_meshManager = new ObjectMeshManager(
|
||||
_graphicsDevice,
|
||||
dats,
|
||||
new ConsoleErrorLogger<ObjectMeshManager>());
|
||||
var resources = new AcDream.App.Rendering.ResourceCleanupGroup();
|
||||
OpenGLGraphicsDevice? graphicsDevice = null;
|
||||
ObjectMeshManager? meshManager = null;
|
||||
try
|
||||
{
|
||||
graphicsDevice = new OpenGLGraphicsDevice(
|
||||
gl,
|
||||
logger,
|
||||
new DebugRenderSettings(),
|
||||
resourceRetirement);
|
||||
OpenGLGraphicsDevice ownedGraphicsDevice = graphicsDevice;
|
||||
var graphicsDeviceRelease = new RetryableGpuResourceRelease(
|
||||
ownedGraphicsDevice.Dispose,
|
||||
() =>
|
||||
{
|
||||
ownedGraphicsDevice.ProcessGLQueue();
|
||||
if (ownedGraphicsDevice.HasPendingGLWork)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"WB graphics-device construction cleanup still has queued GL work.");
|
||||
}
|
||||
});
|
||||
resources.Add("WB graphics device", graphicsDeviceRelease.Run);
|
||||
graphicsDevice.ParticleBatcher = new ParticleBatcher(graphicsDevice);
|
||||
// ConsoleErrorLogger surfaces WB's silently-caught exceptions
|
||||
// (ObjectMeshManager.PrepareMeshData try/catch at line ~589).
|
||||
meshManager = new ObjectMeshManager(
|
||||
graphicsDevice,
|
||||
dats,
|
||||
new ConsoleErrorLogger<ObjectMeshManager>());
|
||||
resources.Add("WB object mesh manager", meshManager.Dispose);
|
||||
resources.TransferAll();
|
||||
}
|
||||
catch (Exception constructionFailure)
|
||||
{
|
||||
resources.RollbackConstructionAndThrow(
|
||||
"WbMeshAdapter construction failed and its WB/GL prefix did not cleanly roll back.",
|
||||
constructionFailure);
|
||||
}
|
||||
|
||||
_graphicsDevice = graphicsDevice;
|
||||
_meshManager = meshManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue