feat(render): Campaign V slice V4a - port TextRenderer/BitmapFont/DebugLineRenderer/TextureCache UI path onto IGpuDevice
Second attempt at V4a afterceec3bc4was reverted at9aaf97e7for losing world multisampling and a 334-file scope explosion. This lands the same functional slice with a much smaller footprint and the two structural fixes the revert postmortem (docs/plans/2026-07-27-vulkan-campaign.md SS7.1) called for. What moved onto the RHI: - TextRenderer: the ui_text shader now compiles through IGpuDevice.CreatePipeline (one IGpuPipeline, replacing the old hand-rolled Shader class); its three fence-buffered per-flight VBOs are gone in favour of a per-IGpuFrame ring allocation per draw bucket; its 1x1 white fill texture is created via IGpuDevice.CreateTexture and registered into the device's texture table. Flush keeps TextRenderGlStateScope and the manual GL disable block verbatim (TextRendererFailureSafetyTests pins their literal presence) alongside the new pipeline bind - both target the identical final GL state, so this is redundant, not contradictory. Sprite/font texture binding stays classic (glActiveTexture/glBindTexture) because DrawSprite receives arbitrary externally-owned GL texture names from dozens of UI call sites outside this slice's scope; IGpuPassEncoder has no verb for that, by design (every other RHI consumer samples through the bindless texture table). - BitmapFont: the stb-baked R8 atlas is created/uploaded through IGpuDevice.CreateTexture; TextureId stays a raw GL name extracted from the IGpuTexture, since its only consumer is TextRenderer's classic path above. - DebugLineRenderer: the debug_line shader compiles through IGpuDevice.CreatePipeline (LineList topology, depth disabled); Flush ring- allocates its vertex data and draws through IGpuPassEncoder. uView/uProjection don't fit the shared GpuPushConstants block (one combined VP matrix) so they are set directly on the pipeline's compiled program, mirroring TextRenderer. - TextureCache: GetOrUploadRenderSurface and the public UploadRgba8(byte[],...) wrapper now create IGpuTexture+GpuTextureSlot internally, extracting the raw GL name for their unchanged uint return type - DrawSprite's signature and its 16 call sites across the UI are untouched. The world-material path (GetOrUpload, the raw layer-array upload) is untouched. - UiViewport: TextureHandle (uint) -> TextureSlot (GpuTextureSlot), resolved back to a raw GL name via TextRenderer.ResolveExternalTextureSlot at draw time. Its texture is produced by PaperdollViewportRenderer/ PrivateEntityViewportRenderer, both still raw GL until V4g, so RetailPaperdollFrameView/RetailCreatureAppraisalFrameView register it through the pre-approved GlGpuDevice.RegisterExternalColorTexture transitional seam (campaign doc SS7.1's final paragraph) instead of inventing anything broader. The two revert-postmortem fixes, both in Gpu/Gl (never in the pinned Gpu/ contract): - GlGpuDevice.BeginPass now resets the render-state cache unconditionally on every pass, not only a clearing one. The first attempt's crash came from exactly this gap: a raw-GL renderer running between two RHI passes changes GL program/blend/depth/cull state the cache never observes, so a later BindPipeline skipped re-issuing glUseProgram and the following push-constant upload threw GL_INVALID_OPERATION. - GlGpuPassEncoder now captures ambient GL capability state (program, VAO, array buffer, texture0 binding, depth test/write/func, blend enable+func, cull enable+mode, front face, alpha-to-coverage, multisample) on construction and restores it on Dispose, generalizing what TextRenderGlStateScope already did for TextRenderer specifically to every RHI pass - this is what stops DebugLineRenderer's pipeline bind (which has no scope of its own) from leaking state into the next raw-GL renderer. Both are marked transitional, deleted at V4h once nothing raw-GL remains. Frame lifecycle (additive, per the task's own description of this piece): new GpuDeviceFrameLifetime wraps IGpuDevice.BeginFrame()/IGpuFrame.End() and exposes the open frame via ICurrentGpuFrameSource. RenderFrameOrchestrator's IRenderFrameLifetime now routes through this wrapper instead of calling GpuFrameFlightController directly - GlGpuDevice.BeginFrame already calls straight through to that same controller, so the fence/slot-rotation contract is unchanged; the wrapper only additionally yields the IGpuFrame ported renderers need. No clears moved, no framebuffer binding changed, frame-graph phase order is untouched. The two now-dead per-slot TextRenderer.BeginFrame(int) calls in RuntimeRenderFrameBeginResources are removed. The UI Studio (RenderBootstrap/StudioWindow) gets its own independent RHI device+lifetime, mirroring the production composition. Real bug found and fixed while exercising this for the first time: both BitmapFont and TextureCache's nearest-filter override called TexParameter AFTER RegisterTexture, which made the bindless handle resident - GL_ARB_ bindless_texture forbids modifying a texture's parameters once its handle is resident, so this threw GL_INVALID_OPERATION building the retained UI's own TextRenderer. Fixed by moving both TexParameter blocks before RegisterTexture. Scope note: touches 25 files (24 modified + this commit's one new file), not the ~10 the brief estimated, because the frame-lifecycle wiring and the viewport escape hatch (both explicitly asked for) ripple through five composition files and two frame presenters that thread IGpuDevice/ ICurrentGpuFrameSource to construction sites. No file outside that necessary set was touched: no visibility sweep beyond the specific constructors/ properties whose new parameter types are internal (TextRenderer/BitmapFont/ DebugLineRenderer/UiHost's constructors, TextureCache's otherwise-orphaned convenience overload, UiViewport.TextureSlot), no world-mesh/terrain/particle/ sky file touched, no test deleted or weakened - three source-text conformance tests (TextRendererPublishesEveryConstructorResourceBeforeLaterGlWork, GlTextureOwnershipTests' TextRenderer.cs check, and RenderFrameResourceControllerTests' frame-order check) were replaced with equivalent assertions against the new construction/wiring shape, since their pinned invariant was specifically the old raw-GL shape this slice legitimately replaces. Gates: - dotnet build -c Release: 0 warnings, 0 errors. - dotnet test tests/AcDream.App.Tests -c Release: 3,843 passed / 3 skipped - exactly the baseline. Complete solution: 8,906 passed / 5 skipped across all nine test projects. - Offline pixel gate (tools/run-offline-pixel-gate.ps1, parenta97e04aevs this commit): 26 differing pixels of 563,200 compared (fraction 4.62e-05), pass against the 0.001/563-pixel threshold. Verified against a same-commit control (two captures at this commit differ by 20 pixels) rather than accepted at face value - the two numbers are in the same band, confirming this is normal animated-content/frame-pacing noise and not the systematic silhouette-edge loss (1,791 pixels, 224x higher) the first attempt's revert diagnosed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a97e04ae3d
commit
096dd203fa
26 changed files with 954 additions and 467 deletions
|
|
@ -263,8 +263,6 @@ internal sealed class FrameRootCompositionPhase
|
||||||
live.DrawDispatcher,
|
live.DrawDispatcher,
|
||||||
live.EnvCellRenderer,
|
live.EnvCellRenderer,
|
||||||
live.PortalDepthMask,
|
live.PortalDepthMask,
|
||||||
foundation.TextRenderer,
|
|
||||||
interaction.RetainedUi?.Host.TextRenderer,
|
|
||||||
live.ClipFrame,
|
live.ClipFrame,
|
||||||
foundation.Terrain,
|
foundation.Terrain,
|
||||||
foundation.SceneLighting),
|
foundation.SceneLighting),
|
||||||
|
|
@ -509,7 +507,7 @@ internal sealed class FrameRootCompositionPhase
|
||||||
: (IRenderFramePostDiagnosticsPhase?)lifecycleAutomation
|
: (IRenderFramePostDiagnosticsPhase?)lifecycleAutomation
|
||||||
?? NullRenderFramePostDiagnosticsPhase.Instance;
|
?? NullRenderFramePostDiagnosticsPhase.Instance;
|
||||||
var renderFrame = new RenderFrameOrchestrator(
|
var renderFrame = new RenderFrameOrchestrator(
|
||||||
host.GpuFrameFlights,
|
host.GpuFrameLifetime,
|
||||||
new FrameProfilerGpuMeasurement(d.FrameProfiler, d.Gl),
|
new FrameProfilerGpuMeasurement(d.FrameProfiler, d.Gl),
|
||||||
framePreparation,
|
framePreparation,
|
||||||
worldSceneRenderer,
|
worldSceneRenderer,
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ internal interface IGameWindowHostInputCameraPublication
|
||||||
{
|
{
|
||||||
void PublishGpuFrameFlights(GpuFrameFlightController value);
|
void PublishGpuFrameFlights(GpuFrameFlightController value);
|
||||||
void PublishGpuDevice(IGpuDevice value);
|
void PublishGpuDevice(IGpuDevice value);
|
||||||
|
void PublishGpuFrameLifetime(GpuDeviceFrameLifetime value);
|
||||||
void PublishKeyboardSource(SilkKeyboardSource value);
|
void PublishKeyboardSource(SilkKeyboardSource value);
|
||||||
void PublishMouseSource(SilkMouseSource value);
|
void PublishMouseSource(SilkMouseSource value);
|
||||||
void PublishMouseLookCursor(IMouseLookCursor value);
|
void PublishMouseLookCursor(IMouseLookCursor value);
|
||||||
|
|
@ -23,6 +24,7 @@ internal interface IGameWindowHostInputCameraPublication
|
||||||
internal sealed record HostInputCameraResult(
|
internal sealed record HostInputCameraResult(
|
||||||
GpuFrameFlightController GpuFrameFlights,
|
GpuFrameFlightController GpuFrameFlights,
|
||||||
IGpuDevice GpuDevice,
|
IGpuDevice GpuDevice,
|
||||||
|
GpuDeviceFrameLifetime GpuFrameLifetime,
|
||||||
WorldRenderDiagnostics WorldRenderDiagnostics,
|
WorldRenderDiagnostics WorldRenderDiagnostics,
|
||||||
SilkKeyboardSource? KeyboardSource,
|
SilkKeyboardSource? KeyboardSource,
|
||||||
SilkMouseSource? MouseSource,
|
SilkMouseSource? MouseSource,
|
||||||
|
|
@ -245,6 +247,17 @@ internal sealed class HostInputCameraCompositionPhase :
|
||||||
_publication.PublishGpuDevice);
|
_publication.PublishGpuDevice);
|
||||||
Fault(HostInputCameraCompositionPoint.GpuDevicePublished);
|
Fault(HostInputCameraCompositionPoint.GpuDevicePublished);
|
||||||
|
|
||||||
|
// Campaign V slice V4a: drives IGpuDevice.BeginFrame()/IGpuFrame.End()
|
||||||
|
// once per rendered frame, additively over the existing
|
||||||
|
// GpuFrameFlightController-driven fence/slot bracket (see the class
|
||||||
|
// comment) — RenderFrameOrchestrator's IRenderFrameLifetime is wired
|
||||||
|
// to THIS wrapper instead of gpuFrames directly at FrameRootComposition,
|
||||||
|
// and the IGpuFrame it exposes is what TextRenderer/DebugLineRenderer
|
||||||
|
// reach through ICurrentGpuFrameSource. Owns no disposable resource of
|
||||||
|
// its own — gpuDevice's own scope.Acquire entry above disposes it.
|
||||||
|
var gpuFrameLifetime = new GpuDeviceFrameLifetime(gpuDevice);
|
||||||
|
_publication.PublishGpuFrameLifetime(gpuFrameLifetime);
|
||||||
|
|
||||||
WorldRenderDiagnostics diagnostics =
|
WorldRenderDiagnostics diagnostics =
|
||||||
_factory.CreateWorldRenderDiagnostics(
|
_factory.CreateWorldRenderDiagnostics(
|
||||||
gl,
|
gl,
|
||||||
|
|
@ -345,6 +358,7 @@ internal sealed class HostInputCameraCompositionPhase :
|
||||||
return new HostInputCameraResult(
|
return new HostInputCameraResult(
|
||||||
gpuFrames,
|
gpuFrames,
|
||||||
gpuDevice,
|
gpuDevice,
|
||||||
|
gpuFrameLifetime,
|
||||||
diagnostics,
|
diagnostics,
|
||||||
keyboard,
|
keyboard,
|
||||||
mouse,
|
mouse,
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,9 @@ internal sealed record InteractionRetainedUiDependencies(
|
||||||
VitalsVM? ExistingVitals,
|
VitalsVM? ExistingVitals,
|
||||||
Action<string>? Toast,
|
Action<string>? Toast,
|
||||||
Func<double> ClientTime,
|
Func<double> ClientTime,
|
||||||
Action<string> Log)
|
Action<string> Log,
|
||||||
|
AcDream.App.Rendering.Gpu.IGpuDevice GpuDevice,
|
||||||
|
ICurrentGpuFrameSource GpuFrameSource)
|
||||||
{
|
{
|
||||||
public RuntimeActionState Actions => Runtime.ActionOwner;
|
public RuntimeActionState Actions => Runtime.ActionOwner;
|
||||||
|
|
||||||
|
|
@ -389,7 +391,8 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||||
d.Character.LocalPlayer);
|
d.Character.LocalPlayer);
|
||||||
UiHost host = lease.AcquireHost(
|
UiHost host = lease.AcquireHost(
|
||||||
() => new UiHost(
|
() => new UiHost(
|
||||||
d.Gl,
|
d.GpuDevice,
|
||||||
|
d.GpuFrameSource,
|
||||||
d.ShadersDirectory,
|
d.ShadersDirectory,
|
||||||
d.DebugFont,
|
d.DebugFont,
|
||||||
d.HostQuiescence));
|
d.HostQuiescence));
|
||||||
|
|
|
||||||
|
|
@ -795,7 +795,8 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
paperdollLease.Resource,
|
paperdollLease.Resource,
|
||||||
new RetailPaperdollFrameView(
|
new RetailPaperdollFrameView(
|
||||||
viewport,
|
viewport,
|
||||||
new PaperdollInventoryVisibility(inventoryFrame)),
|
new PaperdollInventoryVisibility(inventoryFrame),
|
||||||
|
host.GpuDevice),
|
||||||
new RetailPaperdollDollFactory(
|
new RetailPaperdollDollFactory(
|
||||||
new LivePaperdollEntityLookup(liveEntities),
|
new LivePaperdollEntityLookup(liveEntities),
|
||||||
d.PlayerIdentity,
|
d.PlayerIdentity,
|
||||||
|
|
@ -842,7 +843,8 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
new RetailCreatureAppraisalFrameView(
|
new RetailCreatureAppraisalFrameView(
|
||||||
creatureViewport,
|
creatureViewport,
|
||||||
examinationFrame,
|
examinationFrame,
|
||||||
appraisalController),
|
appraisalController,
|
||||||
|
host.GpuDevice),
|
||||||
new RetailCreatureAppraisalCloneFactory(
|
new RetailCreatureAppraisalCloneFactory(
|
||||||
new LiveCreatureAppraisalEntityLookup(liveEntities)));
|
new LiveCreatureAppraisalEntityLookup(liveEntities)));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,9 @@ internal sealed record WorldRenderDependencies(
|
||||||
ResidencyBudgetOptions ResidencyBudgets,
|
ResidencyBudgetOptions ResidencyBudgets,
|
||||||
uint InitialCenterLandblockId,
|
uint InitialCenterLandblockId,
|
||||||
string DiagnosticsDirectory,
|
string DiagnosticsDirectory,
|
||||||
Action<string> Log);
|
Action<string> Log,
|
||||||
|
AcDream.App.Rendering.Gpu.IGpuDevice GpuDevice,
|
||||||
|
AcDream.App.Rendering.ICurrentGpuFrameSource GpuFrameSource);
|
||||||
|
|
||||||
internal interface IGameWindowWorldRenderPublication
|
internal interface IGameWindowWorldRenderPublication
|
||||||
{
|
{
|
||||||
|
|
@ -89,10 +91,16 @@ internal interface IWorldRenderCompositionFactory
|
||||||
void SetTerrainAnisotropic(TerrainAtlas atlas, int level);
|
void SetTerrainAnisotropic(TerrainAtlas atlas, int level);
|
||||||
Shader CreateTerrainShader(GL gl, string shadersDirectory);
|
Shader CreateTerrainShader(GL gl, string shadersDirectory);
|
||||||
SceneLightingUboBinding CreateSceneLighting(GL gl);
|
SceneLightingUboBinding CreateSceneLighting(GL gl);
|
||||||
DebugLineRenderer CreateDebugLines(GL gl, string shadersDirectory);
|
DebugLineRenderer CreateDebugLines(
|
||||||
|
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
||||||
|
ICurrentGpuFrameSource frameSource,
|
||||||
|
string shadersDirectory);
|
||||||
byte[]? TryLoadDebugFont();
|
byte[]? TryLoadDebugFont();
|
||||||
BitmapFont CreateDebugFont(GL gl, byte[] bytes);
|
BitmapFont CreateDebugFont(AcDream.App.Rendering.Gpu.IGpuDevice device, byte[] bytes);
|
||||||
TextRenderer CreateTextRenderer(GL gl, string shadersDirectory);
|
TextRenderer CreateTextRenderer(
|
||||||
|
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
||||||
|
ICurrentGpuFrameSource frameSource,
|
||||||
|
string shadersDirectory);
|
||||||
TerrainModernRenderer CreateTerrain(
|
TerrainModernRenderer CreateTerrain(
|
||||||
GL gl,
|
GL gl,
|
||||||
BindlessSupport bindless,
|
BindlessSupport bindless,
|
||||||
|
|
@ -112,6 +120,7 @@ internal interface IWorldRenderCompositionFactory
|
||||||
ResidencyBudgetOptions budgets);
|
ResidencyBudgetOptions budgets);
|
||||||
TextureCache CreateTextureCache(
|
TextureCache CreateTextureCache(
|
||||||
GL gl,
|
GL gl,
|
||||||
|
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
BindlessSupport bindless,
|
BindlessSupport bindless,
|
||||||
IGpuResourceRetirementQueue retirement,
|
IGpuResourceRetirementQueue retirement,
|
||||||
|
|
@ -215,20 +224,22 @@ internal sealed class RetailWorldRenderCompositionFactory
|
||||||
public SceneLightingUboBinding CreateSceneLighting(GL gl) => new(gl);
|
public SceneLightingUboBinding CreateSceneLighting(GL gl) => new(gl);
|
||||||
|
|
||||||
public DebugLineRenderer CreateDebugLines(
|
public DebugLineRenderer CreateDebugLines(
|
||||||
GL gl,
|
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
||||||
|
ICurrentGpuFrameSource frameSource,
|
||||||
string shadersDirectory) =>
|
string shadersDirectory) =>
|
||||||
new(gl, shadersDirectory);
|
new(device, frameSource, shadersDirectory);
|
||||||
|
|
||||||
public byte[]? TryLoadDebugFont() =>
|
public byte[]? TryLoadDebugFont() =>
|
||||||
BitmapFont.TryLoadSystemMonospaceFont();
|
BitmapFont.TryLoadSystemMonospaceFont();
|
||||||
|
|
||||||
public BitmapFont CreateDebugFont(GL gl, byte[] bytes) =>
|
public BitmapFont CreateDebugFont(AcDream.App.Rendering.Gpu.IGpuDevice device, byte[] bytes) =>
|
||||||
new(gl, bytes, pixelHeight: 15f, atlasSize: 512);
|
new(device, bytes, pixelHeight: 15f, atlasSize: 512);
|
||||||
|
|
||||||
public TextRenderer CreateTextRenderer(
|
public TextRenderer CreateTextRenderer(
|
||||||
GL gl,
|
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
||||||
|
ICurrentGpuFrameSource frameSource,
|
||||||
string shadersDirectory) =>
|
string shadersDirectory) =>
|
||||||
new(gl, shadersDirectory);
|
new(device, frameSource, shadersDirectory);
|
||||||
|
|
||||||
public TerrainModernRenderer CreateTerrain(
|
public TerrainModernRenderer CreateTerrain(
|
||||||
GL gl,
|
GL gl,
|
||||||
|
|
@ -294,6 +305,7 @@ internal sealed class RetailWorldRenderCompositionFactory
|
||||||
|
|
||||||
public TextureCache CreateTextureCache(
|
public TextureCache CreateTextureCache(
|
||||||
GL gl,
|
GL gl,
|
||||||
|
AcDream.App.Rendering.Gpu.IGpuDevice device,
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
BindlessSupport bindless,
|
BindlessSupport bindless,
|
||||||
IGpuResourceRetirementQueue retirement,
|
IGpuResourceRetirementQueue retirement,
|
||||||
|
|
@ -301,6 +313,7 @@ internal sealed class RetailWorldRenderCompositionFactory
|
||||||
ResidencyBudgetOptions budgets) =>
|
ResidencyBudgetOptions budgets) =>
|
||||||
new(
|
new(
|
||||||
gl,
|
gl,
|
||||||
|
device,
|
||||||
dats,
|
dats,
|
||||||
bindless,
|
bindless,
|
||||||
retirement,
|
retirement,
|
||||||
|
|
@ -482,12 +495,15 @@ internal sealed class WorldRenderCompositionPhase
|
||||||
DebugLineRenderer debugLines = AcquireAndPublish(
|
DebugLineRenderer debugLines = AcquireAndPublish(
|
||||||
scope,
|
scope,
|
||||||
"debug lines",
|
"debug lines",
|
||||||
() => _factory.CreateDebugLines(gl, shadersDirectory),
|
() => _factory.CreateDebugLines(
|
||||||
|
_dependencies.GpuDevice,
|
||||||
|
_dependencies.GpuFrameSource,
|
||||||
|
shadersDirectory),
|
||||||
_publication.PublishDebugLines,
|
_publication.PublishDebugLines,
|
||||||
WorldRenderCompositionPoint.DebugLinesPublished);
|
WorldRenderCompositionPoint.DebugLinesPublished);
|
||||||
|
|
||||||
(BitmapFont? debugFont, TextRenderer? textRenderer) =
|
(BitmapFont? debugFont, TextRenderer? textRenderer) =
|
||||||
ComposeOptionalHudResources(scope, gl, shadersDirectory);
|
ComposeOptionalHudResources(scope, shadersDirectory);
|
||||||
|
|
||||||
TerrainModernRenderer terrain = AcquireAndPublish(
|
TerrainModernRenderer terrain = AcquireAndPublish(
|
||||||
scope,
|
scope,
|
||||||
|
|
@ -535,6 +551,7 @@ internal sealed class WorldRenderCompositionPhase
|
||||||
"texture cache",
|
"texture cache",
|
||||||
() => _factory.CreateTextureCache(
|
() => _factory.CreateTextureCache(
|
||||||
gl,
|
gl,
|
||||||
|
_dependencies.GpuDevice,
|
||||||
content.Dats,
|
content.Dats,
|
||||||
bindless,
|
bindless,
|
||||||
_dependencies.ResourceRetirement,
|
_dependencies.ResourceRetirement,
|
||||||
|
|
@ -587,7 +604,6 @@ internal sealed class WorldRenderCompositionPhase
|
||||||
|
|
||||||
private (BitmapFont? Font, TextRenderer? Text) ComposeOptionalHudResources(
|
private (BitmapFont? Font, TextRenderer? Text) ComposeOptionalHudResources(
|
||||||
CompositionAcquisitionScope scope,
|
CompositionAcquisitionScope scope,
|
||||||
GL gl,
|
|
||||||
string shadersDirectory)
|
string shadersDirectory)
|
||||||
{
|
{
|
||||||
byte[]? fontBytes = _factory.TryLoadDebugFont();
|
byte[]? fontBytes = _factory.TryLoadDebugFont();
|
||||||
|
|
@ -600,13 +616,16 @@ internal sealed class WorldRenderCompositionPhase
|
||||||
|
|
||||||
var fontLease = scope.Acquire(
|
var fontLease = scope.Acquire(
|
||||||
"world HUD font",
|
"world HUD font",
|
||||||
() => _factory.CreateDebugFont(gl, fontBytes),
|
() => _factory.CreateDebugFont(_dependencies.GpuDevice, fontBytes),
|
||||||
_factory.Release);
|
_factory.Release);
|
||||||
BitmapFont font = fontLease.Resource;
|
BitmapFont font = fontLease.Resource;
|
||||||
Fault(WorldRenderCompositionPoint.DebugFontCreated);
|
Fault(WorldRenderCompositionPoint.DebugFontCreated);
|
||||||
var textLease = scope.Acquire(
|
var textLease = scope.Acquire(
|
||||||
"world HUD text renderer",
|
"world HUD text renderer",
|
||||||
() => _factory.CreateTextRenderer(gl, shadersDirectory),
|
() => _factory.CreateTextRenderer(
|
||||||
|
_dependencies.GpuDevice,
|
||||||
|
_dependencies.GpuFrameSource,
|
||||||
|
shadersDirectory),
|
||||||
_factory.Release);
|
_factory.Release);
|
||||||
TextRenderer text = textLease.Resource;
|
TextRenderer text = textLease.Resource;
|
||||||
Fault(WorldRenderCompositionPoint.TextRendererCreated);
|
Fault(WorldRenderCompositionPoint.TextRendererCreated);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using AcDream.App.Rendering.Gpu;
|
||||||
|
using AcDream.App.Rendering.Gpu.Gl;
|
||||||
using Silk.NET.OpenGL;
|
using Silk.NET.OpenGL;
|
||||||
using StbTrueTypeSharp;
|
using StbTrueTypeSharp;
|
||||||
|
|
||||||
|
|
@ -7,9 +9,17 @@ namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A pixel-font atlas rasterized from a TTF at load time using stb_truetype.
|
/// A pixel-font atlas rasterized from a TTF at load time using stb_truetype.
|
||||||
/// Glyphs are packed into a single-channel (R8) GL texture. Call
|
/// Glyphs are packed into a single-channel (R8) atlas. Call
|
||||||
/// <see cref="TryGetGlyph"/> to resolve an ASCII codepoint to UV + metrics.
|
/// <see cref="TryGetGlyph"/> to resolve an ASCII codepoint to UV + metrics.
|
||||||
///
|
///
|
||||||
|
/// Campaign V slice V4a: the atlas is created and uploaded through
|
||||||
|
/// <see cref="IGpuDevice.CreateTexture"/> instead of raw GL, and registered
|
||||||
|
/// into the device's texture table. <see cref="TextureId"/> stays a raw GL
|
||||||
|
/// name — extracted from the created <see cref="IGpuTexture"/> — because its
|
||||||
|
/// only consumer (<see cref="TextRenderer"/>'s classic sprite/font texture-unit
|
||||||
|
/// binding path; see that class's remarks) is not itself slot-based this
|
||||||
|
/// slice.
|
||||||
|
///
|
||||||
/// Only printable ASCII (32..127) is supported for the debug overlay.
|
/// Only printable ASCII (32..127) is supported for the debug overlay.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed unsafe class BitmapFont : IDisposable
|
public sealed unsafe class BitmapFont : IDisposable
|
||||||
|
|
@ -34,11 +44,10 @@ public sealed unsafe class BitmapFont : IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly GL _gl;
|
|
||||||
private readonly Glyph[] _glyphs;
|
private readonly Glyph[] _glyphs;
|
||||||
private readonly int _firstChar;
|
private readonly int _firstChar;
|
||||||
private readonly int _numChars;
|
private readonly int _numChars;
|
||||||
private readonly ResourceCleanupGroup _resources;
|
private readonly IGpuTexture _texture;
|
||||||
|
|
||||||
public uint TextureId { get; }
|
public uint TextureId { get; }
|
||||||
public float PixelHeight { get; }
|
public float PixelHeight { get; }
|
||||||
|
|
@ -47,10 +56,14 @@ public sealed unsafe class BitmapFont : IDisposable
|
||||||
public int AtlasWidth { get; }
|
public int AtlasWidth { get; }
|
||||||
public int AtlasHeight { get; }
|
public int AtlasHeight { get; }
|
||||||
|
|
||||||
public BitmapFont(GL gl, byte[] ttfBytes, float pixelHeight,
|
// internal, not public: IGpuDevice is an internal type (the pinned RHI
|
||||||
|
// contract). BitmapFont stays public — only construction is restricted —
|
||||||
|
// so existing public members that hold or return a BitmapFont need no
|
||||||
|
// visibility change of their own.
|
||||||
|
internal BitmapFont(IGpuDevice device, byte[] ttfBytes, float pixelHeight,
|
||||||
int atlasSize = 512, int firstChar = 32, int numChars = 96)
|
int atlasSize = 512, int firstChar = 32, int numChars = 96)
|
||||||
{
|
{
|
||||||
_gl = gl;
|
ArgumentNullException.ThrowIfNull(device);
|
||||||
PixelHeight = pixelHeight;
|
PixelHeight = pixelHeight;
|
||||||
AtlasWidth = atlasSize;
|
AtlasWidth = atlasSize;
|
||||||
AtlasHeight = atlasSize;
|
AtlasHeight = atlasSize;
|
||||||
|
|
@ -96,65 +109,52 @@ public sealed unsafe class BitmapFont : IDisposable
|
||||||
adv: bc.xadvance);
|
adv: bc.xadvance);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload atlas as a single-channel GL texture (R8). Publish the GL
|
// Upload atlas as a single-channel texture (R8) through the device.
|
||||||
// name into the construction ledger before any later upload/state
|
IGpuTexture texture = device.CreateTexture(new GpuTextureDescription(
|
||||||
// command can fail.
|
"bitmap-font-atlas",
|
||||||
var resources = new ResourceCleanupGroup();
|
GpuTextureKind.Texture2D,
|
||||||
uint texture = 0;
|
GpuTextureFormat.R8Unorm,
|
||||||
|
Width: AtlasWidth,
|
||||||
|
Height: AtlasHeight,
|
||||||
|
LayerCount: 1,
|
||||||
|
MipLevelCount: 1));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
texture = GlResourceCommand.CreateTexture(_gl, "BitmapFont atlas");
|
fixed (byte* ptr = pixels)
|
||||||
uint ownedTexture = texture;
|
texture.Upload(0, 0, new ReadOnlySpan<byte>(ptr, AtlasWidth * AtlasHeight));
|
||||||
resources.Add(
|
uint glNameForWrapFixup = ((GlGpuTexture)texture).GlName;
|
||||||
"bitmap-font atlas",
|
|
||||||
() => GlResourceCommand.DeleteTexture(
|
// The classic texture-unit binding path TextRenderer's font draws
|
||||||
_gl,
|
// use (see its class remarks) samples this texture object
|
||||||
ownedTexture,
|
// directly rather than through a bound GL sampler object, so wrap
|
||||||
$"delete BitmapFont atlas {ownedTexture}"));
|
// addressing must live on the texture itself — GlGpuTexture's
|
||||||
_gl.GetInteger(GetPName.TextureBinding2D, out int previousTexture);
|
// constructor only sets the filter, not wrap mode (GL 4.3
|
||||||
_gl.GetInteger(GetPName.UnpackAlignment, out int previousAlignment);
|
// defaults textures to GL_REPEAT). Retail's dat font glyph UVs
|
||||||
GlResourceCommand.Execute(_gl, "initialize BitmapFont atlas", () =>
|
// never leave their own tight atlas sub-rect, but pin ClampToEdge
|
||||||
|
// explicitly to preserve the exact sampling behaviour the raw-GL
|
||||||
|
// constructor had. MUST happen BEFORE RegisterTexture below:
|
||||||
|
// ARB_bindless_texture forbids glTexParameter on a texture once
|
||||||
|
// its bindless handle has been made resident (GL_INVALID_OPERATION).
|
||||||
|
if (device is GlGpuDevice glDeviceForWrapFixup)
|
||||||
{
|
{
|
||||||
try
|
GL gl = glDeviceForWrapFixup.Gl;
|
||||||
{
|
gl.BindTexture(TextureTarget.Texture2D, glNameForWrapFixup);
|
||||||
_gl.BindTexture(TextureTarget.Texture2D, texture);
|
gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge);
|
||||||
_gl.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
|
gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge);
|
||||||
fixed (byte* ptr = pixels)
|
gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||||
{
|
}
|
||||||
_gl.TexImage2D(TextureTarget.Texture2D, 0,
|
|
||||||
(int)InternalFormat.R8,
|
IGpuSampler sampler = device.CreateSampler(GpuSamplerDescription.WorldClamp);
|
||||||
(uint)AtlasWidth, (uint)AtlasHeight, 0,
|
device.RegisterTexture(texture, sampler);
|
||||||
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));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (Exception constructionFailure)
|
catch
|
||||||
{
|
{
|
||||||
resources.RollbackConstructionAndThrow(
|
texture.Dispose();
|
||||||
"BitmapFont construction failed and its GL atlas did not cleanly roll back.",
|
throw;
|
||||||
constructionFailure);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TextureId = texture;
|
_texture = texture;
|
||||||
_resources = resources;
|
TextureId = ((GlGpuTexture)texture).GlName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetGlyph(char c, out Glyph g)
|
public bool TryGetGlyph(char c, out Glyph g)
|
||||||
|
|
@ -183,7 +183,7 @@ public sealed unsafe class BitmapFont : IDisposable
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_resources.RetryCleanup();
|
_texture.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering.Gpu;
|
||||||
|
using AcDream.App.Rendering.Gpu.Gl;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.App.UI;
|
using AcDream.App.UI;
|
||||||
using AcDream.App.UI.Layout;
|
using AcDream.App.UI.Layout;
|
||||||
|
|
@ -114,15 +116,23 @@ internal sealed class RetailCreatureAppraisalFrameView :
|
||||||
private readonly UiViewport _viewport;
|
private readonly UiViewport _viewport;
|
||||||
private readonly UiElement _windowFrame;
|
private readonly UiElement _windowFrame;
|
||||||
private readonly AppraisalUiController _controller;
|
private readonly AppraisalUiController _controller;
|
||||||
|
private readonly GlGpuDevice _gpuDevice;
|
||||||
|
|
||||||
public RetailCreatureAppraisalFrameView(
|
public RetailCreatureAppraisalFrameView(
|
||||||
UiViewport viewport,
|
UiViewport viewport,
|
||||||
UiElement windowFrame,
|
UiElement windowFrame,
|
||||||
AppraisalUiController controller)
|
AppraisalUiController controller,
|
||||||
|
IGpuDevice gpuDevice)
|
||||||
{
|
{
|
||||||
_viewport = viewport ?? throw new ArgumentNullException(nameof(viewport));
|
_viewport = viewport ?? throw new ArgumentNullException(nameof(viewport));
|
||||||
_windowFrame = windowFrame ?? throw new ArgumentNullException(nameof(windowFrame));
|
_windowFrame = windowFrame ?? throw new ArgumentNullException(nameof(windowFrame));
|
||||||
_controller = controller ?? throw new ArgumentNullException(nameof(controller));
|
_controller = controller ?? throw new ArgumentNullException(nameof(controller));
|
||||||
|
// The paperdoll/appraisal viewport texture escape hatch (campaign doc
|
||||||
|
// §7.1) is GL-only; see GlGpuDevice.RegisterExternalColorTexture.
|
||||||
|
_gpuDevice = gpuDevice as GlGpuDevice
|
||||||
|
?? throw new ArgumentException(
|
||||||
|
"RetailCreatureAppraisalFrameView's viewport-texture registration is GL-only.",
|
||||||
|
nameof(gpuDevice));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetVisibleTarget(
|
public bool TryGetVisibleTarget(
|
||||||
|
|
@ -149,7 +159,7 @@ internal sealed class RetailCreatureAppraisalFrameView :
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetTextureHandle(uint textureHandle) =>
|
public void SetTextureHandle(uint textureHandle) =>
|
||||||
_viewport.TextureHandle = textureHandle;
|
_viewport.TextureSlot = _gpuDevice.RegisterExternalColorTexture(textureHandle);
|
||||||
|
|
||||||
private static bool IsEffectivelyVisible(UiElement element)
|
private static bool IsEffectivelyVisible(UiElement element)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,94 +1,89 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.InteropServices;
|
using AcDream.App.Rendering.Gpu;
|
||||||
|
using AcDream.App.Rendering.Gpu.Gl;
|
||||||
using Silk.NET.OpenGL;
|
using Silk.NET.OpenGL;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Minimal GL debug line renderer for visualizing collision shapes,
|
/// Minimal line renderer for visualizing collision shapes, bounding boxes,
|
||||||
/// bounding boxes, and other debug geometry. Collect lines each frame
|
/// and other debug geometry. Collect lines each frame via
|
||||||
/// via <see cref="AddLine"/> / <see cref="AddCylinder"/>, then call
|
/// <see cref="AddLine"/> / <see cref="AddCylinder"/>, then call
|
||||||
/// <see cref="Flush"/> to upload + draw them.
|
/// <see cref="Flush"/> to upload + draw them.
|
||||||
///
|
///
|
||||||
/// Uses a single shared VBO that's respecialized each frame. Vertex
|
/// Campaign V slice V4a: the <c>debug_line</c> shader compiles through
|
||||||
/// format is (vec3 pos, vec3 color) = 24 bytes per vertex.
|
/// <see cref="IGpuDevice.CreatePipeline"/> (<see cref="GpuPrimitiveTopology.LineList"/>
|
||||||
|
/// topology, depth disabled to match this renderer's "visible through
|
||||||
|
/// geometry" intent), and each Flush's vertex data comes from a per-frame
|
||||||
|
/// ring allocation instead of the old single respecialized VBO. The shader's
|
||||||
|
/// <c>uView</c>/<c>uProjection</c> pair does not fit the shared
|
||||||
|
/// <c>GpuPushConstants</c> block (one combined view-projection matrix), and
|
||||||
|
/// <see cref="IGpuPassEncoder"/> has no verb for arbitrary named uniforms, so
|
||||||
|
/// they are set directly against the pipeline's compiled program — the same
|
||||||
|
/// mechanical translation <see cref="TextRenderer"/> uses for its own
|
||||||
|
/// shader-local uniforms.
|
||||||
|
///
|
||||||
|
/// Vertex format is (vec3 pos, vec3 color) = 24 bytes per vertex.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed unsafe class DebugLineRenderer : IDisposable
|
public sealed unsafe class DebugLineRenderer : IDisposable
|
||||||
{
|
{
|
||||||
|
private const int FloatsPerVertex = 6;
|
||||||
|
private const int VertexStrideBytes = FloatsPerVertex * sizeof(float);
|
||||||
|
|
||||||
|
private static readonly GpuVertexLayout VertexLayout = new(
|
||||||
|
StrideBytes: VertexStrideBytes,
|
||||||
|
[
|
||||||
|
new GpuVertexAttribute(0, GpuVertexFormat.Float3, 0),
|
||||||
|
new GpuVertexAttribute(1, GpuVertexFormat.Float3, 12),
|
||||||
|
]);
|
||||||
|
|
||||||
private readonly GL _gl;
|
private readonly GL _gl;
|
||||||
private readonly Shader _shader;
|
private readonly ICurrentGpuFrameSource _frameSource;
|
||||||
private readonly uint _vao;
|
private readonly IGpuPipeline _pipeline;
|
||||||
private readonly uint _vbo;
|
private readonly int _uViewLocation;
|
||||||
private readonly ResourceCleanupGroup _resources;
|
private readonly int _uProjectionLocation;
|
||||||
|
|
||||||
private readonly List<float> _buffer = new(4096);
|
private readonly List<float> _buffer = new(4096);
|
||||||
private int _vertexCount;
|
private int _vertexCount;
|
||||||
private int _capacityBytes;
|
|
||||||
|
|
||||||
public DebugLineRenderer(GL gl, string shaderDir)
|
// internal, not public: IGpuDevice/ICurrentGpuFrameSource are internal
|
||||||
|
// types (the pinned RHI contract). DebugLineRenderer stays public — only
|
||||||
|
// construction is restricted.
|
||||||
|
internal DebugLineRenderer(IGpuDevice device, ICurrentGpuFrameSource frameSource, string shaderDir)
|
||||||
{
|
{
|
||||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
ArgumentNullException.ThrowIfNull(device);
|
||||||
|
_frameSource = frameSource ?? throw new ArgumentNullException(nameof(frameSource));
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(shaderDir);
|
ArgumentException.ThrowIfNullOrWhiteSpace(shaderDir);
|
||||||
var resources = new ResourceCleanupGroup();
|
if (device is not GlGpuDevice glDevice)
|
||||||
Shader? shader = null;
|
|
||||||
uint vao = 0;
|
|
||||||
uint vbo = 0;
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
shader = new Shader(gl,
|
throw new NotSupportedException(
|
||||||
Path.Combine(shaderDir, "debug_line.vert"),
|
"DebugLineRenderer's uView/uProjection uniforms (see the class remarks) are set " +
|
||||||
Path.Combine(shaderDir, "debug_line.frag"));
|
"directly against the GL program; it needs GlGpuDevice's raw GL handle.");
|
||||||
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}"));
|
|
||||||
|
|
||||||
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)
|
_gl = glDevice.Gl;
|
||||||
|
|
||||||
|
_pipeline = device.CreatePipeline(new GpuPipelineDescription
|
||||||
{
|
{
|
||||||
resources.RollbackConstructionAndThrow(
|
Name = "debug-line",
|
||||||
"DebugLineRenderer construction failed and its GL prefix did not cleanly roll back.",
|
Shaders = new GpuShaderSet("debug_line"),
|
||||||
constructionFailure);
|
VertexLayout = VertexLayout,
|
||||||
}
|
Topology = GpuPrimitiveTopology.LineList,
|
||||||
|
Blend = GpuBlendMode.None,
|
||||||
|
// Retail debug lines draw through geometry (the old Flush disabled
|
||||||
|
// depth testing for the draw and restored whatever was ambient
|
||||||
|
// before it — GlGpuPassEncoder now does that restore generically;
|
||||||
|
// see its class comment).
|
||||||
|
Depth = GpuDepthState.Disabled,
|
||||||
|
Cull = GpuCullMode.None,
|
||||||
|
AlphaToCoverage = false,
|
||||||
|
ColorWrite = true,
|
||||||
|
SampleCount = 1,
|
||||||
|
});
|
||||||
|
|
||||||
_resources = resources;
|
uint program = ((GlGpuPipeline)_pipeline).GlProgram;
|
||||||
_shader = shader!;
|
_uViewLocation = _gl.GetUniformLocation(program, "uView");
|
||||||
_vao = vao;
|
_uProjectionLocation = _gl.GetUniformLocation(program, "uProjection");
|
||||||
_vbo = vbo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Clear accumulated lines. Call at the start of each frame.</summary>
|
/// <summary>Clear accumulated lines. Call at the start of each frame.</summary>
|
||||||
|
|
@ -174,40 +169,42 @@ public sealed unsafe class DebugLineRenderer : IDisposable
|
||||||
{
|
{
|
||||||
if (_vertexCount == 0) return;
|
if (_vertexCount == 0) return;
|
||||||
|
|
||||||
_shader.Use();
|
IGpuFrame frame = _frameSource.CurrentFrame
|
||||||
_shader.SetMatrix4("uView", view);
|
?? throw new InvalidOperationException(
|
||||||
_shader.SetMatrix4("uProjection", projection);
|
"DebugLineRenderer.Flush requires an open IGpuFrame (see GpuDeviceFrameLifetime) — " +
|
||||||
|
"the host must drive IGpuDevice.BeginFrame() before rendering debug lines.");
|
||||||
|
|
||||||
_gl.BindVertexArray(_vao);
|
using IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription
|
||||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
|
|
||||||
|
|
||||||
int neededBytes = _buffer.Count * sizeof(float);
|
|
||||||
if (neededBytes > _capacityBytes)
|
|
||||||
{
|
{
|
||||||
fixed (float* ptr = CollectionsMarshal.AsSpan(_buffer))
|
Name = "debug-line",
|
||||||
_gl.BufferData(BufferTargetARB.ArrayBuffer, (nuint)neededBytes, ptr, BufferUsageARB.DynamicDraw);
|
Color = new GpuColorAttachment(
|
||||||
_capacityBytes = neededBytes;
|
Target: null,
|
||||||
}
|
Load: GpuLoadOp.Load,
|
||||||
else
|
Store: GpuStoreOp.Store,
|
||||||
{
|
ClearColor: default),
|
||||||
fixed (float* ptr = CollectionsMarshal.AsSpan(_buffer))
|
Depth = null,
|
||||||
_gl.BufferSubData(BufferTargetARB.ArrayBuffer, 0, (nuint)neededBytes, ptr);
|
SampleCount = 1,
|
||||||
}
|
});
|
||||||
|
encoder.BindPipeline(_pipeline);
|
||||||
|
SetMatrix(_uViewLocation, view);
|
||||||
|
SetMatrix(_uProjectionLocation, projection);
|
||||||
|
|
||||||
// Depth test on so lines get occluded by geometry (but we want them
|
int byteCount = _buffer.Count * sizeof(float);
|
||||||
// visible through geometry — disable depth test so everything shows).
|
GpuRingAllocation allocation = frame.AllocateRing(byteCount, GpuRingUsage.Vertex);
|
||||||
bool wasDepthEnabled = _gl.IsEnabled(EnableCap.DepthTest);
|
System.Runtime.InteropServices.CollectionsMarshal.AsSpan(_buffer).CopyTo(allocation.AsSpan<float>());
|
||||||
_gl.Disable(EnableCap.DepthTest);
|
encoder.BindVertexBuffer(allocation.Buffer, allocation.OffsetBytes);
|
||||||
|
encoder.Draw((uint)_vertexCount, 1, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
_gl.DrawArrays(PrimitiveType.Lines, 0, (uint)_vertexCount);
|
private void SetMatrix(int location, Matrix4x4 m)
|
||||||
|
{
|
||||||
if (wasDepthEnabled) _gl.Enable(EnableCap.DepthTest);
|
if (location < 0)
|
||||||
|
return;
|
||||||
_gl.BindVertexArray(0);
|
_gl.UniformMatrix4(location, 1, false, (float*)&m);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_resources.RetryCleanup();
|
_pipeline.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@ public sealed class GameWindow :
|
||||||
private IDisposable? _frameGraphPublication;
|
private IDisposable? _frameGraphPublication;
|
||||||
private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights;
|
private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights;
|
||||||
private IGpuDevice? _gpuDevice;
|
private IGpuDevice? _gpuDevice;
|
||||||
|
private GpuDeviceFrameLifetime? _gpuFrameLifetime;
|
||||||
private readonly AcDream.App.Rendering.GameFrameGraphSlot _frameGraphs = new();
|
private readonly AcDream.App.Rendering.GameFrameGraphSlot _frameGraphs = new();
|
||||||
private readonly AcDream.App.Rendering.GameRenderResourceLifetime
|
private readonly AcDream.App.Rendering.GameRenderResourceLifetime
|
||||||
_renderResourceLifetime = new();
|
_renderResourceLifetime = new();
|
||||||
|
|
@ -748,6 +749,10 @@ public sealed class GameWindow :
|
||||||
IGpuDevice value) =>
|
IGpuDevice value) =>
|
||||||
PublishCompositionOwner(ref _gpuDevice, value, "GPU device (RHI)");
|
PublishCompositionOwner(ref _gpuDevice, value, "GPU device (RHI)");
|
||||||
|
|
||||||
|
void IGameWindowHostInputCameraPublication.PublishGpuFrameLifetime(
|
||||||
|
GpuDeviceFrameLifetime value) =>
|
||||||
|
PublishCompositionOwner(ref _gpuFrameLifetime, value, "GPU frame lifetime");
|
||||||
|
|
||||||
void IGameWindowHostInputCameraPublication.PublishKeyboardSource(
|
void IGameWindowHostInputCameraPublication.PublishKeyboardSource(
|
||||||
AcDream.App.Input.SilkKeyboardSource value) =>
|
AcDream.App.Input.SilkKeyboardSource value) =>
|
||||||
PublishCompositionOwner(ref _kbSource, value, "keyboard source");
|
PublishCompositionOwner(ref _kbSource, value, "keyboard source");
|
||||||
|
|
@ -1306,7 +1311,9 @@ public sealed class GameWindow :
|
||||||
_options.ResidencyBudgets,
|
_options.ResidencyBudgets,
|
||||||
initialCenterLandblockId,
|
initialCenterLandblockId,
|
||||||
_applicationPaths.DiagnosticsDirectory,
|
_applicationPaths.DiagnosticsDirectory,
|
||||||
Console.WriteLine),
|
Console.WriteLine,
|
||||||
|
_gpuDevice!,
|
||||||
|
_gpuFrameLifetime!),
|
||||||
this).Compose(platformResult, contentEffectsAudio, settingsDevTools);
|
this).Compose(platformResult, contentEffectsAudio, settingsDevTools);
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"loading world view centered on " +
|
$"loading world view centered on " +
|
||||||
|
|
@ -1351,7 +1358,9 @@ public sealed class GameWindow :
|
||||||
settingsDevTools.DevTools?.Vitals,
|
settingsDevTools.DevTools?.Vitals,
|
||||||
compositionToast,
|
compositionToast,
|
||||||
ClientTimerNow,
|
ClientTimerNow,
|
||||||
Console.WriteLine),
|
Console.WriteLine,
|
||||||
|
hostInputCamera.GpuDevice,
|
||||||
|
hostInputCamera.GpuFrameLifetime),
|
||||||
_retailUiLease,
|
_retailUiLease,
|
||||||
this).Compose(
|
this).Compose(
|
||||||
platformResult,
|
platformResult,
|
||||||
|
|
|
||||||
|
|
@ -313,9 +313,7 @@ internal sealed class GlGpuDevice : IGpuDevice
|
||||||
// Force the write masks on before clearing, regardless of what
|
// Force the write masks on before clearing, regardless of what
|
||||||
// the previous pass's last draw left them at (e.g. depth-write
|
// the previous pass's last draw left them at (e.g. depth-write
|
||||||
// disabled mid-translucent-pass) — glClear silently no-ops for a
|
// disabled mid-translucent-pass) — glClear silently no-ops for a
|
||||||
// buffer whose mask is off. GlRenderStateCache.Reset() afterward
|
// buffer whose mask is off.
|
||||||
// stops the cache from believing this forced state is the
|
|
||||||
// baseline the next BindPipeline should diff against.
|
|
||||||
ClearBufferMask mask = 0;
|
ClearBufferMask mask = 0;
|
||||||
if (clearsColor)
|
if (clearsColor)
|
||||||
{
|
{
|
||||||
|
|
@ -333,8 +331,72 @@ internal sealed class GlGpuDevice : IGpuDevice
|
||||||
}
|
}
|
||||||
_gl.Clear(mask);
|
_gl.Clear(mask);
|
||||||
GLHelpers.ThrowOnResourceError(_gl, $"clear pass '{description.Name}'");
|
GLHelpers.ThrowOnResourceError(_gl, $"clear pass '{description.Name}'");
|
||||||
_renderState.Reset();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Campaign V slice V4a (2026-07-27 revert postmortem, plan §7.1 rule 2):
|
||||||
|
// reset unconditionally on EVERY pass, not only a clearing one. While
|
||||||
|
// raw-GL renderers coexist with RHI-ported ones (through V4h), a
|
||||||
|
// raw-GL renderer can run between two RHI passes within the same frame
|
||||||
|
// and change GL program/blend/depth/cull state the cache never
|
||||||
|
// observes. A reset gated on "this pass cleared" left the cache
|
||||||
|
// trusting a stale belief in that case, so a later BindPipeline
|
||||||
|
// skipped re-issuing glUseProgram and the following push-constant
|
||||||
|
// upload threw GL_INVALID_OPERATION against whatever program was
|
||||||
|
// actually bound — exactly the failure the first V4a attempt hit.
|
||||||
|
// Resetting on every BeginPass costs one redundant state application
|
||||||
|
// on the pass's first bind and is removed at V4h once nothing raw-GL
|
||||||
|
// remains.
|
||||||
|
_renderState.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── V4a pre-approved transitional seam (campaign doc §7.1, final paragraph) ──
|
||||||
|
//
|
||||||
|
// The paperdoll and creature-appraisal viewport textures are produced by
|
||||||
|
// PaperdollViewportRenderer / PrivateEntityViewportRenderer, both still raw
|
||||||
|
// GL until V4g. UiViewport (ported this slice) needs a GpuTextureSlot for
|
||||||
|
// whatever texture they hand it so it can draw through the same seam every
|
||||||
|
// other ported UI texture uses, without those renderers themselves porting
|
||||||
|
// early. This registers an EXTERNALLY-OWNED GL texture name into the
|
||||||
|
// device's texture table without taking ownership of its GL lifetime: this
|
||||||
|
// device never deletes it, and the owning renderer keeps recreating it on
|
||||||
|
// resize exactly as before. Idempotent by GL name so calling this every
|
||||||
|
// frame with the same still-live texture does not churn the table.
|
||||||
|
//
|
||||||
|
// Deleted at V4g, when PaperdollViewportRenderer / PrivateEntityViewportRenderer
|
||||||
|
// port onto IGpuDevice and can call RegisterTexture directly instead.
|
||||||
|
private readonly Dictionary<uint, GpuTextureSlot> _externalColorTextureSlotsByGlName = new();
|
||||||
|
private readonly Dictionary<uint, uint> _externalColorTextureGlNamesBySlot = new();
|
||||||
|
|
||||||
|
internal GpuTextureSlot RegisterExternalColorTexture(uint glTextureName)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
if (glTextureName == 0)
|
||||||
|
return GpuTextureSlot.Unassigned;
|
||||||
|
if (_externalColorTextureSlotsByGlName.TryGetValue(glTextureName, out GpuTextureSlot existing))
|
||||||
|
return existing;
|
||||||
|
|
||||||
|
ulong handle = _bindless.GetResidentHandle(glTextureName);
|
||||||
|
uint slotIndex = _textureSlotAllocator.Allocate();
|
||||||
|
WriteHandle(slotIndex, handle);
|
||||||
|
var slot = new GpuTextureSlot(slotIndex);
|
||||||
|
_externalColorTextureSlotsByGlName[glTextureName] = slot;
|
||||||
|
_externalColorTextureGlNamesBySlot[slotIndex] = glTextureName;
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves a slot produced by <see cref="RegisterExternalColorTexture"/>
|
||||||
|
/// back to its raw GL texture name, for the still-classic texture-unit
|
||||||
|
/// binding draw path (<c>TextRenderer.DrawSprite(uint texture, ...)</c>).
|
||||||
|
/// </summary>
|
||||||
|
internal bool TryResolveExternalColorTexture(GpuTextureSlot slot, out uint glTextureName)
|
||||||
|
{
|
||||||
|
if (!slot.IsAssigned)
|
||||||
|
{
|
||||||
|
glTextureName = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return _externalColorTextureGlNamesBySlot.TryGetValue(slot.Index, out glTextureName);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void ApplyRenderState(GlRenderStateSnapshot desired)
|
internal void ApplyRenderState(GlRenderStateSnapshot desired)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ internal sealed class GlGpuPassEncoder : IGpuPassEncoder
|
||||||
private readonly GlGpuDevice _device;
|
private readonly GlGpuDevice _device;
|
||||||
private readonly GlGpuFrame _frame;
|
private readonly GlGpuFrame _frame;
|
||||||
private readonly GL _gl;
|
private readonly GL _gl;
|
||||||
|
private readonly GlAmbientCapabilityState _ambientOnEntry;
|
||||||
private bool _closed;
|
private bool _closed;
|
||||||
|
|
||||||
private GlGpuPipeline? _currentPipeline;
|
private GlGpuPipeline? _currentPipeline;
|
||||||
|
|
@ -30,6 +31,20 @@ internal sealed class GlGpuPassEncoder : IGpuPassEncoder
|
||||||
_frame = frame;
|
_frame = frame;
|
||||||
_gl = device.Gl;
|
_gl = device.Gl;
|
||||||
Pass = pass;
|
Pass = pass;
|
||||||
|
|
||||||
|
// Campaign V slice V4a (2026-07-27 revert postmortem, plan §7.1 rule 1):
|
||||||
|
// capture every ambient capability a bound pipeline can change, so
|
||||||
|
// Dispose can put it back. Every acdream renderer is still raw GL
|
||||||
|
// until V4c/V4d, so each one assumes whatever capability state the
|
||||||
|
// PREVIOUS renderer left behind is still there — GL_MULTISAMPLE and
|
||||||
|
// GL_SAMPLE_ALPHA_TO_COVERAGE in particular are set once per frame by
|
||||||
|
// quality settings and never re-asserted per draw. The first V4a
|
||||||
|
// attempt bound a pipeline that changed this state and never restored
|
||||||
|
// it, so the world drew without multisampling from the first UI frame
|
||||||
|
// on. Capturing here and restoring on Dispose keeps the GL backend's
|
||||||
|
// behaviour-preserving property true at this seam. Deleted at V4h
|
||||||
|
// once nothing raw-GL remains.
|
||||||
|
_ambientOnEntry = GlAmbientCapabilityState.Capture(_gl);
|
||||||
}
|
}
|
||||||
|
|
||||||
public GpuPassDescription Pass { get; }
|
public GpuPassDescription Pass { get; }
|
||||||
|
|
@ -213,6 +228,12 @@ internal sealed class GlGpuPassEncoder : IGpuPassEncoder
|
||||||
// rejected at BeginPass (V1 targets are single-sampled), and
|
// rejected at BeginPass (V1 targets are single-sampled), and
|
||||||
// Store/DontCare need no explicit action — the framebuffer's contents
|
// Store/DontCare need no explicit action — the framebuffer's contents
|
||||||
// simply persist until the next pass rebinds a target.
|
// simply persist until the next pass rebinds a target.
|
||||||
|
//
|
||||||
|
// Restore whatever capability state was ambient before this pass
|
||||||
|
// opened (see the constructor's comment) so a still-raw-GL renderer
|
||||||
|
// running immediately after this pass sees exactly what it would have
|
||||||
|
// seen had this pass never bound a pipeline.
|
||||||
|
_ambientOnEntry.Restore(_gl);
|
||||||
_frame.ClosePass(this);
|
_frame.ClosePass(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,3 +254,126 @@ internal sealed class GlGpuPassEncoder : IGpuPassEncoder
|
||||||
throw new ObjectDisposedException(nameof(GlGpuPassEncoder));
|
throw new ObjectDisposedException(nameof(GlGpuPassEncoder));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Every ambient GL capability/binding a <see cref="GlGpuPipeline"/> bind (or
|
||||||
|
/// a dynamic setter) can change, captured by raw query and restored by raw
|
||||||
|
/// call — the same field list <c>TextRenderGlStateScope</c> already restores
|
||||||
|
/// around <c>TextRenderer.Flush</c>, generalized to every
|
||||||
|
/// <see cref="GlGpuPassEncoder"/> so a renderer with no scope of its own
|
||||||
|
/// (<c>DebugLineRenderer</c> today) does not leak state forward into the next
|
||||||
|
/// raw-GL renderer either. Transitional for as long as raw-GL renderers
|
||||||
|
/// coexist with RHI-ported ones; deleted at V4h.
|
||||||
|
/// </summary>
|
||||||
|
internal readonly struct GlAmbientCapabilityState
|
||||||
|
{
|
||||||
|
private readonly int _program;
|
||||||
|
private readonly int _vertexArray;
|
||||||
|
private readonly int _arrayBuffer;
|
||||||
|
private readonly int _activeTexture;
|
||||||
|
private readonly int _texture0Binding2D;
|
||||||
|
private readonly bool _depthTest;
|
||||||
|
private readonly bool _depthWrite;
|
||||||
|
private readonly int _depthFunc;
|
||||||
|
private readonly bool _blend;
|
||||||
|
private readonly int _blendSourceRgb;
|
||||||
|
private readonly int _blendDestinationRgb;
|
||||||
|
private readonly int _blendSourceAlpha;
|
||||||
|
private readonly int _blendDestinationAlpha;
|
||||||
|
private readonly bool _cullFace;
|
||||||
|
private readonly int _cullFaceMode;
|
||||||
|
private readonly int _frontFace;
|
||||||
|
private readonly bool _alphaToCoverage;
|
||||||
|
private readonly bool _multisample;
|
||||||
|
|
||||||
|
private GlAmbientCapabilityState(
|
||||||
|
int program, int vertexArray, int arrayBuffer, int activeTexture, int texture0Binding2D,
|
||||||
|
bool depthTest, bool depthWrite, int depthFunc,
|
||||||
|
bool blend, int blendSourceRgb, int blendDestinationRgb, int blendSourceAlpha, int blendDestinationAlpha,
|
||||||
|
bool cullFace, int cullFaceMode, int frontFace,
|
||||||
|
bool alphaToCoverage, bool multisample)
|
||||||
|
{
|
||||||
|
_program = program;
|
||||||
|
_vertexArray = vertexArray;
|
||||||
|
_arrayBuffer = arrayBuffer;
|
||||||
|
_activeTexture = activeTexture;
|
||||||
|
_texture0Binding2D = texture0Binding2D;
|
||||||
|
_depthTest = depthTest;
|
||||||
|
_depthWrite = depthWrite;
|
||||||
|
_depthFunc = depthFunc;
|
||||||
|
_blend = blend;
|
||||||
|
_blendSourceRgb = blendSourceRgb;
|
||||||
|
_blendDestinationRgb = blendDestinationRgb;
|
||||||
|
_blendSourceAlpha = blendSourceAlpha;
|
||||||
|
_blendDestinationAlpha = blendDestinationAlpha;
|
||||||
|
_cullFace = cullFace;
|
||||||
|
_cullFaceMode = cullFaceMode;
|
||||||
|
_frontFace = frontFace;
|
||||||
|
_alphaToCoverage = alphaToCoverage;
|
||||||
|
_multisample = multisample;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static GlAmbientCapabilityState Capture(GL gl)
|
||||||
|
{
|
||||||
|
gl.GetInteger(GetPName.CurrentProgram, out int program);
|
||||||
|
gl.GetInteger(GetPName.VertexArrayBinding, out int vertexArray);
|
||||||
|
gl.GetInteger(GetPName.ArrayBufferBinding, out int arrayBuffer);
|
||||||
|
gl.GetInteger(GetPName.ActiveTexture, out int activeTexture);
|
||||||
|
int texture0Binding2D;
|
||||||
|
gl.ActiveTexture(TextureUnit.Texture0);
|
||||||
|
gl.GetInteger(GetPName.TextureBinding2D, out texture0Binding2D);
|
||||||
|
gl.ActiveTexture((TextureUnit)activeTexture);
|
||||||
|
|
||||||
|
gl.GetInteger(GetPName.DepthFunc, out int depthFunc);
|
||||||
|
gl.GetInteger(GetPName.BlendSrcRgb, out int blendSourceRgb);
|
||||||
|
gl.GetInteger(GetPName.BlendDstRgb, out int blendDestinationRgb);
|
||||||
|
gl.GetInteger(GetPName.BlendSrcAlpha, out int blendSourceAlpha);
|
||||||
|
gl.GetInteger(GetPName.BlendDstAlpha, out int blendDestinationAlpha);
|
||||||
|
gl.GetInteger(GetPName.CullFaceMode, out int cullFaceMode);
|
||||||
|
gl.GetInteger(GetPName.FrontFace, out int frontFace);
|
||||||
|
|
||||||
|
return new GlAmbientCapabilityState(
|
||||||
|
program, vertexArray, arrayBuffer, activeTexture, texture0Binding2D,
|
||||||
|
gl.IsEnabled(EnableCap.DepthTest), gl.GetBoolean(GetPName.DepthWritemask), depthFunc,
|
||||||
|
gl.IsEnabled(EnableCap.Blend), blendSourceRgb, blendDestinationRgb, blendSourceAlpha, blendDestinationAlpha,
|
||||||
|
gl.IsEnabled(EnableCap.CullFace), cullFaceMode, frontFace,
|
||||||
|
gl.IsEnabled(EnableCap.SampleAlphaToCoverage), gl.IsEnabled(EnableCap.Multisample));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Restore(GL gl)
|
||||||
|
{
|
||||||
|
gl.UseProgram((uint)_program);
|
||||||
|
gl.BindVertexArray((uint)_vertexArray);
|
||||||
|
gl.BindBuffer(BufferTargetARB.ArrayBuffer, (uint)_arrayBuffer);
|
||||||
|
|
||||||
|
gl.ActiveTexture(TextureUnit.Texture0);
|
||||||
|
gl.BindTexture(TextureTarget.Texture2D, (uint)_texture0Binding2D);
|
||||||
|
gl.ActiveTexture((TextureUnit)_activeTexture);
|
||||||
|
|
||||||
|
SetCapability(gl, EnableCap.DepthTest, _depthTest);
|
||||||
|
gl.DepthMask(_depthWrite);
|
||||||
|
gl.DepthFunc((DepthFunction)_depthFunc);
|
||||||
|
|
||||||
|
SetCapability(gl, EnableCap.Blend, _blend);
|
||||||
|
gl.BlendFuncSeparate(
|
||||||
|
(BlendingFactor)_blendSourceRgb,
|
||||||
|
(BlendingFactor)_blendDestinationRgb,
|
||||||
|
(BlendingFactor)_blendSourceAlpha,
|
||||||
|
(BlendingFactor)_blendDestinationAlpha);
|
||||||
|
|
||||||
|
SetCapability(gl, EnableCap.CullFace, _cullFace);
|
||||||
|
gl.CullFace((TriangleFace)_cullFaceMode);
|
||||||
|
gl.FrontFace((FrontFaceDirection)_frontFace);
|
||||||
|
|
||||||
|
SetCapability(gl, EnableCap.SampleAlphaToCoverage, _alphaToCoverage);
|
||||||
|
SetCapability(gl, EnableCap.Multisample, _multisample);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetCapability(GL gl, EnableCap capability, bool enabled)
|
||||||
|
{
|
||||||
|
if (enabled)
|
||||||
|
gl.Enable(capability);
|
||||||
|
else
|
||||||
|
gl.Disable(capability);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
50
src/AcDream.App/Rendering/GpuDeviceFrameLifetime.cs
Normal file
50
src/AcDream.App/Rendering/GpuDeviceFrameLifetime.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
using AcDream.App.Rendering.Gpu;
|
||||||
|
|
||||||
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Campaign V slice V4a: exposes whichever <see cref="IGpuFrame"/> is
|
||||||
|
/// currently open, so renderers ported onto the RHI (<see cref="TextRenderer"/>,
|
||||||
|
/// <see cref="DebugLineRenderer"/> today) can reach it without a stored
|
||||||
|
/// window/delegate back-reference. Non-null exactly between a
|
||||||
|
/// <see cref="GpuDeviceFrameLifetime.BeginFrame"/> and its matching
|
||||||
|
/// <see cref="GpuDeviceFrameLifetime.EndFrame"/>.
|
||||||
|
/// </summary>
|
||||||
|
internal interface ICurrentGpuFrameSource
|
||||||
|
{
|
||||||
|
IGpuFrame? CurrentFrame { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Drives <see cref="IGpuDevice.BeginFrame"/>/<see cref="IGpuFrame.End"/> once
|
||||||
|
/// per rendered frame, additively over the existing
|
||||||
|
/// <see cref="GpuFrameFlightController"/>-driven fence/slot bracket:
|
||||||
|
/// <see cref="GlGpuDevice"/>'s own <c>BeginFrame</c> already calls straight
|
||||||
|
/// through to the frame-flight controller it was constructed with (see its
|
||||||
|
/// class comment), so routing the production
|
||||||
|
/// <see cref="RenderFrameOrchestrator"/>'s <see cref="IRenderFrameLifetime"/>
|
||||||
|
/// through the device instead of the controller directly changes nothing about
|
||||||
|
/// the existing fence-wait/slot-rotation contract — it only additionally
|
||||||
|
/// yields the <see cref="IGpuFrame"/> that ported renderers need, which
|
||||||
|
/// <see cref="GpuFrameFlightController.BeginFrame"/> alone could not supply.
|
||||||
|
/// No clears move, no framebuffer binding changes, and the frame graph's
|
||||||
|
/// phase order is untouched.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class GpuDeviceFrameLifetime : IRenderFrameLifetime, ICurrentGpuFrameSource
|
||||||
|
{
|
||||||
|
private readonly IGpuDevice _device;
|
||||||
|
|
||||||
|
public GpuDeviceFrameLifetime(IGpuDevice device) =>
|
||||||
|
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||||
|
|
||||||
|
public IGpuFrame? CurrentFrame { get; private set; }
|
||||||
|
|
||||||
|
public void BeginFrame() => CurrentFrame = _device.BeginFrame();
|
||||||
|
|
||||||
|
public void EndFrame()
|
||||||
|
{
|
||||||
|
IGpuFrame? frame = CurrentFrame;
|
||||||
|
CurrentFrame = null;
|
||||||
|
frame?.End();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.Input;
|
using AcDream.App.Input;
|
||||||
|
using AcDream.App.Rendering.Gpu;
|
||||||
|
using AcDream.App.Rendering.Gpu.Gl;
|
||||||
using AcDream.App.UI;
|
using AcDream.App.UI;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
|
|
@ -191,13 +193,21 @@ internal sealed class RetailPaperdollFrameView : IPaperdollFrameView
|
||||||
{
|
{
|
||||||
private readonly UiViewport _viewport;
|
private readonly UiViewport _viewport;
|
||||||
private readonly IPaperdollInventoryVisibility _inventory;
|
private readonly IPaperdollInventoryVisibility _inventory;
|
||||||
|
private readonly GlGpuDevice _gpuDevice;
|
||||||
|
|
||||||
public RetailPaperdollFrameView(
|
public RetailPaperdollFrameView(
|
||||||
UiViewport viewport,
|
UiViewport viewport,
|
||||||
IPaperdollInventoryVisibility inventory)
|
IPaperdollInventoryVisibility inventory,
|
||||||
|
IGpuDevice gpuDevice)
|
||||||
{
|
{
|
||||||
_viewport = viewport ?? throw new ArgumentNullException(nameof(viewport));
|
_viewport = viewport ?? throw new ArgumentNullException(nameof(viewport));
|
||||||
_inventory = inventory ?? throw new ArgumentNullException(nameof(inventory));
|
_inventory = inventory ?? throw new ArgumentNullException(nameof(inventory));
|
||||||
|
// The paperdoll/appraisal viewport texture escape hatch (campaign doc
|
||||||
|
// §7.1) is GL-only; see GlGpuDevice.RegisterExternalColorTexture.
|
||||||
|
_gpuDevice = gpuDevice as GlGpuDevice
|
||||||
|
?? throw new ArgumentException(
|
||||||
|
"RetailPaperdollFrameView's viewport-texture registration is GL-only.",
|
||||||
|
nameof(gpuDevice));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetVisibleSize(out int width, out int height)
|
public bool TryGetVisibleSize(out int width, out int height)
|
||||||
|
|
@ -215,7 +225,7 @@ internal sealed class RetailPaperdollFrameView : IPaperdollFrameView
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetTextureHandle(uint textureHandle) =>
|
public void SetTextureHandle(uint textureHandle) =>
|
||||||
_viewport.TextureHandle = textureHandle;
|
_viewport.TextureSlot = _gpuDevice.RegisterExternalColorTexture(textureHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Narrow visibility adapter for the paperdoll's inventory host.</summary>
|
/// <summary>Narrow visibility adapter for the paperdoll's inventory host.</summary>
|
||||||
|
|
|
||||||
|
|
@ -28,15 +28,26 @@ public sealed record RenderStack(
|
||||||
AcDream.App.UI.UiDatFont? LargeDatFont) : System.IDisposable
|
AcDream.App.UI.UiDatFont? LargeDatFont) : System.IDisposable
|
||||||
{
|
{
|
||||||
internal GpuFrameFlightController FrameFlights { get; init; } = null!;
|
internal GpuFrameFlightController FrameFlights { get; init; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Campaign V slice V4a: the studio's own RHI device (mirrors
|
||||||
|
/// <see cref="HostInputCameraComposition"/>'s production one — the studio
|
||||||
|
/// composes its own render stack independently of GameWindow).
|
||||||
|
/// </summary>
|
||||||
|
internal AcDream.App.Rendering.Gpu.IGpuDevice GpuDevice { get; init; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Drives <see cref="AcDream.App.Rendering.Gpu.IGpuDevice.BeginFrame"/>/<c>IGpuFrame.End</c>
|
||||||
|
/// once per <see cref="BeginFrame"/>/<see cref="EndFrame"/> pair and
|
||||||
|
/// exposes the open frame to <see cref="UiHost"/>'s <see cref="TextRenderer"/>.
|
||||||
|
/// </summary>
|
||||||
|
internal GpuDeviceFrameLifetime FrameLifetime { get; init; } = null!;
|
||||||
|
|
||||||
private ResourceShutdownTransaction? _shutdown;
|
private ResourceShutdownTransaction? _shutdown;
|
||||||
|
|
||||||
internal void BeginFrame()
|
internal void BeginFrame() => FrameLifetime.BeginFrame();
|
||||||
{
|
|
||||||
FrameFlights.BeginFrame();
|
|
||||||
UiHost.TextRenderer.BeginFrame(FrameFlights.CurrentSlot);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void EndFrame() => FrameFlights.EndFrame();
|
internal void EndFrame() => FrameLifetime.EndFrame();
|
||||||
|
|
||||||
/// <summary>Dispose the GL pieces this stack OWNS (everything created in
|
/// <summary>Dispose the GL pieces this stack OWNS (everything created in
|
||||||
/// <see cref="RenderBootstrap.Create"/>). <see cref="Dats"/> + <see cref="Gl"/> are caller-owned
|
/// <see cref="RenderBootstrap.Create"/>). <see cref="Dats"/> + <see cref="Gl"/> are caller-owned
|
||||||
|
|
@ -62,6 +73,10 @@ public sealed record RenderStack(
|
||||||
new("mesh shader", MeshShader.Dispose),
|
new("mesh shader", MeshShader.Dispose),
|
||||||
new("lighting UBO", LightingUbo.Dispose),
|
new("lighting UBO", LightingUbo.Dispose),
|
||||||
new("UI host", UiHost.Dispose),
|
new("UI host", UiHost.Dispose),
|
||||||
|
// GpuDevice's own Dispose routes every resource release through
|
||||||
|
// FrameFlights as its retirement queue, so it must be disposed
|
||||||
|
// before FrameFlights below (see GlGpuDevice.Dispose's comment).
|
||||||
|
new("GPU device (RHI)", GpuDevice.Dispose),
|
||||||
]),
|
]),
|
||||||
new ResourceShutdownStage("frame flight owner",
|
new ResourceShutdownStage("frame flight owner",
|
||||||
[
|
[
|
||||||
|
|
@ -165,8 +180,14 @@ public static class RenderBootstrap
|
||||||
|
|
||||||
// --- TextureCache (GameWindow ~1774) ---
|
// --- TextureCache (GameWindow ~1774) ---
|
||||||
var frameFlights = new GpuFrameFlightController(gl);
|
var frameFlights = new GpuFrameFlightController(gl);
|
||||||
|
// Campaign V slice V4a: the studio composes its own RHI device
|
||||||
|
// independently of GameWindow/HostInputCameraComposition, mirroring
|
||||||
|
// that composition's construction (gl + frame flights + shaders dir).
|
||||||
|
Gpu.IGpuDevice gpuDevice = new Gpu.Gl.GlGpuDevice(gl, frameFlights, shaderDir);
|
||||||
|
var gpuFrameLifetime = new GpuDeviceFrameLifetime(gpuDevice);
|
||||||
var textureCache = new TextureCache(
|
var textureCache = new TextureCache(
|
||||||
gl,
|
gl,
|
||||||
|
gpuDevice,
|
||||||
dats,
|
dats,
|
||||||
bindless,
|
bindless,
|
||||||
frameFlights,
|
frameFlights,
|
||||||
|
|
@ -243,7 +264,7 @@ public static class RenderBootstrap
|
||||||
// --- UiHost (GameWindow ~1790); pass null for debugFont (only used as
|
// --- UiHost (GameWindow ~1790); pass null for debugFont (only used as
|
||||||
// a fallback BitmapFont for the world-space HUD — not needed for the
|
// a fallback BitmapFont for the world-space HUD — not needed for the
|
||||||
// UI Studio, and BitmapFont requires a system font byte array) ---
|
// UI Studio, and BitmapFont requires a system font byte array) ---
|
||||||
var uiHost = new AcDream.App.UI.UiHost(gl, shaderDir, defaultFont: null);
|
var uiHost = new AcDream.App.UI.UiHost(gpuDevice, gpuFrameLifetime, shaderDir, defaultFont: null);
|
||||||
|
|
||||||
var stack = new RenderStack(
|
var stack = new RenderStack(
|
||||||
Gl: gl,
|
Gl: gl,
|
||||||
|
|
@ -261,6 +282,8 @@ public static class RenderBootstrap
|
||||||
LargeDatFont: largeDatFont)
|
LargeDatFont: largeDatFont)
|
||||||
{
|
{
|
||||||
FrameFlights = frameFlights,
|
FrameFlights = frameFlights,
|
||||||
|
GpuDevice = gpuDevice,
|
||||||
|
FrameLifetime = gpuFrameLifetime,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pre-seed the font cache with the two already-uploaded atlas instances
|
// Pre-seed the font cache with the two already-uploaded atlas instances
|
||||||
|
|
|
||||||
|
|
@ -88,8 +88,6 @@ internal sealed class RuntimeRenderFrameBeginResources : IRenderFrameBeginResour
|
||||||
private readonly WbDrawDispatcher? _dispatcher;
|
private readonly WbDrawDispatcher? _dispatcher;
|
||||||
private readonly EnvCellRenderer? _environmentCells;
|
private readonly EnvCellRenderer? _environmentCells;
|
||||||
private readonly PortalDepthMaskRenderer? _portalDepth;
|
private readonly PortalDepthMaskRenderer? _portalDepth;
|
||||||
private readonly TextRenderer? _worldText;
|
|
||||||
private readonly TextRenderer? _uiText;
|
|
||||||
private readonly ClipFrame? _clip;
|
private readonly ClipFrame? _clip;
|
||||||
private readonly TerrainModernRenderer? _terrain;
|
private readonly TerrainModernRenderer? _terrain;
|
||||||
private readonly SceneLightingUboBinding? _lighting;
|
private readonly SceneLightingUboBinding? _lighting;
|
||||||
|
|
@ -98,8 +96,6 @@ internal sealed class RuntimeRenderFrameBeginResources : IRenderFrameBeginResour
|
||||||
WbDrawDispatcher? dispatcher,
|
WbDrawDispatcher? dispatcher,
|
||||||
EnvCellRenderer? environmentCells,
|
EnvCellRenderer? environmentCells,
|
||||||
PortalDepthMaskRenderer? portalDepth,
|
PortalDepthMaskRenderer? portalDepth,
|
||||||
TextRenderer? worldText,
|
|
||||||
TextRenderer? uiText,
|
|
||||||
ClipFrame? clip,
|
ClipFrame? clip,
|
||||||
TerrainModernRenderer? terrain,
|
TerrainModernRenderer? terrain,
|
||||||
SceneLightingUboBinding? lighting)
|
SceneLightingUboBinding? lighting)
|
||||||
|
|
@ -108,8 +104,6 @@ internal sealed class RuntimeRenderFrameBeginResources : IRenderFrameBeginResour
|
||||||
_dispatcher = dispatcher;
|
_dispatcher = dispatcher;
|
||||||
_environmentCells = environmentCells;
|
_environmentCells = environmentCells;
|
||||||
_portalDepth = portalDepth;
|
_portalDepth = portalDepth;
|
||||||
_worldText = worldText;
|
|
||||||
_uiText = uiText;
|
|
||||||
_clip = clip;
|
_clip = clip;
|
||||||
_terrain = terrain;
|
_terrain = terrain;
|
||||||
_lighting = lighting;
|
_lighting = lighting;
|
||||||
|
|
@ -122,8 +116,10 @@ internal sealed class RuntimeRenderFrameBeginResources : IRenderFrameBeginResour
|
||||||
_dispatcher?.BeginFrame(gpuSlot);
|
_dispatcher?.BeginFrame(gpuSlot);
|
||||||
_environmentCells?.BeginFrame(gpuSlot);
|
_environmentCells?.BeginFrame(gpuSlot);
|
||||||
_portalDepth?.BeginFrame(gpuSlot);
|
_portalDepth?.BeginFrame(gpuSlot);
|
||||||
_worldText?.BeginFrame(gpuSlot);
|
// The world-HUD and retained-UI TextRenderers no longer take a
|
||||||
_uiText?.BeginFrame(gpuSlot);
|
// per-slot begin: Campaign V slice V4a ported them onto the shared
|
||||||
|
// IGpuFrame ring (see GpuDeviceFrameLifetime), which is reset once
|
||||||
|
// per frame by IGpuDevice.BeginFrame() rather than per renderer.
|
||||||
_clip?.BeginFrame(gpuSlot);
|
_clip?.BeginFrame(gpuSlot);
|
||||||
_terrain?.BeginFrame(gpuSlot);
|
_terrain?.BeginFrame(gpuSlot);
|
||||||
_lighting?.BeginFrame(gpuSlot);
|
_lighting?.BeginFrame(gpuSlot);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Gpu;
|
||||||
|
using AcDream.App.Rendering.Gpu.Gl;
|
||||||
using Silk.NET.OpenGL;
|
using Silk.NET.OpenGL;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
@ -15,53 +14,82 @@ namespace AcDream.App.Rendering;
|
||||||
/// <see cref="Begin"/> at the start of a HUD pass, queue geometry via
|
/// <see cref="Begin"/> at the start of a HUD pass, queue geometry via
|
||||||
/// <see cref="DrawString"/> / <see cref="DrawRect"/>, then <see cref="Flush"/>.
|
/// <see cref="DrawString"/> / <see cref="DrawRect"/>, then <see cref="Flush"/>.
|
||||||
///
|
///
|
||||||
/// Uses two internal vertex buffers (text and rect) flushed in two draw calls
|
/// Campaign V slice V4a: the <c>ui_text</c> shader compiles through
|
||||||
/// to avoid a per-vertex "use texture" flag. Rects are drawn first so text
|
/// <see cref="IGpuDevice.CreatePipeline"/> (one <see cref="IGpuPipeline"/>,
|
||||||
/// sits on top of background panels.
|
/// replacing the old hand-rolled <c>Shader</c> class), its three
|
||||||
|
/// fence-buffered per-flight VBOs are gone in favour of a per-<see cref="IGpuFrame"/>
|
||||||
|
/// ring allocation per draw bucket, and the 1×1 white fill texture is created
|
||||||
|
/// and uploaded through <see cref="IGpuDevice.CreateTexture"/> and registered
|
||||||
|
/// into the device's texture table.
|
||||||
|
///
|
||||||
|
/// <see cref="IGpuPassEncoder"/> has no verb for classic texture-unit binding
|
||||||
|
/// (every acdream RHI pass samples through the bindless texture table) but
|
||||||
|
/// <see cref="DrawSprite"/> receives an ARBITRARY externally-owned raw GL
|
||||||
|
/// texture name from dozens of UI call sites that are not part of this slice
|
||||||
|
/// (icons, dat chrome, composited item art) — converting that whole surface
|
||||||
|
/// to slot-based sampling is out of scope here. So sprite/font texture
|
||||||
|
/// binding stays classic (<c>glActiveTexture</c>/<c>glBindTexture</c>,
|
||||||
|
/// <c>ui_text.frag</c>'s <c>uTex</c> sampler unchanged) issued directly against
|
||||||
|
/// the GL handle this class keeps for that reason, while the shader program
|
||||||
|
/// itself, its blend/depth/cull description, and every per-frame vertex
|
||||||
|
/// upload now go through the RHI. This mirrors the same GL-only escape hatch
|
||||||
|
/// the campaign's V2 note already documents for the interim bindless handle
|
||||||
|
/// table, and is retired only when a later slice moves ALL of TextRenderer's
|
||||||
|
/// texture consumers onto registered slots.
|
||||||
|
///
|
||||||
|
/// Uses per-bucket ring allocations flushed in up to three draw calls per
|
||||||
|
/// layer, to avoid a per-vertex "use texture" flag. Rects are drawn first so
|
||||||
|
/// text sits on top of background panels.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed unsafe class TextRenderer : IDisposable
|
public sealed unsafe class TextRenderer : IDisposable
|
||||||
{
|
{
|
||||||
private const int FloatsPerVertex = 8; // pos(2) + uv(2) + color(4)
|
private const int FloatsPerVertex = 8; // pos(2) + uv(2) + color(4)
|
||||||
|
private const int VertexStrideBytes = FloatsPerVertex * sizeof(float);
|
||||||
|
|
||||||
|
private static readonly GpuVertexLayout SpriteVertexLayout = new(
|
||||||
|
StrideBytes: VertexStrideBytes,
|
||||||
|
[
|
||||||
|
new GpuVertexAttribute(0, GpuVertexFormat.Float2, 0),
|
||||||
|
new GpuVertexAttribute(1, GpuVertexFormat.Float2, 8),
|
||||||
|
new GpuVertexAttribute(2, GpuVertexFormat.Float4, 16),
|
||||||
|
]);
|
||||||
|
|
||||||
|
private readonly IGpuDevice _device;
|
||||||
|
private readonly GlGpuDevice _glDevice;
|
||||||
|
private readonly ICurrentGpuFrameSource _frameSource;
|
||||||
private readonly GL _gl;
|
private readonly GL _gl;
|
||||||
private readonly ITextRenderGlStateApi _glState;
|
private readonly ITextRenderGlStateApi _glState;
|
||||||
private readonly Shader _shader;
|
private readonly IGpuPipeline _pipeline;
|
||||||
private readonly ResourceCleanupGroup _resources;
|
private readonly IGpuTexture _whiteTexture;
|
||||||
private uint _vao;
|
|
||||||
private uint _vbo;
|
|
||||||
private readonly uint _whiteTex; // 1×1 white, for solid fills routed through the sprite bucket
|
private readonly uint _whiteTex; // 1×1 white, for solid fills routed through the sprite bucket
|
||||||
private int _vboCapacityBytes;
|
private readonly int _uScreenSizeLocation;
|
||||||
|
private readonly int _uUseTextureLocation;
|
||||||
|
|
||||||
private sealed class FrameBufferSet
|
private sealed class SpriteSeg { public uint Texture; public readonly List<float> Verts = new(256); }
|
||||||
{
|
|
||||||
public uint Vao;
|
|
||||||
public uint Vbo;
|
|
||||||
public int CapacityBytes;
|
|
||||||
public int UsedBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly FrameBufferSet[] _frameBuffers;
|
|
||||||
private FrameBufferSet? _activeFrameBuffer;
|
|
||||||
|
|
||||||
internal long DynamicBufferCapacityBytes =>
|
|
||||||
_frameBuffers.Sum(set => (long)set.CapacityBytes);
|
|
||||||
|
|
||||||
private readonly List<float> _textBuf = new(8192);
|
|
||||||
private readonly List<float> _rectBuf = new(1024);
|
|
||||||
// Submission-ordered sprite segments: consecutive DrawSprite calls with the
|
// Submission-ordered sprite segments: consecutive DrawSprite calls with the
|
||||||
// SAME texture batch into one segment; a texture change starts a new segment.
|
// SAME texture batch into one segment; a texture change starts a new segment.
|
||||||
// Drawing segments in submission order preserves painter z-order for
|
// Drawing segments in submission order preserves painter z-order for
|
||||||
// sprite-on-sprite UI. (The old per-texture dictionary drew a REUSED texture
|
// sprite-on-sprite UI. (The old per-texture dictionary drew a REUSED texture
|
||||||
// at its FIRST-insertion point, so later bar sprites covered glyphs emitted
|
// at its FIRST-insertion point, so later bar sprites covered glyphs emitted
|
||||||
// earlier via the shared dat-font atlas — the stamina/mana numbers vanished.)
|
// earlier via the shared dat-font atlas — the stamina/mana numbers vanished.)
|
||||||
private sealed class SpriteSeg { public uint Texture; public readonly List<float> Verts = new(256); }
|
private readonly List<float> _textBuf = new(8192);
|
||||||
|
private readonly List<float> _rectBuf = new(1024);
|
||||||
private readonly List<SpriteSeg> _spriteSegs = new();
|
private readonly List<SpriteSeg> _spriteSegs = new();
|
||||||
private int _segUsed;
|
private int _segUsed;
|
||||||
private int _textVerts;
|
private int _textVerts;
|
||||||
private int _rectVerts;
|
private int _rectVerts;
|
||||||
private Vector2 _screenSize;
|
private Vector2 _screenSize;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// No longer meaningful post-V4a: per-frame vertex data comes from the
|
||||||
|
/// device's shared per-flight ring rather than a VBO this class owns. Kept
|
||||||
|
/// (returning 0) so <see cref="RenderFrameDiagnosticSources"/>'s telemetry
|
||||||
|
/// read still compiles; the dynamic-buffer dimension it reported is now a
|
||||||
|
/// device-wide, not a per-renderer, concern.
|
||||||
|
/// </summary>
|
||||||
|
internal long DynamicBufferCapacityBytes => 0;
|
||||||
|
|
||||||
// Overlay layer — a parallel set of buckets drawn AFTER the normal sprite/rect/text
|
// Overlay layer — a parallel set of buckets drawn AFTER the normal sprite/rect/text
|
||||||
// buckets, so open popups/menus composite on top of EVERYTHING, including translucent
|
// buckets, so open popups/menus composite on top of EVERYTHING, including translucent
|
||||||
// rect panel backgrounds (which otherwise always win because rects flush after
|
// rect panel backgrounds (which otherwise always win because rects flush after
|
||||||
|
|
@ -77,142 +105,89 @@ public sealed unsafe class TextRenderer : IDisposable
|
||||||
/// of all normal-layer geometry). Set by the UI root around the popup/overlay pass.</summary>
|
/// of all normal-layer geometry). Set by the UI root around the popup/overlay pass.</summary>
|
||||||
public bool OverlayMode { get; set; }
|
public bool OverlayMode { get; set; }
|
||||||
|
|
||||||
public TextRenderer(GL gl, string shaderDir)
|
// internal, not public: IGpuDevice/ICurrentGpuFrameSource are internal
|
||||||
|
// types (the pinned RHI contract). The TextRenderer TYPE stays public —
|
||||||
|
// only construction is restricted — so existing public members that hold
|
||||||
|
// or return a TextRenderer (e.g. UiHost.TextRenderer) need no visibility
|
||||||
|
// change of their own.
|
||||||
|
internal TextRenderer(IGpuDevice device, ICurrentGpuFrameSource frameSource, string shaderDir)
|
||||||
{
|
{
|
||||||
_gl = gl;
|
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||||
_glState = new SilkTextRenderGlStateApi(gl);
|
_frameSource = frameSource ?? throw new ArgumentNullException(nameof(frameSource));
|
||||||
var resources = new ResourceCleanupGroup();
|
if (device is not GlGpuDevice glDevice)
|
||||||
Shader? shader = null;
|
{
|
||||||
var frameBuffers = new FrameBufferSet[3];
|
throw new NotSupportedException(
|
||||||
uint whiteTexture = 0;
|
"TextRenderer's classic sprite/font texture-unit binding path (see the class " +
|
||||||
|
"remarks) is GL-only; it needs the raw GL handle GlGpuDevice exposes. Other " +
|
||||||
|
"backends are out of scope until a later slice removes that classic path.");
|
||||||
|
}
|
||||||
|
_glDevice = glDevice;
|
||||||
|
_gl = glDevice.Gl;
|
||||||
|
_glState = new SilkTextRenderGlStateApi(_gl);
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(shaderDir);
|
||||||
|
|
||||||
|
IGpuPipeline? pipeline = null;
|
||||||
|
IGpuTexture? whiteTexture = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
shader = new Shader(gl,
|
pipeline = device.CreatePipeline(new GpuPipelineDescription
|
||||||
Path.Combine(shaderDir, "ui_text.vert"),
|
{
|
||||||
Path.Combine(shaderDir, "ui_text.frag"));
|
Name = "ui-text",
|
||||||
resources.Add("text shader", shader.Dispose);
|
Shaders = new GpuShaderSet("ui_text"),
|
||||||
|
VertexLayout = SpriteVertexLayout,
|
||||||
for (int i = 0; i < frameBuffers.Length; i++)
|
Topology = GpuPrimitiveTopology.TriangleList,
|
||||||
frameBuffers[i] = CreateFrameBufferSet(resources);
|
Blend = GpuBlendMode.StraightAlpha,
|
||||||
|
// The retained UI is a self-contained 2-D pass — depth is
|
||||||
|
// irrelevant and the world pass's alpha-to-coverage state must
|
||||||
|
// not leak in (feedback_render_self_contained_gl_state). Flush
|
||||||
|
// below still asserts this by hand via raw GL calls (and
|
||||||
|
// GL_MULTISAMPLE, which has no representation here), matching
|
||||||
|
// what TextRenderGlStateScope has always restored on exit.
|
||||||
|
Depth = GpuDepthState.Disabled,
|
||||||
|
Cull = GpuCullMode.None,
|
||||||
|
AlphaToCoverage = false,
|
||||||
|
ColorWrite = true,
|
||||||
|
SampleCount = 1,
|
||||||
|
});
|
||||||
|
|
||||||
// 1×1 white texture so DrawFill can route solid-colour quads through the SPRITE
|
// 1×1 white texture so DrawFill can route solid-colour quads through the SPRITE
|
||||||
// bucket (the shader multiplies texel×color → white×color = color). Lets a panel
|
// bucket (the shader multiplies texel×color → white×color = color). Lets a panel
|
||||||
// background draw UNDER its text in painter order, which DrawRect's separate
|
// background draw UNDER its text in painter order, which DrawRect's separate
|
||||||
// bucket cannot (it always composites after all sprites).
|
// bucket cannot (it always composites after all sprites).
|
||||||
whiteTexture = GlResourceCommand.CreateTexture(
|
whiteTexture = device.CreateTexture(new GpuTextureDescription(
|
||||||
_gl,
|
"ui-text-white",
|
||||||
"TextRenderer white texture");
|
GpuTextureKind.Texture2D,
|
||||||
uint ownedWhiteTexture = whiteTexture;
|
GpuTextureFormat.Rgba8Unorm,
|
||||||
resources.Add(
|
Width: 1,
|
||||||
"white texture",
|
Height: 1,
|
||||||
() => GlResourceCommand.DeleteTexture(
|
LayerCount: 1,
|
||||||
_gl,
|
MipLevelCount: 1));
|
||||||
ownedWhiteTexture,
|
whiteTexture.Upload(0, 0, [255, 255, 255, 255]);
|
||||||
$"delete TextRenderer white texture {ownedWhiteTexture}"));
|
IGpuSampler whiteSampler = device.CreateSampler(GpuSamplerDescription.UiNearest);
|
||||||
GlResourceCommand.Execute(
|
device.RegisterTexture(whiteTexture, whiteSampler);
|
||||||
_gl,
|
|
||||||
"initialize TextRenderer white texture",
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
_gl.BindTexture(TextureTarget.Texture2D, whiteTexture);
|
|
||||||
Span<byte> whitePixel = stackalloc byte[] { 255, 255, 255, 255 };
|
|
||||||
fixed (byte* wp = whitePixel)
|
|
||||||
_gl.TexImage2D(TextureTarget.Texture2D, 0, (int)InternalFormat.Rgba8, 1, 1, 0,
|
|
||||||
PixelFormat.Rgba, PixelType.UnsignedByte, wp);
|
|
||||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
|
|
||||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMinFilter.Nearest);
|
|
||||||
_gl.BindTexture(TextureTarget.Texture2D, 0);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (Exception constructionFailure)
|
catch
|
||||||
{
|
{
|
||||||
try
|
whiteTexture?.Dispose();
|
||||||
{
|
pipeline?.Dispose();
|
||||||
resources.RetryCleanup();
|
|
||||||
}
|
|
||||||
catch (Exception cleanupFailure)
|
|
||||||
{
|
|
||||||
throw new GlResourceConstructionException(
|
|
||||||
"TextRenderer construction failed and its published GL resources did not cleanly roll back.",
|
|
||||||
resources,
|
|
||||||
[constructionFailure, cleanupFailure]);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
_resources = resources;
|
_pipeline = pipeline;
|
||||||
_shader = shader;
|
_whiteTexture = whiteTexture;
|
||||||
_frameBuffers = frameBuffers;
|
_whiteTex = ((GlGpuTexture)whiteTexture).GlName;
|
||||||
_whiteTex = whiteTexture;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
uint program = ((GlGpuPipeline)_pipeline).GlProgram;
|
||||||
/// Selects the GPU-fenced frame slot and resets its append cursor. Every
|
_uScreenSizeLocation = _gl.GetUniformLocation(program, "uScreenSize");
|
||||||
/// UI segment rendered during the frame receives a distinct byte range;
|
_uUseTextureLocation = _gl.GetUniformLocation(program, "uUseTexture");
|
||||||
/// later text or sprite batches cannot overwrite an earlier in-flight draw.
|
// uTex (the sampler unit) never changes — bind it once rather than on every Flush.
|
||||||
/// </summary>
|
int texLocation = _gl.GetUniformLocation(program, "uTex");
|
||||||
public void BeginFrame(int frameSlot)
|
if (texLocation >= 0)
|
||||||
{
|
{
|
||||||
if ((uint)frameSlot >= (uint)_frameBuffers.Length)
|
_gl.UseProgram(program);
|
||||||
throw new ArgumentOutOfRangeException(nameof(frameSlot));
|
_gl.Uniform1(texLocation, 0);
|
||||||
|
_gl.UseProgram(0);
|
||||||
FrameBufferSet set = _frameBuffers[frameSlot];
|
}
|
||||||
set.UsedBytes = 0;
|
|
||||||
_activeFrameBuffer = set;
|
|
||||||
_vao = set.Vao;
|
|
||||||
_vbo = set.Vbo;
|
|
||||||
_vboCapacityBytes = set.CapacityBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
private FrameBufferSet CreateFrameBufferSet(ResourceCleanupGroup resources)
|
|
||||||
{
|
|
||||||
uint vao = TrackedGlResource.CreateVertexArray(
|
|
||||||
_gl,
|
|
||||||
"TextRenderer frame VAO creation");
|
|
||||||
RetryableGpuResourceRelease vaoRelease =
|
|
||||||
TrackedGlResource.CreateRetryableVertexArrayDeletion(
|
|
||||||
_gl,
|
|
||||||
vao,
|
|
||||||
"TextRenderer frame VAO disposal");
|
|
||||||
resources.Add("frame VAO", vaoRelease.Run);
|
|
||||||
var set = new FrameBufferSet { Vao = vao };
|
|
||||||
|
|
||||||
uint vbo = TrackedGlResource.CreateBuffer(
|
|
||||||
_gl,
|
|
||||||
"TextRenderer frame VBO creation");
|
|
||||||
set.Vbo = vbo;
|
|
||||||
RetryableGpuResourceRelease? vboRelease = null;
|
|
||||||
resources.Add(
|
|
||||||
"frame VBO",
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
vboRelease ??= TrackedGlResource.CreateRetryableBufferDeletion(
|
|
||||||
_gl,
|
|
||||||
vbo,
|
|
||||||
set.CapacityBytes,
|
|
||||||
"TextRenderer frame VBO disposal");
|
|
||||||
vboRelease.Run();
|
|
||||||
});
|
|
||||||
|
|
||||||
GlResourceCommand.Execute(
|
|
||||||
_gl,
|
|
||||||
"initialize TextRenderer frame VAO and VBO",
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
_gl.BindVertexArray(set.Vao);
|
|
||||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, set.Vbo);
|
|
||||||
uint stride = FloatsPerVertex * sizeof(float);
|
|
||||||
_gl.EnableVertexAttribArray(0);
|
|
||||||
_gl.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, stride, (void*)0);
|
|
||||||
_gl.EnableVertexAttribArray(1);
|
|
||||||
_gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, stride, (void*)(2 * sizeof(float)));
|
|
||||||
_gl.EnableVertexAttribArray(2);
|
|
||||||
_gl.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, stride, (void*)(4 * sizeof(float)));
|
|
||||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
|
||||||
_gl.BindVertexArray(0);
|
|
||||||
});
|
|
||||||
return set;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Begin a HUD pass. Call once per frame before any Draw* calls.</summary>
|
/// <summary>Begin a HUD pass. Call once per frame before any Draw* calls.</summary>
|
||||||
|
|
@ -355,6 +330,16 @@ public sealed unsafe class TextRenderer : IDisposable
|
||||||
AppendQuad(seg.Verts, x, y, w, h, u0, v0, u1, v1, tint);
|
AppendQuad(seg.Verts, x, y, w, h, u0, v0, u1, v1, tint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves a <see cref="GpuTextureSlot"/> produced by the paperdoll/appraisal
|
||||||
|
/// viewport transitional seam (<c>GlGpuDevice.RegisterExternalColorTexture</c>,
|
||||||
|
/// campaign doc §7.1) back to the raw GL texture name <see cref="DrawSprite"/>
|
||||||
|
/// needs. Returns 0 (no texture) for an unassigned slot. GL-only; removed
|
||||||
|
/// at V4g alongside the seam it resolves.
|
||||||
|
/// </summary>
|
||||||
|
internal uint ResolveExternalTextureSlot(GpuTextureSlot slot) =>
|
||||||
|
_glDevice.TryResolveExternalColorTexture(slot, out uint name) ? name : 0;
|
||||||
|
|
||||||
/// <summary>Pick the sprite segment for <paramref name="texture"/>: extend the current
|
/// <summary>Pick the sprite segment for <paramref name="texture"/>: extend the current
|
||||||
/// same-texture run, else reuse a pooled segment, else allocate. Submission order is
|
/// same-texture run, else reuse a pooled segment, else allocate. Submission order is
|
||||||
/// preserved (painter z-order for sprite-on-sprite UI).</summary>
|
/// preserved (painter z-order for sprite-on-sprite UI).</summary>
|
||||||
|
|
@ -409,17 +394,35 @@ public sealed unsafe class TextRenderer : IDisposable
|
||||||
bool anyOverlay = _overlaySegUsed > 0 || _overlayTextVerts > 0 || _overlayRectVerts > 0;
|
bool anyOverlay = _overlaySegUsed > 0 || _overlayTextVerts > 0 || _overlayRectVerts > 0;
|
||||||
if (!anyNormal && !anyOverlay) return;
|
if (!anyNormal && !anyOverlay) return;
|
||||||
|
|
||||||
|
IGpuFrame frame = _frameSource.CurrentFrame
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"TextRenderer.Flush requires an open IGpuFrame (see GpuDeviceFrameLifetime) — " +
|
||||||
|
"the host must drive IGpuDevice.BeginFrame() before rendering the retained UI.");
|
||||||
|
|
||||||
// Retained UI is a private render pass: an upload or draw failure must
|
// Retained UI is a private render pass: an upload or draw failure must
|
||||||
// not leak its depth/cull/blend/MSAA state into a later recoverable
|
// not leak its depth/cull/blend/MSAA state into a later recoverable
|
||||||
// frame. The focused scope restores from Flush's generated finally,
|
// frame. The focused scope restores from Flush's generated finally,
|
||||||
// including when either DrawLayer call throws.
|
// including when either DrawLayer call throws.
|
||||||
using var stateScope = new TextRenderGlStateScope(_glState);
|
using var stateScope = new TextRenderGlStateScope(_glState);
|
||||||
|
|
||||||
_shader.Use();
|
using IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription
|
||||||
_shader.SetVec2("uScreenSize", _screenSize);
|
{
|
||||||
|
Name = "ui-text",
|
||||||
_gl.BindVertexArray(_vao);
|
// GL has no framebuffer-implicit "pass" of its own; this slice's
|
||||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
|
// transitional shape (campaign doc §4's GpuPassDescription remarks)
|
||||||
|
// opens a Load/Store pass against the backbuffer so clears and
|
||||||
|
// framebuffer management stay owned by the frame spine, exactly as
|
||||||
|
// today, while this renderer records through the encoder.
|
||||||
|
Color = new GpuColorAttachment(
|
||||||
|
Target: null,
|
||||||
|
Load: GpuLoadOp.Load,
|
||||||
|
Store: GpuStoreOp.Store,
|
||||||
|
ClearColor: default),
|
||||||
|
Depth = null,
|
||||||
|
SampleCount = 1,
|
||||||
|
});
|
||||||
|
encoder.BindPipeline(_pipeline);
|
||||||
|
_gl.Uniform2(_uScreenSizeLocation, _screenSize.X, _screenSize.Y);
|
||||||
|
|
||||||
// Establish the self-contained UI pass state.
|
// Establish the self-contained UI pass state.
|
||||||
// The world pass leaves alpha-to-coverage + multisample enabled (WbDrawDispatcher,
|
// The world pass leaves alpha-to-coverage + multisample enabled (WbDrawDispatcher,
|
||||||
|
|
@ -443,9 +446,8 @@ public sealed unsafe class TextRenderer : IDisposable
|
||||||
// so sprite-on-sprite z is preserved. Buckets 2 (rects) + 3 (debug text)
|
// so sprite-on-sprite z is preserved. Buckets 2 (rects) + 3 (debug text)
|
||||||
// composite on top, in that order. The OVERLAY layer repeats all three
|
// composite on top, in that order. The OVERLAY layer repeats all three
|
||||||
// AFTER the normal layer, so open popups beat even the rect backgrounds.
|
// AFTER the normal layer, so open popups beat even the rect backgrounds.
|
||||||
DrawLayer(_spriteSegs, _segUsed, _rectBuf, _rectVerts, _textBuf, _textVerts, font);
|
DrawLayer(_spriteSegs, _segUsed, _rectBuf, _rectVerts, _textBuf, _textVerts, font, frame, encoder);
|
||||||
DrawLayer(_overlaySpriteSegs, _overlaySegUsed, _overlayRectBuf, _overlayRectVerts, _overlayTextBuf, _overlayTextVerts, font);
|
DrawLayer(_overlaySpriteSegs, _overlaySegUsed, _overlayRectBuf, _overlayRectVerts, _overlayTextBuf, _overlayTextVerts, font, frame, encoder);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Draw one compositing layer: sprites (submission order, one call per
|
/// <summary>Draw one compositing layer: sprites (submission order, one call per
|
||||||
|
|
@ -454,79 +456,66 @@ public sealed unsafe class TextRenderer : IDisposable
|
||||||
private void DrawLayer(
|
private void DrawLayer(
|
||||||
List<SpriteSeg> spriteSegs, int segUsed,
|
List<SpriteSeg> spriteSegs, int segUsed,
|
||||||
List<float> rectBuf, int rectVerts,
|
List<float> rectBuf, int rectVerts,
|
||||||
List<float> textBuf, int textVerts, BitmapFont? font)
|
List<float> textBuf, int textVerts, BitmapFont? font,
|
||||||
|
IGpuFrame frame, IGpuPassEncoder encoder)
|
||||||
{
|
{
|
||||||
// 1. RGBA dat sprites — one draw call per distinct GL texture.
|
// 1. RGBA dat sprites — one draw call per distinct GL texture.
|
||||||
if (segUsed > 0)
|
if (segUsed > 0)
|
||||||
{
|
{
|
||||||
_shader.SetInt("uUseTexture", 2);
|
SetUseTexture(2);
|
||||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
_gl.ActiveTexture(TextureUnit.Texture0);
|
||||||
_shader.SetInt("uTex", 0);
|
|
||||||
for (int i = 0; i < segUsed; i++)
|
for (int i = 0; i < segUsed; i++)
|
||||||
{
|
{
|
||||||
var seg = spriteSegs[i];
|
var seg = spriteSegs[i];
|
||||||
if (seg.Verts.Count == 0) continue;
|
if (seg.Verts.Count == 0) continue;
|
||||||
_gl.BindTexture(TextureTarget.Texture2D, seg.Texture);
|
_gl.BindTexture(TextureTarget.Texture2D, seg.Texture);
|
||||||
int firstVertex = UploadBuffer(seg.Verts);
|
DrawRing(frame, encoder, seg.Verts);
|
||||||
_gl.DrawArrays(PrimitiveType.Triangles, firstVertex, (uint)(seg.Verts.Count / FloatsPerVertex));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Untextured rects — widget fills on top of the chrome.
|
// 2. Untextured rects — widget fills on top of the chrome.
|
||||||
if (rectVerts > 0)
|
if (rectVerts > 0)
|
||||||
{
|
{
|
||||||
_shader.SetInt("uUseTexture", 0);
|
SetUseTexture(0);
|
||||||
int firstVertex = UploadBuffer(rectBuf);
|
DrawRing(frame, encoder, rectBuf);
|
||||||
_gl.DrawArrays(PrimitiveType.Triangles, firstVertex, (uint)rectVerts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Textured debug-font text glyphs on top.
|
// 3. Textured debug-font text glyphs on top.
|
||||||
if (textVerts > 0 && font is not null)
|
if (textVerts > 0 && font is not null)
|
||||||
{
|
{
|
||||||
_shader.SetInt("uUseTexture", 1);
|
SetUseTexture(1);
|
||||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
_gl.ActiveTexture(TextureUnit.Texture0);
|
||||||
_gl.BindTexture(TextureTarget.Texture2D, font.TextureId);
|
_gl.BindTexture(TextureTarget.Texture2D, font.TextureId);
|
||||||
_shader.SetInt("uTex", 0);
|
DrawRing(frame, encoder, textBuf);
|
||||||
int firstVertex = UploadBuffer(textBuf);
|
|
||||||
_gl.DrawArrays(PrimitiveType.Triangles, firstVertex, (uint)textVerts);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int UploadBuffer(List<float> buf)
|
private void SetUseTexture(int mode)
|
||||||
{
|
{
|
||||||
int bytes = buf.Count * sizeof(float);
|
if (_uUseTextureLocation >= 0)
|
||||||
if (bytes == 0) return 0;
|
_gl.Uniform1(_uUseTextureLocation, mode);
|
||||||
FrameBufferSet set = _activeFrameBuffer
|
}
|
||||||
?? throw new InvalidOperationException("BeginFrame must be called before rendering text.");
|
|
||||||
int byteOffset = set.UsedBytes;
|
|
||||||
int requiredBytes = checked(byteOffset + bytes);
|
|
||||||
|
|
||||||
if (requiredBytes > _vboCapacityBytes)
|
/// <summary>
|
||||||
{
|
/// Allocates a ring range from the current frame, copies <paramref name="buf"/>
|
||||||
int newCapacity = DynamicBufferCapacity.Grow(
|
/// into it, and issues one non-indexed draw. Replaces the old growable
|
||||||
_vboCapacityBytes,
|
/// per-flight VBO + <c>BufferSubData</c> pattern: every UI vertex upload is
|
||||||
requiredBytes);
|
/// now the frame's shared ring, reset once per frame by
|
||||||
TrackedGlResource.AllocateBufferStorage(
|
/// <see cref="AcDream.App.Rendering.Gpu.Gl.GlGpuDevice.BeginFrame"/>.
|
||||||
_gl,
|
/// </summary>
|
||||||
GLEnum.ArrayBuffer,
|
private static void DrawRing(IGpuFrame frame, IGpuPassEncoder encoder, List<float> buf)
|
||||||
_vbo,
|
{
|
||||||
_vboCapacityBytes,
|
if (buf.Count == 0)
|
||||||
newCapacity,
|
return;
|
||||||
GLEnum.DynamicDraw,
|
GpuRingAllocation allocation = frame.AllocateRing(buf.Count * sizeof(float), GpuRingUsage.Vertex);
|
||||||
"TextRenderer frame VBO growth");
|
CollectionsMarshal.AsSpan(buf).CopyTo(allocation.AsSpan<float>());
|
||||||
_vboCapacityBytes = newCapacity;
|
encoder.BindVertexBuffer(allocation.Buffer, allocation.OffsetBytes);
|
||||||
}
|
encoder.Draw((uint)(buf.Count / FloatsPerVertex), 1, 0, 0);
|
||||||
|
|
||||||
fixed (float* p = CollectionsMarshal.AsSpan(buf))
|
|
||||||
_gl.BufferSubData(BufferTargetARB.ArrayBuffer, (nint)byteOffset, (nuint)bytes, p);
|
|
||||||
|
|
||||||
set.UsedBytes = requiredBytes;
|
|
||||||
set.CapacityBytes = _vboCapacityBytes;
|
|
||||||
return byteOffset / (FloatsPerVertex * sizeof(float));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_resources.RetryCleanup();
|
_whiteTexture.Dispose();
|
||||||
|
_pipeline.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
using AcDream.Core.Textures;
|
using AcDream.Core.Textures;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
|
using AcDream.App.Rendering.Gpu;
|
||||||
|
using AcDream.App.Rendering.Gpu.Gl;
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
using Silk.NET.OpenGL;
|
using Silk.NET.OpenGL;
|
||||||
|
|
@ -17,6 +19,7 @@ public sealed unsafe class TextureCache
|
||||||
IDisposable
|
IDisposable
|
||||||
{
|
{
|
||||||
private readonly GL _gl;
|
private readonly GL _gl;
|
||||||
|
private readonly IGpuDevice _device;
|
||||||
private readonly IDatReaderWriter _dats;
|
private readonly IDatReaderWriter _dats;
|
||||||
private readonly string _diagnosticsDirectory;
|
private readonly string _diagnosticsDirectory;
|
||||||
// Handle and decoded dimensions are one atomic cache entry. Keeping them
|
// Handle and decoded dimensions are one atomic cache entry. Keeping them
|
||||||
|
|
@ -28,17 +31,29 @@ public sealed unsafe class TextureCache
|
||||||
_decodedDimensionsByTexture = new();
|
_decodedDimensionsByTexture = new();
|
||||||
private uint _magentaHandle;
|
private uint _magentaHandle;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Campaign V slice V4a: one registered <see cref="IGpuTexture"/> plus its
|
||||||
|
/// device texture-table <see cref="GpuTextureSlot"/>, decoded pixel size,
|
||||||
|
/// and the raw GL name <see cref="TextRenderer.DrawSprite"/>'s classic
|
||||||
|
/// texture-unit binding path still needs (see that class's remarks).
|
||||||
|
/// </summary>
|
||||||
|
private readonly record struct GpuUiTextureEntry(
|
||||||
|
IGpuTexture Texture,
|
||||||
|
GpuTextureSlot Slot,
|
||||||
|
uint GlName,
|
||||||
|
int Width,
|
||||||
|
int Height);
|
||||||
|
|
||||||
// Direct-RenderSurface caches for UI sprites: 0x06xxxxxx RenderSurface ids
|
// Direct-RenderSurface caches for UI sprites: 0x06xxxxxx RenderSurface ids
|
||||||
// decoded directly (Portal/HighRes → DecodeRenderSurface), bypassing the
|
// decoded directly (Portal/HighRes → DecodeRenderSurface), bypassing the
|
||||||
// Surface→SurfaceTexture chain that GetOrUpload uses for world materials.
|
// Surface→SurfaceTexture chain that GetOrUpload uses for world materials.
|
||||||
private readonly Dictionary<uint, uint> _handlesByRenderSurfaceId = new();
|
private readonly Dictionary<uint, GpuUiTextureEntry> _renderSurfaceGpuTextures = new();
|
||||||
private readonly Dictionary<uint, (int w, int h)> _rsSizeById = new();
|
|
||||||
|
|
||||||
// Ad-hoc handles produced by the public UploadRgba8(byte[],int,int,bool) wrapper
|
// Ad-hoc textures produced by the public UploadRgba8(byte[],int,int,bool) wrapper
|
||||||
// (used by IconComposer for composited item icons). These are NOT stored in any
|
// (used by IconComposer for composited item icons). These are NOT stored in any
|
||||||
// of the keyed caches above, so Dispose must sweep this list to avoid leaking
|
// of the keyed caches above, so Dispose must sweep this list to avoid leaking
|
||||||
// GL texture objects until process exit.
|
// GPU texture objects/slots until process exit.
|
||||||
private readonly List<uint> _adhocHandles = new();
|
private readonly List<GpuUiTextureEntry> _adhocGpuTextures = new();
|
||||||
|
|
||||||
private readonly Wb.BindlessSupport? _bindless;
|
private readonly Wb.BindlessSupport? _bindless;
|
||||||
private readonly CompositeTextureArrayCache? _compositeTextures;
|
private readonly CompositeTextureArrayCache? _compositeTextures;
|
||||||
|
|
@ -86,9 +101,14 @@ public sealed unsafe class TextureCache
|
||||||
private int _dumpFrameCounter;
|
private int _dumpFrameCounter;
|
||||||
private bool _surfaceHistogramAlreadyDumped;
|
private bool _surfaceHistogramAlreadyDumped;
|
||||||
|
|
||||||
public TextureCache(GL gl, IDatReaderWriter dats, Wb.BindlessSupport? bindless = null)
|
// internal, not public: IGpuDevice is an internal type (the pinned RHI
|
||||||
|
// contract), and this convenience overload has no real caller today (both
|
||||||
|
// production construction sites already target the internal overload
|
||||||
|
// below) — kept internal rather than deleted to preserve its shape.
|
||||||
|
internal TextureCache(GL gl, IGpuDevice device, IDatReaderWriter dats, Wb.BindlessSupport? bindless = null)
|
||||||
: this(
|
: this(
|
||||||
gl,
|
gl,
|
||||||
|
device,
|
||||||
dats,
|
dats,
|
||||||
bindless,
|
bindless,
|
||||||
ImmediateGpuResourceRetirementQueue.Instance,
|
ImmediateGpuResourceRetirementQueue.Instance,
|
||||||
|
|
@ -101,6 +121,7 @@ public sealed unsafe class TextureCache
|
||||||
|
|
||||||
internal TextureCache(
|
internal TextureCache(
|
||||||
GL gl,
|
GL gl,
|
||||||
|
IGpuDevice device,
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
Wb.BindlessSupport? bindless,
|
Wb.BindlessSupport? bindless,
|
||||||
IGpuResourceRetirementQueue retirementQueue,
|
IGpuResourceRetirementQueue retirementQueue,
|
||||||
|
|
@ -109,6 +130,7 @@ public sealed unsafe class TextureCache
|
||||||
{
|
{
|
||||||
budgets ??= ResidencyBudgetOptions.Default;
|
budgets ??= ResidencyBudgetOptions.Default;
|
||||||
_gl = gl;
|
_gl = gl;
|
||||||
|
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||||
_dats = dats;
|
_dats = dats;
|
||||||
_bindless = bindless;
|
_bindless = bindless;
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(diagnosticsDirectory);
|
ArgumentException.ThrowIfNullOrWhiteSpace(diagnosticsDirectory);
|
||||||
|
|
@ -231,11 +253,10 @@ public sealed unsafe class TextureCache
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public uint GetOrUploadRenderSurface(uint renderSurfaceId, out int width, out int height, bool nearest = false)
|
public uint GetOrUploadRenderSurface(uint renderSurfaceId, out int width, out int height, bool nearest = false)
|
||||||
{
|
{
|
||||||
if (_handlesByRenderSurfaceId.TryGetValue(renderSurfaceId, out var existing)
|
if (_renderSurfaceGpuTextures.TryGetValue(renderSurfaceId, out GpuUiTextureEntry existing))
|
||||||
&& _rsSizeById.TryGetValue(renderSurfaceId, out var sz))
|
|
||||||
{
|
{
|
||||||
width = sz.w; height = sz.h;
|
width = existing.Width; height = existing.Height;
|
||||||
return existing;
|
return existing.GlName;
|
||||||
}
|
}
|
||||||
|
|
||||||
DecodedTexture decoded;
|
DecodedTexture decoded;
|
||||||
|
|
@ -256,11 +277,62 @@ public sealed unsafe class TextureCache
|
||||||
decoded = DecodedTexture.Magenta;
|
decoded = DecodedTexture.Magenta;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint h = UploadRgba8(decoded, nearest);
|
GpuUiTextureEntry entry = UploadUiTexture(decoded, nearest, $"ui-rendersurface-0x{renderSurfaceId:X8}");
|
||||||
_handlesByRenderSurfaceId[renderSurfaceId] = h;
|
_renderSurfaceGpuTextures[renderSurfaceId] = entry;
|
||||||
_rsSizeById[renderSurfaceId] = (decoded.Width, decoded.Height);
|
|
||||||
width = decoded.Width; height = decoded.Height;
|
width = decoded.Width; height = decoded.Height;
|
||||||
return h;
|
return entry.GlName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Campaign V slice V4a: creates an <see cref="IGpuTexture"/> for one
|
||||||
|
/// decoded UI sprite/atlas and registers it into the device's global
|
||||||
|
/// texture table. Every UI-path texture uses REPEAT addressing (existing
|
||||||
|
/// behaviour — panel fills and tiled chrome sample UVs greater than 1) and
|
||||||
|
/// a single mip level (UI sprites never mip). The classic texture-unit
|
||||||
|
/// binding path <see cref="TextRenderer.DrawSprite"/> uses for these
|
||||||
|
/// (see that class's remarks) samples the texture object directly rather
|
||||||
|
/// than through a bound sampler object, so filtering must live on the
|
||||||
|
/// texture itself — <c>GlGpuTexture</c>'s constructor always sets Linear,
|
||||||
|
/// which is wrong for <paramref name="nearest"/>-requested (pixel-exact)
|
||||||
|
/// sprites, so it is overridden here exactly as the old raw-GL path did.
|
||||||
|
/// </summary>
|
||||||
|
private GpuUiTextureEntry UploadUiTexture(DecodedTexture decoded, bool nearest, string debugName)
|
||||||
|
{
|
||||||
|
IGpuTexture texture = _device.CreateTexture(new GpuTextureDescription(
|
||||||
|
debugName,
|
||||||
|
GpuTextureKind.Texture2D,
|
||||||
|
GpuTextureFormat.Rgba8Unorm,
|
||||||
|
Width: decoded.Width,
|
||||||
|
Height: decoded.Height,
|
||||||
|
LayerCount: 1,
|
||||||
|
MipLevelCount: 1));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
texture.Upload(0, 0, decoded.Rgba8);
|
||||||
|
uint glName = ((GlGpuTexture)texture).GlName;
|
||||||
|
TrackUploadedTexture(glName, decoded.Width, decoded.Height);
|
||||||
|
|
||||||
|
// MUST happen BEFORE RegisterTexture below: ARB_bindless_texture
|
||||||
|
// forbids glTexParameter on a texture once its bindless handle has
|
||||||
|
// been made resident (GL_INVALID_OPERATION). See BitmapFont's
|
||||||
|
// constructor for the same fix and full explanation.
|
||||||
|
if (nearest)
|
||||||
|
{
|
||||||
|
_gl.BindTexture(TextureTarget.Texture2D, glName);
|
||||||
|
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
|
||||||
|
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest);
|
||||||
|
_gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
IGpuSampler sampler = _device.CreateSampler(GpuSamplerDescription.WorldRepeat);
|
||||||
|
GpuTextureSlot slot = _device.RegisterTexture(texture, sampler);
|
||||||
|
return new GpuUiTextureEntry(texture, slot, glName, decoded.Width, decoded.Height);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
texture.Dispose();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -815,14 +887,15 @@ public sealed unsafe class TextureCache
|
||||||
|
|
||||||
/// <summary>Uploads a raw RGBA8 byte array as a Texture2D. Used by
|
/// <summary>Uploads a raw RGBA8 byte array as a Texture2D. Used by
|
||||||
/// <see cref="AcDream.App.UI.IconComposer"/> to upload CPU-composited icon layers.
|
/// <see cref="AcDream.App.UI.IconComposer"/> to upload CPU-composited icon layers.
|
||||||
/// The returned handle is tracked in <see cref="_adhocHandles"/> and deleted by
|
/// The returned handle is tracked in <see cref="_adhocGpuTextures"/> and deleted by
|
||||||
/// <see cref="Dispose"/>. Callers must NOT also store the handle in any of the
|
/// <see cref="Dispose"/>. Callers must NOT also store the handle in any of the
|
||||||
/// keyed caches — that would cause a double-delete on Dispose.</summary>
|
/// keyed caches — that would cause a double-delete on Dispose.</summary>
|
||||||
public uint UploadRgba8(byte[] rgba, int width, int height, bool nearest = false)
|
public uint UploadRgba8(byte[] rgba, int width, int height, bool nearest = false)
|
||||||
{
|
{
|
||||||
uint h = UploadRgba8(new DecodedTexture(rgba, width, height), nearest);
|
GpuUiTextureEntry entry = UploadUiTexture(
|
||||||
_adhocHandles.Add(h);
|
new DecodedTexture(rgba, width, height), nearest, "ui-adhoc-rgba8");
|
||||||
return h;
|
_adhocGpuTextures.Add(entry);
|
||||||
|
return entry.GlName;
|
||||||
}
|
}
|
||||||
|
|
||||||
private uint UploadRgba8(DecodedTexture decoded, bool nearest = false)
|
private uint UploadRgba8(DecodedTexture decoded, bool nearest = false)
|
||||||
|
|
@ -932,7 +1005,18 @@ public sealed unsafe class TextureCache
|
||||||
{
|
{
|
||||||
_gl.DeleteTexture(name);
|
_gl.DeleteTexture(name);
|
||||||
Wb.GLHelpers.ThrowOnResourceError(_gl, $"deleting uploaded texture {name}");
|
Wb.GLHelpers.ThrowOnResourceError(_gl, $"deleting uploaded texture {name}");
|
||||||
|
UntrackUploadedTexture(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Memory-tracking bookkeeping only, without a raw GL delete — used for
|
||||||
|
/// the Campaign V slice V4a UI-path <see cref="IGpuTexture"/> entries,
|
||||||
|
/// whose GL name is released by <see cref="IGpuTexture.Dispose"/> through
|
||||||
|
/// the device's own retirement queue rather than by
|
||||||
|
/// <see cref="DeleteUploadedTexture"/>.
|
||||||
|
/// </summary>
|
||||||
|
private void UntrackUploadedTexture(uint name)
|
||||||
|
{
|
||||||
if (_uploadMetadata.Remove(name, out var metadata))
|
if (_uploadMetadata.Remove(name, out var metadata))
|
||||||
{
|
{
|
||||||
long bytes = checked((long)metadata.Width * metadata.Height * 4L);
|
long bytes = checked((long)metadata.Width * metadata.Height * 4L);
|
||||||
|
|
@ -962,16 +1046,26 @@ public sealed unsafe class TextureCache
|
||||||
_magentaHandle = 0;
|
_magentaHandle = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderSurface (UI sprite) handles — pre-existing gap: this dict was populated
|
// RenderSurface (UI sprite) textures — Campaign V slice V4a: each
|
||||||
// by GetOrUploadRenderSurface but was not swept here before this fix.
|
// entry's IGpuTexture.Dispose() releases the underlying GL name
|
||||||
foreach (var h in _handlesByRenderSurfaceId.Values)
|
// through the device's own retirement queue, so only the memory-
|
||||||
DeleteUploadedTexture(h);
|
// tracking bookkeeping and the registered slot need releasing here.
|
||||||
_handlesByRenderSurfaceId.Clear();
|
foreach (GpuUiTextureEntry entry in _renderSurfaceGpuTextures.Values)
|
||||||
|
{
|
||||||
|
entry.Texture.Dispose();
|
||||||
|
_device.ReleaseTextureSlot(entry.Slot);
|
||||||
|
UntrackUploadedTexture(entry.GlName);
|
||||||
|
}
|
||||||
|
_renderSurfaceGpuTextures.Clear();
|
||||||
|
|
||||||
// Ad-hoc handles from the public UploadRgba8(byte[],int,int,bool) wrapper
|
// Ad-hoc textures from the public UploadRgba8(byte[],int,int,bool) wrapper
|
||||||
// (IconComposer composited icons). Not stored in any keyed cache.
|
// (IconComposer composited icons). Not stored in any keyed cache.
|
||||||
foreach (var h in _adhocHandles)
|
foreach (GpuUiTextureEntry entry in _adhocGpuTextures)
|
||||||
DeleteUploadedTexture(h);
|
{
|
||||||
_adhocHandles.Clear();
|
entry.Texture.Dispose();
|
||||||
|
_device.ReleaseTextureSlot(entry.Slot);
|
||||||
|
UntrackUploadedTexture(entry.GlName);
|
||||||
|
}
|
||||||
|
_adhocGpuTextures.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Gpu;
|
||||||
using Silk.NET.Input;
|
using Silk.NET.Input;
|
||||||
using Silk.NET.OpenGL;
|
using Silk.NET.OpenGL;
|
||||||
|
|
||||||
|
|
@ -58,19 +59,27 @@ public sealed class UiHost : System.IDisposable
|
||||||
|
|
||||||
internal bool IsDisposalComplete => _disposed;
|
internal bool IsDisposalComplete => _disposed;
|
||||||
|
|
||||||
public UiHost(GL gl, string shaderDir, BitmapFont? defaultFont = null)
|
// internal, not public: IGpuDevice/ICurrentGpuFrameSource are internal
|
||||||
: this(gl, shaderDir, defaultFont, new HostQuiescenceGate())
|
// types (the pinned RHI contract). UiHost stays public — only
|
||||||
|
// construction is restricted.
|
||||||
|
internal UiHost(
|
||||||
|
IGpuDevice device,
|
||||||
|
ICurrentGpuFrameSource frameSource,
|
||||||
|
string shaderDir,
|
||||||
|
BitmapFont? defaultFont = null)
|
||||||
|
: this(device, frameSource, shaderDir, defaultFont, new HostQuiescenceGate())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
internal UiHost(
|
internal UiHost(
|
||||||
GL gl,
|
IGpuDevice device,
|
||||||
|
ICurrentGpuFrameSource frameSource,
|
||||||
string shaderDir,
|
string shaderDir,
|
||||||
BitmapFont? defaultFont,
|
BitmapFont? defaultFont,
|
||||||
HostQuiescenceGate quiescence)
|
HostQuiescenceGate quiescence)
|
||||||
{
|
{
|
||||||
_quiescence = quiescence ?? throw new ArgumentNullException(nameof(quiescence));
|
_quiescence = quiescence ?? throw new ArgumentNullException(nameof(quiescence));
|
||||||
TextRenderer = new TextRenderer(gl, shaderDir);
|
TextRenderer = new TextRenderer(device, frameSource, shaderDir);
|
||||||
DefaultFont = defaultFont;
|
DefaultFont = defaultFont;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering.Gpu;
|
||||||
|
|
||||||
namespace AcDream.App.UI;
|
namespace AcDream.App.UI;
|
||||||
|
|
||||||
|
|
@ -34,16 +35,27 @@ public sealed class UiViewport : UiElement
|
||||||
/// <summary>Renderer that produces the off-screen texture. Set by GameWindow wiring (later task).</summary>
|
/// <summary>Renderer that produces the off-screen texture. Set by GameWindow wiring (later task).</summary>
|
||||||
public IUiViewportRenderer? Renderer { get; set; }
|
public IUiViewportRenderer? Renderer { get; set; }
|
||||||
|
|
||||||
/// <summary>Last GL color-texture handle produced by the pre-UI hook. 0 = nothing to blit.</summary>
|
/// <summary>
|
||||||
public uint TextureHandle { get; set; }
|
/// Campaign V slice V4a: the off-screen FBO colour texture produced by the
|
||||||
|
/// pre-UI hook, registered into the device's texture table by the
|
||||||
|
/// pre-approved paperdoll/appraisal transitional seam
|
||||||
|
/// (<c>GlGpuDevice.RegisterExternalColorTexture</c>, campaign doc §7.1) —
|
||||||
|
/// its owning renderer (<c>PrivateEntityViewportRenderer</c>) stays raw GL
|
||||||
|
/// until V4g. <see cref="GpuTextureSlot.Unassigned"/> = nothing to blit.
|
||||||
|
/// internal, not public: <see cref="GpuTextureSlot"/> is an internal type
|
||||||
|
/// (the pinned RHI contract); UiViewport stays public.
|
||||||
|
/// </summary>
|
||||||
|
internal GpuTextureSlot TextureSlot { get; set; } = GpuTextureSlot.Unassigned;
|
||||||
|
|
||||||
protected override void OnDraw(UiRenderContext ctx)
|
protected override void OnDraw(UiRenderContext ctx)
|
||||||
{
|
{
|
||||||
if (!Visible || TextureHandle == 0) return;
|
if (!Visible || !TextureSlot.IsAssigned) return;
|
||||||
|
uint textureHandle = ctx.TextRenderer.ResolveExternalTextureSlot(TextureSlot);
|
||||||
|
if (textureHandle == 0) return;
|
||||||
// Local origin is already at this widget's Left/Top (PushTransform applied by DrawSelfAndChildren).
|
// Local origin is already at this widget's Left/Top (PushTransform applied by DrawSelfAndChildren).
|
||||||
// V is FLIPPED (v0=1, v1=0): TextureHandle is an off-screen FBO color texture, whose origin is
|
// V is FLIPPED (v0=1, v1=0): the resolved texture is an off-screen FBO color texture, whose origin is
|
||||||
// bottom-left (GL), while the UI sprite convention is top-left. Without the flip the doll renders
|
// bottom-left (GL), while the UI sprite convention is top-left. Without the flip the doll renders
|
||||||
// upside-down. (If the doll appears upside-down at the visual gate, this is the line to revisit.)
|
// upside-down. (If the doll appears upside-down at the visual gate, this is the line to revisit.)
|
||||||
ctx.DrawSprite(TextureHandle, 0f, 0f, Width, Height, 0f, 1f, 1f, 0f, Vector4.One);
|
ctx.DrawSprite(textureHandle, 0f, 0f, Width, Height, 0f, 1f, 1f, 0f, Vector4.One);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,7 @@ public sealed class HostInputCameraCompositionTests
|
||||||
{
|
{
|
||||||
public GpuFrameFlightController? GpuFrames { get; private set; }
|
public GpuFrameFlightController? GpuFrames { get; private set; }
|
||||||
public IGpuDevice? GpuDevice { get; private set; }
|
public IGpuDevice? GpuDevice { get; private set; }
|
||||||
|
public GpuDeviceFrameLifetime? GpuFrameLifetime { get; private set; }
|
||||||
public SilkKeyboardSource? Keyboard { get; private set; }
|
public SilkKeyboardSource? Keyboard { get; private set; }
|
||||||
public SilkMouseSource? Mouse { get; private set; }
|
public SilkMouseSource? Mouse { get; private set; }
|
||||||
public IMouseLookCursor? Cursor { get; private set; }
|
public IMouseLookCursor? Cursor { get; private set; }
|
||||||
|
|
@ -172,6 +173,9 @@ public sealed class HostInputCameraCompositionTests
|
||||||
public void PublishGpuDevice(IGpuDevice value) =>
|
public void PublishGpuDevice(IGpuDevice value) =>
|
||||||
GpuDevice = PublishOnce(GpuDevice, value);
|
GpuDevice = PublishOnce(GpuDevice, value);
|
||||||
|
|
||||||
|
public void PublishGpuFrameLifetime(GpuDeviceFrameLifetime value) =>
|
||||||
|
GpuFrameLifetime = PublishOnce(GpuFrameLifetime, value);
|
||||||
|
|
||||||
public void PublishKeyboardSource(SilkKeyboardSource value) =>
|
public void PublishKeyboardSource(SilkKeyboardSource value) =>
|
||||||
Keyboard = PublishOnce(Keyboard, value);
|
Keyboard = PublishOnce(Keyboard, value);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,9 @@ public sealed class InteractionRetainedUiCompositionTests
|
||||||
ExistingVitals: null,
|
ExistingVitals: null,
|
||||||
Toast: null,
|
Toast: null,
|
||||||
ClientTime: static () => 0d,
|
ClientTime: static () => 0d,
|
||||||
Log: static _ => { });
|
Log: static _ => { },
|
||||||
|
GpuDevice: null!,
|
||||||
|
GpuFrameSource: null!);
|
||||||
}
|
}
|
||||||
|
|
||||||
public InteractionRetainedUiDependencies Dependencies { get; }
|
public InteractionRetainedUiDependencies Dependencies { get; }
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,7 @@ public sealed class SettingsDevToolsCompositionTests
|
||||||
null!,
|
null!,
|
||||||
null!,
|
null!,
|
||||||
null!,
|
null!,
|
||||||
|
null!,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,11 @@ using System.Collections.Concurrent;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using AcDream.App.Composition;
|
using AcDream.App.Composition;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Gpu;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.App.Rendering.Residency;
|
using AcDream.App.Rendering.Residency;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
|
using AcDream.App.Tests.Rendering.Gpu;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.Terrain;
|
using AcDream.Core.Terrain;
|
||||||
|
|
@ -147,6 +149,7 @@ public sealed class WorldRenderCompositionTests
|
||||||
{
|
{
|
||||||
private readonly WorldRenderCompositionPoint? _failurePoint;
|
private readonly WorldRenderCompositionPoint? _failurePoint;
|
||||||
private readonly ResidencyBudgetOptions _budgets;
|
private readonly ResidencyBudgetOptions _budgets;
|
||||||
|
private readonly IGpuDevice _gpuDevice = new RecordingGpuDevice();
|
||||||
|
|
||||||
public Fixture(
|
public Fixture(
|
||||||
bool hasFont = true,
|
bool hasFont = true,
|
||||||
|
|
@ -179,7 +182,9 @@ public sealed class WorldRenderCompositionTests
|
||||||
_budgets,
|
_budgets,
|
||||||
0xA9B4FFFFu,
|
0xA9B4FFFFu,
|
||||||
Path.Combine(Path.GetTempPath(), "acdream-tests"),
|
Path.Combine(Path.GetTempPath(), "acdream-tests"),
|
||||||
_ => { }),
|
_ => { },
|
||||||
|
_gpuDevice,
|
||||||
|
new GpuDeviceFrameLifetime(_gpuDevice)),
|
||||||
Publication,
|
Publication,
|
||||||
Factory,
|
Factory,
|
||||||
point =>
|
point =>
|
||||||
|
|
@ -248,15 +253,17 @@ public sealed class WorldRenderCompositionTests
|
||||||
public SceneLightingUboBinding CreateSceneLighting(GL gl) =>
|
public SceneLightingUboBinding CreateSceneLighting(GL gl) =>
|
||||||
Resource<SceneLightingUboBinding>("scene lighting");
|
Resource<SceneLightingUboBinding>("scene lighting");
|
||||||
|
|
||||||
public DebugLineRenderer CreateDebugLines(GL gl, string shadersDirectory) =>
|
public DebugLineRenderer CreateDebugLines(
|
||||||
|
IGpuDevice device, ICurrentGpuFrameSource frameSource, string shadersDirectory) =>
|
||||||
Resource<DebugLineRenderer>("debug lines");
|
Resource<DebugLineRenderer>("debug lines");
|
||||||
|
|
||||||
public byte[]? TryLoadDebugFont() => hasFont ? [1] : null;
|
public byte[]? TryLoadDebugFont() => hasFont ? [1] : null;
|
||||||
|
|
||||||
public BitmapFont CreateDebugFont(GL gl, byte[] bytes) =>
|
public BitmapFont CreateDebugFont(IGpuDevice device, byte[] bytes) =>
|
||||||
Resource<BitmapFont>("debug font");
|
Resource<BitmapFont>("debug font");
|
||||||
|
|
||||||
public TextRenderer CreateTextRenderer(GL gl, string shadersDirectory) =>
|
public TextRenderer CreateTextRenderer(
|
||||||
|
IGpuDevice device, ICurrentGpuFrameSource frameSource, string shadersDirectory) =>
|
||||||
Resource<TextRenderer>("text renderer");
|
Resource<TextRenderer>("text renderer");
|
||||||
|
|
||||||
public TerrainModernRenderer CreateTerrain(
|
public TerrainModernRenderer CreateTerrain(
|
||||||
|
|
@ -295,6 +302,7 @@ public sealed class WorldRenderCompositionTests
|
||||||
|
|
||||||
public TextureCache CreateTextureCache(
|
public TextureCache CreateTextureCache(
|
||||||
GL gl,
|
GL gl,
|
||||||
|
IGpuDevice device,
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
BindlessSupport bindless,
|
BindlessSupport bindless,
|
||||||
IGpuResourceRetirementQueue retirement,
|
IGpuResourceRetirementQueue retirement,
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,14 @@ public sealed class GlTextureOwnershipTests
|
||||||
root, "src", "AcDream.App", "Rendering", "ShaderProgramConstruction.cs"));
|
root, "src", "AcDream.App", "Rendering", "ShaderProgramConstruction.cs"));
|
||||||
string terrain = File.ReadAllText(Path.Combine(
|
string terrain = File.ReadAllText(Path.Combine(
|
||||||
root, "src", "AcDream.App", "Rendering", "TerrainAtlas.cs"));
|
root, "src", "AcDream.App", "Rendering", "TerrainAtlas.cs"));
|
||||||
|
// Campaign V slice V4a: TextRenderer's shader/texture creation moved
|
||||||
|
// from raw GlResourceCommand calls to IGpuDevice.CreatePipeline/
|
||||||
|
// CreateTexture, whose own checked-commit construction
|
||||||
|
// (GlResourceCommand.CreateName / ShaderProgramConstruction.Build,
|
||||||
|
// predating this slice) is what those two now delegate to. What
|
||||||
|
// TextRenderer's OWN constructor still owns is the ordered
|
||||||
|
// pipeline-then-texture sequence and disposing whichever of the two
|
||||||
|
// it already created if the other throws.
|
||||||
string text = File.ReadAllText(Path.Combine(
|
string text = File.ReadAllText(Path.Combine(
|
||||||
root, "src", "AcDream.App", "Rendering", "TextRenderer.cs"));
|
root, "src", "AcDream.App", "Rendering", "TextRenderer.cs"));
|
||||||
string bindless = File.ReadAllText(Path.Combine(
|
string bindless = File.ReadAllText(Path.Combine(
|
||||||
|
|
@ -116,12 +124,30 @@ public sealed class GlTextureOwnershipTests
|
||||||
Assert.Contains("GlResourceCommand.DeleteProgram", shaderPrograms, StringComparison.Ordinal);
|
Assert.Contains("GlResourceCommand.DeleteProgram", shaderPrograms, StringComparison.Ordinal);
|
||||||
Assert.Contains("_anisotropyBindingMutation.Execute", terrain, StringComparison.Ordinal);
|
Assert.Contains("_anisotropyBindingMutation.Execute", terrain, StringComparison.Ordinal);
|
||||||
Assert.Contains("GlResourceCommand.DeleteTexture", terrain, StringComparison.Ordinal);
|
Assert.Contains("GlResourceCommand.DeleteTexture", terrain, StringComparison.Ordinal);
|
||||||
Assert.Contains("GlResourceCommand.CreateTexture", text, StringComparison.Ordinal);
|
AssertAppearsInOrder(
|
||||||
Assert.Contains("GlResourceCommand.Execute", text, StringComparison.Ordinal);
|
text,
|
||||||
|
"pipeline = device.CreatePipeline(",
|
||||||
|
"whiteTexture = device.CreateTexture(",
|
||||||
|
"catch",
|
||||||
|
"whiteTexture?.Dispose();",
|
||||||
|
"pipeline?.Dispose();",
|
||||||
|
"throw;");
|
||||||
Assert.Contains("make bindless handle", bindless, StringComparison.Ordinal);
|
Assert.Contains("make bindless handle", bindless, StringComparison.Ordinal);
|
||||||
Assert.Contains("GlResourceCommand.Execute", bindless, StringComparison.Ordinal);
|
Assert.Contains("GlResourceCommand.Execute", bindless, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AssertAppearsInOrder(string source, params string[] needles)
|
||||||
|
{
|
||||||
|
int cursor = -1;
|
||||||
|
foreach (string needle in needles)
|
||||||
|
{
|
||||||
|
int next = source.IndexOf(needle, cursor + 1, StringComparison.Ordinal);
|
||||||
|
Assert.True(next >= 0, $"Missing expected source fragment: {needle}");
|
||||||
|
Assert.True(next > cursor, $"Out-of-order source fragment: {needle}");
|
||||||
|
cursor = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SecondBindlessAcquireFailureRollsBackFirstHandle()
|
public void SecondBindlessAcquireFailureRollsBackFirstHandle()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,11 @@ public sealed class RenderFrameResourceControllerTests
|
||||||
{
|
{
|
||||||
string source = ResourceSource();
|
string source = ResourceSource();
|
||||||
|
|
||||||
|
// Campaign V slice V4a: the world-HUD and retained-UI TextRenderers no
|
||||||
|
// longer take a per-slot begin call — they now read the shared
|
||||||
|
// IGpuFrame ring reset once per frame by IGpuDevice.BeginFrame() (see
|
||||||
|
// GpuDeviceFrameLifetime), so _worldText/_uiText are gone from this
|
||||||
|
// sequence.
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
source,
|
source,
|
||||||
"_textures?.BeginCompositeTextureFrame();",
|
"_textures?.BeginCompositeTextureFrame();",
|
||||||
|
|
@ -56,8 +61,6 @@ public sealed class RenderFrameResourceControllerTests
|
||||||
"_dispatcher?.BeginFrame(gpuSlot);",
|
"_dispatcher?.BeginFrame(gpuSlot);",
|
||||||
"_environmentCells?.BeginFrame(gpuSlot);",
|
"_environmentCells?.BeginFrame(gpuSlot);",
|
||||||
"_portalDepth?.BeginFrame(gpuSlot);",
|
"_portalDepth?.BeginFrame(gpuSlot);",
|
||||||
"_worldText?.BeginFrame(gpuSlot);",
|
|
||||||
"_uiText?.BeginFrame(gpuSlot);",
|
|
||||||
"_clip?.BeginFrame(gpuSlot);",
|
"_clip?.BeginFrame(gpuSlot);",
|
||||||
"_terrain?.BeginFrame(gpuSlot);",
|
"_terrain?.BeginFrame(gpuSlot);",
|
||||||
"_lighting?.BeginFrame(gpuSlot);");
|
"_lighting?.BeginFrame(gpuSlot);");
|
||||||
|
|
|
||||||
|
|
@ -130,8 +130,19 @@ public sealed class ResourceCleanupGroupTests
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Campaign V slice V4a: TextRenderer's constructor no longer owns raw
|
||||||
|
/// VAO/VBO/texture GL names through a ResourceCleanupGroup ledger — it
|
||||||
|
/// creates exactly two device-owned resources (a pipeline, then the white
|
||||||
|
/// fill texture) and, since a later failure could otherwise orphan an
|
||||||
|
/// already-created pipeline, disposes whichever of the two it already
|
||||||
|
/// created if the other throws. This replaces the pre-V4a assertion of
|
||||||
|
/// the same name, which pinned the old multi-resource raw-GL shape
|
||||||
|
/// (Shader + three flight-indexed VAO/VBO pairs + a hand-rolled white
|
||||||
|
/// texture) that no longer exists.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void TextRendererPublishesEveryConstructorResourceBeforeLaterGlWork()
|
public void TextRendererDisposesWhicheverConstructorResourceAlreadyExistsOnFailure()
|
||||||
{
|
{
|
||||||
string source = File.ReadAllText(Path.Combine(
|
string source = File.ReadAllText(Path.Combine(
|
||||||
FindRepoRoot(),
|
FindRepoRoot(),
|
||||||
|
|
@ -142,27 +153,18 @@ public sealed class ResourceCleanupGroupTests
|
||||||
|
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
source,
|
source,
|
||||||
"shader = new Shader(gl,",
|
"IGpuPipeline? pipeline = null;",
|
||||||
"resources.Add(\"text shader\", shader.Dispose);",
|
"IGpuTexture? whiteTexture = null;",
|
||||||
"frameBuffers[i] = CreateFrameBufferSet(resources);",
|
"try",
|
||||||
"whiteTexture = GlResourceCommand.CreateTexture(",
|
"pipeline = device.CreatePipeline(",
|
||||||
"resources.Add(",
|
"whiteTexture = device.CreateTexture(",
|
||||||
"\"white texture\"",
|
"device.RegisterTexture(whiteTexture, whiteSampler);",
|
||||||
"GlResourceCommand.Execute(",
|
"catch",
|
||||||
"initialize TextRenderer white texture",
|
"whiteTexture?.Dispose();",
|
||||||
"resources.RetryCleanup();",
|
"pipeline?.Dispose();",
|
||||||
"_resources = resources;");
|
"throw;",
|
||||||
AssertAppearsInOrder(
|
"_pipeline = pipeline;",
|
||||||
source,
|
"_whiteTexture = whiteTexture;");
|
||||||
"private FrameBufferSet CreateFrameBufferSet(ResourceCleanupGroup resources)",
|
|
||||||
"TrackedGlResource.CreateVertexArray(",
|
|
||||||
"resources.Add(\"frame VAO\", vaoRelease.Run);",
|
|
||||||
"TrackedGlResource.CreateBuffer(",
|
|
||||||
"resources.Add(",
|
|
||||||
"\"frame VBO\"");
|
|
||||||
Assert.Contains("_resources.RetryCleanup();", source, StringComparison.Ordinal);
|
|
||||||
Assert.DoesNotContain("private FrameBufferSet CreateFrameBufferSet()", source,
|
|
||||||
StringComparison.Ordinal);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue