diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp index 0bc1a7c21..c5de807ad 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp @@ -227,6 +227,45 @@ inline VertexCopyPlan PlanVertexCopies(std::span fetches) { return plan; } +inline std::optional ZeroFetchVertex(std::span attributes) { + std::uint32_t zeroFrom = 0; + for (const auto& attribute : attributes) { + const auto stride = (attribute.resource.fields[1] >> 16u) & 0x3fffu; + if (NullVertexDescriptor(attribute) || attribute.fetchIndex != 0 || stride == 0) continue; + if (VertexBufferOutOfBoundsSelect(attribute) > 1u) return std::nullopt; + zeroFrom = std::max(zeroFrom, attribute.resource.fields[2]); + } + return zeroFrom; +} + +inline std::optional ClampDrawIndices(std::span indices, std::uint32_t indexSize, bool skipRestart, std::uint32_t limit) { + Require(indexSize == 2 || indexSize == 4, "unsupported index size"); + const auto restartIndex = indexSize == 2 ? 0xffffu : 0xffffffffu; + std::optional highest; + for (std::size_t offset = 0; offset + indexSize <= indices.size(); offset += indexSize) { + std::uint32_t index = 0; + if (indexSize == 2) { + std::uint16_t value = 0; + std::memcpy(&value, indices.data() + offset, sizeof(value)); + index = value; + } else { + std::memcpy(&index, indices.data() + offset, sizeof(index)); + } + if (skipRestart && index == restartIndex) continue; + if (index > limit) { + index = limit; + if (indexSize == 2) { + const auto value = static_cast(index); + std::memcpy(indices.data() + offset, &value, sizeof(value)); + } else { + std::memcpy(indices.data() + offset, &index, sizeof(index)); + } + } + highest = std::max(highest.value_or(0u), index); + } + return highest; +} + inline std::optional HighestDrawIndex(std::span indices, std::uint32_t indexSize, bool skipRestart) { Require(indexSize == 2 || indexSize == 4, "unsupported index size"); const auto restartIndex = indexSize == 2 ? 0xffffu : 0xffffffffu; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 2f3bf711d..93cb7a68d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1115,6 +1115,24 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P inputs.nothing = true; return inputs; } + constexpr std::uint32_t ZeroFetchSlack = 0x10000u; + if (const auto zeroFrom = state.stages.mesh ? std::nullopt : ZeroFetchVertex(shaders.front().program->vertexAttributes)) { + const auto limit = *zeroFrom > draw.firstVertex ? *zeroFrom - draw.firstVertex : 0u; + if (*highest >= limit && *highest - limit >= ZeroFetchSlack) { + GuestMemory::FlushGpuWrites(draw.indexAddress, static_cast(indexBytes)); + auto clamped = std::make_shared(context, static_cast(indexBytes), VK_BUFFER_USAGE_INDEX_BUFFER_BIT); + const auto bytes = clamped->Bytes().first(static_cast(indexBytes)); + GuestMemory::Read(draw.indexAddress, bytes, draw.indexSize); + highest = ClampDrawIndices(bytes, draw.indexSize, skipRestart, limit); + import = nullptr; + copy = DrawInputCopy{}; + copy.buffer = std::move(clamped); + if (!highest) { + inputs.nothing = true; + return inputs; + } + } + } Require(*highest <= context.limits.maxDrawIndexedIndexValue, "index exceeds the device's indexed draw limit"); Require(!fanGeometry || !state.primitiveRestart || *highest != (draw.indexSize == 2 ? 0xffffu : 0xffffffffu), "primitive restart in a triangle fan geometry draw is unsupported"); inputs.maxIndex = *highest; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 39c8dd8b2..bb890c28d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -4215,6 +4215,26 @@ void validationTests() { expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "metadata disagrees"); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 2, 1) == 80, "incorrect strided vertex range"); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 3, 1) == 112, "an index past the records still spans its fetch"); + { + auto wider = attribute; + wider.resource.fields[2] = 9; + auto instanced = attribute; + instanced.fetchIndex = 1; + instanced.resource.fields[2] = 1000; + auto constant = attribute; + constant.resource.fields[1] = 0; + const std::array attributes{attribute, wider, instanced, constant}; + Require(AgcDriver::Graphics::ZeroFetchVertex(attributes) == 9u, "the first vertex reading zeros is not past the largest per-vertex record count"); + auto clamped = attribute; + clamped.resource.fields[3] |= 2u << 28u; + const std::array selective{attribute, clamped}; + Require(!AgcDriver::Graphics::ZeroFetchVertex(selective).has_value(), "an attribute with out-of-bounds select 2 was taken to read zeros past its records"); + std::array indices{0xffffffffu, 2u, 0x12345u, 0xffffffffu}; + Require(AgcDriver::Graphics::ClampDrawIndices(std::as_writable_bytes(std::span(indices)), 4, true, 9u) == 9u && indices == std::array{0xffffffffu, 2u, 9u, 0xffffffffu}, "clamping kept an index past the limit or moved a restart index"); + Require(AgcDriver::Graphics::ClampDrawIndices(std::as_writable_bytes(std::span(indices)), 4, false, 9u) == 9u && indices == std::array{9u, 2u, 9u, 9u}, "clamping without restart kept a 0xffffffff index"); + std::array narrow{0xffffu, 1u}; + Require(AgcDriver::Graphics::ClampDrawIndices(std::as_writable_bytes(std::span(narrow)), 2, false, 4u) == 4u && narrow[0] == 4u && narrow[1] == 1u, "clamping 16-bit indices failed"); + } attribute.fetchIndex = 1; Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 48, "instance attributes used the vertex index"); Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2, 1) == 80, "first instance was ignored"); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 4481d2cbc..297e683f9 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -61,6 +61,7 @@ Throughout the project, every function at every stage either **does exactly what - [kern.sdk_version](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the system software version as the 4-byte value `0x01000000` (1.00), the value `sceKernelGetSystemSwVersion` reports; a write returns -1 with `EPERM`. The major version is byte 3 and the minor version byte 2, in hex digits that read as decimal (13.60 is `0x1360xxxx`), as [Capy Mobile](https://github.com/khalifa007/CapyMobile/blob/2758c016/src/ps5/platform.c#L64-L71) (PPSA92280), [remote_lua_loader](https://github.com/n0llptr/remote_lua_loader/blob/7c2840e475/savedata/misc.lua#L195-L209) and the [lapse payload](https://github.com/NetflixNHack/Netflix-N-Hack/blob/605eb93ba5/PS4/payloads/lapse.js#L436-L450) read it. Capy Mobile only displays it. The console's firmware version is not modeled and the low two bytes are unknown - PPSA01530, PPSA23827 and PPSA13356 imports declared without parameters, signatures unknown, added without an implementation because the titles fail to load without them: [sceNetShowIfconfig](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet), [sce::Json::Value::setNullAccessCallBack](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [kqueue and kevent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel), and [scePadUnknown_n3kSX62fgNo](../../core/libs/prx/libScePad/Export.cpp) (libScePad), whose real name is unknown. [_sceUltWaitingQueueResourcePoolCreate, _sceUltSemaphoreCreate, _sceUltMutexOptParamInitialize, _sceUltMutexCreate, _sceUltQueueDataResourcePoolCreate and _sceUltQueueCreate](../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to the exports without the underscore; that their argument lists match is inferred from the other underscore Ult exports, not measured. - [sceVoiceQoSSetConnectionAttribute](../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS, NID `KCGOxg8iX7s`) is a throwing stub declared without parameters, like the other endpoint calls of the library: PPSA04159 fails to load without it and its signature is unknown. The name hashes to the imported NID (`tools/nid_names.py`) +- [Indices far past every vertex fetch](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - when a draw's highest index is at least 64Ki vertices past the largest per-vertex record count, every index past that count is moved to it in a copy of the indices, so the zero-padded vertex copies are not sized to the original index (PPSA04159 draws a list of `0xffffffff` with primitive restart off, which asked for 160 GiB). Each attribute fetched by vertex index reads zeros there as at the original index (out-of-bounds select 0 or 1); a draw with another out-of-bounds select keeps its indices. The vertex index the shader sees changes for the moved indices, which a shader that reads it directly would notice - [sceVideoOutGetOutputStatusWithVrr](../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - the name hashes to the imported NID. PPSA26344 version 02.002.000 passes a VideoOut handle and an output pointer at three call sites. It compares a 32-bit value at byte 4 with 2, tests bit 0x10 at byte 16, and, when set, compares low 16-bit values at bytes 48, 50 and 52 with 48, 60 and 120. The full output layout, size, field meanings and console errors are unknown. Every call throws without reading or writing caller memory. - [sceHttp2WebSocketSetPingInterval](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2, NID `YfaXPnc8PJE`) - imported by `libdiscord_partner_sdk.prx` of PPSA23126 (issue 2648). The parameters, the unit of the interval and the console errors are unknown. Every call throws. - [sceKernelGetProsperoSystemSwVersion](../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports system software version `0x01000000` ("01.000.000"); the console's firmware version is not modeled. The 0x28-byte block (a 64-bit size at 0, the version string at 8 and the packed version at 0x24) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which Prospero Multi Tools (PPSA99110) and Prospero Explorer (PPSA99109) are built with and which sets the size to 40, and the same fields in [etaHEN](https://github.com/etaHEN/etaHEN/blob/main/Source%20Code/daemon/include/ipc.hpp) and [shsrv](https://github.com/ps5-payload-dev/shsrv/blob/master/sh.c). A size other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL` without writing; whether the console checks the size is unverified, since etaHEN and shsrv declare a 0x30-byte block and do not set it