mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-28 05:30:57 +08:00
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:
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user