perf(agc): expand draw recipes to support depth targets

This commit is contained in:
Kikoarl 2026-10-13 08:00:11 +02:00
parent d1670009fb
commit 3ebad9045e
4 changed files with 160 additions and 2 deletions

View File

@ -84,6 +84,7 @@ struct DrawRecipe {
std::optional<Graphics::State> masked;
std::set<std::uint32_t> fragmentOutputs;
VkPipelineStageFlags shaderStages = 0;
bool hasDepth = false;
};
}

View File

@ -2144,7 +2144,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
// only a template the resource cache serves under this content key (reusable: no lease,
// no copied writes, every direct region import- or mirror-served) of a direct draw, so a
// hit's proof is the template's ProveCurrent and nothing needs completion work.
if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && !state.depth && resolved.moved.empty() && !multisampled) {
if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && resolved.moved.empty() && !multisampled) {
auto recipe = std::make_shared<DrawRecipe>();
recipe->device = context.device;
recipe->templateRef = resources;
@ -2163,6 +2163,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
recipe->masked = masked;
recipe->fragmentOutputs = inputs.fragmentOutputs;
recipe->shaderStages = inputs.shaderStages;
recipe->hasDepth = state.depth.has_value();
*recipeOut = std::move(recipe);
}
timing.Mark("draw_and_resource_release");
@ -2440,7 +2441,9 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
Require(!draw.indirect, "a draw recipe covers direct draws only");
auto* recorder = Recorder::Active();
if (!RecordDraws() || recorder == nullptr || DumpTargetLimit() != 0) return miss(DrawRecipeMiss::NotRecordable);
Require(recipe.targets.size() == state.colors.size() && recipe.targetViews.size() == state.colors.size(), "draw recipe targets do not match the state");
if (recipe.hasDepth != state.depth.has_value()) return miss(DrawRecipeMiss::TargetGone);
const std::size_t expectedViews = state.colors.size() + (recipe.hasDepth ? 1u : 0u);
Require(recipe.targets.size() == state.colors.size() && recipe.targetViews.size() == expectedViews, "draw recipe targets do not match the state");
auto inputs = prepareDrawInputs(context, state, draw, shaders, outcome, timer, &recipe);
if (inputs.nothing) {
result.recorded = true;
@ -2466,6 +2469,12 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
if (resident == nullptr) return miss(DrawRecipeMiss::TargetGone);
targets.push_back(std::move(resident));
}
if (recipe.hasDepth) {
timer.phase(PhaseSetup);
const auto depthView = DepthSurfaceView(context, *state.depth);
timer.phase(PhaseReadTarget);
if (recipe.targetViews.back() != depthView) return miss(DrawRecipeMiss::TargetGone);
}
timer.phase(PhasePrepare);
// The template's proof (rules R6/R7): ProveCurrent, T1 included, the alias checks the trimmed
// key leaves to a hit repeated; a failure removes the template from the cache (the batch keeps

View File

@ -1,9 +1,19 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#endif
#include <array>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <span>
#include <string>
@ -124,6 +134,142 @@ void Check(bool frontDrawn, bool backDrawn, std::string_view what) {
}
}
class GuestBlock {
public:
static constexpr std::size_t BlockBytes = 65536;
GuestBlock() {
#ifdef _WIN32
block = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
#else
block = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, BlockBytes));
#endif
Require(block != nullptr, "cannot allocate a guest block");
std::memset(block, 0, BlockBytes);
GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true);
}
~GuestBlock() {
GuestAllocations::Mutation().Remove(block);
#ifdef _WIN32
VirtualFree(block, 0, MEM_RELEASE);
#else
std::free(block);
#endif
}
GuestBlock(const GuestBlock&) = delete;
GuestBlock& operator=(const GuestBlock&) = delete;
std::uint8_t* Data() const { return block; }
std::uint64_t Address() const { return static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(block)); }
private:
std::uint8_t* block = nullptr;
};
void DepthDrawRecipe(AgcDriver::VulkanDevice& device) {
constexpr std::uint32_t TargetWidth = 128;
constexpr std::uint32_t TargetHeight = 128;
const AgcDriver::Graphics::ColorTargetLayout colorLayout(TargetWidth, TargetHeight, AgcDriver::Graphics::ColorTileMode::RenderTarget, 4);
const GuestBlock guestVertices;
const GuestBlock guestTarget;
alignas(256) static std::array<float, TargetWidth * TargetHeight> RecipeDepthMemory{};
alignas(256) static std::array<float, TargetWidth * TargetHeight> RecipeAlternateDepthMemory{};
std::memset(guestTarget.Data(), static_cast<int>(Kept), GuestBlock::BlockBytes);
RecipeDepthMemory.fill(1.0f);
RecipeAlternateDepthMemory.fill(1.0f);
auto* targetVertices = reinterpret_cast<std::array<float, 4>*>(guestVertices.Data());
for (std::size_t index = 0; index < Corners.size(); ++index) {
targetVertices[index] = {Corners[index][0] * 2.0f / TargetWidth - 1.0f, 1.0f - Corners[index][1] * 2.0f / TargetHeight, SurfaceDepth, 1.0f};
}
const auto target = device.Target();
std::vector<std::uint32_t> vertexUserData(4, 0u);
const auto vertexBuffer = BufferDescriptor(targetVertices, 16u, static_cast<std::uint32_t>(Corners.size()));
std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin());
const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}};
ShaderRecompiler::RecompileRequest vertex{
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}},
{WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory},
target,
{0, 0, 0, 64}
};
vertex.useCache = false;
const auto vertexResult = ShaderRecompiler::Recompile(vertex);
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
ShaderRecompiler::ShaderPixelStageInfo pixel{};
pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY);
pixel.posX = true;
pixel.posY = true;
pixel.targetOutputMode[0] = 9;
pixel.targetExportMapping.fill(0xe4u);
const std::vector<std::uint32_t> pixelUserData(8, 0u);
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}};
ShaderRecompiler::RecompileRequest fragment{
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(PixelCode.data()), PixelCode, 0, {}},
{WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory},
target,
PixelPushLayout(vertexPush, target)
};
fragment.useCache = false;
const auto pixelResult = ShaderRecompiler::Recompile(fragment);
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
{ShaderStage::Vertex, &vertexResult, 0},
{ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}
}};
AgcDriver::Graphics::State state{};
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt};
state.color = {guestTarget.Address(), {TargetWidth, TargetHeight}, VK_FORMAT_R8G8B8A8_UNORM, colorLayout.Bytes(), 0xe4u, AgcDriver::Graphics::ColorTileMode::RenderTarget};
state.colors = {state.color};
state.hasColorTarget = true;
state.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast<std::uintptr_t>(RecipeDepthMemory.data()), 0, {TargetWidth, TargetHeight}, VK_FORMAT_D32_SFLOAT, SurfaceDepth, 0};
state.depthTest = true;
state.depthWrite = false;
state.depthCompare = VK_COMPARE_OP_LESS;
state.depthBias = true;
state.depthBiasConstant = -Bias;
state.depthBiasPerFace = false;
state.backDepthBiasConstant = -Bias;
state.renderExtent = {TargetWidth, TargetHeight};
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
state.viewport = {0, static_cast<float>(TargetHeight), static_cast<float>(TargetWidth), -static_cast<float>(TargetHeight), 0, 1};
state.negativeOneToOne = false;
state.scissor = {{0, 0}, {TargetWidth, TargetHeight}};
state.cullMode = VK_CULL_MODE_NONE;
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
state.blend.colorWriteMask = 15;
state.blends = {state.blend};
state.blendConstants = {};
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Corners.size()), 0, 1, 0, false};
std::shared_ptr<const AgcDriver::DrawRecipe> recipe;
device.Draw(state, draw, shaders, {}, &recipe);
device.WaitIdle();
Require(recipe != nullptr, "a direct draw with depth target must produce a recipe");
Require(recipe->hasDepth, "the recipe must record depth target presence");
Require(recipe->targetViews.size() == 2, "the recipe must record color and depth views");
const auto outcome = device.DrawFromRecipe(state, draw, shaders, {}, recipe);
device.WaitIdle();
Require(outcome == AgcDriver::RecipeOutcome::Recorded, "drawing with depth from recipe must record successfully");
auto mutatedAddressState = state;
mutatedAddressState.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast<std::uintptr_t>(RecipeAlternateDepthMemory.data()), 0, {TargetWidth, TargetHeight}, VK_FORMAT_D32_SFLOAT, SurfaceDepth, 0};
const auto rejectedAddressOutcome = device.DrawFromRecipe(mutatedAddressState, draw, shaders, {}, recipe);
Require(rejectedAddressOutcome == AgcDriver::RecipeOutcome::Rebuild, "a recipe with depth must be rejected when depth target address changes");
auto removedDepthState = state;
removedDepthState.depth.reset();
const auto rejectedDepthOutcome = device.DrawFromRecipe(removedDepthState, draw, shaders, {}, recipe);
Require(rejectedDepthOutcome == AgcDriver::RecipeOutcome::Rebuild, "a recipe with depth must be rejected when depth target is removed");
}
}
int main() {
@ -138,6 +284,7 @@ int main() {
Check(true, true, "both faces pulled closer");
Draw(*device, -Bias, 0.0f);
Check(true, false, "only front faces biased");
DepthDrawRecipe(*device);
std::puts("per-face depth bias tests passed");
return 0;
} catch (const std::exception& error) {

View File

@ -34,6 +34,7 @@
- [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. Planes with mips, a base slice other than 0, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below).
- [SW_64KB_D_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the existing texture block/mip geometry and equations from [AMD addrlib GFX10 non-RB+](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) for 16 pipes and 256-byte interleave. The address-reference tests use legal pipe/bank XOR bits for that configuration. Raw column bits outside the source configuration's pipe/bank mask 0xcf00 throw; their console meaning is unresolved. Console address equations and render results have not been measured.
- [Per-face depth bias](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): Vulkan has one depth bias per draw, so a draw that rasterizes both faces with different front and back bias (PA_SU_SC_MODE_CNTL, PA_SU_POLY_OFFSET_FRONT/BACK_*) is recorded twice, front faces with the back faces culled and then back faces with the front faces culled. The vertex stages run twice, so a vertex shader that writes memory writes twice, and all front faces land before all back faces instead of in primitive order, which differs only for blending or stencil updates that depend on the order.
- [Draw recipes with depth targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): direct draw recipes record and replay with active depth targets, matching the bound depth surface view by its target view identity; altered depth surface addresses or incompatible view handles trigger a recipe miss and pipeline rebuild.
- [Partial rect-list primitives](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): a rect-list draw whose vertex or index count is not a multiple of 3 draws its complete rectangles and leaves out the 1 or 2 vertices after them, because rect lists are drawn as Vulkan patch lists of 3 control points, which generate [⌊vertexCount/3⌋ patches](https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html#drawing-patch-lists). What the console draws for the incomplete rectangle is not measured; Palworld (PPSA23126) draws 4 vertices × 9 instances as a rect list when its character creation opens.
- [Separate alpha blend into 8_8 SWAP_ALT targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver): the second channel of a two-channel target with `COMP_SWAP` 1 holds the alpha export, but Vulkan blends it with the color equation of the R8G8 attachment, so a draw whose alpha equation differs from its color equation is recorded twice, the first channel with the color equation and the second with the alpha factors and operation moved into the color equation. The vertex stages run twice, so a vertex shader that writes memory writes twice, and occlusion counts include both passes. Draws that write depth or stencil, pixel shaders that write memory and factors of the destination alpha throw. That the hardware blends the second channel with the alpha equation is taken from Mesa's `si_translate_colorswap` and radeonsi's use of it for luminance-alpha formats, not measured on hardware.
- [Unexported pixel inputs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver): a pixel input whose `SPI_PS_INPUT_CNTL` parameter the previous stage does not export is programmed as a default-value input (`OFFSET` bit 5, `DEFAULT_VAL` 0, FP16 high half too) and reads (0, 0, 0, 0), as radeonsi's `SI_PS_INPUT_CNTL_UNUSED` does and as rect-list draws already do. The console's value for an unwritten parameter cache entry is not measured on hardware.