feat(agc-driver): blend into COLOR_8_8 SWAP_ALT targets with a separate alpha equation
An 8_8 target with SWAP_ALT keeps the x export in its first channel and the alpha export in its second, and the color block blends that second channel with the alpha equation. Blending into such a target threw. With one equation for color and alpha the draw blends as before. With a separate alpha equation the pipeline draws twice: the first pass writes the first channel with the color equation, the second writes the second channel with the alpha factors and operation moved into its color equation. The split is refused, by throwing, for draws that write depth or stencil, use per-face depth bias, or whose pixel shader writes memory, since those would repeat; factors of the destination alpha throw since the target keeps it in its second channel. Pipeline libraries are not used for split pipelines.
This commit is contained in:
parent
6040a9626e
commit
ba0b042925
|
|
@ -907,6 +907,8 @@ endforeach()
|
|||
|
||||
target_include_directories(agc_driver_dual_source_blend_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
target_include_directories(agc_driver_swap_alt_blend_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
target_include_directories(agc_driver_packed_unorm_color_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
foreach(stage IN ITEMS vert frag)
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ public:
|
|||
bool SplitsFaces() const { return backFaces != VK_NULL_HANDLE; }
|
||||
bool Optimized() const { return optimized != nullptr && optimized->handle.load() != VK_NULL_HANDLE; }
|
||||
void ContinueBackFaces(VkCommandBuffer commands, const State& state) const;
|
||||
bool SplitsSecondChannel() const { return secondChannel != VK_NULL_HANDLE; }
|
||||
void ContinueSecondChannel(VkCommandBuffer commands) const;
|
||||
void PushConstants(VkCommandBuffer commands, VkShaderStageFlags stages, std::span<const std::byte, PipelinePushConstantBytes> bytes) const;
|
||||
// Forgets the Vulkan objects without destroying them: for entries of a device that is already gone.
|
||||
void Abandon() noexcept;
|
||||
|
|
@ -73,6 +75,7 @@ private:
|
|||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
VkPipeline backFaces = VK_NULL_HANDLE;
|
||||
VkPipeline secondChannel = VK_NULL_HANDLE;
|
||||
std::size_t attachments = 0;
|
||||
std::size_t colorAttachments = 0;
|
||||
bool depthBounds = false;
|
||||
|
|
|
|||
|
|
@ -179,6 +179,7 @@ inline constexpr std::array<DrawKeyRange, 47> DrawKeyRegisters{{
|
|||
// GE_PRIM_TYPE, GE_MULTI_PRIM_IB_RESET_EN, the geometry subgroup sizes.
|
||||
{RegisterBank::UserConfig, 0x242, 1}, {RegisterBank::UserConfig, 0x24b, 1}, {RegisterBank::UserConfig, 0x25b, 1},
|
||||
}};
|
||||
bool SplitsAlternateAlpha(const State& state);
|
||||
// Whether DrawKeyRegisters holds the read.
|
||||
bool DrawKeyCovers(RegisterRead read);
|
||||
|
||||
|
|
|
|||
|
|
@ -1736,6 +1736,10 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
|
|||
record.pipeline->ContinueBackFaces(commands, state);
|
||||
recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset);
|
||||
}
|
||||
if (record.pipeline->SplitsSecondChannel()) {
|
||||
record.pipeline->ContinueSecondChannel(commands);
|
||||
recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset);
|
||||
}
|
||||
if (meshArguments != nullptr) recorder->Keep(meshArguments);
|
||||
if (args != nullptr) CountIndirectDraw(record.indirect->path, record.indirect->readMs, rewritten);
|
||||
auto checkRecords = indirectRecordCheck(record.indirect);
|
||||
|
|
@ -2208,6 +2212,10 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
|
|||
pipeline->ContinueBackFaces(commands, state);
|
||||
recordDrawCommands(context, commands, state, draw, inputs, args != nullptr ? &indirect : nullptr, argumentBuffer, argumentOffset);
|
||||
}
|
||||
if (pipeline->SplitsSecondChannel()) {
|
||||
pipeline->ContinueSecondChannel(commands);
|
||||
recordDrawCommands(context, commands, state, draw, inputs, args != nullptr ? &indirect : nullptr, argumentBuffer, argumentOffset);
|
||||
}
|
||||
if (meshArguments != nullptr && recorded) recorder->Keep(meshArguments);
|
||||
if (args != nullptr) CountIndirectDraw(indirect.path, indirect.readMs, rewritten);
|
||||
auto checkRecords = indirectRecordCheck(args != nullptr ? &indirect : nullptr);
|
||||
|
|
|
|||
|
|
@ -128,9 +128,35 @@ void appendKey(std::vector<std::byte>& key, const TValue& value) {
|
|||
key.insert(key.end(), bytes.begin(), bytes.end());
|
||||
}
|
||||
|
||||
bool fragmentWritesMemory(std::span<const CompiledShader> shaders) {
|
||||
const auto any = [](const std::vector<bool>& written, std::uint32_t count) { return written.size() < count || std::ranges::any_of(written, [](bool element) { return element; }); };
|
||||
for (const auto& shader : shaders) {
|
||||
if (shader.stage != ShaderRecompiler::ShaderStage::Fragment || shader.program == nullptr) continue;
|
||||
for (const auto& binding : shader.program->bindings) {
|
||||
switch (binding.role) {
|
||||
case ShaderRecompiler::DescriptorRole::GuestBuffers:
|
||||
if (binding.kind != ShaderRecompiler::DescriptorKind::UniformBuffer && binding.kind != ShaderRecompiler::DescriptorKind::UniformTexelBuffer && any(binding.bufferWritten, binding.count)) return true;
|
||||
break;
|
||||
case ShaderRecompiler::DescriptorRole::GuestImages:
|
||||
if (binding.kind == ShaderRecompiler::DescriptorKind::StorageImage && any(binding.imageWritten, binding.count)) return true;
|
||||
break;
|
||||
case ShaderRecompiler::DescriptorRole::Gds:
|
||||
case ShaderRecompiler::DescriptorRole::BdaPagetable:
|
||||
return true;
|
||||
case ShaderRecompiler::DescriptorRole::GuestSamplers:
|
||||
case ShaderRecompiler::DescriptorRole::FaultBuffer:
|
||||
case ShaderRecompiler::DescriptorRole::FlattenedSrt:
|
||||
case ShaderRecompiler::DescriptorRole::ShaderData:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& state, const VertexInputLayout& input, const ShaderResources& resources, std::span<const CompiledShader> shaders) {
|
||||
using Stage = ShaderRecompiler::ShaderStage;
|
||||
if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList) return false;
|
||||
if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || SplitsAlternateAlpha(state)) return false;
|
||||
keys = {};
|
||||
for (const auto& shader : shaders) {
|
||||
if (shader.stage == Stage::Geometry || shader.program->PipelineVariantId() == 0) return false;
|
||||
|
|
@ -347,8 +373,28 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
|
|||
depthStencil.front = state.stencilFront;
|
||||
depthStencil.back = state.stencilBack;
|
||||
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
|
||||
blend.attachmentCount = static_cast<std::uint32_t>(state.blends.size());
|
||||
blend.pAttachments = state.blends.empty() ? nullptr : state.blends.data();
|
||||
const bool alternateAlpha = SplitsAlternateAlpha(state);
|
||||
std::vector<VkPipelineColorBlendAttachmentState> firstChannels(state.blends.begin(), state.blends.end());
|
||||
std::vector<VkPipelineColorBlendAttachmentState> secondChannels(state.blends.begin(), state.blends.end());
|
||||
if (alternateAlpha) {
|
||||
const auto keepsStencil = [](const VkStencilOpState& face) { return face.writeMask == 0 || (face.failOp == VK_STENCIL_OP_KEEP && face.passOp == VK_STENCIL_OP_KEEP && face.depthFailOp == VK_STENCIL_OP_KEEP); };
|
||||
Require(!(state.depth && (state.depthWrite || (state.stencilTest && (!keepsStencil(state.stencilFront) || !keepsStencil(state.stencilBack))))) && !state.depthBiasPerFace, "blending into an 8_8 color target with the alternate component order and a separate alpha blend from a draw that writes depth or stencil is not implemented");
|
||||
Require(!fragmentWritesMemory(shaders), "blending into an 8_8 color target with the alternate component order and a separate alpha blend from a pixel shader that writes memory is not implemented");
|
||||
for (std::size_t index = 0; index < secondChannels.size(); ++index) {
|
||||
const bool split = index < state.colors.size() && state.colors[index].componentMapping == 0xecu && state.blends[index].blendEnable;
|
||||
if (!split) {
|
||||
secondChannels[index].colorWriteMask = 0;
|
||||
continue;
|
||||
}
|
||||
firstChannels[index].colorWriteMask &= ~VK_COLOR_COMPONENT_G_BIT;
|
||||
secondChannels[index].colorWriteMask &= VK_COLOR_COMPONENT_G_BIT;
|
||||
secondChannels[index].srcColorBlendFactor = state.blends[index].srcAlphaBlendFactor;
|
||||
secondChannels[index].dstColorBlendFactor = state.blends[index].dstAlphaBlendFactor;
|
||||
secondChannels[index].colorBlendOp = state.blends[index].alphaBlendOp;
|
||||
}
|
||||
}
|
||||
blend.attachmentCount = static_cast<std::uint32_t>(firstChannels.size());
|
||||
blend.pAttachments = firstChannels.empty() ? nullptr : firstChannels.data();
|
||||
std::copy(state.blendConstants.begin(), state.blendConstants.end(), blend.blendConstants);
|
||||
VkGraphicsPipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
||||
pipelineInfo.flags = context.pipelineExecutableInfo ? VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR : 0;
|
||||
|
|
@ -373,6 +419,10 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
|
|||
timing.Mark("modules_and_state");
|
||||
if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized);
|
||||
else Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines");
|
||||
if (alternateAlpha) {
|
||||
blend.pAttachments = secondChannels.data();
|
||||
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &secondChannel), "vkCreateGraphicsPipelines");
|
||||
}
|
||||
if (state.depthBiasPerFace) {
|
||||
raster.cullMode = VK_CULL_MODE_FRONT_BIT;
|
||||
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &backFaces), "vkCreateGraphicsPipelines");
|
||||
|
|
@ -397,6 +447,7 @@ void Pipeline::release() noexcept {
|
|||
optimized.reset();
|
||||
if (pipeline) context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline")(context.device, pipeline, nullptr);
|
||||
if (backFaces) context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline")(context.device, backFaces, nullptr);
|
||||
if (secondChannel) context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline")(context.device, secondChannel, nullptr);
|
||||
if (renderPass) context.Function<PFN_vkDestroyRenderPass>("vkDestroyRenderPass")(context.device, renderPass, nullptr);
|
||||
if (layout) context.Function<PFN_vkDestroyPipelineLayout>("vkDestroyPipelineLayout")(context.device, layout, nullptr);
|
||||
for (auto module : _modules) {
|
||||
|
|
@ -404,6 +455,7 @@ void Pipeline::release() noexcept {
|
|||
}
|
||||
pipeline = VK_NULL_HANDLE;
|
||||
backFaces = VK_NULL_HANDLE;
|
||||
secondChannel = VK_NULL_HANDLE;
|
||||
renderPass = VK_NULL_HANDLE;
|
||||
layout = VK_NULL_HANDLE;
|
||||
_modules.clear();
|
||||
|
|
@ -416,6 +468,7 @@ void Pipeline::Abandon() noexcept {
|
|||
framebuffers.clear();
|
||||
pipeline = VK_NULL_HANDLE;
|
||||
backFaces = VK_NULL_HANDLE;
|
||||
secondChannel = VK_NULL_HANDLE;
|
||||
renderPass = VK_NULL_HANDLE;
|
||||
layout = VK_NULL_HANDLE;
|
||||
_modules.clear();
|
||||
|
|
@ -540,6 +593,11 @@ void Pipeline::setLibraryState(VkCommandBuffer commands, const State& state) con
|
|||
stencil(VK_STENCIL_FACE_BACK_BIT, depth ? state.stencilBack : VkStencilOpState{});
|
||||
}
|
||||
|
||||
void Pipeline::ContinueSecondChannel(VkCommandBuffer commands) const {
|
||||
Require(secondChannel != VK_NULL_HANDLE, "the pipeline blends every channel in one pass");
|
||||
context.Resolved(&DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, secondChannel);
|
||||
}
|
||||
|
||||
void Pipeline::ContinueBackFaces(VkCommandBuffer commands, const State& state) const {
|
||||
Require(backFaces != VK_NULL_HANDLE, "the pipeline draws both faces in one pass");
|
||||
context.Resolved(&DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, backFaces);
|
||||
|
|
|
|||
|
|
@ -727,7 +727,6 @@ State DecodeState(const QueueState& queue) {
|
|||
state.blendEnable = (blend >> 30u) & 1u;
|
||||
if (state.blendEnable && (mapping == 0x1bu || mapping == 0x93u)) throw std::runtime_error("AGC graphics: blending into a color target with a reversed component order is not implemented");
|
||||
Require(!state.blendEnable || !color.uintExport, "blending into an unsigned integer target is unsupported");
|
||||
if (state.blendEnable && mapping == 0xecu) throw std::runtime_error("AGC graphics: blending into an 8_8 color target with the alternate component order is not implemented");
|
||||
if (state.blendEnable) {
|
||||
Require((read(cx, 0x31c + slot * 0xfu) & 0x10000u) == 0, "blend bypass conflicts with enabled blending");
|
||||
state.srcColorBlendFactor = blendFactor(blend & 0x1fu);
|
||||
|
|
@ -749,6 +748,10 @@ State DecodeState(const QueueState& queue) {
|
|||
state.dstAlphaBlendFactor = alphaFactorWithOpaqueDestination(state.dstAlphaBlendFactor);
|
||||
}
|
||||
for (std::uint32_t i = 0; i < 4; ++i) result.blendConstants[i] = readFloat(cx, 0x105 + i);
|
||||
if (mapping == 0xecu) {
|
||||
const auto destinationAlpha = [](VkBlendFactor factor) { return factor == VK_BLEND_FACTOR_DST_ALPHA || factor == VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA || factor == VK_BLEND_FACTOR_SRC_ALPHA_SATURATE; };
|
||||
if (destinationAlpha(state.srcColorBlendFactor) || destinationAlpha(state.dstColorBlendFactor) || destinationAlpha(state.srcAlphaBlendFactor) || destinationAlpha(state.dstAlphaBlendFactor)) throw std::runtime_error("AGC graphics: blending into an 8_8 color target with the alternate component order with a factor of the destination alpha, which the target keeps in its second channel, is not implemented");
|
||||
}
|
||||
}
|
||||
if (color.packing != ShaderRecompiler::ColorExportPacking::None) {
|
||||
const auto slotExport = (exportFormat >> (4u * color.exportIndex)) & 0xfu;
|
||||
|
|
@ -1036,6 +1039,15 @@ std::vector<RegisterRead>*& RegisterReadLog() {
|
|||
return log;
|
||||
}
|
||||
|
||||
bool SplitsAlternateAlpha(const State& state) {
|
||||
for (std::size_t index = 0; index < state.colors.size() && index < state.blends.size(); ++index) {
|
||||
const auto& blend = state.blends[index];
|
||||
if (state.colors[index].componentMapping != 0xecu || !blend.blendEnable) continue;
|
||||
if (blend.srcColorBlendFactor != blend.srcAlphaBlendFactor || blend.dstColorBlendFactor != blend.dstAlphaBlendFactor || blend.colorBlendOp != blend.alphaBlendOp) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DrawKeyCovers(RegisterRead read) {
|
||||
// One bit per register offset of each bank, set from the table once.
|
||||
static const auto covered = [] {
|
||||
|
|
|
|||
|
|
@ -767,7 +767,12 @@ void ReversedComponentOrderTests() {
|
|||
const auto state = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(state.colors.size() == 1 && state.colors[0].format == VK_FORMAT_R8G8_UNORM && state.colors[0].componentMapping == 0xecu, "an 8_8 target with SWAP_ALT did not store the x and w exports in its two channels");
|
||||
queue.context[0x1e0] = 0x40010001u;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "alternate component order");
|
||||
for (const auto offset : {0x105u, 0x106u, 0x107u, 0x108u}) queue.context[offset] = 0;
|
||||
const auto blended = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(blended.blends.size() == 1 && blended.blends[0].blendEnable, "blending into an 8_8 SWAP_ALT target with one equation for color and alpha was rejected");
|
||||
Require(!AgcDriver::Graphics::SplitsAlternateAlpha(blended), "one blend equation for color and alpha split an 8_8 SWAP_ALT draw");
|
||||
queue.context[0x1e0] = 0x60000001u;
|
||||
Require(AgcDriver::Graphics::SplitsAlternateAlpha(AgcDriver::Graphics::DecodeState(queue)), "a separate alpha blend into an 8_8 SWAP_ALT target did not split its second channel");
|
||||
}
|
||||
for (const auto& [swap, mapping] : {std::pair{2u, 0x1bu}, std::pair{3u, 0x93u}}) {
|
||||
auto queue = makeState();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,300 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "Optimization/ResourceProgram.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::ColorTargetLayout;
|
||||
using AgcDriver::Graphics::ColorTileMode;
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
using Color = std::array<float, 4>;
|
||||
|
||||
constexpr std::uint32_t Width = 256;
|
||||
constexpr std::uint32_t Height = 128;
|
||||
constexpr std::uint32_t ScissorWidth = 128;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::uint32_t SwapAlt8x8Info = (3u << 2u) | (1u << 11u) | 0x8000u;
|
||||
constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u;
|
||||
constexpr std::uint32_t Float32Export = 9;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
|
||||
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
|
||||
};
|
||||
|
||||
constexpr std::array<std::array<float, 4>, 3> Triangle{{
|
||||
{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}
|
||||
}};
|
||||
|
||||
constexpr std::array<std::array<std::uint8_t, 2>, 4> Destinations{{
|
||||
{64, 128}, {255, 0}, {32, 224}, {128, 64}
|
||||
}};
|
||||
|
||||
alignas(256) std::array<std::array<std::uint32_t, 64>, 8> Programs{};
|
||||
std::size_t programCount = 0;
|
||||
|
||||
std::span<const std::uint32_t> Program(const Color& exported) {
|
||||
Require(programCount < Programs.size(), "too many test programs");
|
||||
auto& code = Programs[programCount++];
|
||||
std::size_t at = 0;
|
||||
for (std::uint32_t channel = 0; channel < 4; ++channel) {
|
||||
code[at++] = 0x7e0002ffu | ((4u + channel) << 17u);
|
||||
code[at++] = std::bit_cast<std::uint32_t>(exported[channel]);
|
||||
}
|
||||
code[at++] = 0xf800180fu;
|
||||
code[at++] = 0x07060504u;
|
||||
code[at++] = 0xbf810000u;
|
||||
return std::span<const std::uint32_t>(code.data(), at);
|
||||
}
|
||||
|
||||
struct Block {
|
||||
explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) {
|
||||
#ifdef _WIN32
|
||||
if (watched) {
|
||||
data = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes));
|
||||
GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
}
|
||||
#else
|
||||
if (watched) {
|
||||
void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Require(raw != MAP_FAILED, "cannot map the color target");
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
|
||||
const auto aligned = (begin + BlockBytes - 1) & ~(static_cast<std::uintptr_t>(BlockBytes) - 1);
|
||||
if (aligned != begin) munmap(raw, aligned - begin);
|
||||
if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast<void*>(aligned + bytes), begin + BlockBytes - aligned);
|
||||
data = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, bytes));
|
||||
}
|
||||
#endif
|
||||
Require(data != nullptr, "cannot allocate the color target");
|
||||
Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched");
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Add(data, bytes, true, true, true);
|
||||
}
|
||||
~Block() {
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release");
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Remove(data);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (watched) {
|
||||
GuestArena::GuestArenaReset_nid_postfix(data, bytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(data, bytes);
|
||||
} else {
|
||||
VirtualFree(data, 0, MEM_RELEASE);
|
||||
}
|
||||
#else
|
||||
if (watched) {
|
||||
munmap(data, bytes);
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
std::free(data);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Block(const Block&) = delete;
|
||||
Block& operator=(const Block&) = delete;
|
||||
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); }
|
||||
std::size_t bytes;
|
||||
bool watched;
|
||||
std::uint8_t* data = nullptr;
|
||||
};
|
||||
|
||||
AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) {
|
||||
AgcDriver::Registers cx;
|
||||
cx[0x318] = static_cast<std::uint32_t>(block.Address() >> 8u);
|
||||
cx[0x31b] = 0;
|
||||
cx[0x31c] = SwapAlt8x8Info;
|
||||
cx[0x31d] = 0;
|
||||
cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u);
|
||||
cx[0x3b8] = TiledAttrib3;
|
||||
cx[0x390] = static_cast<std::uint32_t>(block.Address() >> 40u);
|
||||
const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0);
|
||||
Require(color.format == VK_FORMAT_R8G8_UNORM && color.componentMapping == 0xecu && color.elementBytes == 2 && color.bytes == block.bytes, "the 8_8 SWAP_ALT target did not decode as a 256x128 R8G8 surface holding the x and w exports");
|
||||
return color;
|
||||
}
|
||||
|
||||
VkPipelineColorBlendAttachmentState Blend(VkBlendFactor source, VkBlendFactor destination, VkBlendOp operation, VkBlendFactor sourceAlpha, VkBlendFactor destinationAlpha, VkBlendOp alphaOperation) {
|
||||
VkPipelineColorBlendAttachmentState blend{};
|
||||
blend.blendEnable = VK_TRUE;
|
||||
blend.srcColorBlendFactor = source;
|
||||
blend.dstColorBlendFactor = destination;
|
||||
blend.colorBlendOp = operation;
|
||||
blend.srcAlphaBlendFactor = sourceAlpha;
|
||||
blend.dstAlphaBlendFactor = destinationAlpha;
|
||||
blend.alphaBlendOp = alphaOperation;
|
||||
blend.colorWriteMask = 0xf;
|
||||
return blend;
|
||||
}
|
||||
|
||||
void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, const VkPipelineColorBlendAttachmentState& blend, std::span<const std::uint32_t> pixelCode) {
|
||||
const auto target = device.Target();
|
||||
constexpr std::uint32_t waveSize = 64;
|
||||
std::vector<std::uint32_t> vertexUserData(4, 0u);
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(Triangle.data());
|
||||
const std::array<std::uint32_t, 4> vertexBuffer{static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (16u << 16u), static_cast<std::uint32_t>(Triangle.size()), 0x01016facu};
|
||||
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());
|
||||
|
||||
AgcDriver::Graphics::State state{};
|
||||
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt};
|
||||
state.colors = {color};
|
||||
state.color = color;
|
||||
state.hasColorTarget = true;
|
||||
state.renderExtent = {Width, Height};
|
||||
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
state.viewport = {0, static_cast<float>(Height), static_cast<float>(Width), -static_cast<float>(Height), 0, 1};
|
||||
state.negativeOneToOne = false;
|
||||
state.scissor = {{0, 0}, {ScissorWidth, Height}};
|
||||
state.cullMode = VK_CULL_MODE_NONE;
|
||||
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
state.blends = {blend};
|
||||
state.blend = blend;
|
||||
state.blendConstants = {};
|
||||
|
||||
const auto codeAddress = reinterpret_cast<std::uintptr_t>(pixelCode.data());
|
||||
const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, Float32Export}};
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}};
|
||||
ShaderRecompiler::RecompileRequest fragment{
|
||||
{ShaderStage::Fragment, codeAddress, pixelCode, 0, {}},
|
||||
{waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)), 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)}
|
||||
}};
|
||||
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Triangle.size()), 0, 1, 0, false};
|
||||
device.Draw(state, draw, shaders);
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> Fill(Block& block, const ColorTargetLayout& layout) {
|
||||
std::vector<std::uint8_t> bytes(block.bytes);
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
const auto& color = Destinations[(x + y) % Destinations.size()];
|
||||
const auto index = layout.Offset(x, y);
|
||||
bytes[index] = color[0];
|
||||
bytes[index + 1] = color[1];
|
||||
}
|
||||
}
|
||||
std::memcpy(block.data, bytes.data(), block.bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
void Check(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector<std::uint8_t>& before, const std::function<std::array<float, 2>(float first, float second)>& blended, const std::string& what) {
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back");
|
||||
device.WaitIdle();
|
||||
std::vector<std::uint8_t> stored(block.bytes);
|
||||
AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1);
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
const auto index = layout.Offset(x, y);
|
||||
const auto& destination = Destinations[(x + y) % Destinations.size()];
|
||||
const auto expected = blended(static_cast<float>(destination[0]) / 255.0f, static_cast<float>(destination[1]) / 255.0f);
|
||||
for (std::uint32_t channel = 0; channel < 2; ++channel) {
|
||||
const bool inside = x < ScissorWidth;
|
||||
const auto want = std::clamp(expected[channel], 0.0f, 1.0f) * 255.0f;
|
||||
const bool match = inside ? std::fabs(static_cast<float>(stored[index + channel]) - want) <= 1.0f : stored[index + channel] == before[index + channel];
|
||||
if (match) continue;
|
||||
char detail[160];
|
||||
std::snprintf(detail, sizeof(detail), ": channel %u of pixel (%u, %u) %s the scissor holds %u, expected %.2f", channel, x, y, inside ? "inside" : "outside", stored[index + channel], inside ? want : static_cast<float>(before[index + channel]));
|
||||
throw std::runtime_error(what + detail);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BlendCase {
|
||||
const char* name;
|
||||
VkPipelineColorBlendAttachmentState blend;
|
||||
bool split;
|
||||
std::function<std::array<float, 2>(float first, float second)> blended;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 2);
|
||||
Block block(layout.Bytes());
|
||||
const Color exported{0.25f, 0.75f, 0.125f, 0.5f};
|
||||
const float x = exported[0];
|
||||
const float w = exported[3];
|
||||
const std::array<BlendCase, 4> cases{{
|
||||
{"SRC_ALPHA + ONE_MINUS_SRC_ALPHA, alpha ONE + ZERO", Blend(VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD), true,
|
||||
[&](float r, float) { return std::array<float, 2>{x * w + r * (1.0f - w), w}; }},
|
||||
{"ONE + ONE, alpha ZERO + ONE_MINUS_SRC_ALPHA", Blend(VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, VK_BLEND_FACTOR_ZERO, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD), true,
|
||||
[&](float r, float g) { return std::array<float, 2>{x + r, g * (1.0f - w)}; }},
|
||||
{"ONE + ZERO, alpha ONE reverse-subtract ONE", Blend(VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_REVERSE_SUBTRACT), true,
|
||||
[&](float, float g) { return std::array<float, 2>{x, g - w}; }},
|
||||
{"ONE + ONE_MINUS_SRC_ALPHA for color and alpha", Blend(VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD), false,
|
||||
[&](float r, float g) { return std::array<float, 2>{x + r * (1.0f - w), w + g * (1.0f - w)}; }},
|
||||
}};
|
||||
for (const auto& test : cases) {
|
||||
const auto before = Fill(block, layout);
|
||||
const auto color = DecodeTarget(block);
|
||||
AgcDriver::Graphics::State probe{};
|
||||
probe.colors = {color};
|
||||
probe.blends = {test.blend};
|
||||
Require(AgcDriver::Graphics::SplitsAlternateAlpha(probe) == test.split, (std::string(test.name) + ": the split decision is wrong").c_str());
|
||||
Draw(*device, color, test.blend, Program(exported));
|
||||
Check(*device, block, layout, before, test.blended, test.name);
|
||||
}
|
||||
std::puts("8_8 SWAP_ALT blend tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue