fix(agc): rect-list/NGG strips, Index8 expand, and GE_INDX_OFFSET

NGG single-rect UI needs triangle-strip expansion; Prospero Index8 must
expand to host u16; glyphs need base vertex from GE_INDX_OFFSET. Skip
param-less rect-lists instead of inventing colour draws.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
MarcelMediaDev
2026-07-23 01:46:52 +01:00
parent 83745325c3
commit 4b9d2cafac
9 changed files with 606 additions and 94 deletions
+194 -48
View File
@@ -104,6 +104,10 @@ public static partial class AgcExports
private const uint ComputeNumThreadZ = 0x209;
private const uint SpiPsInputCntl0 = 0x191;
private const uint VgtPrimitiveType = 0x242;
private const uint VgtIndexType = 0x243;
// GE_INDX_OFFSET — base vertex for DrawIndexed / firstVertex for
// DrawIndexAuto. Glyph meshes and UI icon batches rely on this.
private const uint GeIndxOffset = 0x24A;
private const uint PaScScreenScissorTl = 0x0C;
private const uint PaScScreenScissorBr = 0x0D;
private const uint CbTargetMask = 0x8E;
@@ -453,6 +457,7 @@ public static partial class AgcExports
uint AttributeCount,
uint VertexCount,
uint InstanceCount,
int BaseVertex,
GuestIndexBuffer? IndexBuffer,
IReadOnlyList<TranslatedImageBinding> Textures,
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
@@ -3705,7 +3710,8 @@ public static partial class AgcExports
vertexBuffers,
pendingComposite.RenderState,
pendingComposite.DepthTarget,
pendingComposite.PixelShaderAddress);
pendingComposite.PixelShaderAddress,
pendingComposite.BaseVertex);
TraceAgcShader(
$"agc.deferred_composite ps=0x{pendingComposite.PixelShaderAddress:X16} " +
$"src=0x{pendingComposite.Textures.FirstOrDefault()?.Descriptor.Address ?? 0:X16} " +
@@ -5728,6 +5734,10 @@ public static partial class AgcExports
}
directDestination[startRegister + index] = value;
if (op == ItSetUconfigReg)
{
ApplyUcIndexTypeIfNeeded(state, startRegister + index, value);
}
}
return;
@@ -5762,6 +5772,25 @@ public static partial class AgcExports
// Dropping it leaves stale depth/render-control state active in
// later passes.
destination[registerOffset] = value;
if (register == RUcRegsIndirect)
{
ApplyUcIndexTypeIfNeeded(state, registerOffset, value);
}
}
}
/// <summary>
/// GraphicsDcbSetIndexSize writes VGT_INDEX_TYPE via SET_UCONFIG_REG.
/// Mirror that into <see cref="SubmittedDcbState.IndexSize"/>.
/// </summary>
private static void ApplyUcIndexTypeIfNeeded(
SubmittedDcbState state,
uint registerOffset,
uint value)
{
if (registerOffset == VgtIndexType)
{
state.IndexSize = value & 0x3;
}
}
@@ -5958,7 +5987,8 @@ public static partial class AgcExports
depthOnlyDraw.IndexBuffer,
vertexBuffers,
renderState,
depthOnlyDraw.PixelShaderAddress);
depthOnlyDraw.PixelShaderAddress,
depthOnlyDraw.BaseVertex);
if (_traceAgcShader)
{
@@ -6115,7 +6145,8 @@ public static partial class AgcExports
sharedVertexBuffers,
translatedDraw.RenderState,
translatedDraw.DepthTarget,
translatedDraw.PixelShaderAddress);
translatedDraw.PixelShaderAddress,
translatedDraw.BaseVertex);
}
}
else
@@ -6157,7 +6188,8 @@ public static partial class AgcExports
translatedDraw.IndexBuffer,
vertexBuffers,
renderState,
translatedDraw.PixelShaderAddress);
translatedDraw.PixelShaderAddress,
translatedDraw.BaseVertex);
}
else
{
@@ -6460,6 +6492,7 @@ public static partial class AgcExports
AttributeCount: 0,
vertexCount,
state.InstanceCount,
GetBaseVertex(state),
indexed ? CreateGuestIndexBuffer(ctx, state, vertexCount) : null,
textures,
exportEvaluation.GlobalMemoryBindings,
@@ -6676,6 +6709,22 @@ public static partial class AgcExports
}
}
state.UcRegisters.TryGetValue(VgtPrimitiveType, out var earlyPrimitiveType);
if (IsRectListPrimitive(earlyPrimitiveType) &&
(exportEvaluation.VertexInputs is null || exportEvaluation.VertexInputs.Count == 0) &&
!VertexProgramExportsParameters(exportState.Program) &&
GetInterpolatedAttributeCount(pixelState) != 0)
{
ReturnPooledEvaluationArrays(exportEvaluation);
ReturnPooledEvaluationArrays(pixelEvaluation);
error =
$"rect-list-no-param-exports ps_inputs={GetInterpolatedAttributeCount(pixelState)}";
TraceAgcShader(
$"agc.rect_list_skip es=0x{exportShaderAddress:X16} " +
$"ps=0x{pixelShaderAddress:X16} {error}");
return false;
}
// Every bound color target the shader exports to. Deferred renderers
// draw a multi-render-target G-buffer (up to eight slots) in one pass.
// Fall back to slot 0 if we cannot match any export to a bound target.
@@ -6965,6 +7014,7 @@ public static partial class AgcExports
GetInterpolatedAttributeCount(pixelState),
vertexCount,
state.InstanceCount,
GetBaseVertex(state),
indexed ? CreateGuestIndexBuffer(ctx, state, vertexCount) : null,
textures,
globalMemoryBindings,
@@ -7276,6 +7326,22 @@ public static partial class AgcExports
ColorFunc: 0,
};
private static AgcIndexHelpers.ProsperoIndexType GetProsperoIndexType(SubmittedDcbState state) =>
// IndexSize is latched from ItIndexType and from UC VGT_INDEX_TYPE
// writes. Do not fall back to a stale UC value when IndexSize is 0 —
// that mis-classified 16-bit draws as index8 and blanked meshes.
AgcIndexHelpers.Decode(state.IndexSize);
/// <summary>
/// ResolveVertexOffset for the common UC path: GE_INDX_OFFSET is the
/// DrawIndexed vertexOffset / DrawAuto firstVertex. Embedded-fetch SGPR
/// fallback is not required when the game latches this register (GTA UI).
/// </summary>
private static int GetBaseVertex(SubmittedDcbState state) =>
state.UcRegisters.TryGetValue(GeIndxOffset, out var indexOffset)
? unchecked((int)indexOffset)
: 0;
private static GuestIndexBuffer? CreateGuestIndexBuffer(
CpuContext ctx,
SubmittedDcbState state,
@@ -7286,17 +7352,40 @@ public static partial class AgcExports
return null;
}
var is32Bit = state.IndexSize != 0;
var bytesPerIndex = is32Bit ? sizeof(uint) : sizeof(ushort);
var byteOffset = checked((ulong)state.DrawIndexOffset * (uint)bytesPerIndex);
var byteCount = checked((int)(indexCount * (uint)bytesPerIndex));
var data = GuestDataPool.Shared.Rent(byteCount);
var span = data.AsSpan(0, byteCount);
var indexType = GetProsperoIndexType(state);
var guestBytesPerIndex = AgcIndexHelpers.GetGuestStrideBytes(indexType);
var byteOffset = checked((ulong)state.DrawIndexOffset * (uint)guestBytesPerIndex);
var guestByteCount = checked((int)(indexCount * (uint)guestBytesPerIndex));
var address = state.IndexBufferAddress + byteOffset;
// Host backends only bind u16/u32. Expand kIndex8 -> u16.
if (indexType == AgcIndexHelpers.ProsperoIndexType.Index8)
{
var guestData = GuestDataPool.Shared.Rent(guestByteCount);
var guestSpan = guestData.AsSpan(0, guestByteCount);
if (!ctx.Memory.TryRead(address, guestSpan) &&
!KernelMemoryCompatExports.TryReadTrackedLibcHeap(address, guestSpan))
{
GuestDataPool.Shared.Return(guestData);
return null;
}
var hostByteCount = checked((int)(indexCount * sizeof(ushort)));
var hostData = GuestDataPool.Shared.Rent(hostByteCount);
AgcIndexHelpers.ExpandIndex8ToU16(
guestSpan,
hostData.AsSpan(0, hostByteCount));
GuestDataPool.Shared.Return(guestData);
return new GuestIndexBuffer(hostData, hostByteCount, Is32Bit: false, Pooled: true);
}
var is32Bit = indexType == AgcIndexHelpers.ProsperoIndexType.Index32;
var data = GuestDataPool.Shared.Rent(guestByteCount);
var span = data.AsSpan(0, guestByteCount);
if (ctx.Memory.TryRead(address, span) ||
KernelMemoryCompatExports.TryReadTrackedLibcHeap(address, span))
{
return new GuestIndexBuffer(data, byteCount, is32Bit, Pooled: true);
return new GuestIndexBuffer(data, guestByteCount, is32Bit, Pooled: true);
}
GuestDataPool.Shared.Return(data);
@@ -7310,7 +7399,10 @@ public static partial class AgcExports
bool indexed,
out uint recordCount)
{
recordCount = Math.Max(drawCount, Math.Max(state.InstanceCount, 1u));
var baseVertex = (uint)Math.Max(GetBaseVertex(state), 0);
recordCount = Math.Max(
baseVertex + drawCount,
Math.Max(state.InstanceCount, 1u));
if (!indexed)
{
return true;
@@ -7321,8 +7413,8 @@ public static partial class AgcExports
return false;
}
var is32Bit = state.IndexSize != 0;
var bytesPerIndex = is32Bit ? sizeof(uint) : sizeof(ushort);
var indexType = GetProsperoIndexType(state);
var bytesPerIndex = AgcIndexHelpers.GetGuestStrideBytes(indexType);
var byteOffset = checked((ulong)state.DrawIndexOffset * (uint)bytesPerIndex);
var address = state.IndexBufferAddress + byteOffset;
const int chunkBytes = 64 * 1024;
@@ -7347,12 +7439,22 @@ public static partial class AgcExports
for (var index = 0; index < chunkIndices; index++)
{
var value = is32Bit
? BinaryPrimitives.ReadUInt32LittleEndian(
span.Slice(index * sizeof(uint), sizeof(uint)))
: BinaryPrimitives.ReadUInt16LittleEndian(
span.Slice(index * sizeof(ushort), sizeof(ushort)));
if (value == (is32Bit ? uint.MaxValue : ushort.MaxValue))
uint value = indexType switch
{
AgcIndexHelpers.ProsperoIndexType.Index32 =>
BinaryPrimitives.ReadUInt32LittleEndian(
span.Slice(index * sizeof(uint), sizeof(uint))),
AgcIndexHelpers.ProsperoIndexType.Index8 => span[index],
_ => BinaryPrimitives.ReadUInt16LittleEndian(
span.Slice(index * sizeof(ushort), sizeof(ushort))),
};
var restart = indexType switch
{
AgcIndexHelpers.ProsperoIndexType.Index32 => uint.MaxValue,
AgcIndexHelpers.ProsperoIndexType.Index8 => 0xFFu,
_ => ushort.MaxValue,
};
if (value == restart)
{
// Primitive-restart markers do not address vertex data.
continue;
@@ -7372,17 +7474,24 @@ public static partial class AgcExports
}
var indexedRecords = sawIndex && maxIndex != uint.MaxValue
? maxIndex + 1
: 1u;
? baseVertex + maxIndex + 1
: Math.Max(baseVertex + 1, 1u);
recordCount = Math.Max(indexedRecords, Math.Max(state.InstanceCount, 1u));
if (_traceVertexRanges &&
Interlocked.Increment(ref _tracedVertexRangeCount) <= 512)
{
var indexBits = indexType switch
{
AgcIndexHelpers.ProsperoIndexType.Index32 => 32,
AgcIndexHelpers.ProsperoIndexType.Index8 => 8,
_ => 16,
};
Console.Error.WriteLine(
$"[LOADER][TRACE] agc.vertex_range indexed=1 draw_count={drawCount} " +
$"max_index={(sawIndex ? maxIndex : 0)} records={recordCount} " +
$"instances={state.InstanceCount} index_size={(is32Bit ? 32 : 16)} " +
$"index_addr=0x{state.IndexBufferAddress:X16} offset={state.DrawIndexOffset}");
$"max_index={(sawIndex ? maxIndex : 0)} base_vertex={baseVertex} " +
$"records={recordCount} instances={state.InstanceCount} " +
$"index_size={indexBits} index_addr=0x{state.IndexBufferAddress:X16} " +
$"offset={state.DrawIndexOffset}");
}
return true;
}
@@ -7392,6 +7501,20 @@ public static partial class AgcExports
? (packedMasks >> (int)(target * 4)) & 0xFu
: 0;
private static bool VertexProgramExportsParameters(Gen5ShaderProgram program)
{
foreach (var instruction in program.Instructions)
{
if (instruction.Control is Gen5ExportControl export &&
export.Target is >= 32 and < 64)
{
return true;
}
}
return false;
}
private static uint GetInterpolatedAttributeCount(Gen5ShaderState state)
{
var maxAttribute = -1;
@@ -8126,10 +8249,15 @@ public static partial class AgcExports
? $"{entry.Name}=0x{value:X8}"
: $"{entry.Name}=missing"));
var blend = draw.RenderState.Blend;
var rectExpanded = AgcPrimitiveHelpers.GetRectListDrawVertexCount(
draw.PrimitiveType,
draw.VertexCount,
indexed: draw.IndexBuffer is not null,
hasVertexBuffers: draw.VertexInputs.Count > 0);
TraceAgcShader(
$"agc.shader_draw es=0x{draw.ExportShaderAddress:X16} " +
$"ps=0x{draw.PixelShaderAddress:X16} spirv={draw.PixelShader.Payload.Length} " +
$"primitive=0x{draw.PrimitiveType:X} " +
$"primitive=0x{draw.PrimitiveType:X} verts={draw.VertexCount}->{rectExpanded} " +
$"blend={(blend.Enable ? 1 : 0)}:{blend.ColorSrcFactor}/{blend.ColorDstFactor}/{blend.ColorFunc} " +
$"write_mask=0x{blend.WriteMask:X} scissor={scissor} viewport={viewport} " +
$"raster=[{raster}] " +
@@ -9415,36 +9543,51 @@ public static partial class AgcExports
TranslatedGuestDraw draw,
IReadOnlyList<GuestVertexBuffer> vertexBuffers)
{
if (draw.PrimitiveType != 0x11 ||
draw.IndexBuffer is not null ||
vertexBuffers.Count == 0 ||
_rectListTraceCount >= 8 ||
Interlocked.Increment(ref _rectListTraceCount) > 8)
if (!AgcPrimitiveHelpers.IsRectListPrimitive(draw.PrimitiveType) ||
_rectListTraceCount >= 16 ||
Interlocked.Increment(ref _rectListTraceCount) > 16)
{
return;
}
var buffer = vertexBuffers[0];
var stride = Math.Max(buffer.Stride, 4u);
var expanded = AgcPrimitiveHelpers.GetRectListDrawVertexCount(
draw.PrimitiveType,
draw.VertexCount,
indexed: draw.IndexBuffer is not null,
hasVertexBuffers: vertexBuffers.Count > 0);
var text = new System.Text.StringBuilder();
for (var vertex = 0; vertex < 3; vertex++)
{
var baseOffset = (int)(buffer.OffsetBytes + vertex * stride);
if (baseOffset + 16 > buffer.Length)
{
break;
}
text.Append(
$"agc.rectlist prim=0x{draw.PrimitiveType:X} verts={draw.VertexCount}->{expanded} " +
$"indexed={(draw.IndexBuffer is not null ? 1 : 0)} vb={vertexBuffers.Count}");
var x = BitConverter.ToSingle(buffer.Data, baseOffset);
var y = BitConverter.ToSingle(buffer.Data, baseOffset + 4);
var z = BitConverter.ToSingle(buffer.Data, baseOffset + 8);
var w = BitConverter.ToSingle(buffer.Data, baseOffset + 12);
text.Append($" v{vertex}=({x:0.###},{y:0.###},{z:0.###},{w:0.###})");
if (vertexBuffers.Count > 0)
{
var buffer = vertexBuffers[0];
var stride = Math.Max(buffer.Stride, 4u);
text.Append(
$" stride={buffer.Stride} " +
$"fmt={buffer.DataFormat}/{buffer.NumberFormat}x{buffer.ComponentCount}");
for (var vertex = 0; vertex < 3; vertex++)
{
var baseOffset = (int)(buffer.OffsetBytes + vertex * stride);
if (baseOffset + 16 > buffer.Length)
{
break;
}
var x = BitConverter.ToSingle(buffer.Data, baseOffset);
var y = BitConverter.ToSingle(buffer.Data, baseOffset + 4);
var z = BitConverter.ToSingle(buffer.Data, baseOffset + 8);
var w = BitConverter.ToSingle(buffer.Data, baseOffset + 12);
text.Append($" v{vertex}=({x:0.###},{y:0.###},{z:0.###},{w:0.###})");
}
}
else
{
text.Append(" procedural=1");
}
TraceAgcShader(
$"agc.rectlist verts={draw.VertexCount} stride={buffer.Stride} " +
$"fmt={buffer.DataFormat}/{buffer.NumberFormat}x{buffer.ComponentCount}{text}");
TraceAgcShader(text.ToString());
}
private static int _textureDumpCount;
@@ -11709,6 +11852,9 @@ public static partial class AgcExports
private static bool IsEsGeometryShaderType(byte shaderType) =>
shaderType is 2 or 6;
private static bool IsRectListPrimitive(uint primitiveType) =>
AgcPrimitiveHelpers.IsRectListPrimitive(primitiveType);
private static int SetIndirectPatchAddress(CpuContext ctx, string registerSpace)
{
var commandAddress = ctx[CpuRegister.Rdi];
+49
View File
@@ -0,0 +1,49 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
namespace SharpEmu.Libs.Agc;
/// <summary>Prospero/AGC IndexType helpers (gpu_defs / renderDraw index8 expand).</summary>
internal static class AgcIndexHelpers
{
internal enum ProsperoIndexType : uint
{
Index16 = 0,
Index32 = 1,
Index8 = 2,
}
internal static ProsperoIndexType Decode(uint raw) =>
(raw & 0x3u) switch
{
1 => ProsperoIndexType.Index32,
2 => ProsperoIndexType.Index8,
_ => ProsperoIndexType.Index16,
};
internal static int GetGuestStrideBytes(ProsperoIndexType indexType) =>
indexType switch
{
ProsperoIndexType.Index32 => sizeof(uint),
ProsperoIndexType.Index8 => sizeof(byte),
_ => sizeof(ushort),
};
/// <summary>Expand guest u8 indices to host u16 (Vulkan/Metal bindable).</summary>
internal static void ExpandIndex8ToU16(ReadOnlySpan<byte> source, Span<byte> destination)
{
if (destination.Length < source.Length * sizeof(ushort))
{
throw new ArgumentException("destination too small for u8->u16 expansion.");
}
for (var index = 0; index < source.Length; index++)
{
BinaryPrimitives.WriteUInt16LittleEndian(
destination.Slice(index * sizeof(ushort), sizeof(ushort)),
source[index]);
}
}
}
@@ -0,0 +1,107 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.Agc;
/// <summary>
/// Shared Prospero/AGC primitive-type helpers (renderDraw / gpu_defs).
/// </summary>
internal static class AgcPrimitiveHelpers
{
internal const uint PrimitiveRectList = 7;
internal const uint PrimitiveRectListLegacy = 0x11;
internal enum GsOutputPrimitiveType : uint
{
Points = 0,
Lines = 1,
Triangles = 2,
Rectangle2D = 3,
RectList = 4,
}
internal static bool IsRectListPrimitive(uint primitiveType) =>
primitiveType is PrimitiveRectList or PrimitiveRectListLegacy;
/// <summary>
/// Maps draw prim type to VGT_GS_OUT_PRIM_TYPE when NGG is not enabled
/// on the GS (GraphicsPrimitiveTypeToGsOut).
/// </summary>
internal static uint PrimitiveTypeToGsOut(uint primitiveType) =>
primitiveType switch
{
1 => (uint)GsOutputPrimitiveType.Points, // PointList
2 or 3 or 10 or 11 or 18 => (uint)GsOutputPrimitiveType.Lines,
PrimitiveRectList => (uint)GsOutputPrimitiveType.Rectangle2D,
PrimitiveRectListLegacy => (uint)GsOutputPrimitiveType.RectList,
_ => (uint)GsOutputPrimitiveType.Triangles,
};
/// <summary>
/// Rect-list auto-draw topology selection.
/// NGG single-rect UI quads (DualSense prompts) submit count 1/3/4 and
/// must become a 4-vert triangle strip — even when the VS has embedded
/// vertex-buffer fetches (those still show up as host VBs). Indexed and
/// larger auto counts stay triangle list so the loading video is safe.
/// </summary>
internal static bool ShouldDrawRectListAsTriangleStrip(
uint primitiveType,
bool indexed,
uint vertexCount,
bool hasVertexBuffers = false)
{
_ = hasVertexBuffers;
if (indexed || !IsRectListPrimitive(primitiveType))
{
return false;
}
if (primitiveType == PrimitiveRectListLegacy)
{
return true;
}
// NGG kRectList: strip for auto + ngg_rectlist_draw.
// Restrict to the single-rect counts GTA UI actually submits.
return vertexCount is 1 or 3 or 4;
}
/// <summary>
/// Host vertex count for auto rect-list draws that expand to a strip.
/// NGG single-rect: always 4. Legacy 0x11: 3 -> 4.
/// </summary>
internal static uint GetRectListDrawVertexCount(
uint primitiveType,
uint vertexCount,
bool indexed,
bool hasVertexBuffers = false)
{
if (!ShouldDrawRectListAsTriangleStrip(
primitiveType,
indexed,
vertexCount,
hasVertexBuffers))
{
return vertexCount;
}
if (primitiveType == PrimitiveRectList)
{
return 4;
}
if (primitiveType == PrimitiveRectListLegacy && vertexCount == 3)
{
return 4;
}
return vertexCount;
}
/// <summary>
/// Legacy helper — prefer
/// <see cref="ShouldDrawRectListAsTriangleStrip"/>.
/// </summary>
internal static bool IsRectListTriangleStrip(uint primitiveType) =>
IsRectListPrimitive(primitiveType);
}
+4 -2
View File
@@ -109,7 +109,8 @@ internal interface IGuestGpuBackend
GuestIndexBuffer? indexBuffer = null,
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
GuestRenderState? renderState = null,
ulong shaderAddress = 0);
ulong shaderAddress = 0,
int baseVertex = 0);
void SubmitOffscreenTranslatedDraw(
IGuestCompiledShader pixelShader,
@@ -125,7 +126,8 @@ internal interface IGuestGpuBackend
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
GuestRenderState? renderState = null,
GuestDepthTarget? depthTarget = null,
ulong shaderAddress = 0);
ulong shaderAddress = 0,
int baseVertex = 0);
void SubmitStorageTranslatedDraw(
IGuestCompiledShader pixelShader,
@@ -253,7 +253,8 @@ internal sealed class MetalGuestGpuBackend : IGuestGpuBackend
GuestIndexBuffer? indexBuffer = null,
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
GuestRenderState? renderState = null,
ulong shaderAddress = 0) =>
ulong shaderAddress = 0,
int baseVertex = 0) =>
MetalVideoPresenter.SubmitDepthOnlyTranslatedDraw(
Msl(pixelShader),
textures,
@@ -267,7 +268,8 @@ internal sealed class MetalGuestGpuBackend : IGuestGpuBackend
indexBuffer,
vertexBuffers,
renderState,
shaderAddress);
shaderAddress,
baseVertex);
public void SubmitOffscreenTranslatedDraw(
IGuestCompiledShader pixelShader,
@@ -283,7 +285,8 @@ internal sealed class MetalGuestGpuBackend : IGuestGpuBackend
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
GuestRenderState? renderState = null,
GuestDepthTarget? depthTarget = null,
ulong shaderAddress = 0) =>
ulong shaderAddress = 0,
int baseVertex = 0) =>
MetalVideoPresenter.SubmitOffscreenTranslatedDraw(
Msl(pixelShader),
textures,
@@ -298,7 +301,8 @@ internal sealed class MetalGuestGpuBackend : IGuestGpuBackend
vertexBuffers,
renderState,
depthTarget,
shaderAddress);
shaderAddress,
baseVertex);
public void SubmitStorageTranslatedDraw(
IGuestCompiledShader pixelShader,
@@ -1,6 +1,7 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.Libs.Agc;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Metal;
@@ -87,7 +88,8 @@ internal static partial class MetalVideoPresenter
uint InstanceCount,
uint PrimitiveType,
GuestIndexBuffer? IndexBuffer,
GuestRenderState RenderState);
GuestRenderState RenderState,
int BaseVertex = 0);
private sealed record OffscreenGuestDraw(
TranslatedGuestDraw Draw,
@@ -245,7 +247,8 @@ internal static partial class MetalVideoPresenter
IReadOnlyList<GuestVertexBuffer>? vertexBuffers,
GuestRenderState? renderState,
GuestDepthTarget? depthTarget,
ulong shaderAddress)
ulong shaderAddress,
int baseVertex = 0)
{
if (targets.Count == 0)
{
@@ -293,7 +296,8 @@ internal static partial class MetalVideoPresenter
instanceCount,
primitiveType,
indexBuffer,
effectiveRenderState),
effectiveRenderState,
baseVertex),
ToArray(targets),
depthTarget,
PublishTarget: true,
@@ -321,7 +325,8 @@ internal static partial class MetalVideoPresenter
GuestIndexBuffer? indexBuffer,
IReadOnlyList<GuestVertexBuffer>? vertexBuffers,
GuestRenderState? renderState,
ulong shaderAddress)
ulong shaderAddress,
int baseVertex = 0)
{
if (depthTarget.Address == 0 || depthTarget.Width == 0 || depthTarget.Height == 0)
{
@@ -348,7 +353,8 @@ internal static partial class MetalVideoPresenter
instanceCount,
primitiveType,
indexBuffer,
renderState ?? GuestRenderState.Default),
renderState ?? GuestRenderState.Default,
baseVertex),
[new GuestRenderTarget(Address: 0, depthTarget.Width, depthTarget.Height, Format: 10, NumberType: 0)],
depthTarget,
PublishTarget: false,
@@ -981,17 +987,38 @@ internal static partial class MetalVideoPresenter
private static void EncodeDrawCall(nint encoder, TranslatedGuestDraw draw)
{
var primitive = GetPrimitiveType(draw.PrimitiveType);
var vertexCount = draw.PrimitiveType == 0x11 && draw.IndexBuffer is null
? 4u
: draw.VertexCount;
var indexed = draw.IndexBuffer is not null;
var hasVertexBuffers = draw.VertexBuffers.Length > 0;
var primitive = GetPrimitiveType(
draw.PrimitiveType,
indexed,
draw.VertexCount,
hasVertexBuffers);
var vertexCount = AgcPrimitiveHelpers.GetRectListDrawVertexCount(
draw.PrimitiveType,
draw.VertexCount,
indexed,
hasVertexBuffers);
var baseVertex = (nuint)Math.Max(draw.BaseVertex, 0);
if (draw.IndexBuffer is { } indexBuffer)
{
var device = MetalNative.Send(encoder, MetalNative.Selector("device"));
var slice = AllocateUpload(
device, Math.Max(indexBuffer.Length, 1), out var buffer, out var offset);
indexBuffer.Data.AsSpan(0, Math.Min(indexBuffer.Length, indexBuffer.Data.Length))
.CopyTo(slice);
var source = indexBuffer.Data.AsSpan(
0,
Math.Min(indexBuffer.Length, indexBuffer.Data.Length));
// Metal drawIndexed without baseVertex: bake GE_INDX_OFFSET into
// the uploaded indices so glyph batches still hit the right verts.
if (draw.BaseVertex != 0)
{
BakeBaseVertexIntoIndices(source, slice, indexBuffer.Is32Bit, draw.BaseVertex);
}
else
{
source.CopyTo(slice);
}
MetalNative.SendDrawIndexedPrimitives(
encoder,
MetalNative.Selector("drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:"),
@@ -1012,12 +1039,46 @@ internal static partial class MetalVideoPresenter
encoder,
MetalNative.Selector("drawPrimitives:vertexStart:vertexCount:instanceCount:"),
primitive,
0,
baseVertex,
vertexCount,
Math.Max(draw.InstanceCount, 1));
}
}
private static void BakeBaseVertexIntoIndices(
ReadOnlySpan<byte> source,
Span<byte> destination,
bool is32Bit,
int baseVertex)
{
if (is32Bit)
{
var count = source.Length / sizeof(uint);
for (var index = 0; index < count; index++)
{
var value = BinaryPrimitives.ReadUInt32LittleEndian(
source.Slice(index * sizeof(uint), sizeof(uint)));
var adjusted = unchecked((uint)(value + baseVertex));
BinaryPrimitives.WriteUInt32LittleEndian(
destination.Slice(index * sizeof(uint), sizeof(uint)),
adjusted);
}
return;
}
var shortCount = source.Length / sizeof(ushort);
for (var index = 0; index < shortCount; index++)
{
var value = BinaryPrimitives.ReadUInt16LittleEndian(
source.Slice(index * sizeof(ushort), sizeof(ushort)));
var adjusted = unchecked((ushort)(value + baseVertex));
BinaryPrimitives.WriteUInt16LittleEndian(
destination.Slice(index * sizeof(ushort), sizeof(ushort)),
adjusted);
}
}
private static bool TryGetDrawPipeline(
nint device,
TranslatedGuestDraw draw,
@@ -2062,13 +2123,30 @@ internal static partial class MetalVideoPresenter
return 3;
case 6:
case 0x11:
return 4;
default:
return 3;
}
}
private static nuint GetPrimitiveType(
uint guestPrimitiveType,
bool indexed,
uint vertexCount,
bool hasVertexBuffers)
{
if (AgcPrimitiveHelpers.ShouldDrawRectListAsTriangleStrip(
guestPrimitiveType,
indexed,
vertexCount,
hasVertexBuffers))
{
return 4; // MTLPrimitiveTypeTriangleStrip
}
return GetPrimitiveType(guestPrimitiveType);
}
private static bool IsIntegerFormat(Gen5PixelOutputKind kind) =>
kind is Gen5PixelOutputKind.Uint or Gen5PixelOutputKind.Sint;
@@ -181,7 +181,8 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
GuestIndexBuffer? indexBuffer = null,
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
GuestRenderState? renderState = null,
ulong shaderAddress = 0) =>
ulong shaderAddress = 0,
int baseVertex = 0) =>
VulkanVideoPresenter.SubmitDepthOnlyTranslatedDraw(
Spirv(pixelShader),
textures,
@@ -195,7 +196,8 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
indexBuffer,
vertexBuffers,
renderState,
shaderAddress);
shaderAddress,
baseVertex);
public void SubmitOffscreenTranslatedDraw(
IGuestCompiledShader pixelShader,
@@ -211,7 +213,8 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
GuestRenderState? renderState = null,
GuestDepthTarget? depthTarget = null,
ulong shaderAddress = 0) =>
ulong shaderAddress = 0,
int baseVertex = 0) =>
VulkanVideoPresenter.SubmitOffscreenTranslatedDraw(
Spirv(pixelShader),
textures,
@@ -226,7 +229,8 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
vertexBuffers,
renderState,
depthTarget,
shaderAddress);
shaderAddress,
baseVertex);
public void SubmitStorageTranslatedDraw(
IGuestCompiledShader pixelShader,
@@ -45,7 +45,8 @@ internal sealed record VulkanTranslatedGuestDraw(
uint InstanceCount,
uint PrimitiveType,
GuestIndexBuffer? IndexBuffer,
GuestRenderState RenderState);
GuestRenderState RenderState,
int BaseVertex = 0);
internal sealed record VulkanOffscreenGuestDraw(
VulkanTranslatedGuestDraw Draw,
@@ -469,7 +470,8 @@ internal static unsafe class VulkanVideoPresenter
uint.TryParse(Environment.GetEnvironmentVariable("SHARPEMU_SKIP_TALL_COMPUTE_Z"), out var z)
? z
: 0;
private const uint GuestPrimitiveRectList = 0x11;
private const uint GuestPrimitiveRectList = AgcPrimitiveHelpers.PrimitiveRectListLegacy;
private const uint GuestPrimitiveRectListNgg = AgcPrimitiveHelpers.PrimitiveRectList;
private static readonly object _gate = new();
private readonly record struct PendingGuestWork(
@@ -1029,7 +1031,8 @@ internal static unsafe class VulkanVideoPresenter
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
GuestRenderState? renderState = null,
GuestDepthTarget? depthTarget = null,
ulong shaderAddress = 0)
ulong shaderAddress = 0,
int baseVertex = 0)
{
SubmitOffscreenTranslatedDraw(
pixelSpirv,
@@ -1045,7 +1048,8 @@ internal static unsafe class VulkanVideoPresenter
vertexBuffers,
renderState,
depthTarget,
shaderAddress);
shaderAddress,
baseVertex);
}
// Manual scans (targets are <= 8) so the per-draw validation does not
@@ -1098,7 +1102,8 @@ internal static unsafe class VulkanVideoPresenter
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
GuestRenderState? renderState = null,
GuestDepthTarget? depthTarget = null,
ulong shaderAddress = 0)
ulong shaderAddress = 0,
int baseVertex = 0)
{
if (pixelSpirv.Length == 0 ||
targets.Count == 0 ||
@@ -1165,7 +1170,8 @@ internal static unsafe class VulkanVideoPresenter
instanceCount,
primitiveType,
indexBuffer,
effectiveRenderState),
effectiveRenderState,
baseVertex),
targets.ToArray(),
depthTarget,
PublishTarget: true,
@@ -1190,7 +1196,8 @@ internal static unsafe class VulkanVideoPresenter
GuestIndexBuffer? indexBuffer = null,
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
GuestRenderState? renderState = null,
ulong shaderAddress = 0)
ulong shaderAddress = 0,
int baseVertex = 0)
{
if (pixelSpirv.Length == 0 ||
depthTarget.Address == 0 ||
@@ -1220,7 +1227,8 @@ internal static unsafe class VulkanVideoPresenter
instanceCount,
primitiveType,
indexBuffer,
renderState ?? GuestRenderState.Default),
renderState ?? GuestRenderState.Default,
baseVertex),
[new GuestRenderTarget(
Address: 0,
depthTarget.Width,
@@ -3129,6 +3137,7 @@ internal static unsafe class VulkanVideoPresenter
public bool Index32Bit;
public uint VertexCount = 3;
public uint InstanceCount = 1;
public int BaseVertex;
public PrimitiveTopology Topology = PrimitiveTopology.TriangleList;
public GuestBlendState[] Blends = [GuestBlendState.Default];
public GuestBlendConstant BlendConstant;
@@ -6190,9 +6199,18 @@ internal static unsafe class VulkanVideoPresenter
GlobalMemoryBuffers =
new GlobalBufferResource[draw.GlobalMemoryBuffers.Count],
VertexBuffers = new VertexBufferResource[draw.VertexBuffers.Count],
VertexCount = GetDrawVertexCount(draw.PrimitiveType, draw.VertexCount, draw.IndexBuffer),
VertexCount = GetDrawVertexCount(
draw.PrimitiveType,
draw.VertexCount,
draw.IndexBuffer,
draw.VertexBuffers.Count > 0),
InstanceCount = Math.Max(draw.InstanceCount, 1),
Topology = GetPrimitiveTopology(draw.PrimitiveType),
BaseVertex = draw.BaseVertex,
Topology = GetPrimitiveTopology(
draw.PrimitiveType,
indexed: draw.IndexBuffer is not null,
vertexCount: draw.VertexCount,
hasVertexBuffers: draw.VertexBuffers.Count > 0),
Blends = draw.RenderState.Blends.ToArray(),
BlendConstant = draw.RenderState.BlendConstant,
Scissor = draw.RenderState.Scissor,
@@ -9680,7 +9698,11 @@ internal static unsafe class VulkanVideoPresenter
_vk.FreeMemory(_device, allocation.Memory, null);
}
private static PrimitiveTopology GetPrimitiveTopology(uint primitiveType) =>
private static PrimitiveTopology GetPrimitiveTopology(
uint primitiveType,
bool indexed,
uint vertexCount,
bool hasVertexBuffers) =>
primitiveType switch
{
1 => PrimitiveTopology.PointList,
@@ -9688,7 +9710,12 @@ internal static unsafe class VulkanVideoPresenter
3 => PrimitiveTopology.LineStrip,
5 => PrimitiveTopology.TriangleFan,
6 => PrimitiveTopology.TriangleStrip,
GuestPrimitiveRectList => PrimitiveTopology.TriangleStrip,
GuestPrimitiveRectListNgg or GuestPrimitiveRectList
when AgcPrimitiveHelpers.ShouldDrawRectListAsTriangleStrip(
primitiveType,
indexed,
vertexCount,
hasVertexBuffers) => PrimitiveTopology.TriangleStrip,
_ => PrimitiveTopology.TriangleList,
};
@@ -9846,15 +9873,13 @@ internal static unsafe class VulkanVideoPresenter
private static uint GetDrawVertexCount(
uint primitiveType,
uint vertexCount,
GuestIndexBuffer? indexBuffer)
{
if (primitiveType == GuestPrimitiveRectList && indexBuffer is null)
{
return 4;
}
return vertexCount;
}
GuestIndexBuffer? indexBuffer,
bool hasVertexBuffers) =>
AgcPrimitiveHelpers.GetRectListDrawVertexCount(
primitiveType,
vertexCount,
indexed: indexBuffer is not null,
hasVertexBuffers);
private static BlendFactor ToVkBlendFactor(uint factor) =>
factor switch
@@ -15737,12 +15762,14 @@ internal static unsafe class VulkanVideoPresenter
resources.IndexBuffer,
0,
resources.Index32Bit ? IndexType.Uint32 : IndexType.Uint16);
// vertexOffset = ResolveVertexOffset(GE_INDX_OFFSET).
// GTA UI glyphs use relative indices + a nonzero base vertex.
_vk.CmdDrawIndexed(
_commandBuffer,
resources.VertexCount,
resources.InstanceCount,
0,
0,
resources.BaseVertex,
0);
}
else
@@ -15751,7 +15778,7 @@ internal static unsafe class VulkanVideoPresenter
_commandBuffer,
resources.VertexCount,
resources.InstanceCount,
0,
(uint)Math.Max(resources.BaseVertex, 0),
0);
}
@@ -0,0 +1,95 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.Libs.Agc;
using Xunit;
namespace SharpEmu.Libs.Tests.Agc;
/// <summary>
/// Regression coverage for the AGC UI path: index8 expansion and rect-list
/// vertex counts / topology selection.
/// </summary>
public sealed class AgcRectListIndexHelpersTests
{
[Theory]
[InlineData(0u, 0u, 2)] // Index16
[InlineData(1u, 1u, 4)] // Index32
[InlineData(2u, 2u, 1)] // Index8
[InlineData(0x402u, 2u, 1)] // UC 0x400|size -> Index8
public void IndexType_DecodeAndStride_MatchProspero(uint raw, uint expected, int stride)
{
var decoded = AgcIndexHelpers.Decode(raw);
Assert.Equal((AgcIndexHelpers.ProsperoIndexType)expected, decoded);
Assert.Equal(stride, AgcIndexHelpers.GetGuestStrideBytes(decoded));
}
[Fact]
public void ExpandIndex8ToU16_PreservesValues()
{
ReadOnlySpan<byte> source = [0x00, 0x01, 0xFF, 0x7F];
Span<byte> destination = stackalloc byte[8];
AgcIndexHelpers.ExpandIndex8ToU16(source, destination);
Assert.Equal(0, BinaryPrimitives.ReadUInt16LittleEndian(destination[..2]));
Assert.Equal(1, BinaryPrimitives.ReadUInt16LittleEndian(destination.Slice(2, 2)));
Assert.Equal(255, BinaryPrimitives.ReadUInt16LittleEndian(destination.Slice(4, 2)));
Assert.Equal(127, BinaryPrimitives.ReadUInt16LittleEndian(destination.Slice(6, 2)));
}
[Theory]
// NGG single-rect UI (DualSense): expand even when VBs are present
[InlineData(7u, 3u, false, true, 4u)]
[InlineData(7u, 1u, false, false, 4u)]
[InlineData(7u, 4u, false, true, 4u)]
// Indexed / multi-vert auto: keep guest count (loading video)
[InlineData(7u, 3u, true, false, 3u)]
[InlineData(7u, 6u, false, true, 6u)]
[InlineData(7u, 4u, true, true, 4u)]
[InlineData(0x11u, 3u, false, false, 4u)]
[InlineData(0x11u, 6u, false, false, 6u)]
[InlineData(4u, 3u, false, false, 3u)]
public void RectListDrawVertexCount_MatchesExpansion(
uint primitiveType,
uint vertexCount,
bool indexed,
bool hasVertexBuffers,
uint expected)
{
Assert.Equal(
expected,
AgcPrimitiveHelpers.GetRectListDrawVertexCount(
primitiveType,
vertexCount,
indexed,
hasVertexBuffers));
}
[Theory]
[InlineData(7u, false, 3u, true, true)]
[InlineData(7u, false, 6u, true, false)]
[InlineData(7u, true, 3u, false, false)]
[InlineData(0x11u, false, 3u, true, true)]
[InlineData(0x11u, true, 3u, false, false)]
public void RectListTriangleStrip_MatchesGuards(
uint primitiveType,
bool indexed,
uint vertexCount,
bool hasVertexBuffers,
bool expected) =>
Assert.Equal(
expected,
AgcPrimitiveHelpers.ShouldDrawRectListAsTriangleStrip(
primitiveType,
indexed,
vertexCount,
hasVertexBuffers));
[Theory]
[InlineData(7u, (uint)AgcPrimitiveHelpers.GsOutputPrimitiveType.Rectangle2D)]
[InlineData(0x11u, (uint)AgcPrimitiveHelpers.GsOutputPrimitiveType.RectList)]
[InlineData(4u, (uint)AgcPrimitiveHelpers.GsOutputPrimitiveType.Triangles)]
[InlineData(1u, (uint)AgcPrimitiveHelpers.GsOutputPrimitiveType.Points)]
public void PrimitiveTypeToGsOut_MatchesProspero(uint primitiveType, uint expected) =>
Assert.Equal(expected, AgcPrimitiveHelpers.PrimitiveTypeToGsOut(primitiveType));
}