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
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue