diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index 5348d41..3c13408 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -2,9 +2,10 @@ // SPDX-License-Identifier: GPL-2.0-or-later using SharpEmu.HLE; +using SharpEmu.Libs.Kernel; using SharpEmu.Libs.VideoOut; using System.Buffers.Binary; -using System.Threading; +using System.Runtime.CompilerServices; namespace SharpEmu.Libs.Agc; @@ -16,7 +17,9 @@ public static class AgcExports private const uint ItIndexBufferSize = 0x13; private const uint ItIndexBase = 0x26; private const uint ItIndexType = 0x2A; + private const uint ItNumInstances = 0x2F; private const uint ItDrawIndexOffset2 = 0x35; + private const uint ItEventWrite = 0x46; private const uint ItSetShReg = 0x76; private const uint RZero = 0x00; private const uint RDrawIndexAuto = 0x04; @@ -25,11 +28,16 @@ public static class AgcExports private const uint RShRegsIndirect = 0x11; private const uint RCxRegsIndirect = 0x12; private const uint RUcRegsIndirect = 0x13; + private const uint RAcquireMem = 0x14; private const uint RFlip = 0x17; private const uint SpiShaderPgmLoPs = 0x8; private const uint SpiShaderPgmHiPs = 0x9; private const uint SpiShaderPgmLoEs = 0xC8; private const uint SpiShaderPgmHiEs = 0xC9; + private const uint SpiPsInputEna = 0x1B3; + private const uint SpiPsInputAddr = 0x1B4; + private const uint ComputePgmLo = 0x20C; + private const uint ComputePgmHi = 0x20D; private const uint SpiPsInputCntl0 = 0x191; private const uint VgtPrimitiveType = 0x242; private const uint PsTextureUserDataRegister = 0xC; @@ -37,6 +45,10 @@ public static class AgcExports private const uint Gen5TextureType2D = 9; private const ulong VideoOutPixelFormatA8R8G8B8Srgb = 0x80000000; private const ulong VideoOutPixelFormatA8B8G8R8Srgb = 0x80002200; + private const uint RegisterDefaultsVersion7 = 7; + private const uint RegisterDefaultsVersion8 = 8; + private const int RegisterDefaultsSize = 0x40; + private const int RegisterDefaultBlockSize = 16 * 8; private const ulong ShaderUserDataOffset = 0x08; private const ulong ShaderCodeOffset = 0x10; @@ -58,9 +70,86 @@ public static class AgcExports private const ulong ShaderSpecialVgtGsOutPrimTypeOffset = 0x20; private const ulong ShaderSpecialGeUserVgprEnOffset = 0x28; private const uint CbSetShRegisterRangeMarker = 0x6875000D; + private static readonly object _submitTraceGate = new(); + private static readonly HashSet _tracedDcbSizes = new(); + private static readonly HashSet<(ulong Es, ulong Ps, GuestDrawKind Kind)> _tracedShaderTranslations = new(); private static readonly object _softwarePresenterGate = new(); private static readonly Dictionary<(ulong Source, ulong Destination), ulong> _softwarePresenterFingerprints = new(); - private static int _submitTraceCaptured; + private static readonly object _registerDefaultsGate = new(); + private static readonly ConditionalWeakTable _registerDefaultsAllocations = new(); + + private static readonly RegisterDefaultGroup[] PrimaryRegisterDefaults = + [ + new(0, 3, 0x0BC65DA4, [new(0x08F, 0)]), + new(0, 4, 0x9E5AD592, [new(0x08E, 0)]), + new(0, 12, 0x6DE4C312, [new(0x203, 0)]), + new(0, 72, 0x38E92C91, + [ + new(0x318, 0), + new(0x31B, 0), + new(0x31C, 0), + new(0x31D, 0), + new(0x31E, 0x48), + new(0x31F, 0), + new(0x321, 0), + new(0x323, 0), + new(0x324, 0), + new(0x325, 0), + new(0x390, 0), + new(0x398, 0), + new(0x3A0, 0), + new(0x3A8, 0), + new(0x3B0, 0), + new(0x3B8, 0x0006C000), + ]), + new(0, 73, 0x0B177B43, [new(0x00C, 0), new(0x00D, 0x40004000)]), + new(0, 74, 0x48531062, [new(0x191, 0)]), + new(0, 76, 0x7690AF6F, + [ + new(0x10F, 0x4E7E0000), + new(0x111, 0x4E7E0000), + new(0x113, 0x4E7E0000), + new(0x110, 0), + new(0x112, 0), + new(0x114, 0), + new(0x094, 0x80000000), + new(0x095, 0x40004000), + new(0x0B4, 0), + new(0x0B5, 0), + ]), + new(1, 13, 0xC918DF3E, [new(0x20C, 0), new(0x20D, 0)]), + new(1, 14, 0xC9751C9C, [new(0x0C8, 0), new(0x0C9, 0)]), + new(1, 18, 0xC9E01B31, [new(0x008, 0), new(0x009, 0)]), + new(2, 3, 0x105971C2, [new(0x25B, 0)]), + new(2, 7, 0x40D49AD1, [new(0x262, 0)]), + new(2, 12, 0x9EBFAB10, [new(0x242, 0)]), + ]; + + private static readonly RegisterDefaultGroup[] InternalRegisterDefaults = + [ + new(0, 0, 0x8FB4EDB5, [new(0x00E, 0)]), + new(0, 1, 0xB994AD29, [new(0x2AF, 0)]), + new(0, 2, 0xD427322F, [new(0x314, 0)]), + new(0, 3, 0xF58FEA31, [new(0x1B5, 0)]), + new(1, 0, 0x6AC156EF, [new(0x216, 0)]), + new(1, 1, 0x6AC15610, [new(0x217, 0)]), + new(1, 2, 0x6AC15009, [new(0x219, 0)]), + new(1, 3, 0x6AC153BA, [new(0x21A, 0)]), + new(1, 4, 0xBE7DCD73, [new(0x27D, 0)]), + new(1, 5, 0x0C4B1438, [new(0x22A, 0)]), + new(1, 6, 0xDB00D71A, [new(0x204, 0)]), + new(1, 7, 0xDB00D249, [new(0x205, 0)]), + new(1, 8, 0xDB00EC60, [new(0x206, 0)]), + new(1, 9, 0x0C4D6FE4, [new(0x080, 0)]), + new(1, 10, 0x0C4A80EF, [new(0x100, 0)]), + new(1, 11, 0x0DD283E7, [new(0x006, 0)]), + new(1, 12, 0xC620E68C, [new(0x081, 0)]), + new(1, 13, 0xC67EFACF, [new(0x101, 0)]), + new(1, 14, 0xD9E6D9F7, [new(0x001, 0)]), + new(2, 0, 0x31F34B9F, [new(0x24F, 0)]), + new(2, 1, 0xAC0F9E76, [new(0x80003FFF, 0)]), + new(2, 2, 0x929FD95D, [new(0x250, 0)]), + ]; private readonly record struct TextureDescriptor( ulong Address, @@ -70,6 +159,57 @@ public static class AgcExports uint TileMode, uint Type); + private sealed class SubmittedDcbState + { + public Dictionary CxRegisters { get; } = new(); + public Dictionary ShRegisters { get; } = new(); + public Dictionary UcRegisters { get; } = new(); + } + + private readonly record struct RegisterDefaultValue(uint Offset, uint Value); + + private readonly record struct RegisterDefaultGroup( + uint Space, + uint Index, + uint Type, + RegisterDefaultValue[] Registers); + + private sealed record RegisterDefaultsAllocation(ulong Primary, ulong Internal); + + [SysAbiExport( + Nid = "23LRUSvYu1M", + ExportName = "sceAgcInit", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int Init(CpuContext ctx) + { + var stateAddress = ctx[CpuRegister.Rdi]; + var version = (uint)ctx[CpuRegister.Rsi]; + if (stateAddress == 0 || !IsSupportedRegisterDefaultsVersion(version)) + { + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); + } + + TraceAgc($"agc.init state=0x{stateAddress:X16} version={version}"); + return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); + } + + [SysAbiExport( + Nid = "2JtWUUiYBXs", + ExportName = "sceAgcGetRegisterDefaults2", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int GetRegisterDefaults2(CpuContext ctx) => + ReturnRegisterDefaults(ctx, internalDefaults: false); + + [SysAbiExport( + Nid = "wRbq6ZjNop4", + ExportName = "sceAgcGetRegisterDefaults2Internal", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int GetRegisterDefaults2Internal(CpuContext ctx) => + ReturnRegisterDefaults(ctx, internalDefaults: true); + [SysAbiExport( Nid = "f3dg2CSgRKY", ExportName = "sceAgcCreateShader", @@ -80,7 +220,7 @@ public static class AgcExports var destinationAddress = ctx[CpuRegister.Rdi]; var headerAddress = ctx[CpuRegister.Rsi]; var codeAddress = ctx[CpuRegister.Rdx]; - if (destinationAddress == 0 || headerAddress == 0 || codeAddress == 0) + if (headerAddress == 0 || codeAddress == 0) { return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } @@ -128,7 +268,8 @@ public static class AgcExports return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } - if (!ctx.TryWriteUInt64(destinationAddress, headerAddress)) + if (destinationAddress != 0 && + !ctx.TryWriteUInt64(destinationAddress, headerAddress)) { return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } @@ -430,6 +571,136 @@ public static class AgcExports return ReturnPointer(ctx, commandAddress); } + [SysAbiExport( + Nid = "tSBxhAPyytQ", + ExportName = "sceAgcDcbSetNumInstances", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DcbSetNumInstances(CpuContext ctx) + { + var commandBufferAddress = ctx[CpuRegister.Rdi]; + var instanceCount = (uint)ctx[CpuRegister.Rsi]; + if (commandBufferAddress == 0) + { + return ReturnPointer(ctx, 0); + } + + if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) || + !TryWriteUInt32(ctx, commandAddress, Pm4(2, ItNumInstances, 0)) || + !TryWriteUInt32(ctx, commandAddress + 4, instanceCount)) + { + return ReturnPointer(ctx, 0); + } + + TraceAgc($"agc.dcb_set_num_instances buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} count={instanceCount}"); + return ReturnPointer(ctx, commandAddress); + } + + [SysAbiExport( + Nid = "Yw0jKSqop+E", + ExportName = "sceAgcDcbDrawIndexAuto", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DcbDrawIndexAuto(CpuContext ctx) + { + var commandBufferAddress = ctx[CpuRegister.Rdi]; + var indexCount = (uint)ctx[CpuRegister.Rsi]; + var modifier = ctx[CpuRegister.Rdx]; + if (commandBufferAddress == 0 || modifier != 0x4000_0000) + { + return ReturnPointer(ctx, 0); + } + + if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 7, out var commandAddress) || + !TryWriteUInt32(ctx, commandAddress, Pm4(7, ItNop, RDrawIndexAuto)) || + !TryWriteUInt32(ctx, commandAddress + 4, indexCount) || + !TryWriteUInt32(ctx, commandAddress + 8, 0) || + !TryWriteUInt32(ctx, commandAddress + 12, 0) || + !TryWriteUInt32(ctx, commandAddress + 16, 0) || + !TryWriteUInt32(ctx, commandAddress + 20, 0) || + !TryWriteUInt32(ctx, commandAddress + 24, 0)) + { + return ReturnPointer(ctx, 0); + } + + TraceAgc($"agc.dcb_draw_index_auto buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} count={indexCount}"); + return ReturnPointer(ctx, commandAddress); + } + + [SysAbiExport( + Nid = "aJf+j5yntiU", + ExportName = "sceAgcDcbEventWrite", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DcbEventWrite(CpuContext ctx) + { + var commandBufferAddress = ctx[CpuRegister.Rdi]; + var eventType = (uint)(ctx[CpuRegister.Rsi] & 0xFF); + var eventAddress = ctx[CpuRegister.Rdx]; + if (commandBufferAddress == 0 || eventType > 0x3F || eventAddress != 0) + { + return ReturnPointer(ctx, 0); + } + + if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) || + !TryWriteUInt32(ctx, commandAddress, Pm4(2, ItEventWrite, 0)) || + !TryWriteUInt32(ctx, commandAddress + 4, eventType)) + { + return ReturnPointer(ctx, 0); + } + + TraceAgc($"agc.dcb_event_write buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} type={eventType}"); + return ReturnPointer(ctx, commandAddress); + } + + [SysAbiExport( + Nid = "57labkp+rSQ", + ExportName = "sceAgcDcbAcquireMem", + Target = Generation.Gen5, + LibraryName = "libSceAgc")] + public static int DcbAcquireMem(CpuContext ctx) + { + var commandBufferAddress = ctx[CpuRegister.Rdi]; + var engine = (uint)(ctx[CpuRegister.Rsi] & 0xFF); + var cbDbOp = (uint)ctx[CpuRegister.Rdx]; + var gcrControl = (uint)ctx[CpuRegister.Rcx]; + var baseAddress = ctx[CpuRegister.R8]; + var sizeBytes = ctx[CpuRegister.R9]; + if (!TryReadUInt32(ctx, ctx[CpuRegister.Rsp] + sizeof(ulong), out var pollCycles)) + { + return ReturnPointer(ctx, 0); + } + + var noSize = sizeBytes == ulong.MaxValue; + if (commandBufferAddress == 0 || + engine > 1 || + (!noSize && (sizeBytes & 0xFF) != 0) || + (!noSize && (sizeBytes >> 40) != 0) || + (baseAddress & 0xFF) != 0 || + (baseAddress >> 40) != 0) + { + return ReturnPointer(ctx, 0); + } + + if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 8, out var commandAddress) || + !TryWriteUInt32(ctx, commandAddress, Pm4(8, ItNop, RAcquireMem)) || + !TryWriteUInt32(ctx, commandAddress + 4, (engine << 31) | cbDbOp) || + !TryWriteUInt32(ctx, commandAddress + 8, noSize ? 0 : (uint)(sizeBytes >> 8)) || + !TryWriteUInt32(ctx, commandAddress + 12, 0) || + !TryWriteUInt32(ctx, commandAddress + 16, (uint)(baseAddress >> 8)) || + !TryWriteUInt32(ctx, commandAddress + 20, 0) || + !TryWriteUInt32(ctx, commandAddress + 24, pollCycles / 40) || + !TryWriteUInt32(ctx, commandAddress + 28, gcrControl)) + { + return ReturnPointer(ctx, 0); + } + + TraceAgc( + $"agc.dcb_acquire_mem buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} " + + $"engine={engine} cbdb=0x{cbDbOp:X8} gcr=0x{gcrControl:X8} base=0x{baseAddress:X16} size=0x{sizeBytes:X16}"); + return ReturnPointer(ctx, commandAddress); + } + [SysAbiExport( Nid = "l4fM9K-Lyks", ExportName = "sceAgcDcbSetIndexBuffer", @@ -567,10 +838,20 @@ public static class AgcExports return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } - TraceAgc($"agc.driver_submit_dcb packet=0x{packetAddress:X16} addr=0x{commandAddress:X16} dwords={dwordCount}"); - var tracePackets = - string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal) && - Interlocked.CompareExchange(ref _submitTraceCaptured, 1, 0) == 0; + var tracePackets = false; + if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal)) + { + lock (_submitTraceGate) + { + tracePackets = _tracedDcbSizes.Add(dwordCount); + } + } + + if (tracePackets) + { + TraceAgc($"agc.driver_submit_dcb packet=0x{packetAddress:X16} addr=0x{commandAddress:X16} dwords={dwordCount}"); + } + ParseSubmittedDcb(ctx, commandAddress, dwordCount, tracePackets); ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; @@ -596,6 +877,8 @@ public static class AgcExports } TextureDescriptor? presenterTexture = null; + GuestDrawKind guestDrawKind = GuestDrawKind.None; + var state = new SubmittedDcbState(); var sawIndexedDraw = false; var offset = 0u; while (offset < dwordCount) @@ -625,12 +908,25 @@ public static class AgcExports presenterTexture = texture; } + ApplySubmittedRegisters(ctx, state, currentAddress, length, op, register); + if (op == ItDrawIndexOffset2 && length >= 5 && TryReadUInt32(ctx, currentAddress + 4, out var indexCount) && indexCount != 0) { sawIndexedDraw = true; + TryTranslateGuestDraw(ctx, state, indexCount, ref guestDrawKind); + } + + if (op == ItNop && + register == RDrawIndexAuto && + length >= 2 && + TryReadUInt32(ctx, currentAddress + 4, out var autoIndexCount) && + autoIndexCount != 0) + { + sawIndexedDraw = true; + TryTranslateGuestDraw(ctx, state, autoIndexCount, ref guestDrawKind); } if (op == ItNop && register == RFlip && length >= 6) @@ -654,6 +950,18 @@ public static class AgcExports unchecked((int)videoOutHandle), displayBufferIndex); } + else if (sawIndexedDraw && + guestDrawKind != GuestDrawKind.None && + VideoOutExports.TryGetDisplayBufferInfo( + unchecked((int)videoOutHandle), + displayBufferIndex, + out var displayBuffer)) + { + VulkanVideoPresenter.SubmitGuestDraw( + guestDrawKind, + displayBuffer.Width, + displayBuffer.Height); + } _ = VideoOutExports.SubmitFlipFromAgc(ctx, unchecked((int)videoOutHandle), displayBufferIndex, unchecked((int)flipMode), flipArg); } @@ -662,6 +970,120 @@ public static class AgcExports } } + private static void ApplySubmittedRegisters( + CpuContext ctx, + SubmittedDcbState state, + ulong packetAddress, + uint packetLength, + uint op, + uint register) + { + if (op == ItSetShReg) + { + if (packetLength < 3 || + !TryReadUInt32(ctx, packetAddress + sizeof(uint), out var startRegister)) + { + return; + } + + for (uint index = 0; index < packetLength - 2; index++) + { + if (!TryReadUInt32( + ctx, + packetAddress + 8 + ((ulong)index * sizeof(uint)), + out var value)) + { + return; + } + + state.ShRegisters[startRegister + index] = value; + } + + return; + } + + if (op != ItNop || + register is not (RCxRegsIndirect or RShRegsIndirect or RUcRegsIndirect) || + packetLength < 4 || + !TryReadUInt32(ctx, packetAddress + sizeof(uint), out var registerCount) || + !TryReadUInt64(ctx, packetAddress + 8, out var registersAddress)) + { + return; + } + + var destination = register switch + { + RCxRegsIndirect => state.CxRegisters, + RShRegsIndirect => state.ShRegisters, + _ => state.UcRegisters, + }; + for (uint index = 0; index < registerCount; index++) + { + var entryAddress = registersAddress + ((ulong)index * 8); + if (!TryReadUInt32(ctx, entryAddress, out var registerOffset) || + !TryReadUInt32(ctx, entryAddress + sizeof(uint), out var value)) + { + return; + } + + if (registerOffset != 0) + { + destination[registerOffset] = value; + } + } + } + + private static void TryTranslateGuestDraw( + CpuContext ctx, + SubmittedDcbState state, + uint vertexCount, + ref GuestDrawKind drawKind) + { + if (drawKind != GuestDrawKind.None || + vertexCount != 3 || + !TryGetShaderAddress(state.ShRegisters, SpiShaderPgmLoEs, SpiShaderPgmHiEs, out var exportShaderAddress) || + !TryGetShaderAddress(state.ShRegisters, SpiShaderPgmLoPs, SpiShaderPgmHiPs, out var pixelShaderAddress) || + !state.CxRegisters.TryGetValue(SpiPsInputEna, out var psInputEna) || + !state.CxRegisters.TryGetValue(SpiPsInputAddr, out var psInputAddr) || + !Gen5ShaderTranslator.TryTranslate( + ctx, + exportShaderAddress, + pixelShaderAddress, + psInputEna, + psInputAddr, + out drawKind)) + { + return; + } + + lock (_submitTraceGate) + { + if (_tracedShaderTranslations.Add((exportShaderAddress, pixelShaderAddress, drawKind))) + { + TraceAgc( + $"agc.shader_translated kind={drawKind} es=0x{exportShaderAddress:X16} " + + $"ps=0x{pixelShaderAddress:X16} vertices={vertexCount}"); + } + } + } + + private static bool TryGetShaderAddress( + IReadOnlyDictionary registers, + uint loRegister, + uint hiRegister, + out ulong address) + { + address = 0; + if (!registers.TryGetValue(loRegister, out var lo) || + !registers.TryGetValue(hiRegister, out var hi)) + { + return false; + } + + address = ((ulong)hi << 40) | ((ulong)lo << 8); + return address != 0; + } + private static bool TryReadTextureDescriptor( CpuContext ctx, ulong packetAddress, @@ -802,6 +1224,7 @@ public static class AgcExports _softwarePresenterFingerprints[fingerprintKey] = fingerprint; } + VideoOutExports.SubmitHostRgbaFrame(sourceBytes, source.Width, source.Height); TraceAgc( $"agc.software_presenter src=0x{source.Address:X16} {source.Width}x{source.Height} fmt={source.Format} " + $"dst=0x{destination.Address:X16} {destination.Width}x{destination.Height} fingerprint=0x{fingerprint:X16}"); @@ -895,8 +1318,20 @@ public static class AgcExports return false; } - var expectedLo = shaderType == 2 ? SpiShaderPgmLoEs : shaderType == 1 ? SpiShaderPgmLoPs : 0; - var expectedHi = shaderType == 2 ? SpiShaderPgmHiEs : shaderType == 1 ? SpiShaderPgmHiPs : 0; + var expectedLo = shaderType switch + { + 0 => ComputePgmLo, + 1 => SpiShaderPgmLoPs, + 2 => SpiShaderPgmLoEs, + _ => 0u, + }; + var expectedHi = shaderType switch + { + 0 => ComputePgmHi, + 1 => SpiShaderPgmHiPs, + 2 => SpiShaderPgmHiEs, + _ => 0u, + }; if (expectedLo == 0 || loRegister != expectedLo || hiRegister != expectedHi) { TraceCreateShader(0, headerAddress, codeAddress, $"unexpected-registers type={shaderType} lo=0x{loRegister:X8} hi=0x{hiRegister:X8}"); @@ -1031,6 +1466,150 @@ public static class AgcExports return ctx.TryWriteUInt64(fieldAddress, fieldAddress + relativeAddress); } + private static int ReturnRegisterDefaults(CpuContext ctx, bool internalDefaults) + { + var version = (uint)ctx[CpuRegister.Rdi]; + if (!IsSupportedRegisterDefaultsVersion(version)) + { + return ReturnPointer(ctx, 0); + } + + if (!TryGetRegisterDefaultsAllocation(ctx, out var allocation)) + { + return ReturnPointer(ctx, 0); + } + + var address = internalDefaults ? allocation.Internal : allocation.Primary; + TraceAgc($"agc.get_register_defaults internal={internalDefaults} version={version} address=0x{address:X16}"); + return ReturnPointer(ctx, address); + } + + private static bool IsSupportedRegisterDefaultsVersion(uint version) + { + return version is RegisterDefaultsVersion7 or RegisterDefaultsVersion8; + } + + private static bool TryGetRegisterDefaultsAllocation( + CpuContext ctx, + out RegisterDefaultsAllocation allocation) + { + lock (_registerDefaultsGate) + { + if (_registerDefaultsAllocations.TryGetValue(ctx.Memory, out allocation!)) + { + return true; + } + + if (!TryBuildRegisterDefaults( + ctx, + PrimaryRegisterDefaults, + cxTableLength: 78, + shTableLength: 29, + ucTableLength: 20, + out var primaryAddress) || + !TryBuildRegisterDefaults( + ctx, + InternalRegisterDefaults, + cxTableLength: 4, + shTableLength: 15, + ucTableLength: 3, + out var internalAddress)) + { + allocation = null!; + return false; + } + + allocation = new RegisterDefaultsAllocation(primaryAddress, internalAddress); + _registerDefaultsAllocations.Add(ctx.Memory, allocation); + return true; + } + } + + private static bool TryBuildRegisterDefaults( + CpuContext ctx, + RegisterDefaultGroup[] groups, + int cxTableLength, + int shTableLength, + int ucTableLength, + out ulong address) + { + var cxTableOffset = AlignUp(RegisterDefaultsSize, sizeof(ulong)); + var shTableOffset = cxTableOffset + (cxTableLength * sizeof(ulong)); + var ucTableOffset = shTableOffset + (shTableLength * sizeof(ulong)); + var typesOffset = AlignUp(ucTableOffset + (ucTableLength * sizeof(ulong)), sizeof(uint)); + var registerBlocksOffset = AlignUp(typesOffset + (groups.Length * 3 * sizeof(uint)), sizeof(ulong)); + var blobLength = registerBlocksOffset + (groups.Length * RegisterDefaultBlockSize); + + if (!KernelMemoryCompatExports.TryAllocateHleData(ctx, (ulong)blobLength, 0x1000, out address)) + { + return false; + } + + var blob = new byte[blobLength]; + WriteBlobUInt64(blob, 0x00, address + (ulong)cxTableOffset); + WriteBlobUInt64(blob, 0x08, address + (ulong)shTableOffset); + WriteBlobUInt64(blob, 0x10, address + (ulong)ucTableOffset); + WriteBlobUInt64(blob, 0x30, address + (ulong)typesOffset); + WriteBlobUInt32(blob, 0x38, (uint)groups.Length); + + for (var groupIndex = 0; groupIndex < groups.Length; groupIndex++) + { + var group = groups[groupIndex]; + if (group.Registers.Length > 16) + { + return false; + } + + var tableOffset = group.Space switch + { + 0 => cxTableOffset, + 1 => shTableOffset, + 2 => ucTableOffset, + _ => -1, + }; + var tableLength = group.Space switch + { + 0 => cxTableLength, + 1 => shTableLength, + 2 => ucTableLength, + _ => 0, + }; + if (tableOffset < 0 || group.Index >= tableLength) + { + return false; + } + + var registerBlockOffset = registerBlocksOffset + (groupIndex * RegisterDefaultBlockSize); + WriteBlobUInt64( + blob, + tableOffset + ((int)group.Index * sizeof(ulong)), + address + (ulong)registerBlockOffset); + + var typeEntryOffset = typesOffset + (groupIndex * 3 * sizeof(uint)); + WriteBlobUInt32(blob, typeEntryOffset, group.Type); + WriteBlobUInt32(blob, typeEntryOffset + sizeof(uint), (group.Index * 4) + group.Space); + + for (var registerIndex = 0; registerIndex < group.Registers.Length; registerIndex++) + { + var register = group.Registers[registerIndex]; + var registerOffset = registerBlockOffset + (registerIndex * 2 * sizeof(uint)); + WriteBlobUInt32(blob, registerOffset, register.Offset); + WriteBlobUInt32(blob, registerOffset + sizeof(uint), register.Value); + } + } + + return ctx.Memory.TryWrite(address, blob); + } + + private static int AlignUp(int value, int alignment) => + (value + alignment - 1) & -alignment; + + private static void WriteBlobUInt32(Span blob, int offset, uint value) => + BinaryPrimitives.WriteUInt32LittleEndian(blob[offset..], value); + + private static void WriteBlobUInt64(Span blob, int offset, ulong value) => + BinaryPrimitives.WriteUInt64LittleEndian(blob[offset..], value); + private static int ReturnPointer(CpuContext ctx, ulong pointer) { ctx[CpuRegister.Rax] = pointer; diff --git a/src/SharpEmu.Libs/Agc/Gen5ShaderTranslator.cs b/src/SharpEmu.Libs/Agc/Gen5ShaderTranslator.cs new file mode 100644 index 0000000..9309306 --- /dev/null +++ b/src/SharpEmu.Libs/Agc/Gen5ShaderTranslator.cs @@ -0,0 +1,73 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using SharpEmu.Libs.VideoOut; +using System.Buffers.Binary; + +namespace SharpEmu.Libs.Agc; + +internal static class Gen5ShaderTranslator +{ + private static readonly uint[] FullscreenBarycentricEs = + [ + 0xBFA00001, 0x7E000000, 0x7E000000, 0x7E000000, + 0x93EBFF03, 0x00080008, 0x8F6A8C6B, 0x8700FF03, + 0x000000FF, 0x887C6A00, 0xBF900009, 0x81EA6BC0, + 0x90FE6AC1, 0xF8000941, 0x00000000, 0x81EA00C0, + 0xBF8CFF0F, 0x90FE6AC1, 0x36040A81, 0x2C060A81, + 0x7E000280, 0x7E0202F2, 0xD7460002, 0x03050302, + 0xD7460003, 0x03050303, 0x7E040B02, 0x7E060B03, + 0xF80008CF, 0x01000302, 0xBF810000, + ]; + + private static readonly uint[] FullscreenBarycentricPs = + [ + 0xD52F0000, 0x00000200, + 0xD52F0001, 0x00000602, + 0xF8001C0F, 0x00000100, + 0xBF810000, + ]; + + public static bool TryTranslate( + CpuContext ctx, + ulong exportShaderAddress, + ulong pixelShaderAddress, + uint psInputEna, + uint psInputAddr, + out GuestDrawKind drawKind) + { + drawKind = GuestDrawKind.None; + if (exportShaderAddress == 0 || + pixelShaderAddress == 0 || + psInputEna != 0x00000002 || + psInputAddr != 0x00000002 || + !MatchesProgram(ctx, exportShaderAddress, FullscreenBarycentricEs) || + !MatchesProgram(ctx, pixelShaderAddress, FullscreenBarycentricPs)) + { + return false; + } + + drawKind = GuestDrawKind.FullscreenBarycentric; + return true; + } + + private static bool MatchesProgram(CpuContext ctx, ulong address, ReadOnlySpan expected) + { + var bytes = new byte[expected.Length * sizeof(uint)]; + if (!ctx.Memory.TryRead(address, bytes)) + { + return false; + } + + for (var index = 0; index < expected.Length; index++) + { + if (BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(index * sizeof(uint))) != expected[index]) + { + return false; + } + } + + return true; + } +} diff --git a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs index ebedf77..f9d963e 100644 --- a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs +++ b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs @@ -44,6 +44,36 @@ public static class VideoOutExports private static int _nextHandle = 1; private static int _frameDumpCount; private static long _nextFrameDumpIndex; + private static string _windowTitle = "SharpEmu VideoOut"; + + public static void ConfigureApplicationInfo(string? title, string? titleId, string? version) + { + var parts = new List(); + if (!string.IsNullOrWhiteSpace(title)) + { + parts.Add(title.Trim()); + } + + if (!string.IsNullOrWhiteSpace(titleId)) + { + parts.Add($"[{titleId.Trim()}]"); + } + + var application = parts.Count == 0 ? "VideoOut" : string.Join(' ', parts); + var versionSuffix = string.IsNullOrWhiteSpace(version) ? string.Empty : $" v{version.Trim()}"; + lock (_stateGate) + { + _windowTitle = $"SharpEmu - {application}{versionSuffix}"; + } + } + + internal static string GetWindowTitle() + { + lock (_stateGate) + { + return _windowTitle; + } + } private sealed class VideoOutPortState { @@ -210,7 +240,15 @@ public static class VideoOutExports lock (_stateGate) { - port.FlipEvents.Add(new FlipEventRegistration(equeue, userData)); + var existingIndex = port.FlipEvents.FindIndex(registration => registration.Equeue == equeue); + if (existingIndex >= 0) + { + port.FlipEvents[existingIndex] = new FlipEventRegistration(equeue, userData); + } + else + { + port.FlipEvents.Add(new FlipEventRegistration(equeue, userData)); + } } TraceVideoOut($"videoout.add_flip_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}"); @@ -284,7 +322,8 @@ public static class VideoOutExports return OrbisVideoOutErrorInvalidEvent; } - return ctx.TryWriteUInt64(dataAddress, data >> 16) + var decodedData = unchecked((ulong)(unchecked((long)data) >> 16)); + return ctx.TryWriteUInt64(dataAddress, decodedData) ? (int)OrbisGen2Result.ORBIS_GEN2_OK : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } @@ -610,7 +649,7 @@ public static class VideoOutExports return OrbisVideoOutErrorInvalidIndex; } - ulong eventData; + ulong eventHint; List flipEvents; lock (_stateGate) { @@ -621,9 +660,8 @@ public static class VideoOutExports port.CurrentBuffer = bufferIndex; port.FlipCount++; - var eventCount = Math.Min(port.FlipCount, 0xFUL); - var timeBits = (ulong)Environment.TickCount64 & 0xFFFUL; - eventData = timeBits | (eventCount << 12) | ((unchecked((ulong)flipArg) & 0x0000_FFFF_FFFF_FFFFUL) << 16); + eventHint = SceVideoOutInternalEventFlip | + ((unchecked((ulong)flipArg) & 0x0000_FFFF_FFFF_FFFFUL) << 16); flipEvents = new List(port.FlipEvents); } @@ -637,15 +675,12 @@ public static class VideoOutExports foreach (var flipEvent in flipEvents) { - _ = KernelEventQueueCompatExports.EnqueueEvent( + _ = KernelEventQueueCompatExports.TriggerDisplayEvent( flipEvent.Equeue, - new KernelEventQueueCompatExports.KernelQueuedEvent( - SceVideoOutInternalEventFlip, - OrbisKernelEventFilterVideoOut, - 0, - 0, - eventData, - flipEvent.UserData)); + SceVideoOutInternalEventFlip, + OrbisKernelEventFilterVideoOut, + eventHint, + flipEvent.UserData); } TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEvents.Count}"); diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index f8b59f3..90d5e45 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -12,14 +12,17 @@ using VkSemaphore = Silk.NET.Vulkan.Semaphore; namespace SharpEmu.Libs.VideoOut; +internal enum GuestDrawKind +{ + None, + FullscreenBarycentric, +} + internal static unsafe class VulkanVideoPresenter { private static readonly object _gate = new(); private static Thread? _thread; - private static byte[]? _latestFrame; - private static uint _latestWidth; - private static uint _latestHeight; - private static long _latestSequence; + private static Presentation? _latestPresentation; private static bool _closed; public static void Submit(byte[] bgraFrame, uint width, uint height) @@ -36,10 +39,42 @@ internal static unsafe class VulkanVideoPresenter return; } - _latestFrame = bgraFrame; - _latestWidth = width; - _latestHeight = height; - _latestSequence++; + var sequence = (_latestPresentation?.Sequence ?? 0) + 1; + _latestPresentation = new Presentation(bgraFrame, width, height, sequence, GuestDrawKind.None); + if (_thread is not null) + { + return; + } + + _thread = new Thread(Run) + { + IsBackground = true, + Name = "SharpEmu Vulkan VideoOut", + }; + _thread.Start(); + } + } + + public static void SubmitGuestDraw(GuestDrawKind drawKind, uint width, uint height) + { + if (drawKind == GuestDrawKind.None || width == 0 || height == 0) + { + return; + } + + lock (_gate) + { + if (_closed || + _latestPresentation is { Pixels: null } latest && + latest.DrawKind == drawKind && + latest.Width == width && + latest.Height == height) + { + return; + } + + var sequence = (_latestPresentation?.Sequence ?? 0) + 1; + _latestPresentation = new Presentation(null, width, height, sequence, drawKind); if (_thread is not null) { return; @@ -60,8 +95,8 @@ internal static unsafe class VulkanVideoPresenter uint height; lock (_gate) { - width = _latestWidth; - height = _latestHeight; + width = _latestPresentation?.Width ?? 1280; + height = _latestPresentation?.Height ?? 720; } try @@ -83,25 +118,36 @@ internal static unsafe class VulkanVideoPresenter } } - private static bool TryTakeFrame(long presentedSequence, out Frame frame) + private static bool TryTakePresentation(long presentedSequence, out Presentation presentation) { lock (_gate) { - if (_latestFrame is null || _latestSequence == presentedSequence) + if (_latestPresentation is not { } latest || latest.Sequence == presentedSequence) { - frame = default; + presentation = default; return false; } - frame = new Frame(_latestFrame, _latestWidth, _latestHeight, _latestSequence); + presentation = latest; return true; } } - private readonly record struct Frame(byte[] Pixels, uint Width, uint Height, long Sequence); + private readonly record struct Presentation( + byte[]? Pixels, + uint Width, + uint Height, + long Sequence, + GuestDrawKind DrawKind); private sealed class Presenter : IDisposable { + private const string FullscreenBarycentricVertexSpirv = + "AwIjBwAAAQALAAgAMgAAAAAAAAARAAIAAQAAAAsABgABAAAAR0xTTC5zdGQuNDUwAAAAAA4AAwAAAAAAAQAAAA8ACAAAAAAABAAAAG1haW4AAAAADQAAABoAAAApAAAAAwADAAIAAADCAQAABQAEAAQAAABtYWluAAAAAAUABgALAAAAZ2xfUGVyVmVydGV4AAAAAAYABgALAAAAAAAAAGdsX1Bvc2l0aW9uAAYABwALAAAAAQAAAGdsX1BvaW50U2l6ZQAAAAAGAAcACwAAAAIAAABnbF9DbGlwRGlzdGFuY2UABgAHAAsAAAADAAAAZ2xfQ3VsbERpc3RhbmNlAAUAAwANAAAAAAAAAAUABgAaAAAAZ2xfVmVydGV4SW5kZXgAAAUABQAdAAAAaW5kZXhhYmxlAAAABQAFACkAAABiYXJ5Y2VudHJpYwAFAAUALwAAAGluZGV4YWJsZQAAAEcAAwALAAAAAgAAAEgABQALAAAAAAAAAAsAAAAAAAAASAAFAAsAAAABAAAACwAAAAEAAABIAAUACwAAAAIAAAALAAAAAwAAAEgABQALAAAAAwAAAAsAAAAEAAAARwAEABoAAAALAAAAKgAAAEcABAApAAAAHgAAAAAAAAATAAIAAgAAACEAAwADAAAAAgAAABYAAwAGAAAAIAAAABcABAAHAAAABgAAAAQAAAAVAAQACAAAACAAAAAAAAAAKwAEAAgAAAAJAAAAAQAAABwABAAKAAAABgAAAAkAAAAeAAYACwAAAAcAAAAGAAAACgAAAAoAAAAgAAQADAAAAAMAAAALAAAAOwAEAAwAAAANAAAAAwAAABUABAAOAAAAIAAAAAEAAAArAAQADgAAAA8AAAAAAAAAFwAEABAAAAAGAAAAAgAAACsABAAIAAAAEQAAAAMAAAAcAAQAEgAAABAAAAARAAAAKwAEAAYAAAATAAAAAACAvywABQAQAAAAFAAAABMAAAATAAAAKwAEAAYAAAAVAAAAAABAQCwABQAQAAAAFgAAABUAAAATAAAALAAFABAAAAAXAAAAEwAAABUAAAAsAAYAEgAAABgAAAAUAAAAFgAAABcAAAAgAAQAGQAAAAEAAAAOAAAAOwAEABkAAAAaAAAAAQAAACAABAAcAAAABwAAABIAAAAgAAQAHgAAAAcAAAAQAAAAKwAEAAYAAAAhAAAAAAAAACsABAAGAAAAIgAAAAAAgD8gAAQAJgAAAAMAAAAHAAAAIAAEACgAAAADAAAAEAAAADsABAAoAAAAKQAAAAMAAAAsAAUAEAAAACoAAAAiAAAAIQAAACwABQAQAAAAKwAAACEAAAAiAAAALAAFABAAAAAsAAAAIQAAACEAAAAsAAYAEgAAAC0AAAAqAAAAKwAAACwAAAA2AAUAAgAAAAQAAAAAAAAAAwAAAPgAAgAFAAAAOwAEABwAAAAdAAAABwAAADsABAAcAAAALwAAAAcAAAA9AAQADgAAABsAAAAaAAAAPgADAB0AAAAYAAAAQQAFAB4AAAAfAAAAHQAAABsAAAA9AAQAEAAAACAAAAAfAAAAUQAFAAYAAAAjAAAAIAAAAAAAAABRAAUABgAAACQAAAAgAAAAAQAAAFAABwAHAAAAJQAAACMAAAAkAAAAIQAAACIAAABBAAUAJgAAACcAAAANAAAADwAAAD4AAwAnAAAAJQAAAD0ABAAOAAAALgAAABoAAAA+AAMALwAAAC0AAABBAAUAHgAAADAAAAAvAAAALgAAAD0ABAAQAAAAMQAAADAAAAA+AAMAKQAAADEAAAD9AAEAOAABAA=="; + + private const string FullscreenBarycentricFragmentSpirv = + "AwIjBwAAAQALAAgAEgAAAAAAAAARAAIAAQAAAAsABgABAAAAR0xTTC5zdGQuNDUwAAAAAA4AAwAAAAAAAQAAAA8ABwAEAAAABAAAAG1haW4AAAAACQAAAAwAAAAQAAMABAAAAAcAAAADAAMAAgAAAMIBAAAFAAQABAAAAG1haW4AAAAABQAFAAkAAABvdXRDb2xvcgAAAAAFAAUADAAAAGJhcnljZW50cmljAEcABAAJAAAAHgAAAAAAAABHAAQADAAAAB4AAAAAAAAAEwACAAIAAAAhAAMAAwAAAAIAAAAWAAMABgAAACAAAAAXAAQABwAAAAYAAAAEAAAAIAAEAAgAAAADAAAABwAAADsABAAIAAAACQAAAAMAAAAXAAQACgAAAAYAAAACAAAAIAAEAAsAAAABAAAACgAAADsABAALAAAADAAAAAEAAAArAAQABgAAAA4AAAAAAAAANgAFAAIAAAAEAAAAAAAAAAMAAAD4AAIABQAAAD0ABAAKAAAADQAAAAwAAABRAAUABgAAAA8AAAANAAAAAAAAAFEABQAGAAAAEAAAAA0AAAABAAAAUAAHAAcAAAARAAAADwAAABAAAAAOAAAADgAAAD4AAwAJAAAAEQAAAP0AAQA4AAEA"; + private readonly IWindow _window; private Vk _vk = null!; private KhrSurface _surfaceApi = null!; @@ -114,9 +160,14 @@ internal static unsafe class VulkanVideoPresenter private uint _queueFamilyIndex; private SwapchainKHR _swapchain; private Image[] _swapchainImages = []; + private ImageView[] _swapchainImageViews = []; + private Framebuffer[] _framebuffers = []; private bool[] _imageInitialized = []; private Format _swapchainFormat; private Extent2D _extent; + private RenderPass _renderPass; + private PipelineLayout _pipelineLayout; + private Pipeline _barycentricPipeline; private CommandPool _commandPool; private CommandBuffer _commandBuffer; private VkSemaphore _imageAvailable; @@ -127,12 +178,13 @@ internal static unsafe class VulkanVideoPresenter private long _presentedSequence; private bool _vulkanReady; private bool _firstFramePresented; + private bool _firstGuestDrawPresented; public Presenter(uint width, uint height) { var options = WindowOptions.DefaultVulkan; options.Size = new Vector2D((int)width, (int)height); - options.Title = "SharpEmu VideoOut"; + options.Title = VideoOutExports.GetWindowTitle(); options.WindowBorder = WindowBorder.Fixed; options.VSync = true; _window = Window.Create(options); @@ -158,6 +210,7 @@ internal static unsafe class VulkanVideoPresenter CreateDevice(); CreateSwapchain(); CreateCommandResources(); + CreateGuestDrawResources(); _vulkanReady = true; Console.Error.WriteLine( $"[LOADER][INFO] Vulkan VideoOut ready: {_extent.Width}x{_extent.Height}, format={_swapchainFormat}"); @@ -323,7 +376,7 @@ internal static unsafe class VulkanVideoPresenter ImageColorSpace = surfaceFormat.ColorSpace, ImageExtent = _extent, ImageArrayLayers = 1, - ImageUsage = ImageUsageFlags.TransferDstBit, + ImageUsage = ImageUsageFlags.TransferDstBit | ImageUsageFlags.ColorAttachmentBit, ImageSharingMode = SharingMode.Exclusive, PreTransform = capabilities.CurrentTransform, CompositeAlpha = compositeAlpha, @@ -377,6 +430,218 @@ internal static unsafe class VulkanVideoPresenter CreateStagingBuffer((ulong)_extent.Width * _extent.Height * 4); } + private void CreateGuestDrawResources() + { + var colorAttachment = new AttachmentDescription + { + Format = _swapchainFormat, + Samples = SampleCountFlags.Count1Bit, + LoadOp = AttachmentLoadOp.Clear, + StoreOp = AttachmentStoreOp.Store, + StencilLoadOp = AttachmentLoadOp.DontCare, + StencilStoreOp = AttachmentStoreOp.DontCare, + InitialLayout = ImageLayout.Undefined, + FinalLayout = ImageLayout.PresentSrcKhr, + }; + var colorReference = new AttachmentReference + { + Attachment = 0, + Layout = ImageLayout.ColorAttachmentOptimal, + }; + var subpass = new SubpassDescription + { + PipelineBindPoint = PipelineBindPoint.Graphics, + ColorAttachmentCount = 1, + PColorAttachments = &colorReference, + }; + var dependency = new SubpassDependency + { + SrcSubpass = Vk.SubpassExternal, + DstSubpass = 0, + SrcStageMask = PipelineStageFlags.ColorAttachmentOutputBit, + DstStageMask = PipelineStageFlags.ColorAttachmentOutputBit, + DstAccessMask = AccessFlags.ColorAttachmentWriteBit, + }; + var renderPassInfo = new RenderPassCreateInfo + { + SType = StructureType.RenderPassCreateInfo, + AttachmentCount = 1, + PAttachments = &colorAttachment, + SubpassCount = 1, + PSubpasses = &subpass, + DependencyCount = 1, + PDependencies = &dependency, + }; + Check(_vk.CreateRenderPass(_device, &renderPassInfo, null, out _renderPass), "vkCreateRenderPass"); + + _swapchainImageViews = new ImageView[_swapchainImages.Length]; + _framebuffers = new Framebuffer[_swapchainImages.Length]; + for (var index = 0; index < _swapchainImages.Length; index++) + { + var viewInfo = new ImageViewCreateInfo + { + SType = StructureType.ImageViewCreateInfo, + Image = _swapchainImages[index], + ViewType = ImageViewType.Type2D, + Format = _swapchainFormat, + Components = new ComponentMapping( + ComponentSwizzle.Identity, + ComponentSwizzle.Identity, + ComponentSwizzle.Identity, + ComponentSwizzle.Identity), + SubresourceRange = ColorSubresourceRange(), + }; + Check( + _vk.CreateImageView(_device, &viewInfo, null, out _swapchainImageViews[index]), + "vkCreateImageView"); + + var imageView = _swapchainImageViews[index]; + var framebufferInfo = new FramebufferCreateInfo + { + SType = StructureType.FramebufferCreateInfo, + RenderPass = _renderPass, + AttachmentCount = 1, + PAttachments = &imageView, + Width = _extent.Width, + Height = _extent.Height, + Layers = 1, + }; + Check( + _vk.CreateFramebuffer(_device, &framebufferInfo, null, out _framebuffers[index]), + "vkCreateFramebuffer"); + } + + var layoutInfo = new PipelineLayoutCreateInfo + { + SType = StructureType.PipelineLayoutCreateInfo, + }; + Check( + _vk.CreatePipelineLayout(_device, &layoutInfo, null, out _pipelineLayout), + "vkCreatePipelineLayout"); + CreateBarycentricPipeline(); + } + + private void CreateBarycentricPipeline() + { + var vertexBytes = Convert.FromBase64String(FullscreenBarycentricVertexSpirv); + var fragmentBytes = Convert.FromBase64String(FullscreenBarycentricFragmentSpirv); + var vertexModule = CreateShaderModule(vertexBytes); + var fragmentModule = CreateShaderModule(fragmentBytes); + var entryPoint = (byte*)SilkMarshal.StringToPtr("main"); + try + { + var shaderStages = stackalloc PipelineShaderStageCreateInfo[2]; + shaderStages[0] = new PipelineShaderStageCreateInfo + { + SType = StructureType.PipelineShaderStageCreateInfo, + Stage = ShaderStageFlags.VertexBit, + Module = vertexModule, + PName = entryPoint, + }; + shaderStages[1] = new PipelineShaderStageCreateInfo + { + SType = StructureType.PipelineShaderStageCreateInfo, + Stage = ShaderStageFlags.FragmentBit, + Module = fragmentModule, + PName = entryPoint, + }; + + var vertexInput = new PipelineVertexInputStateCreateInfo + { + SType = StructureType.PipelineVertexInputStateCreateInfo, + }; + var inputAssembly = new PipelineInputAssemblyStateCreateInfo + { + SType = StructureType.PipelineInputAssemblyStateCreateInfo, + Topology = PrimitiveTopology.TriangleList, + }; + var viewport = new Viewport(0, 0, _extent.Width, _extent.Height, 0, 1); + var scissor = new Rect2D(new Offset2D(0, 0), _extent); + var viewportState = new PipelineViewportStateCreateInfo + { + SType = StructureType.PipelineViewportStateCreateInfo, + ViewportCount = 1, + PViewports = &viewport, + ScissorCount = 1, + PScissors = &scissor, + }; + var rasterization = new PipelineRasterizationStateCreateInfo + { + SType = StructureType.PipelineRasterizationStateCreateInfo, + PolygonMode = PolygonMode.Fill, + CullMode = CullModeFlags.None, + FrontFace = FrontFace.CounterClockwise, + LineWidth = 1, + }; + var multisample = new PipelineMultisampleStateCreateInfo + { + SType = StructureType.PipelineMultisampleStateCreateInfo, + RasterizationSamples = SampleCountFlags.Count1Bit, + }; + var colorBlendAttachment = new PipelineColorBlendAttachmentState + { + ColorWriteMask = + ColorComponentFlags.RBit | + ColorComponentFlags.GBit | + ColorComponentFlags.BBit | + ColorComponentFlags.ABit, + }; + var colorBlend = new PipelineColorBlendStateCreateInfo + { + SType = StructureType.PipelineColorBlendStateCreateInfo, + AttachmentCount = 1, + PAttachments = &colorBlendAttachment, + }; + var pipelineInfo = new GraphicsPipelineCreateInfo + { + SType = StructureType.GraphicsPipelineCreateInfo, + StageCount = 2, + PStages = shaderStages, + PVertexInputState = &vertexInput, + PInputAssemblyState = &inputAssembly, + PViewportState = &viewportState, + PRasterizationState = &rasterization, + PMultisampleState = &multisample, + PColorBlendState = &colorBlend, + Layout = _pipelineLayout, + RenderPass = _renderPass, + Subpass = 0, + }; + Check( + _vk.CreateGraphicsPipelines( + _device, + default, + 1, + &pipelineInfo, + null, + out _barycentricPipeline), + "vkCreateGraphicsPipelines"); + } + finally + { + SilkMarshal.Free((nint)entryPoint); + _vk.DestroyShaderModule(_device, fragmentModule, null); + _vk.DestroyShaderModule(_device, vertexModule, null); + } + } + + private ShaderModule CreateShaderModule(byte[] code) + { + fixed (byte* codePointer = code) + { + var createInfo = new ShaderModuleCreateInfo + { + SType = StructureType.ShaderModuleCreateInfo, + CodeSize = (nuint)code.Length, + PCode = (uint*)codePointer, + }; + Check( + _vk.CreateShaderModule(_device, &createInfo, null, out var module), + "vkCreateShaderModule"); + return module; + } + } + private void CreateStagingBuffer(ulong size) { var bufferInfo = new BufferCreateInfo @@ -420,19 +685,34 @@ internal static unsafe class VulkanVideoPresenter private void Render(double _) { - if (!_vulkanReady || !TryTakeFrame(_presentedSequence, out var frame)) + if (!_vulkanReady || !TryTakePresentation(_presentedSequence, out var presentation)) { return; } - var pixels = frame.Width == _extent.Width && frame.Height == _extent.Height - ? frame.Pixels - : ScaleBgra(frame.Pixels, frame.Width, frame.Height, _extent.Width, _extent.Height); - if ((ulong)pixels.Length > _stagingSize) + if (presentation.Pixels is null && + presentation.DrawKind != GuestDrawKind.FullscreenBarycentric) { return; } + byte[]? pixels = null; + if (presentation.Pixels is { } sourcePixels) + { + pixels = presentation.Width == _extent.Width && presentation.Height == _extent.Height + ? sourcePixels + : ScaleBgra( + sourcePixels, + presentation.Width, + presentation.Height, + _extent.Width, + _extent.Height); + if ((ulong)pixels.Length > _stagingSize) + { + return; + } + } + uint imageIndex; Check( _swapchainApi.AcquireNextImage( @@ -444,15 +724,18 @@ internal static unsafe class VulkanVideoPresenter &imageIndex), "vkAcquireNextImageKHR"); - void* mapped; - Check( - _vk.MapMemory(_device, _stagingMemory, 0, (ulong)pixels.Length, 0, &mapped), - "vkMapMemory"); - fixed (byte* source = pixels) + if (pixels is not null) { - System.Buffer.MemoryCopy(source, mapped, pixels.Length, pixels.Length); + void* mapped; + Check( + _vk.MapMemory(_device, _stagingMemory, 0, (ulong)pixels.Length, 0, &mapped), + "vkMapMemory"); + fixed (byte* source = pixels) + { + System.Buffer.MemoryCopy(source, mapped, pixels.Length, pixels.Length); + } + _vk.UnmapMemory(_device, _stagingMemory); } - _vk.UnmapMemory(_device, _stagingMemory); Check(_vk.ResetCommandBuffer(_commandBuffer, 0), "vkResetCommandBuffer"); var beginInfo = new CommandBufferBeginInfo @@ -462,6 +745,93 @@ internal static unsafe class VulkanVideoPresenter }; Check(_vk.BeginCommandBuffer(_commandBuffer, &beginInfo), "vkBeginCommandBuffer"); + PipelineStageFlags waitStage; + if (pixels is not null) + { + RecordUpload(imageIndex); + waitStage = PipelineStageFlags.TransferBit; + } + else if (presentation.DrawKind == GuestDrawKind.FullscreenBarycentric) + { + var clearValue = default(ClearValue); + var renderPassInfo = new RenderPassBeginInfo + { + SType = StructureType.RenderPassBeginInfo, + RenderPass = _renderPass, + Framebuffer = _framebuffers[imageIndex], + RenderArea = new Rect2D(new Offset2D(0, 0), _extent), + ClearValueCount = 1, + PClearValues = &clearValue, + }; + _vk.CmdBeginRenderPass( + _commandBuffer, + &renderPassInfo, + SubpassContents.Inline); + _vk.CmdBindPipeline( + _commandBuffer, + PipelineBindPoint.Graphics, + _barycentricPipeline); + _vk.CmdDraw(_commandBuffer, 3, 1, 0, 0); + _vk.CmdEndRenderPass(_commandBuffer); + waitStage = PipelineStageFlags.ColorAttachmentOutputBit; + } + else + { + throw new InvalidOperationException( + $"Unsupported translated guest draw: {presentation.DrawKind}."); + } + + Check(_vk.EndCommandBuffer(_commandBuffer), "vkEndCommandBuffer"); + + var imageAvailable = _imageAvailable; + var commandBuffer = _commandBuffer; + var renderFinished = _renderFinished; + var submitInfo = new SubmitInfo + { + SType = StructureType.SubmitInfo, + WaitSemaphoreCount = 1, + PWaitSemaphores = &imageAvailable, + PWaitDstStageMask = &waitStage, + CommandBufferCount = 1, + PCommandBuffers = &commandBuffer, + SignalSemaphoreCount = 1, + PSignalSemaphores = &renderFinished, + }; + Check(_vk.QueueSubmit(_queue, 1, &submitInfo, default), "vkQueueSubmit"); + + var swapchain = _swapchain; + var presentInfo = new PresentInfoKHR + { + SType = StructureType.PresentInfoKhr, + WaitSemaphoreCount = 1, + PWaitSemaphores = &renderFinished, + SwapchainCount = 1, + PSwapchains = &swapchain, + PImageIndices = &imageIndex, + }; + Check(_swapchainApi.QueuePresent(_queue, &presentInfo), "vkQueuePresentKHR"); + Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle"); + _imageInitialized[imageIndex] = true; + _presentedSequence = presentation.Sequence; + if (!_firstFramePresented) + { + _firstFramePresented = true; + Console.Error.WriteLine( + $"[LOADER][INFO] Vulkan VideoOut presented first frame: " + + $"{presentation.Width}x{presentation.Height}"); + } + + if (pixels is null && !_firstGuestDrawPresented) + { + _firstGuestDrawPresented = true; + Console.Error.WriteLine( + $"[LOADER][INFO] Vulkan VideoOut presented translated guest draw: " + + $"{presentation.DrawKind}"); + } + } + + private void RecordUpload(uint imageIndex) + { var oldLayout = _imageInitialized[imageIndex] ? ImageLayout.PresentSrcKhr : ImageLayout.Undefined; @@ -479,7 +849,9 @@ internal static unsafe class VulkanVideoPresenter }; _vk.CmdPipelineBarrier( _commandBuffer, - _imageInitialized[imageIndex] ? PipelineStageFlags.BottomOfPipeBit : PipelineStageFlags.TopOfPipeBit, + _imageInitialized[imageIndex] + ? PipelineStageFlags.BottomOfPipeBit + : PipelineStageFlags.TopOfPipeBit, PipelineStageFlags.TransferBit, 0, 0, @@ -529,45 +901,6 @@ internal static unsafe class VulkanVideoPresenter null, 1, &toPresent); - Check(_vk.EndCommandBuffer(_commandBuffer), "vkEndCommandBuffer"); - - var waitStage = PipelineStageFlags.TransferBit; - var imageAvailable = _imageAvailable; - var commandBuffer = _commandBuffer; - var renderFinished = _renderFinished; - var submitInfo = new SubmitInfo - { - SType = StructureType.SubmitInfo, - WaitSemaphoreCount = 1, - PWaitSemaphores = &imageAvailable, - PWaitDstStageMask = &waitStage, - CommandBufferCount = 1, - PCommandBuffers = &commandBuffer, - SignalSemaphoreCount = 1, - PSignalSemaphores = &renderFinished, - }; - Check(_vk.QueueSubmit(_queue, 1, &submitInfo, default), "vkQueueSubmit"); - - var swapchain = _swapchain; - var presentInfo = new PresentInfoKHR - { - SType = StructureType.PresentInfoKhr, - WaitSemaphoreCount = 1, - PWaitSemaphores = &renderFinished, - SwapchainCount = 1, - PSwapchains = &swapchain, - PImageIndices = &imageIndex, - }; - Check(_swapchainApi.QueuePresent(_queue, &presentInfo), "vkQueuePresentKHR"); - Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle"); - _imageInitialized[imageIndex] = true; - _presentedSequence = frame.Sequence; - if (!_firstFramePresented) - { - _firstFramePresented = true; - Console.Error.WriteLine( - $"[LOADER][INFO] Vulkan VideoOut presented first guest frame: {frame.Width}x{frame.Height}"); - } } private Extent2D ChooseExtent(SurfaceCapabilitiesKHR capabilities) @@ -669,6 +1002,32 @@ internal static unsafe class VulkanVideoPresenter { _vk.DestroySemaphore(_device, _renderFinished, null); } + if (_barycentricPipeline.Handle != 0) + { + _vk.DestroyPipeline(_device, _barycentricPipeline, null); + } + if (_pipelineLayout.Handle != 0) + { + _vk.DestroyPipelineLayout(_device, _pipelineLayout, null); + } + foreach (var framebuffer in _framebuffers) + { + if (framebuffer.Handle != 0) + { + _vk.DestroyFramebuffer(_device, framebuffer, null); + } + } + if (_renderPass.Handle != 0) + { + _vk.DestroyRenderPass(_device, _renderPass, null); + } + foreach (var imageView in _swapchainImageViews) + { + if (imageView.Handle != 0) + { + _vk.DestroyImageView(_device, imageView, null); + } + } if (_commandPool.Handle != 0) { _vk.DestroyCommandPool(_device, _commandPool, null);