feat(agc): draw point lists on the vertex path with their point size

A point list routed through the vertex path was refused, and so was
PA_CL_VS_OUT_CNTL.USE_VTX_POINT_SIZE on it. OliSe Player (PPSA01153)
stops on the latter with two of its MilkDrop presets.

The vertex path now draws point lists as Vulkan points. The vertex stage
writes PointSize: the misc export's X clamped to PA_SU_POINT_MINMAX when
USE_VTX_POINT_SIZE is set, and the PA_SU_POINT_SIZE height otherwise.
The point state travels in the recompile request (version 31) and the
cache and draw keys. Non-square register sizes, inverted clamps, point
sprite texture coordinates and, without largePoints, sizes other than
one pixel throw.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
milescovielloandClaude Opus 5.5 authored and JJenkx committed 2026-10-21 09:13:09 -04:00
1 parent 27bcd6a29e
commit e7ea490325
22 files changed
+375 -16

No files matched your search

@@ -250,9 +250,10 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
if (program.binary.stage == Stage::Fragment || roles[i] == Role::GeometryBack) return;
decodeReads[i].clear();
vertexInfos[i] = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &decodeReads[i], false, 8u - program.firstUserSgpr);
if (vertexInfos[i]) vertexInfos[i]->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, readRegister(queue.context, 0x207), program.tailContinuation.get()) | graphics.layerExports;
if (vertexInfos[i]) {
vertexInfos[i]->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, readRegister(queue.context, 0x207), program.tailContinuation.get()) | graphics.layerExports;
vertexInfos[i]->depthRemapMode = graphics.depthRemap.mode;
Graphics::ApplyPointSize(graphics, *vertexInfos[i]);
}
};
if (!registerKey) {
@@ -2,6 +2,7 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include <algorithm>
#include <bit>
namespace AgcDriver::DriverDetail {
@@ -51,6 +52,10 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
mix(vertex.fetchBufferReg);
mix(vertex.fetchEmbedded);
mix(vertex.paClVsOutCntl);
mix(vertex.pointList);
mix(std::bit_cast<std::uint32_t>(vertex.pointSize));
mix(std::bit_cast<std::uint32_t>(vertex.pointSizeMin));
mix(std::bit_cast<std::uint32_t>(vertex.pointSizeMax));
for (std::uint32_t r = 0; r < vertex.resourcesNum; ++r) {
for (const auto field : vertex.resources[r].fields) mix(field);
const auto& destination = vertex.resourcesDst[r];
@@ -213,7 +213,7 @@ void Driver::traceDrawKeyLookup(const std::array<std::uint64_t, 5>& components,
}
bool Driver::sameVertexInfo(const ShaderRecompiler::ShaderVertexStageInfo& a, const ShaderRecompiler::ShaderVertexStageInfo& b) {
if (a.resourcesNum != b.resourcesNum || a.fetchAttribReg != b.fetchAttribReg || a.fetchBufferReg != b.fetchBufferReg || a.fetchEmbedded != b.fetchEmbedded || a.paClVsOutCntl != b.paClVsOutCntl) return false;
if (a.resourcesNum != b.resourcesNum || a.fetchAttribReg != b.fetchAttribReg || a.fetchBufferReg != b.fetchBufferReg || a.fetchEmbedded != b.fetchEmbedded || a.paClVsOutCntl != b.paClVsOutCntl || a.pointList != b.pointList || a.pointSize != b.pointSize || a.pointSizeMin != b.pointSizeMin || a.pointSizeMax != b.pointSizeMax) return false;
if (a.depthRemapMode != b.depthRemapMode) return false;
for (std::uint32_t i = 0; i < a.resourcesNum && i < a.resources.size(); ++i) {
if (a.resources[i].fields != b.resources[i].fields) return false;
@@ -1435,7 +1435,10 @@ std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decod
prepared.push_back({program.snapshot, {program.codeOffset, nullptr}, program.binary.stage, offset, nullptr});
continue;
}
if (vertex) vertex->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, decoded.state.paClVsOutCntl, program.tailContinuation.get()) | decoded.state.layerExports;
if (vertex) {
vertex->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, decoded.state.paClVsOutCntl, program.tailContinuation.get()) | decoded.state.layerExports;
Graphics::ApplyPointSize(decoded.state, *vertex);
}
ShaderRecompiler::RecompileRequest request{program.binary, {wave, program.firstUserSgpr, program.userData, {}, fragment ? std::optional(decoded.pixel) : std::nullopt, vertex, memory, RegisteredFloatMode(*program.snapshot)}, target, {0, 0, offset, room}, ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, decoded.state.stages.mesh, decoded.state.stages.tessellation, {}}};
std::vector<std::uint64_t> key;
ShaderRecompiler::BuildPreparedShaderKey(request, key);
@@ -314,6 +314,7 @@ struct VulkanDevice::State {
bool fragmentShaderBarycentric = false;
bool shaderTerminateInvocation = false;
bool geometryShader = false;
bool largePoints = false;
bool sampleRateShading = false;
bool storageImageMultisample = false;
bool shaderClipDistance = false;
@@ -1702,6 +1703,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
enabled.sampleRateShading = available.sampleRateShading;
enabled.geometryShader = available.geometryShader;
state->geometryShader = enabled.geometryShader == VK_TRUE;
enabled.largePoints = available.largePoints;
state->largePoints = enabled.largePoints == VK_TRUE;
state->sampleRateShading = enabled.sampleRateShading == VK_TRUE;
if (enabled.sampleRateShading) state->capabilities.push_back(spv::CapabilityInterpolationFunction);
if (enabled.geometryShader) state->capabilities.push_back(spv::CapabilityGeometry);
@@ -3829,6 +3832,7 @@ Graphics::Context VulkanDevice::buildContext() const {
context.imageTableUpdateAfterBind = ImageTableUpdateAfterBind(context);
context.samplerTableUpdateAfterBind = SamplerTableUpdateAfterBind(context);
context.geometryShader = state->geometryShader;
context.largePoints = state->largePoints;
context.sampleRateShading = state->sampleRateShading;
context.fillModeNonSolid = state->fillModeNonSolid;
context.maintenance5 = state->maintenance5;
@@ -237,6 +237,7 @@ struct Context {
bool imageTableUpdateAfterBind = false;
bool samplerTableUpdateAfterBind = false;
bool geometryShader = false;
bool largePoints = false;
bool sampleRateShading = false;
bool fillModeNonSolid = false;
bool maintenance5 = false;
@@ -153,6 +153,10 @@ struct State {
bool customSampleLocations = false;
bool sampleLocationGrid = false;
std::uint32_t paClVsOutCntl = 0;
bool vertexPoints = false;
float pointSize = 1.0f;
float pointSizeMin = 0.0f;
float pointSizeMax = 0.0f;
std::array<std::uint16_t, 4> quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu};
bool pixelEarlyZ = false;
};
@@ -162,6 +166,7 @@ struct State {
inline constexpr std::uint32_t PositionExportLayoutBits = 0xffffu | (1u << 21u) | (1u << 22u) | (1u << 23u);
ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup = false);
void ApplyPointSize(const State& state, ShaderRecompiler::ShaderVertexStageInfo& vertex);
State DecodeState(const QueueState& queue, bool vertexWorkgroup = false);
void ApplyDepthRemap(const Context& context, State& state);
void NormalizeUnusedViewportDepth(State& state, const ShaderRecompiler::ShaderPixelStageInfo& pixel);
@@ -666,6 +666,7 @@ bool ValidationKey(const Context& context, std::span<const CompiledShader> shade
add(context.viewportIndexLayer);
add(context.shaderTerminateInvocation);
add(state.paClVsOutCntl);
add(state.vertexPoints);
add(state.stages.mesh.has_value());
if (state.stages.mesh) {
const auto& mesh = *state.stages.mesh;
@@ -341,6 +341,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
Require(!state.negativeOneToOne || context.depthClipControl, "negative-one-to-one depth clipping requires VK_EXT_depth_clip_control with depthClipControl enabled");
Require(!state.perSampleShading || context.sampleRateShading, "per-sample pixel shading requires the sampleRateShading device feature");
Require(!state.mergedCoverage || (context.mergedCoverageSampleCounts & state.samples) != 0, "an overrasterized draw (a multisampled rasterizer into one-sample color targets) requires VK_NV_framebuffer_mixed_samples with merged coverage reduction");
Require(!state.vertexPoints || context.largePoints || (state.pointSize == 1.0f && ((state.paClVsOutCntl & (1u << 16u)) == 0 || (state.pointSizeMin == 1.0f && state.pointSizeMax == 1.0f))), "points of a size other than one pixel require the largePoints device feature");
Require(state.conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT || context.conservativeRasterization, "conservative rasterization requires VK_EXT_conservative_rasterization with at most 1/256 pixel of overestimation and degenerate triangles rasterized");
ValidateProvokingVertex(context, state, shaders);
if (state.rectList) Require(context.tessellationShader && context.limits.maxTessellationPatchSize >= 4, "rect-list requires tessellation with four output control points");
@@ -53,6 +53,8 @@ struct Module {
bool layerOutput = false;
bool meshCapability = false;
std::uint32_t primitiveInputs = 0;
bool pointSizeOutput = false;
bool pointSize = false;
std::map<std::uint32_t, std::vector<std::uint32_t>> modes;
const std::vector<std::uint32_t>& Type(std::uint32_t id) const {
@@ -149,6 +151,10 @@ struct Module {
Require((value == spv::BuiltInVertexIndex || value == spv::BuiltInInstanceIndex || value == spv::BuiltInBaseInstance) && signature == "i32", "unsupported vertex built-in input");
} else if (vertex && storage == spv::StorageClassOutput) {
if (value == spv::BuiltInLayer && layerOutput && (signature == "u32" || signature == "i32")) return;
if (value == spv::BuiltInPointSize && pointSizeOutput && signature == "f32" && !pointSize) {
pointSize = true;
return;
}
Require(value == spv::BuiltInPosition && signature == "f32x4" && !position, "unsupported or duplicate vertex built-in output");
position = true;
} else {
@@ -186,6 +192,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
Require(words[4] == 0, "unsupported SPIR-V schema: " + std::to_string(words[4]));
Module module;
module.pointSizeOutput = state.vertexPoints && stage == Stage::Vertex;
std::uint32_t entries = 0;
std::uint32_t memoryModels = 0;
std::uint32_t entryPoint = 0;
@@ -190,6 +190,32 @@ std::uint32_t inactiveVertexPointSize(const QueueState& queue) {
}
}
bool vertexPointList(const QueueState& queue) {
const auto stages = find(queue.context, 0x2d5);
const auto primitive = find(queue.userConfig, 0x242, RegisterBank::UserConfig);
return stages != queue.context.end() && primitive != queue.userConfig.end() && (stages->second & 0x24u) == 0 && primitive->second == 1u;
}
void decodePointSize(const QueueState& queue, State& result) {
const auto& cx = queue.context;
const auto size = read(cx, 0x280);
const auto limits = read(cx, 0x281);
const bool fromVertex = (result.paClVsOutCntl & (1u << 16u)) != 0;
Require(!fromVertex || (result.paClVsOutCntl & (1u << 21u)) != 0, "USE_VTX_POINT_SIZE without VS_OUT_MISC_VEC_ENA is unsupported");
Require(fromVertex || (size & 0xffffu) == size >> 16u, "points whose PA_SU_POINT_SIZE width and height differ are unsupported");
Require((limits & 0xffffu) <= limits >> 16u, "PA_SU_POINT_MINMAX with a minimum above its maximum is unsupported");
result.pointSize = static_cast<float>(size & 0xffffu) / 8.0f;
result.pointSizeMin = static_cast<float>(limits & 0xffffu) / 8.0f;
result.pointSizeMax = static_cast<float>(limits >> 16u) / 8.0f;
const auto interpolation = find(cx, 0x1b5);
const auto inputs = find(cx, 0x1b6);
if (interpolation == cx.end() || (interpolation->second & 2u) == 0 || inputs == cx.end()) return;
for (std::uint32_t input = 0; input < (inputs->second & 0x3fu); ++input) {
const auto control = find(cx, 0x191 + input);
Require(control == cx.end() || (control->second & 0x820000u) == 0, "point sprite texture coordinates (SPI_PS_INPUT_CNTL PT_SPRITE_TEX) are not implemented");
}
}
constexpr std::uint32_t DepthControlMask = ~0x007007f0u;
// EXEC_ON_HIER_FAIL / EXEC_ON_NOOP / EXEC_IF_OVERLAPPED (bits 9, 10, 17) only force the pixel shader
// to run, which it always does here.
@@ -1044,6 +1070,15 @@ std::array<std::uint8_t, 32> PixelSampleLocations(const State& state, const Shad
return locations;
}
void ApplyPointSize(const State& state, ShaderRecompiler::ShaderVertexStageInfo& vertex) {
if (!state.vertexPoints) return;
vertex.paClVsOutCntl |= state.paClVsOutCntl & (1u << 16u);
vertex.pointList = true;
vertex.pointSize = state.pointSize;
vertex.pointSizeMin = state.pointSizeMin;
vertex.pointSizeMax = state.pointSizeMax;
}
ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) {
const auto value = read(queue.context, 0x2d5);
const auto validate = [&](bool condition, const char* reason) {
@@ -1218,7 +1253,11 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
const auto primitive = read(queue.userConfig, 0x242, RegisterBank::UserConfig);
APS5_LOG_OUT_DEBUG("DecodeState primitive=%u path=%u vertexWave=%u", primitive, static_cast<unsigned>(result.stages.path), result.stages.vertexWaveSize);
switch (primitive) {
case 1: Require(result.stages.mesh.has_value(), "point-list vertex rendering requires point-size output support"); result.topology = VK_PRIMITIVE_TOPOLOGY_POINT_LIST; break;
case 1:
Require(result.stages.mesh.has_value() || result.stages.path == ShaderPath::Vertex, "point lists are only drawn through the vertex and geometry paths");
result.topology = VK_PRIMITIVE_TOPOLOGY_POINT_LIST;
result.vertexPoints = result.stages.path == ShaderPath::Vertex;
break;
case 2: result.topology = VK_PRIMITIVE_TOPOLOGY_LINE_LIST; break;
case 3: result.topology = VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; break;
case 7:
@@ -1243,9 +1282,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
}
APS5_LOG_OUT_DEBUG("Topology=%u", static_cast<unsigned>(result.topology));
result.primitiveRestart = read(queue.userConfig, 0x24b, RegisterBank::UserConfig) != 0 && !result.rectList && !result.quadList && result.topology != VK_PRIMITIVE_TOPOLOGY_PATCH_LIST;
zero(cx, 0x207, ~(LayerExports | ClipCullExports | inactiveVertexPointSize(queue)), "layer, viewport or auxiliary vertex exports");
zero(cx, 0x207, ~(LayerExports | ClipCullExports | (result.vertexPoints ? 1u << 16u : inactiveVertexPointSize(queue))), "layer, viewport or auxiliary vertex exports");
Require(std::popcount(read(cx, 0x207) & 0xffffu) <= 8, "more than eight clip and cull distances are unsupported");
result.paClVsOutCntl = read(cx, 0x207);
if (result.vertexPoints) decodePointSize(queue, result);
result.samples = decodeSamples(cx);
Require(coveredCentroidSupported(queue), "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs is not implemented");
decodeSampleLocations(cx, result);
@@ -2006,7 +2046,7 @@ std::string DrawRejection(const QueueState& queue, bool indexed) {
if (value(queue.userConfig, 0x242, primitive) && (primitive & 0x3fu) != 1 && (primitive & 0x3fu) != 2 && (primitive & 0x3fu) != 3 && (primitive & 0x3fu) != 4 && (primitive & 0x3fu) != 5 && (primitive & 0x3fu) != 6 && (primitive & 0x3fu) != 21) return "AGC graphics: primitive restart is only supported for point, line and triangle topologies";
if (value(cx, 0x103, resetIndex) && (resetIndex & 0xffffu) != 0xffffu) return "AGC graphics: primitive restart index other than all ones is unsupported";
}
if (auto reason = nonzero(cx, 0x207, ~(LayerExports | ClipCullExports | inactiveVertexPointSize(queue)), "layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason;
if (auto reason = nonzero(cx, 0x207, ~(LayerExports | ClipCullExports | (vertexPointList(queue) ? 1u << 16u : inactiveVertexPointSize(queue))), "layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason;
if (value(cx, 0x207, word) && std::popcount(word & 0xffffu) > 8) return require(false, "more than eight clip and cull distances are unsupported");
if (value(cx, 0x200, word)) {
const auto renderControl = find(cx, 0x000);
@@ -162,9 +162,15 @@ void inactiveVertexPointSizeTests() {
}
throw std::runtime_error("potentially active point size passed the state decoder");
};
for (const auto primitive : {1u, 9u}) {
{
auto queue = pointSizeState();
queue.userConfig[0x242] = primitive;
queue.userConfig[0x242] = 9;
rejected(queue);
}
{
auto queue = pointSizeState();
queue.userConfig[0x242] = 1;
queue.context[0x2d5] = 0x2020;
rejected(queue);
}
for (const auto stages : {0x2004u, 0x2020u}) {
@@ -221,6 +227,66 @@ void PolygonPrimitiveTests() {
Require(indexedState.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN && indexedState.primitiveRestart, "indexed polygon decoding did not keep primitive restart");
}
void vertexPointListTests() {
const auto pointState = [](std::uint32_t control, std::uint32_t size, std::uint32_t limits) {
auto queue = makeState();
queue.userConfig[0x242] = 1;
queue.context[0x1b3] = queue.context[0x1b4] = 2;
queue.context[0x207] = control;
queue.context[0x280] = size;
queue.context[0x281] = limits;
return queue;
};
{
const auto queue = pointState(0x01210000u, 0x00080008u, 0x00080008u);
const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false);
Require(rejection.empty(), "a vertex-path point list with a vertex point size failed the draw precheck: " + rejection);
const auto state = AgcDriver::Graphics::DecodeState(queue);
Require(state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST && state.paClVsOutCntl == 0x01210000u, "a vertex-path point list with a vertex point size did not decode as a point list");
Require(AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Point, "a vertex-path point list is not rasterized as points");
Require(state.vertexPoints && state.pointSize == 1.0f && state.pointSizeMin == 1.0f && state.pointSizeMax == 1.0f, "PA_SU_POINT_SIZE and PA_SU_POINT_MINMAX of 8 did not decode as one-pixel points");
ShaderRecompiler::ShaderVertexStageInfo vertex{};
vertex.paClVsOutCntl = 1u << 21u;
AgcDriver::Graphics::ApplyPointSize(state, vertex);
Require(vertex.pointList && vertex.paClVsOutCntl == ((1u << 16u) | (1u << 21u)) && vertex.pointSize == 1.0f && vertex.pointSizeMin == 1.0f && vertex.pointSizeMax == 1.0f, "the vertex stage did not receive the vertex point size");
}
{
const auto state = AgcDriver::Graphics::DecodeState(pointState(0, 0x00200020u, 0x00100004u));
Require(state.vertexPoints && state.pointSize == 4.0f && state.pointSizeMin == 0.5f && state.pointSizeMax == 2.0f, "a register point size did not decode as diameters");
ShaderRecompiler::ShaderVertexStageInfo vertex{};
vertex.paClVsOutCntl = 1u << 21u;
AgcDriver::Graphics::ApplyPointSize(state, vertex);
Require(vertex.pointList && vertex.paClVsOutCntl == (1u << 21u) && vertex.pointSize == 4.0f, "a register point size turned on the vertex point size");
}
{
const auto state = AgcDriver::Graphics::DecodeState(pointState(0x01210000u, 0x00200010u, 0x00400000u));
Require(state.pointSizeMin == 0.0f && state.pointSizeMax == 8.0f, "a vertex point size refused a register size whose width and height differ");
}
{
auto queue = makeState();
queue.context[0x1b3] = queue.context[0x1b4] = 2;
queue.context[0x207] = 0x01210000u;
const auto state = AgcDriver::Graphics::DecodeState(queue);
Require(!state.vertexPoints, "a triangle draw decoded as vertex points");
ShaderRecompiler::ShaderVertexStageInfo vertex{};
AgcDriver::Graphics::ApplyPointSize(state, vertex);
Require(!vertex.pointList && vertex.paClVsOutCntl == 0, "a triangle draw received a point size");
}
expectFailure([&] { AgcDriver::Graphics::DecodeState(pointState(0, 0x00200010u, 0x00080008u)); }, "points whose PA_SU_POINT_SIZE width and height differ are unsupported");
expectFailure([&] { AgcDriver::Graphics::DecodeState(pointState(0, 0x00080008u, 0x00080010u)); }, "PA_SU_POINT_MINMAX with a minimum above its maximum is unsupported");
expectFailure([&] { AgcDriver::Graphics::DecodeState(pointState(1u << 16u, 0x00080008u, 0x00080008u)); }, "USE_VTX_POINT_SIZE without VS_OUT_MISC_VEC_ENA is unsupported");
for (const auto sprite : {1u << 17u, 1u << 23u}) {
auto queue = pointState(0x01210000u, 0x00080008u, 0x00080008u);
queue.context[0x1b5] = 0x486bu;
queue.context[0x1b6] = 2;
queue.context[0x191] = 0;
queue.context[0x192] = sprite;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "point sprite texture coordinates (SPI_PS_INPUT_CNTL PT_SPRITE_TEX) are not implemented");
queue.context[0x1b5] = 0x4869u;
Require(AgcDriver::Graphics::DecodeState(queue).vertexPoints, "PT_SPRITE_TEX was refused with point sprites off");
}
}
void retainedPixelStageTests() {
using namespace AgcDriver::Graphics;
auto queue = makeState();
@@ -11104,6 +11170,7 @@ int main(int argc, char** argv) {
NggVertexWorkgroupTests();
inactiveVertexPointSizeTests();
PolygonPrimitiveTests();
vertexPointListTests();
TuningFieldTests();
PixelInputLayoutTests();
UnexportedPixelInputTests();
@@ -0,0 +1,157 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <bit>
#include <cstdint>
#include <cstring>
#include <exception>
#include <iostream>
#include <optional>
#include <span>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Extent = 64u;
constexpr std::uint32_t MiscVector = 1u << 21u;
constexpr std::uint32_t VertexPointSize = 1u << 16u;
alignas(256) std::array<std::byte, Extent * Extent * 4u> pixels{};
alignas(256) std::array<std::byte, 512> vertices{};
constexpr std::array<std::uint32_t, 11> positionCode{0xf4080100u, 0xfa000000u, 0x4a0a0a02u, 0x4a0a0b08u, 0x4a0a0a03u, 0xe00c2000u, 0x80010005u, 0xbf8c3f70u, 0xf80008cfu, 0x03020100u, 0xbf810000u};
constexpr std::array<std::uint32_t, 5> pixelCode{0x7e0002f2u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u};
std::vector<std::uint32_t> SizeExportCode(float size) {
std::vector<std::uint32_t> code(positionCode.begin(), positionCode.begin() + 8);
code.insert(code.end(), {0x7e0802ffu, std::bit_cast<std::uint32_t>(size), 0xf80000cfu, 0x03020100u, 0xf80008d1u, 0x00000004u, 0xbf810000u});
return code;
}
struct PointSize {
float size = 1.0f;
float minimum = 0.0f;
float maximum = 0.0f;
bool fromVertex = false;
};
struct Square {
unsigned pixels = 0;
std::uint32_t left = Extent, top = Extent, right = 0, bottom = 0;
};
std::vector<Square> RunCase(AgcDriver::VulkanDevice& device, const char* name, std::span<const std::uint32_t> vertexCode, const PointSize& points, float offset) {
const std::array<std::array<float, 2>, 4> centres{{{16.0f, 16.0f}, {48.0f, 16.0f}, {16.0f, 48.0f}, {48.0f, 48.0f}}};
vertices.fill(std::byte{0});
constexpr std::uint32_t stride = 16u;
for (std::size_t index = 0; index < centres.size(); ++index) {
const std::array<float, 4> position{(centres[index][0] + offset) / Extent * 2.0f - 1.0f, (centres[index][1] + offset) / Extent * 2.0f - 1.0f, 0.0f, 1.0f};
std::memcpy(vertices.data() + index * stride, position.data(), sizeof(position));
}
std::array<std::uint32_t, 4> descriptor{};
const auto tableAddress = reinterpret_cast<std::uintptr_t>(descriptor.data());
std::array<std::uint32_t, 4> userData{static_cast<std::uint32_t>(tableAddress), static_cast<std::uint32_t>(tableAddress >> 32u), 0u, 0u};
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{tableAddress, std::as_bytes(std::span(descriptor))}}};
ShaderRecompiler::ShaderVertexStageInfo vertexInfo{};
vertexInfo.fetchEmbedded = true;
vertexInfo.resourcesNum = 1u;
vertexInfo.resourcesDst[0] = {0, 4, 0, 0};
vertexInfo.paClVsOutCntl = points.fromVertex ? MiscVector | VertexPointSize : 0u;
vertexInfo.pointList = true;
vertexInfo.pointSize = points.size;
vertexInfo.pointSizeMin = points.minimum;
vertexInfo.pointSizeMax = points.maximum;
ShaderRecompiler::RecompileRequest request{{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(vertexCode.data()), vertexCode, 0, {}}, {32u, 0u, userData, std::nullopt, std::nullopt, vertexInfo, memory}, device.Target(), {0u, 0u, 0u, 96u}};
const auto address = reinterpret_cast<std::uintptr_t>(vertices.data());
descriptor = {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u) | (stride << 16u), static_cast<std::uint32_t>(centres.size()), 0x01000facu | (77u << 12u)};
request.context.vertex->resources[0].fields = descriptor;
const auto vertex = ShaderRecompiler::Recompile(request);
const auto pushBytes = static_cast<std::uint32_t>(vertex.pushConstants.size());
ShaderRecompiler::ShaderPixelStageInfo pixelInfo{};
pixelInfo.wave32 = true;
pixelInfo.targetOutputMode[0] = 9u;
pixelInfo.targetExportMapping.fill(0xe4u);
pixelInfo.rasterPrimitive = ShaderRecompiler::RasterPrimitive::Point;
ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), PixelPushLayout(pushBytes, device.Target())};
const auto pixel = ShaderRecompiler::Recompile(pixelRequest);
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, PixelPushOffset(pushBytes, device.Target())}}};
AgcDriver::Graphics::State state{};
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt};
state.color = {reinterpret_cast<std::uintptr_t>(pixels.data()), {Extent, Extent}, VK_FORMAT_R8G8B8A8_UNORM, pixels.size(), 0xe4u};
state.colors = {state.color};
state.hasColorTarget = true;
state.renderExtent = {Extent, Extent};
state.topology = VK_PRIMITIVE_TOPOLOGY_POINT_LIST;
state.paClVsOutCntl = vertexInfo.paClVsOutCntl;
state.vertexPoints = true;
state.pointSize = points.size;
state.pointSizeMin = points.minimum;
state.pointSizeMax = points.maximum;
state.viewport = {0.0f, static_cast<float>(Extent), static_cast<float>(Extent), -static_cast<float>(Extent), 0.0f, 1.0f};
state.scissor = {{0, 0}, {Extent, Extent}};
state.cullMode = VK_CULL_MODE_NONE;
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
state.blend.colorWriteMask = 15u;
state.blends = {state.blend};
AgcDriver::Pm4::DrawParameters draw{0u, static_cast<std::uint32_t>(centres.size()), 0u, 1u, 0u, false};
draw.firstInstance = 0u;
pixels.fill(std::byte{0});
device.Draw(state, draw, shaders);
device.WaitIdle();
std::vector<Square> squares(centres.size());
for (std::uint32_t y = 0; y < Extent; ++y) {
for (std::uint32_t x = 0; x < Extent; ++x) {
const auto index = (y * Extent + x) * 4u;
if (pixels[index] == std::byte{0} && pixels[index + 3u] == std::byte{0}) continue;
auto& square = squares[(y >= Extent / 2u ? 2u : 0u) + (x >= Extent / 2u ? 1u : 0u)];
++square.pixels;
square.left = std::min(square.left, x);
square.top = std::min(square.top, y);
square.right = std::max(square.right, x + 1u);
square.bottom = std::max(square.bottom, y + 1u);
}
}
std::cout << "case " << name << ":";
for (const auto& square : squares) std::cout << " " << square.pixels << " [" << square.left << "," << square.top << ")-(" << square.right << "," << square.bottom << ")";
std::cout << "\n";
return squares;
}
void RequireSquares(const std::vector<Square>& squares, std::uint32_t side, const char* name) {
for (const auto& square : squares) {
Require(square.pixels == side * side && square.right - square.left == side && square.bottom - square.top == side, std::string(name) + ": a point is not a " + std::to_string(side) + "x" + std::to_string(side) + " square");
}
}
void Run(AgcDriver::VulkanDevice& device) {
std::cout << "device: " << device.DeviceName() << "\n";
RequireSquares(RunCase(device, "register size 1", positionCode, {1.0f, 1.0f, 1.0f, false}, 0.5f), 1u, "register size 1");
RequireSquares(RunCase(device, "register size 4", positionCode, {4.0f, 1.0f, 1.0f, false}, 0.0f), 4u, "register size 4");
RequireSquares(RunCase(device, "register size 3 ignores the clamp", positionCode, {3.0f, 6.0f, 8.0f, false}, 0.5f), 3u, "register size 3 ignores the clamp");
const auto two = SizeExportCode(2.0f);
RequireSquares(RunCase(device, "vertex size 2 within [1, 8]", two, {1.0f, 1.0f, 8.0f, true}, 0.0f), 2u, "vertex size 2 within [1, 8]");
const auto six = SizeExportCode(6.0f);
RequireSquares(RunCase(device, "vertex size 6 clamped to 4", six, {1.0f, 0.5f, 4.0f, true}, 0.0f), 4u, "vertex size 6 clamped to 4");
const auto one = SizeExportCode(1.0f);
RequireSquares(RunCase(device, "vertex size 1 clamped to 5", one, {1.0f, 5.0f, 8.0f, true}, 0.5f), 5u, "vertex size 1 clamped to 5");
RequireSquares(RunCase(device, "vertex size over a register size", six, {1.0f, 1.0f, 8.0f, true}, 0.0f), 6u, "vertex size over a register size");
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
Run(*device);
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
+5
View File
@@ -4,6 +4,7 @@
#include "Recompiler.hpp"
#include "RdnaDecoder/RdnaDescriptorFormat.hpp"
#include "prx/libc/include/HostThreadLocal.hpp"
#include <bit>
#include <cstdlib>
#include <stdexcept>
#include <type_traits>
@@ -233,6 +234,10 @@ private:
append(key, value.fetchEmbedded);
append(key, value.paClVsOutCntl);
append(key, value.depthRemapMode);
append(key, value.pointList);
append(key, std::bit_cast<std::uint32_t>(value.pointSize));
append(key, std::bit_cast<std::uint32_t>(value.pointSizeMin));
append(key, std::bit_cast<std::uint32_t>(value.pointSizeMax));
if (value.resourcesNum > value.resources.size()) throw std::runtime_error("Shader cache: invalid vertex resource count");
append(key, value.resourcesNum);
for (std::uint32_t i = 0; i < value.resourcesNum; ++i) {
@@ -1,4 +1,5 @@
#include "ControlFlow/RequestSerializer.hpp"
#include <bit>
#include <stdexcept>
namespace ShaderRecompiler {
@@ -863,6 +864,12 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const
}
writer.WriteBool(request.target.float32FlushToZero);
writer.WriteBool(request.target.float32DenormPreserve);
if (request.context.vertex.has_value()) {
writer.WriteBool(request.context.vertex->pointList);
writer.WriteU32(std::bit_cast<std::uint32_t>(request.context.vertex->pointSize));
writer.WriteU32(std::bit_cast<std::uint32_t>(request.context.vertex->pointSizeMin));
writer.WriteU32(std::bit_cast<std::uint32_t>(request.context.vertex->pointSizeMax));
}
return base64Encode(buffer);
}
@@ -917,6 +924,14 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const
}
if (version >= 36u) result.request.target.float32FlushToZero = reader.ReadBool();
if (version >= 41u) result.request.target.float32DenormPreserve = reader.ReadBool();
if (version >= 33u && result.request.context.vertex.has_value()) {
auto& vertex = *result.request.context.vertex;
vertex.pointList = reader.ReadBool();
vertex.pointSize = std::bit_cast<float>(reader.ReadU32());
vertex.pointSizeMin = std::bit_cast<float>(reader.ReadU32());
vertex.pointSizeMax = std::bit_cast<float>(reader.ReadU32());
result.vertex = vertex;
}
if (version >= 21u && reader.Remaining() != 0u) throw std::runtime_error("RequestSerializer: unexpected trailing data");
return result;
}
@@ -141,6 +141,10 @@ struct ShaderVertexInputInfo {
std::uint32_t scratchSizeDwords = 0;
std::uint32_t hostSubgroupSize = 64;
std::uint32_t paClVsOutCntl = 0;
bool pointList = false;
float pointSize = 1.0f;
float pointSizeMin = 0.0f;
float pointSizeMax = 0.0f;
ShaderClipSpaceTransform clipSpace;
ShaderDepthRemap depthRemap;
ShaderMeshInputInfo mesh;
@@ -588,6 +588,7 @@ void ShaderInfoCollector::Collect(IrProgram& program, const ShaderStageInputInfo
}
CollectBuiltinInputs(program, next);
CollectOutputs(program, inputInfo, next);
if (program.Resources().stage == IrShaderStage::Vertex && inputInfo.vertex != nullptr && inputInfo.vertex->pointList) AddOutput(next, StageOutputKind::PointSize, 0, 0, "gl_PointSize");
CollectSamplerUses(program, next);
program.Info() = std::move(next);
program.Metadata().shaderInfoComplete = true;
+4
View File
@@ -214,6 +214,10 @@ struct ShaderVertexStageInfo {
bool fetchEmbedded;
std::uint32_t paClVsOutCntl = 0;
std::uint32_t depthRemapMode = 0;
bool pointList = false;
float pointSize = 1.0f;
float pointSizeMin = 0.0f;
float pointSizeMax = 0.0f;
};
struct ShaderFloatMode {
@@ -9,6 +9,7 @@
#include "PipelineSpecialization.hpp"
#include <algorithm>
#include <array>
#include <bit>
#include <limits>
#include <stdexcept>
@@ -611,7 +612,12 @@ void EmitAuxPositionExport(SpirvValueEmitContext& ctx, std::uint32_t data, const
const auto f32 = state.module.AllocateId();
state.module.AddFunction(spv::OpBitcast, TypeF32(state), f32, raw);
if (output.pointSize) {
state.module.AddFunction(spv::OpStore, state.pointSizeVariable, f32);
auto size = f32;
if (state.inputInfo.vertex->pointList) {
size = state.module.AllocateId();
state.module.AddFunction(spv::OpExtInst, TypeF32(state), size, GlslStd450(state), GLSLstd450FClamp, f32, ConstantF32(state, std::bit_cast<std::uint32_t>(state.inputInfo.vertex->pointSizeMin)), ConstantF32(state, std::bit_cast<std::uint32_t>(state.inputInfo.vertex->pointSizeMax)));
}
state.module.AddFunction(spv::OpStore, state.pointSizeVariable, size);
continue;
}
const auto storeDistance = [&](StageOutputKind kind, std::uint32_t variable, std::uint32_t index) {
@@ -1556,6 +1562,7 @@ void EmitProgram(SpirvEmitterState& state) {
}
if (AlphaToCoverage(state)) StoreCoverage(state, ConstantI32(state, -1));
EmitBdaAnchors(state);
if (state.pointSizeVariable != 0u && state.inputInfo.vertex != nullptr && state.inputInfo.vertex->pointList) state.module.AddFunction(spv::OpStore, state.pointSizeVariable, ConstantF32(state, std::bit_cast<std::uint32_t>(state.inputInfo.vertex->pointSize)));
if (OrderedPixelShader(state)) {
state.module.AddFunction(spv::OpBeginInvocationInterlockEXT);
}
@@ -190,6 +190,10 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues
vertexStorage.fetchEmbedded = vertex.fetchEmbedded;
vertexStorage.paClVsOutCntl = vertex.paClVsOutCntl;
vertexStorage.depthRemap = {vertex.depthRemapMode};
vertexStorage.pointList = vertex.pointList;
vertexStorage.pointSize = vertex.pointSize;
vertexStorage.pointSizeMin = vertex.pointSizeMin;
vertexStorage.pointSizeMax = vertex.pointSizeMax;
vertexStorage.fetchExternal = false;
vertexStorage.fetchAttribReg = static_cast<int>(vertex.fetchAttribReg);
vertexStorage.fetchBufferReg = static_cast<int>(vertex.fetchBufferReg);
@@ -57,6 +57,11 @@ void InsertPixelDefaultsAfterFrontFace(std::vector<std::uint8_t>& bytes, bool sa
bytes.insert(bytes.begin() + 282u, sampleOrder ? 51u : 35u, 0u);
}
void AppendVertexTail(std::vector<std::uint8_t>& bytes) {
bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u);
bytes.insert(bytes.end(), {0u, 0u, 0u, 0x80u, 0x3fu, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u});
}
std::vector<std::uint8_t> DecodeBase64(std::string_view text) {
constexpr std::string_view alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::vector<std::uint8_t> bytes;
@@ -219,7 +224,7 @@ int main() {
if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u});
if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded);
if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 6u, 0u);
upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u);
AppendVertexTail(upgraded);
Require(serializer.Serialize(request) == EncodeBase64(upgraded));
auto truncated = bytes;
truncated.pop_back();
@@ -259,7 +264,7 @@ int main() {
layered.insert(layered.begin() + 259u, {0u, 0u});
InsertPixelDefaultsAfterFrontFace(layered);
if (fixture.tessellation) layered.insert(layered.end() - 35u, 6u, 0u);
layered.insert(layered.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u);
AppendVertexTail(layered);
Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered));
}
}
@@ -284,7 +289,7 @@ int main() {
InsertMeshBarycentricFlag(bytes);
bytes.insert(bytes.begin() + 259u, {0u, 0u});
InsertPixelDefaultsAfterFrontFace(bytes);
bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u);
AppendVertexTail(bytes);
Require(serializer.Serialize(replay.request) == EncodeBase64(bytes));
auto truncated23 = DecodeBase64(encoded23);
@@ -413,8 +418,8 @@ int main() {
upgraded[4] = 42u;
if (fixture.pixel) upgraded.insert(upgraded.begin() + 317u, 16u, 0u);
if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 1u, 0u);
main38.insert(main38.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u);
upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u);
AppendVertexTail(main38);
AppendVertexTail(upgraded);
Require(serializer.Serialize(request) == EncodeBase64(upgraded));
const auto replay38 = serializer.Deserialize(EncodeBase64(main38));
Require(replay38.request.graphics.has_value() && replay38.request.graphics->tessellation.has_value() == fixture.tessellation);
@@ -437,7 +442,7 @@ int main() {
InsertPixelDefaultsAfterFrontFace(main25);
}
if (fixture.tessellation) main25.insert(main25.end() - 35u, 1u, 0u);
main25.insert(main25.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u);
AppendVertexTail(main25);
Require(serializer.Serialize(replay25.request) == EncodeBase64(main25));
}
}
@@ -526,6 +531,27 @@ int main() {
const auto replay35 = serializer.Deserialize(EncodeBase64(version35));
Require(replay35.request.target.spannedWaves == spanned && !replay35.request.target.float32FlushToZero);
}
ShaderRecompiler::RecompileRequest pointRequest{};
pointRequest.context.vertex = ShaderRecompiler::ShaderVertexStageInfo{};
pointRequest.context.vertex->paClVsOutCntl = 0x01210000u;
pointRequest.context.vertex->pointList = true;
pointRequest.context.vertex->pointSize = 4.0f;
pointRequest.context.vertex->pointSizeMin = 0.5f;
pointRequest.context.vertex->pointSizeMax = 8.0f;
const auto pointEncoded = serializer.Serialize(pointRequest);
const auto pointReplay = serializer.Deserialize(pointEncoded);
Require(pointReplay.request.context.vertex.has_value() && pointReplay.request.context.vertex->paClVsOutCntl == 0x01210000u && pointReplay.request.context.vertex->pointList);
Require(pointReplay.request.context.vertex->pointSize == 4.0f && pointReplay.request.context.vertex->pointSizeMin == 0.5f && pointReplay.request.context.vertex->pointSizeMax == 8.0f);
Require(serializer.Serialize(pointReplay.request) == pointEncoded);
auto pointBytes = DecodeBase64(pointEncoded);
pointBytes.pop_back();
bool pointRejected = false;
try {
static_cast<void>(serializer.Deserialize(EncodeBase64(pointBytes)));
} catch (const std::runtime_error& error) {
pointRejected = std::string_view(error.what()) == "RequestSerializer: truncated data";
}
Require(pointRejected);
return 0;
}
+2 -1
View File
@@ -16,7 +16,8 @@
- [Optional BMP frame dumps](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) use checked host stdio. An open, header, pixel or close failure prints one diagnostic and disables subsequent BMP dumps for the process; queued frames drain without more file I/O. Pixel validation and allocation failures retain the existing fatal path. The CPU fixture checks identical BMP bytes and real pipe failures. VideoOut flips on an RTX 3060 continue after a first dump that cannot be opened; header, pixel and close failures during presentation are unmeasured.
- [Inactive vertex point size](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) - `PA_CL_VS_OUT_CNTL.USE_VTX_POINT_SIZE` is accepted for lines, triangles and rectangles with `GS_EN` and `HS_EN` clear, when polygon mode is disabled or both faces are filled. Points, patches and absent routing or rasterization registers still throw. Bit 16 selects the misc export's X component as PointSize ([AMD CIK register reference](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/programmer-references/CIK_3D_registers_v2.pdf), PA_CL_VS_OUT_CNTL); ignoring it for non-point rasterization follows [Vulkan PointSize](https://docs.vulkan.org/refpages/latest/refpages/source/PointSize.html). This is inferred, not measured on console hardware. XashPS5 (PPSA19111 v1.3) sets `0x01210000` on a filled triangle draw.
- [Inactive vertex point size](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) - `PA_CL_VS_OUT_CNTL.USE_VTX_POINT_SIZE` is accepted for lines, triangles and rectangles with `GS_EN` and `HS_EN` clear, when polygon mode is disabled or both faces are filled. Patches and absent routing or rasterization registers still throw; point lists on that routing use it (next entry). Bit 16 selects the misc export's X component as PointSize ([AMD CIK register reference](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/programmer-references/CIK_3D_registers_v2.pdf), PA_CL_VS_OUT_CNTL); ignoring it for non-point rasterization follows [Vulkan PointSize](https://docs.vulkan.org/refpages/latest/refpages/source/PointSize.html). This is inferred, not measured on console hardware. XashPS5 (PPSA19111 v1.3) sets `0x01210000` on a filled triangle draw.
- [Vertex-path point lists](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a point list with `GS_EN` and `HS_EN` clear draws as Vulkan points, and the vertex program [writes](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `PointSize`: the misc export's X component clamped to `PA_SU_POINT_MINMAX` when `PA_CL_VS_OUT_CNTL.USE_VTX_POINT_SIZE` is set, and the `PA_SU_POINT_SIZE` height otherwise, which the export replaces when it runs. Both registers are read as half sizes in 12.4 fixed point, so a field of 8 is a one-pixel point, as [radeonsi programs them](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state.c#L1047-1064). That the clamp applies to the exported size and not to the register size is inferred from radeonsi, which sets both clamps to the register size when the size is not per vertex; neither is measured on console hardware. Sizes outside the device's `pointSizeRange` are clamped by the device. A register size whose width and height differ, a clamp minimum above its maximum, `USE_VTX_POINT_SIZE` without `VS_OUT_MISC_VEC_ENA`, and point sprite texture coordinates (`SPI_PS_INPUT_CNTL` PT_SPRITE_TEX or PT_SPRITE_TEX_ATTR1 with `SPI_INTERP_CONTROL_0.PNT_SPRITE_ENA`) throw, and points of any size other than one pixel need the `largePoints` device feature. OliSe Player (PPSA01153) draws such points with `PA_CL_VS_OUT_CNTL = 0x01210000` for its Che - Escape and Stahlregen + Geiss + Martin - Ouboros (Metal Finish 2) presets; Che - Escape sets both size registers to `0x00080008`.
- [RELEASE_MEM of the system counter](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - a `RELEASE_MEM` with `DATA_SEL = 4` throws: the frequency and epoch of the console's 64-bit system counter are not known. [amdgpu's GFX10 packet header](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/nvd.h) names the value "send 64bit sys counter value", beside 3, the GPU counter, which the driver writes as a 100 MHz count of the host's steady clock. An older KFD header names 4 a CP performance counter instead. A PS4 title through #6977 writes it at the end of each frame.
- [Point and line polygon modes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - front and back polygon types that differ while both faces are drawn throw, as Vulkan has one `polygonMode` per draw. Points are drawn only one pixel wide (`PA_SU_POINT_SIZE = 0x00080008` inside `PA_SU_POINT_MINMAX`, the `VK_KHR_maintenance5` size of a point whose shader does not write `PointSize`); other sizes, a per-vertex size and devices without `maintenance5` throw. Lines must be one pixel wide (`PA_SU_LINE_CNTL`) and use Vulkan's default line rasterization. Depth bias with a point or line polygon mode, and such a mode on a rectangle list, throw. The fields follow the [AMD CIK register reference](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/programmer-references/CIK_3D_registers_v2.pdf); the console's rasterization of these points and lines is not measured.
- [Native multisampled depth clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accept the recognized HTILE mask kernel's direct full-range dispatch after its GPU writes complete. Every metadata byte needs tracked driver-store coverage since notification, no intervening CPU or depth write, and HTILE words proving the current clear value of zero or one. Partial, overlapping, indirect and unproven producers remain unsupported. Existing sampled views retain the native image; guest depth publication and metadata-fill transitions remain unimplemented. Windows shared imports retain write tracking through separate aliases; private host imports that lose tracking and copied or mirrored HTILE uploads remain unsupported. Clear-word encoding and console equivalence remain unmeasured on PS5 hardware.