mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-27 00:28:14 +00:00
feat(agc): merge partial writes into 4_4_4_4 UNORM color targets
A CB_TARGET_MASK that writes fewer than four components of a 4_4_4_4 target leaves the other nibbles of each packed word as they were, which an R16_UINT attachment cannot do. Such draws now close the attachment's color write mask, and the pixel shader merges the written nibbles into the target through a storage image: inside a pixel interlock it reads the word, keeps the bits the mask leaves alone and stores it back. The merge (export index, word mask, sample count) is part of the static shader key, request version 40 and shader disk cache format 42; the storage image is appended to the shader's images with a single runtime mode, and its T# is encoded from the target's surface at draw time, so the draw resolves it to the target's resident image, which Draw requires. Merging shaders declare early fragment tests; shaders that kill under late tests, export depth, stencil or coverage, use alpha-to-coverage or a quad sample mask are refused, as are multisampled, mipmapped, volume and array targets and more than one merged target per draw. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
acdbc02c8f
commit
48a7efcbec
31 files changed
+670
-38
No files matched your search
@@ -1956,6 +1956,8 @@ target_include_directories(agc_driver_packed_unorm_color_target_tests PRIVATE $<
|
||||
|
||||
target_include_directories(agc_driver_packed_unorm4444_color_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
target_include_directories(agc_driver_packed_unorm4444_partial_write_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
target_include_directories(agc_driver_color1555_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
target_include_directories(agc_driver_color565_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
@@ -165,6 +165,7 @@ std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Su
|
||||
product->pixel.targetExportPacking = Graphics::ExportPackings(product->state);
|
||||
product->pixel.dualSourceBlend = product->state.dualSourceBlend;
|
||||
product->pixel.reversedBlend = product->state.reversedBlend;
|
||||
product->pixel.targetMerge = Graphics::ExportTargetMerge(product->state);
|
||||
product->pixel.alphaToCoverage = product->state.alphaToCoverage;
|
||||
product->pixel.quadPixelMask = Graphics::QuadPixelMask(product->state);
|
||||
product->pixel.quadSampleMask = Graphics::QuadSampleMasks(product->state);
|
||||
|
||||
@@ -74,6 +74,10 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
|
||||
for (const auto value : pixel.targetExportPacking) mix(static_cast<std::uint64_t>(value));
|
||||
mix(pixel.dualSourceBlend);
|
||||
mix(pixel.reversedBlend);
|
||||
mix(pixel.targetMerge.target);
|
||||
mix(pixel.targetMerge.mask);
|
||||
mix(pixel.targetMerge.samples);
|
||||
for (const auto word : pixel.targetMerge.descriptor) mix(word);
|
||||
mix(pixel.alphaToCoverage);
|
||||
mix(static_cast<std::uint64_t>(pixel.rasterPrimitive));
|
||||
mix(pixel.ditheredAlphaToCoverage);
|
||||
|
||||
@@ -228,7 +228,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) {
|
||||
const auto& s = a.state;
|
||||
const auto& t = b.state;
|
||||
const auto sameColor = [](const Graphics::ColorTarget& x, const Graphics::ColorTarget& y) {
|
||||
return x.address == y.address && x.extent.width == y.extent.width && x.extent.height == y.extent.height && x.format == y.format && x.bytes == y.bytes && x.componentMapping == y.componentMapping && x.tileMode == y.tileMode && x.elementBytes == y.elementBytes && x.dccAddress == y.dccAddress && x.dccAlphaOnMsb == y.dccAlphaOnMsb && x.slot == y.slot && x.exportIndex == y.exportIndex;
|
||||
return x.address == y.address && x.extent.width == y.extent.width && x.extent.height == y.extent.height && x.format == y.format && x.bytes == y.bytes && x.componentMapping == y.componentMapping && x.tileMode == y.tileMode && x.elementBytes == y.elementBytes && x.dccAddress == y.dccAddress && x.dccAlphaOnMsb == y.dccAlphaOnMsb && x.slot == y.slot && x.exportIndex == y.exportIndex && x.partialWriteMask == y.partialWriteMask;
|
||||
};
|
||||
const auto sameBlend = [](const VkPipelineColorBlendAttachmentState& x, const VkPipelineColorBlendAttachmentState& y) {
|
||||
return x.blendEnable == y.blendEnable && x.srcColorBlendFactor == y.srcColorBlendFactor && x.dstColorBlendFactor == y.dstColorBlendFactor && x.colorBlendOp == y.colorBlendOp && x.srcAlphaBlendFactor == y.srcAlphaBlendFactor && x.dstAlphaBlendFactor == y.dstAlphaBlendFactor && x.alphaBlendOp == y.alphaBlendOp && x.colorWriteMask == y.colorWriteMask;
|
||||
@@ -262,7 +262,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) {
|
||||
const auto& y = b.programs[i];
|
||||
if (x.binary.stage != y.binary.stage || x.binary.codeAddress != y.binary.codeAddress || x.userDataBase != y.userDataBase || x.firstUserSgpr != y.firstUserSgpr || x.userData != y.userData || x.snapshot != y.snapshot || x.codeOffset != y.codeOffset || x.tailContinuation != y.tailContinuation) return false;
|
||||
}
|
||||
if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.reversedBlend != q.reversedBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask || p.iteratesSamples != q.iteratesSamples || p.positionLocation != q.positionLocation || p.sampleLocations != q.sampleLocations) return false;
|
||||
if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.reversedBlend != q.reversedBlend || p.targetMerge != q.targetMerge || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask || p.iteratesSamples != q.iteratesSamples || p.positionLocation != q.positionLocation || p.sampleLocations != q.sampleLocations) return false;
|
||||
if (s.samples != t.samples || s.mergedCoverage != t.mergedCoverage || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ float EffectiveMinLod(const GuestTextureResource& resource);
|
||||
|
||||
bool IsNullTextureResource(std::span<const std::uint32_t> words);
|
||||
GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words);
|
||||
std::array<std::uint32_t, 8> EncodeTextureResource(const GuestTextureResource& resource);
|
||||
bool DescriptorSingleLevel(std::span<const std::uint32_t> words);
|
||||
bool FirstLayerDescriptor(std::span<const std::uint32_t> words, std::array<std::uint32_t, 8>& narrowed);
|
||||
bool MatchesGuestDimension(ShaderRecompiler::DescriptorImageShape shape, TextureDimension dimension);
|
||||
|
||||
@@ -68,6 +68,7 @@ struct ColorTarget {
|
||||
std::uint32_t pipeBankXor = 0;
|
||||
std::uint32_t samples = 1;
|
||||
std::uint32_t dccControl = 0;
|
||||
std::uint16_t partialWriteMask = 0;
|
||||
};
|
||||
|
||||
struct DepthTarget {
|
||||
@@ -169,6 +170,8 @@ std::array<std::uint8_t, 32> PixelSampleLocations(const State& state, const Shad
|
||||
VkExtent2D SampleLocationGrid(const State& state);
|
||||
std::array<std::uint8_t, 8> ExportMappings(const State& state);
|
||||
std::array<ShaderRecompiler::ColorExportPacking, 8> ExportPackings(const State& state);
|
||||
std::uint16_t PackedWriteMask(std::uint8_t componentMapping, std::uint32_t exportedMask);
|
||||
ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state);
|
||||
ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state);
|
||||
bool PartialSampleMask(const State& state);
|
||||
bool UniformSampleMask(const State& state);
|
||||
|
||||
@@ -2433,6 +2433,12 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
|
||||
for (const auto& binding : targets) {
|
||||
if (binding.multisampled) Require(std::find(resources->MultisampleSources().begin(), resources->MultisampleSources().end(), binding.color.surfaceAddress) == resources->MultisampleSources().end(), "sampling a multisampled color target the draw renders to is not implemented");
|
||||
}
|
||||
for (const auto& binding : targets) {
|
||||
if (binding.color.partialWriteMask == 0) continue;
|
||||
Require(lean && binding.resident != nullptr && !binding.proxied, "a partial write of a packed color target is merged into its resident image by a recorded draw only");
|
||||
const auto images = resources->StorageImages();
|
||||
Require(std::ranges::any_of(images, [&](const std::pair<VkImage, bool>& image) { return image.first == binding.resident->Image() && image.second; }), "the partial write of a packed color target does not store into the target's resident image");
|
||||
}
|
||||
if (!lean && !resolved.moved.empty()) {
|
||||
resolved.resources = std::make_shared<ShaderResources>(context, shaders, state.color, static_cast<std::uint32_t>(state.colors.size()), draw.indexAddress, static_cast<std::size_t>(indexBytes), snapshots);
|
||||
resolved.moved.clear();
|
||||
|
||||
@@ -242,6 +242,40 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words)
|
||||
return result;
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 8> EncodeTextureResource(const GuestTextureResource& resource) {
|
||||
Require(resource.dimension == TextureDimension::k2D && resource.samples == 1u, "only single-sample 2D surfaces have a texture descriptor encoding");
|
||||
Require(resource.mipCount == 1u && resource.allocatedMipCount <= 1u && resource.baseLevel == 0u && resource.lastLevel == 0u && resource.depthOrLastArray == 0u && resource.baseArray == 0u && resource.dccAddress == 0u && resource.minLod == 0u && !resource.iterate256 && resource.bcSwizzle == 0u, "only single-level, single-layer surfaces without metadata have a texture descriptor encoding");
|
||||
Require(resource.width >= 1u && resource.width <= 0x4000u && resource.height >= 1u && resource.height <= 0x4000u && resource.format < 0x200u, "the surface's extent or format has no texture descriptor encoding");
|
||||
const auto xorMask = resource.tileMode == TextureTileMode::kS4KBX || resource.tileMode == TextureTileMode::kD4KBX ? 0xfffull : 0xffffull;
|
||||
Require(XorSwizzleMode(resource.tileMode) != 0 ? (resource.baseAddress & xorMask) == 0 && (resource.pipeBankXor & ~xorMask) == 0 : resource.pipeBankXor == 0, "the surface's pipe and bank XOR has no texture descriptor encoding");
|
||||
const auto base = (resource.baseAddress | resource.pipeBankXor) >> 8u;
|
||||
Require(base != 0 && (resource.baseAddress & 0xffu) == 0 && base <= 0xffffffffffull, "the surface's address has no texture descriptor encoding");
|
||||
std::uint32_t tileMode = 0;
|
||||
switch (resource.tileMode) {
|
||||
case TextureTileMode::kLinear: tileMode = 0x00; break;
|
||||
case TextureTileMode::kStandard256B: tileMode = 0x01; break;
|
||||
case TextureTileMode::kD256B: tileMode = 0x02; break;
|
||||
case TextureTileMode::kStandard4KB: tileMode = 0x05; break;
|
||||
case TextureTileMode::kD4KB: tileMode = 0x06; break;
|
||||
case TextureTileMode::kStandard64KB: tileMode = 0x09; break;
|
||||
case TextureTileMode::kD64KB: tileMode = 0x0a; break;
|
||||
case TextureTileMode::kS64KBT: tileMode = 0x11; break;
|
||||
case TextureTileMode::kD64KBT: tileMode = 0x12; break;
|
||||
case TextureTileMode::kS4KBX: tileMode = 0x15; break;
|
||||
case TextureTileMode::kD4KBX: tileMode = 0x16; break;
|
||||
case TextureTileMode::kZ64KBX: tileMode = 0x18; break;
|
||||
case TextureTileMode::kS64KBX: tileMode = 0x19; break;
|
||||
case TextureTileMode::kD64KBX: tileMode = 0x1a; break;
|
||||
case TextureTileMode::kR64KBX: tileMode = 0x1b; break;
|
||||
}
|
||||
std::array<std::uint32_t, 8> words{};
|
||||
words[0] = static_cast<std::uint32_t>(base);
|
||||
words[1] = static_cast<std::uint32_t>(base >> 32u) | (resource.format << 20u) | (((resource.width - 1u) & 3u) << 30u);
|
||||
words[2] = ((resource.width - 1u) >> 2u) | ((resource.height - 1u) << 14u);
|
||||
words[3] = resource.dstSelX | (resource.dstSelY << 3u) | (resource.dstSelZ << 6u) | (resource.dstSelW << 9u) | (tileMode << 20u) | (9u << 28u);
|
||||
return words;
|
||||
}
|
||||
|
||||
float EffectiveMinLod(const GuestTextureResource& resource) {
|
||||
if (resource.minLod <= resource.baseLevel * 256u) return 0.0f;
|
||||
return std::min(static_cast<float>(resource.minLod) / 256.0f, static_cast<float>(resource.lastLevel));
|
||||
|
||||
@@ -35,7 +35,7 @@ static_assert(sizeof(VkStencilOpState) == 28, "VkStencilOpState changed: update
|
||||
static_assert(sizeof(VkVertexInputBindingDescription) == 12, "VkVertexInputBindingDescription changed: update the recipe codec");
|
||||
static_assert(sizeof(VkVertexInputAttributeDescription) == 16, "VkVertexInputAttributeDescription changed: update the recipe codec");
|
||||
#if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__)
|
||||
static_assert(sizeof(State) == 784, "State changed: update CanonicalPipelineState and the recipe codec");
|
||||
static_assert(sizeof(State) == 792, "State changed: update CanonicalPipelineState and the recipe codec");
|
||||
static_assert(sizeof(ShaderStages) == 112, "ShaderStages changed: update the recipe codec");
|
||||
static_assert(sizeof(ShaderRecompiler::MeshConfiguration) == 40, "MeshConfiguration changed: update the recipe codec");
|
||||
static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "TessellationConfiguration changed: update the recipe codec");
|
||||
|
||||
@@ -1397,7 +1397,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
|
||||
result.blends.assign(exportCount, VkPipelineColorBlendAttachmentState{});
|
||||
std::array<float, 3> requiredBlendConstants{};
|
||||
std::uint32_t requiredBlendMask = 0;
|
||||
for (const auto& color : result.colors) {
|
||||
for (auto& color : result.colors) {
|
||||
const auto slot = color.slot;
|
||||
const auto blend = read(cx, 0x1e0 + slot);
|
||||
APS5_LOG_OUT_DEBUG("Blend %u control=0x%x", slot, blend);
|
||||
@@ -1465,8 +1465,16 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
|
||||
const auto slotExport = (exportFormat >> (4u * color.exportIndex)) & 0xfu;
|
||||
if (slotExport == 5 || slotExport == 6) throw std::runtime_error("AGC graphics: color export format " + std::to_string(slotExport) + " into " + packed + " target is unsupported");
|
||||
Require(!state.blendEnable, "blending into " + packed + " color target is unsupported");
|
||||
const std::uint32_t packedMask = color.packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4 ? 0xfu : 7u;
|
||||
Require((exportedMask & packedMask) == packedMask, "partial writes of " + packed + " color target are unsupported");
|
||||
const bool nibbles = color.packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4;
|
||||
const std::uint32_t packedMask = nibbles ? 0xfu : 7u;
|
||||
Require(nibbles || (exportedMask & packedMask) == packedMask, "partial writes of " + packed + " color target are unsupported");
|
||||
if ((exportedMask & packedMask) != packedMask) {
|
||||
Require(color.samples == 1u, "partial writes of a multisampled " + std::string(PackedColorTargetName(color.packing)) + " color target are not implemented");
|
||||
Require(color.mipCount == 1u && color.depth == 1u && color.layers == 1u && color.arrayLastSlice == 0u, "partial writes of a mipmapped, volume or array " + std::string(PackedColorTargetName(color.packing)) + " color target are not implemented");
|
||||
Require(std::ranges::none_of(result.colors, [](const ColorTarget& other) { return other.partialWriteMask != 0; }), "partial writes of more than one packed color target are not implemented");
|
||||
color.partialWriteMask = PackedWriteMask(mapping, exportedMask);
|
||||
state.colorWriteMask = 0;
|
||||
}
|
||||
}
|
||||
if (state.blendEnable && !reversed && (secondSource(state.srcColorBlendFactor) || secondSource(state.dstColorBlendFactor) || secondSource(state.srcAlphaBlendFactor) || secondSource(state.dstAlphaBlendFactor))) {
|
||||
const auto addition = [](VkBlendOp op) { return op == VK_BLEND_OP_ADD || op == VK_BLEND_OP_SUBTRACT || op == VK_BLEND_OP_REVERSE_SUBTRACT; };
|
||||
@@ -1483,7 +1491,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
|
||||
for (std::uint32_t i = 0; i < 3; ++i) {
|
||||
if ((requiredBlendMask & (1u << i)) != 0) result.blendConstants[i] = requiredBlendConstants[i];
|
||||
}
|
||||
if (!result.colors.empty()) result.blend = result.blends[result.colors.front().exportIndex];
|
||||
if (!result.colors.empty()) {
|
||||
result.color = result.colors.front();
|
||||
result.blend = result.blends[result.colors.front().exportIndex];
|
||||
}
|
||||
Require(result.reversedBlend == 0 || (exportSlots.size() == 1 && exportCount == 1), "blending into a color target with a reversed component order through a second source color needs that target to be the shader's only color export");
|
||||
if (result.dualSourceBlend) {
|
||||
Require(exportSlots.size() == 2 && exportSlots[0] == 0 && exportSlots[1] == 1 && exportCount == 1, "dual-source blending needs pixel exports to MRT slots 0 and 1 and color writes to slot 0 alone");
|
||||
@@ -1510,6 +1521,27 @@ std::array<ShaderRecompiler::ColorExportPacking, 8> ExportPackings(const State&
|
||||
return packings;
|
||||
}
|
||||
|
||||
std::uint16_t PackedWriteMask(std::uint8_t componentMapping, std::uint32_t exportedMask) {
|
||||
std::uint16_t mask = 0;
|
||||
for (std::uint32_t component = 0; component < 4; ++component) {
|
||||
if (((exportedMask >> ((componentMapping >> (2u * component)) & 3u)) & 1u) != 0) mask = static_cast<std::uint16_t>(mask | (0xfu << (4u * component)));
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state) {
|
||||
ShaderRecompiler::TargetMerge merge;
|
||||
for (const auto& color : state.colors) {
|
||||
if (color.partialWriteMask == 0) continue;
|
||||
Require(merge.mask == 0 && color.exportIndex < 8u && color.samples == 1u, "a draw merges into one single-sample packed color target at most");
|
||||
merge.target = static_cast<std::uint8_t>(color.exportIndex);
|
||||
merge.mask = color.partialWriteMask;
|
||||
merge.samples = static_cast<std::uint8_t>(color.samples);
|
||||
merge.descriptor = EncodeTextureResource(SurfaceForTarget(color));
|
||||
}
|
||||
return merge;
|
||||
}
|
||||
|
||||
bool PartialSampleMask(const State& state) {
|
||||
const auto full = (1u << state.samples) - 1u;
|
||||
return std::ranges::any_of(state.quadSampleMask, [&](std::uint16_t mask) { return (mask & full) != full; });
|
||||
|
||||
@@ -8803,9 +8803,82 @@ void unorm4_4_4_4TargetTests() {
|
||||
Require(state.color.format == VK_FORMAT_R16_UINT && state.color.elementBytes == 2 && state.color.componentMapping == 0x1bu && state.color.packing == ColorExportPacking::Unorm4_4_4_4, "a 4_4_4_4 unorm target did not decode to an R16_UINT attachment with reversed exports");
|
||||
const auto packings = AgcDriver::Graphics::ExportPackings(state);
|
||||
Require(packings[0] == ColorExportPacking::Unorm4_4_4_4 && std::all_of(packings.begin() + 1, packings.end(), [](ColorExportPacking packing) { return packing == ColorExportPacking::None; }), "export 0 did not carry the 4_4_4_4 unorm packing");
|
||||
Require(state.color.partialWriteMask == 0u && state.blends[0].colorWriteMask == 0xfu && AgcDriver::Graphics::ExportTargetMerge(state).mask == 0u, "a full write of a 4_4_4_4 unorm target merged into the target");
|
||||
auto changed = queue;
|
||||
changed.context[0x8e] = 7;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "partial writes of a 4_4_4_4 unorm color target");
|
||||
for (const auto& [targetMask, wordMask] : {std::pair{1u, 0xf000u}, std::pair{7u, 0xfff0u}, std::pair{8u, 0x000fu}, std::pair{0xau, 0x0f0fu}}) {
|
||||
changed.context[0x8e] = targetMask;
|
||||
const auto partial = AgcDriver::Graphics::DecodeState(changed);
|
||||
Require(partial.color.partialWriteMask == wordMask && partial.colors.front().partialWriteMask == wordMask && partial.blends[0].colorWriteMask == 0u && partial.blend.colorWriteMask == 0u, "CB_TARGET_MASK " + std::to_string(targetMask) + " of a 4_4_4_4 unorm target did not merge its nibbles behind a closed write mask");
|
||||
const auto merge = AgcDriver::Graphics::ExportTargetMerge(partial);
|
||||
const auto expected = AgcDriver::Graphics::SurfaceForTarget(partial.color);
|
||||
const auto surface = AgcDriver::Graphics::DecodeTextureResource(merge.descriptor);
|
||||
Require(merge.target == 0u && merge.mask == wordMask && merge.samples == 1u && surface.baseAddress == expected.baseAddress && surface.pipeBankXor == expected.pipeBankXor && surface.width == expected.width && surface.height == expected.height && surface.format == expected.format && surface.tileMode == expected.tileMode && surface.dimension == AgcDriver::Graphics::TextureDimension::k2D && surface.mipCount == 1u && surface.samples == 1u, "the merge descriptor of CB_TARGET_MASK " + std::to_string(targetMask) + " does not decode to the target's surface");
|
||||
}
|
||||
{
|
||||
changed.context[0x8e] = 1;
|
||||
const auto partial = AgcDriver::Graphics::DecodeState(changed);
|
||||
constexpr std::array<std::uint32_t, 3> mergeCode{0xf800180fu, 0x07060504u, 0xbf810000u};
|
||||
auto merging = packedExportRequest(changed, partial, mergeCode);
|
||||
merging.context.pixel->targetMerge = AgcDriver::Graphics::ExportTargetMerge(partial);
|
||||
const std::array<std::uint32_t, 3> capabilities{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat};
|
||||
merging.target.supportedCapabilities = capabilities;
|
||||
merging.target.registrationWordLimit = 1u << 20u;
|
||||
const ShaderRecompiler::RequestSerializer mergeSerializer;
|
||||
Require(mergeSerializer.Deserialize(mergeSerializer.Serialize(merging)).request.context.pixel->targetMerge == merging.context.pixel->targetMerge, "the 4_4_4_4 target merge was lost in serialization");
|
||||
auto unmerged = merging;
|
||||
unmerged.context.pixel->targetMerge = {};
|
||||
auto moved = merging;
|
||||
moved.context.pixel->targetMerge.descriptor[0] += 0x100u;
|
||||
std::vector<std::uint64_t> mergedKey, unmergedKey, movedKey;
|
||||
ShaderRecompiler::RecompileCacheKey::BuildInterface(merging, mergedKey);
|
||||
ShaderRecompiler::RecompileCacheKey::BuildInterface(unmerged, unmergedKey);
|
||||
ShaderRecompiler::RecompileCacheKey::BuildInterface(moved, movedKey);
|
||||
Require(mergedKey != unmergedKey && mergedKey == movedKey, "the static shader key does not separate merging from plain writes, or depends on the merged target's address");
|
||||
for (const bool registration : {false, true}) {
|
||||
merging.target.registrationModules = registration;
|
||||
const auto merged = ShaderRecompiler::Recompile(merging);
|
||||
const auto words = merged.spirv.Copy();
|
||||
std::size_t reads = 0, writes = 0;
|
||||
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
|
||||
reads += static_cast<spv::Op>(words[at] & 0xffffu) == spv::OpImageRead ? 1u : 0u;
|
||||
writes += static_cast<spv::Op>(words[at] & 0xffffu) == spv::OpImageWrite ? 1u : 0u;
|
||||
}
|
||||
const auto what = std::string(registration ? "a registration" : "a specialized") + " 4_4_4_4 merge module";
|
||||
Require(merged.registrationModule == registration && reads == 1u && writes == 1u && interlockShape(words) == "capability extension ordered begin end " && declaresEarlyFragmentTests(merging), what + " did not read and write the target once inside a pixel interlock after early tests: " + interlockShape(words));
|
||||
const auto bound = std::ranges::any_of(merged.bindings, [&](const ShaderRecompiler::DescriptorBinding& binding) {
|
||||
if (binding.role != ShaderRecompiler::DescriptorRole::GuestImages || binding.kind != ShaderRecompiler::DescriptorKind::StorageImage) return false;
|
||||
for (std::size_t offset = 0; offset + 8u <= binding.guestDescriptor.size(); offset += 8u) {
|
||||
if (std::equal(merging.context.pixel->targetMerge.descriptor.begin(), merging.context.pixel->targetMerge.descriptor.end(), binding.guestDescriptor.begin() + static_cast<std::ptrdiff_t>(offset))) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
Require(bound, what + " does not bind the merged target as a storage image");
|
||||
ShaderRecompiler::RecompileResult vertex;
|
||||
vertex.spirv = makeModule({});
|
||||
const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
|
||||
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &merged, 0}}};
|
||||
Require(AgcDriver::Graphics::ValidateShaders(shaders, partial, subgroup, false) == std::set<std::uint32_t>{0u}, what + " lost its export to attachment 0");
|
||||
}
|
||||
auto lacking = merging;
|
||||
const std::array<std::uint32_t, 2> noInterlock{spv::CapabilityShader, spv::CapabilityStorageImageReadWithoutFormat};
|
||||
lacking.target.supportedCapabilities = noInterlock;
|
||||
expectFailure([&] { static_cast<void>(ShaderRecompiler::Recompile(lacking)); }, "fragmentShaderPixelInterlock");
|
||||
auto unformatted = merging;
|
||||
const std::array<std::uint32_t, 2> noFormatlessReads{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT};
|
||||
unformatted.target.supportedCapabilities = noFormatlessReads;
|
||||
expectFailure([&] { static_cast<void>(ShaderRecompiler::Recompile(unformatted)); }, "storage image reads without a format");
|
||||
auto killing = merging;
|
||||
killing.context.pixel->pixelKillEnable = true;
|
||||
killing.context.pixel->earlyZ = false;
|
||||
expectFailure([&] { static_cast<void>(ShaderRecompiler::Recompile(killing)); }, "kills pixels under late tests");
|
||||
auto exporting = merging;
|
||||
exporting.context.pixel->depthExportEnable = true;
|
||||
expectFailure([&] { static_cast<void>(ShaderRecompiler::Recompile(exporting)); }, "exports depth");
|
||||
}
|
||||
changed = queue;
|
||||
changed.context[0x8e] = 0xau;
|
||||
changed.context[0x31d] = (changed.context.at(0x31d) & ~0x3f000u) | (1u << 12u) | (1u << 15u);
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "partial writes of a multisampled 4_4_4_4 unorm color target");
|
||||
for (const auto& [number, swap] : {std::pair{1u, 2u}, std::pair{4u, 2u}, std::pair{0u, 0u}, std::pair{0u, 1u}, std::pair{0u, 3u}}) {
|
||||
changed = queue;
|
||||
changed.context[0x31c] = (changed.context[0x31c] & ~((7u << 8u) | (3u << 11u))) | (number << 8u) | (swap << 11u);
|
||||
|
||||
@@ -4746,7 +4746,7 @@ void verifyPixelRequestSerialization() {
|
||||
value.posZ, value.posW, value.frontFace, value.frontFaceAllBits, value.ancillary, value.sampleShading,
|
||||
value.noPerspective, value.linearCentroid, value.pixelKillEnable, value.depthExportEnable,
|
||||
value.sampleMaskExportEnable, value.earlyZ, value.executeOnNoop, value.conservativeZExport, value.orderedPixelShader,
|
||||
value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage, value.stencilRefExportEnable, value.rasterPrimitive, value.layerExported, value.ditheredAlphaToCoverage, value.reversedBlend);
|
||||
value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage, value.stencilRefExportEnable, value.rasterPrimitive, value.layerExported, value.ditheredAlphaToCoverage, value.reversedBlend, value.targetMerge);
|
||||
};
|
||||
const std::array<std::uint32_t, 1> code{0xbf810000u};
|
||||
const std::array<std::uint32_t, 3> userData{0x12345678u, 0xabcdef01u, 0x87654321u};
|
||||
@@ -4794,6 +4794,7 @@ void verifyPixelRequestSerialization() {
|
||||
pixel.stencilRefExportEnable = true;
|
||||
pixel.ditheredAlphaToCoverage = true;
|
||||
pixel.reversedBlend = 0x570001u;
|
||||
pixel.targetMerge = {1u, 0x0ff0u, 1u, {0x100u, 0x2u, 0x3u, 0x4u, 0x5u, 0x6u, 0x7u, 0x8u}};
|
||||
const std::array<std::array<std::uint8_t, 8>, 3> mappings{{
|
||||
{0x00u, 0xe4u, 0xc6u, 0x1bu, 0xffu, 0x80u, 0x55u, 0xaau},
|
||||
{0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u},
|
||||
@@ -4826,6 +4827,14 @@ void verifyPixelRequestSerialization() {
|
||||
withoutReversedBlend.context.pixel->reversedBlend = 0u;
|
||||
RecompileCacheKey::Build(withoutReversedBlend, replayKey);
|
||||
require(key == replayKey && RecompileCacheKey::ContextHash(request) == RecompileCacheKey::ContextHash(withoutReversedBlend), "the reversed-order blend code changed the static shader identity");
|
||||
auto movedMerge = request;
|
||||
movedMerge.context.pixel->targetMerge.descriptor[0] = 0x200u;
|
||||
RecompileCacheKey::Build(movedMerge, replayKey);
|
||||
require(key == replayKey, "the merged target's descriptor changed the static shader identity");
|
||||
auto withoutMerge = request;
|
||||
withoutMerge.context.pixel->targetMerge = {};
|
||||
RecompileCacheKey::Build(withoutMerge, replayKey);
|
||||
require(key != replayKey, "a color target merge did not change the static shader identity");
|
||||
auto withoutCoverage = request;
|
||||
withoutCoverage.context.pixel->alphaToCoverage = false;
|
||||
RecompileCacheKey::Build(withoutCoverage, replayKey);
|
||||
@@ -4848,7 +4857,7 @@ void verifyPixelRequestSerialization() {
|
||||
minimal.context.pixel->layerExported = true;
|
||||
minimal.context.pixel->ditheredAlphaToCoverage = true;
|
||||
const auto encoded = serializer.Serialize(minimal);
|
||||
require(requestPrefix(encoded, 8u) == "NVNQQScAAAA=", "new requests did not use serialization version 39");
|
||||
require(requestPrefix(encoded, 8u) == "NVNQQSgAAAA=", "new requests did not use serialization version 40");
|
||||
const std::array<std::uint32_t, 2> fallbacks{0x40u, 0x1ac4u};
|
||||
auto withFallbacks = minimal;
|
||||
withFallbacks.context.linkedCallFallbacks = fallbacks;
|
||||
@@ -4966,7 +4975,7 @@ void verifyPixelRequestSerialization() {
|
||||
for (std::size_t bytes = 0; bytes < 45u; ++bytes) {
|
||||
expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted");
|
||||
}
|
||||
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSgAAAA="}) {
|
||||
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSkAAAA="}) {
|
||||
expectFailure([&] { static_cast<void>(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted");
|
||||
}
|
||||
static constexpr std::string_view version18Request{
|
||||
@@ -5104,8 +5113,23 @@ void verifyVertexInfoVersion19() {
|
||||
while (allBitsOffset < plainPixel.size() && plainPixel[allBitsOffset] == allBitsBytes[allBitsOffset]) ++allBitsOffset;
|
||||
require(allBitsOffset == flagOffset + 2u, "the front-face all-bits flag does not follow the dithered coverage flag");
|
||||
require(blendOffset == allBitsOffset + 18u, "the reversed-order blend code is not serialized after the front-face all-bits flag, quad pixel mask and quad sample masks");
|
||||
constexpr std::size_t sampleMaskBytes = 16u;
|
||||
constexpr std::size_t iterationBytes = 35u;
|
||||
constexpr std::size_t mergeBytes = 38u;
|
||||
const auto mergeOffset = static_cast<std::ptrdiff_t>(blendOffset + 4u + iterationBytes);
|
||||
auto merging = pixel;
|
||||
merging.context.pixel->targetMerge = {3u, 0xf00fu, 1u, {1u, 2u, 3u, 4u, 5u, 6u, 7u, 0x89abcdefu}};
|
||||
const auto mergingBytes = decodeBase64(serializer.Serialize(merging));
|
||||
require(mergingBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + mergeOffset, mergingBytes.begin()) && mergingBytes[mergeOffset] == 3u && mergingBytes[mergeOffset + 1] == 0x0fu && mergingBytes[mergeOffset + 2] == 0xf0u && mergingBytes[mergeOffset + 5] == 1u && mergingBytes[mergeOffset + 6] == 1u && mergingBytes[mergeOffset + 34] == 0xefu && mergingBytes[mergeOffset + 37] == 0x89u && std::equal(plainPixel.begin() + mergeOffset + static_cast<std::ptrdiff_t>(mergeBytes), plainPixel.end(), mergingBytes.begin() + mergeOffset + static_cast<std::ptrdiff_t>(mergeBytes)), "the color target merge is not the 38 bytes the version 40 pixel info adds after the pixel iteration fields");
|
||||
require(serializer.Deserialize(serializer.Serialize(merging)).request.context.pixel->targetMerge == merging.context.pixel->targetMerge, "a serialized request lost its color target merge");
|
||||
const auto withoutMerge = [&](std::vector<std::uint8_t> bytes) {
|
||||
bytes.erase(bytes.begin() + mergeOffset, bytes.begin() + mergeOffset + static_cast<std::ptrdiff_t>(mergeBytes));
|
||||
return bytes;
|
||||
};
|
||||
auto version39 = withoutMerge(plainPixel);
|
||||
version39[4] = 39u;
|
||||
const auto replay39 = serializer.Deserialize(encodeBase64(version39));
|
||||
require(replay39.request.context.pixel.has_value() && replay39.request.context.pixel->targetMerge == TargetMerge{} && serializer.Serialize(replay39.request) == encodedPixel, "a version 39 request read a color target merge or misread the fields after it");
|
||||
constexpr std::size_t sampleMaskBytes = 16u;
|
||||
auto maskedPixel = pixel;
|
||||
maskedPixel.context.pixel->quadPixelMask = 0x2u;
|
||||
const auto maskedBytes = decodeBase64(serializer.Serialize(maskedPixel));
|
||||
@@ -5116,7 +5140,7 @@ void verifyVertexInfoVersion19() {
|
||||
sampledPixel.context.pixel->quadSampleMask = {0x1u, 0x2u, 0x4u, 0x8u};
|
||||
const auto sampledBytes = decodeBase64(serializer.Serialize(sampledPixel));
|
||||
require(std::equal(maskedBytes.begin(), maskedBytes.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u), sampledBytes.begin()) && sampledBytes[maskOffset + 1u] == 0x1u && sampledBytes[maskOffset + 5u] == 0x2u && sampledBytes[maskOffset + 9u] == 0x4u && sampledBytes[maskOffset + 13u] == 0x8u, "the quad sample masks are not the four words after the quad pixel mask");
|
||||
auto version30 = sampledBytes;
|
||||
auto version30 = withoutMerge(sampledBytes);
|
||||
version30.pop_back();
|
||||
version30.pop_back();
|
||||
version30.pop_back();
|
||||
@@ -5126,7 +5150,7 @@ void verifyVertexInfoVersion19() {
|
||||
require(replay30.request.context.pixel.has_value() && !replay30.request.context.pixel->iteratesSamples && replay30.request.context.pixel->positionLocation == PixelPositionLocation::Center && replay30.request.context.pixel->sampleLocations == std::array<std::uint8_t, 32>{} && decodeBase64(serializer.Serialize(replay30.request)) == sampledBytes, "a version 30 request read pixel iteration, a position location or sample locations, or misread the fields after its reversed-order blend code");
|
||||
auto spanned = sampledPixel;
|
||||
spanned.target.spannedWaves = true;
|
||||
auto version32 = decodeBase64(serializer.Serialize(spanned));
|
||||
auto version32 = withoutMerge(decodeBase64(serializer.Serialize(spanned)));
|
||||
require(version32.size() > 2u && version32.back() == 0u && version32[version32.size() - 2u] == 0u && version32[version32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag");
|
||||
version32.pop_back();
|
||||
version32.pop_back();
|
||||
@@ -5134,12 +5158,12 @@ void verifyVertexInfoVersion19() {
|
||||
version32[4] = 32u;
|
||||
const auto replay32 = serializer.Deserialize(encodeBase64(version32));
|
||||
require(replay32.request.context.pixel.has_value() && !replay32.request.context.pixel->iteratesSamples && replay32.request.context.pixel->sampleLocations == std::array<std::uint8_t, 32>{} && replay32.request.target.spannedWaves && serializer.Serialize(replay32.request) == serializer.Serialize(spanned), "a version 32 request read pixel iteration or sample locations, or lost its spanned-waves flag");
|
||||
auto version29 = sampledBytes;
|
||||
auto version29 = withoutMerge(sampledBytes);
|
||||
version29.pop_back();
|
||||
version29.pop_back();
|
||||
version29.pop_back();
|
||||
version29.erase(version29.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes));
|
||||
auto version36 = sampledBytes;
|
||||
auto version36 = withoutMerge(sampledBytes);
|
||||
version36.erase(version36.begin() + static_cast<std::ptrdiff_t>(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes - 1u));
|
||||
version36[4] = 36u;
|
||||
const auto replay36 = serializer.Deserialize(encodeBase64(version36));
|
||||
@@ -5147,7 +5171,7 @@ void verifyVertexInfoVersion19() {
|
||||
version29[4] = 29u;
|
||||
const auto replay29 = serializer.Deserialize(encodeBase64(version29));
|
||||
require(replay29.request.context.pixel.has_value() && replay29.request.context.pixel->quadSampleMask == std::array<std::uint16_t, 4>{0x1u, 0x2u, 0x4u, 0x8u} && replay29.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay29.request)) == sampledBytes, "a version 29 request read a reversed-order blend code or misread the fields after the quad sample masks");
|
||||
auto version28 = sampledBytes;
|
||||
auto version28 = withoutMerge(sampledBytes);
|
||||
version28.pop_back();
|
||||
version28.pop_back();
|
||||
version28.pop_back();
|
||||
@@ -5155,7 +5179,7 @@ void verifyVertexInfoVersion19() {
|
||||
version28[4] = 28u;
|
||||
const auto replay28 = serializer.Deserialize(encodeBase64(version28));
|
||||
require(replay28.request.context.pixel.has_value() && replay28.request.context.pixel->quadPixelMask == 0x2u && replay28.request.context.pixel->quadSampleMask == std::array<std::uint16_t, 4>{0xffffu, 0xffffu, 0xffffu, 0xffffu} && replay28.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay28.request)) == maskedBytes, "a version 28 request read quad sample masks or a reversed-order blend code, or misread the fields after them");
|
||||
auto version27 = maskedBytes;
|
||||
auto version27 = withoutMerge(maskedBytes);
|
||||
version27.pop_back();
|
||||
version27.pop_back();
|
||||
version27.pop_back();
|
||||
@@ -5163,7 +5187,7 @@ void verifyVertexInfoVersion19() {
|
||||
version27[4] = 27u;
|
||||
const auto replay27 = serializer.Deserialize(encodeBase64(version27));
|
||||
require(replay27.request.context.pixel.has_value() && replay27.request.context.pixel->quadPixelMask == 0xfu && replay27.request.context.pixel->reversedBlend == 0u && serializer.Serialize(replay27.request) == encodedPixel, "a version 27 request read a quad pixel mask or reversed-order blend code, or misread the fields after it");
|
||||
auto legacyPixel = plainPixel;
|
||||
auto legacyPixel = withoutMerge(plainPixel);
|
||||
legacyPixel.pop_back();
|
||||
legacyPixel.pop_back();
|
||||
legacyPixel.pop_back();
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
#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 <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::ColorTargetLayout;
|
||||
using AgcDriver::Graphics::ColorTileMode;
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
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 Unorm4_4_4_4Info = 0x8000u | (19u << 2u) | (2u << 11u);
|
||||
constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u;
|
||||
constexpr std::uint32_t Float32Export = 9;
|
||||
constexpr std::uint32_t Float16Export = 4;
|
||||
|
||||
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}
|
||||
}};
|
||||
|
||||
alignas(256) std::array<std::array<std::uint32_t, 64>, 64> Programs{};
|
||||
std::size_t programCount = 0;
|
||||
|
||||
std::uint32_t Unorm(float value) {
|
||||
if (!(value > 0.0f)) return 0;
|
||||
if (value >= 1.0f) return 15;
|
||||
return static_cast<std::uint32_t>(std::nearbyint(static_cast<double>(value) * 15.0));
|
||||
}
|
||||
|
||||
std::uint32_t Packed(std::array<float, 4> rgba) {
|
||||
return Unorm(rgba[3]) | (Unorm(rgba[2]) << 4u) | (Unorm(rgba[1]) << 8u) | (Unorm(rgba[0]) << 12u);
|
||||
}
|
||||
|
||||
std::uint32_t Half(float value) {
|
||||
Require(value == 0.0f || (value >= 0.0625f && value <= 1.0f), "the test's half values are normal and at most 1");
|
||||
if (value == 0.0f) return 0;
|
||||
const auto bits = std::bit_cast<std::uint32_t>(value);
|
||||
Require((bits & 0x1fffu) == 0, "the test's half values are exact");
|
||||
return ((((bits >> 23u) & 0xffu) - 112u) << 10u) | ((bits >> 13u) & 0x3ffu);
|
||||
}
|
||||
|
||||
std::span<const std::uint32_t> Program(const std::array<std::uint32_t, 4>& vgprs, bool compressed) {
|
||||
Require(programCount < Programs.size(), "too many test programs");
|
||||
auto& code = Programs[programCount++];
|
||||
std::size_t at = 0;
|
||||
for (std::uint32_t vgpr = 0; vgpr < (compressed ? 2u : 4u); ++vgpr) {
|
||||
code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u);
|
||||
code[at++] = vgprs[vgpr];
|
||||
}
|
||||
code[at++] = compressed ? 0xf8001c0fu : 0xf800180fu;
|
||||
code[at++] = compressed ? 0x00000504u : 0x07060504u;
|
||||
code[at++] = 0xbf810000u;
|
||||
return std::span<const std::uint32_t>(code.data(), at);
|
||||
}
|
||||
|
||||
std::span<const std::uint32_t> Float32Program(std::array<float, 4> rgba) {
|
||||
return Program({std::bit_cast<std::uint32_t>(rgba[0]), std::bit_cast<std::uint32_t>(rgba[1]), std::bit_cast<std::uint32_t>(rgba[2]), std::bit_cast<std::uint32_t>(rgba[3])}, false);
|
||||
}
|
||||
|
||||
std::span<const std::uint32_t> Float16Program(std::array<float, 4> rgba) {
|
||||
return Program({Half(rgba[0]) | (Half(rgba[1]) << 16u), Half(rgba[2]) | (Half(rgba[3]) << 16u), 0, 0}, true);
|
||||
}
|
||||
|
||||
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] = Unorm4_4_4_4Info;
|
||||
cx[0x31d] = 0;
|
||||
cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u);
|
||||
cx[0x3b8] = TiledAttrib3;
|
||||
cx[0x390] = static_cast<std::uint32_t>(block.Address() >> 40u);
|
||||
auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0);
|
||||
Require(color.format == VK_FORMAT_R16_UINT && color.componentMapping == 0x1bu && color.packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4 && color.tileMode == ColorTileMode::RenderTarget && color.bytes == block.bytes, "the tiled 4_4_4_4 unorm target did not decode as a packed R16_UINT target");
|
||||
return color;
|
||||
}
|
||||
|
||||
std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTarget color, std::uint32_t exportedMask, std::uint32_t exportFormat, 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());
|
||||
|
||||
const bool partial = exportedMask != 0xfu;
|
||||
color.partialWriteMask = partial ? AgcDriver::Graphics::PackedWriteMask(color.componentMapping, exportedMask) : 0u;
|
||||
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;
|
||||
VkPipelineColorBlendAttachmentState blend{};
|
||||
blend.colorWriteMask = partial ? 0u : VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
|
||||
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, exportFormat}};
|
||||
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, {}), std::nullopt, pixelMemory},
|
||||
target,
|
||||
PixelPushLayout(vertexPush, target)
|
||||
};
|
||||
const auto handle = ShaderRecompiler::PrepareShader(fragment);
|
||||
AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}};
|
||||
snapshot.prepared->entries.push_back({0, handle});
|
||||
fragment.shader.code = snapshot.code;
|
||||
fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state));
|
||||
fragment.context.pixel->targetExportPacking = AgcDriver::Graphics::ExportPackings(state);
|
||||
fragment.context.pixel->targetMerge = AgcDriver::Graphics::ExportTargetMerge(state);
|
||||
Require((fragment.context.pixel->targetMerge.mask != 0u) == partial && fragment.context.pixel->targetMerge.mask == color.partialWriteMask, "the draw's target merge disagrees with its write mask");
|
||||
const ShaderRecompiler::SrtRuntime runtime{};
|
||||
const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, fragment);
|
||||
const auto pixelResult = invocation.Materialize(*invocation.Capture(runtime));
|
||||
const bool registered = pixelResult->variantId == ShaderRecompiler::GetPreparedArtifact(*handle).variantId;
|
||||
Require(registered != partial, partial ? "the merging draw reused the registered pixel shader" : "the full write did not use the registered pixel shader");
|
||||
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
|
||||
{ShaderStage::Vertex, &vertexResult, 0},
|
||||
{ShaderStage::Fragment, pixelResult.get(), 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);
|
||||
return color.partialWriteMask;
|
||||
}
|
||||
|
||||
std::vector<std::uint16_t> ReadBack(AgcDriver::VulkanDevice& device, const Block& block) {
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back");
|
||||
device.WaitIdle();
|
||||
std::vector<std::uint16_t> stored(block.bytes / 2u);
|
||||
AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1);
|
||||
return stored;
|
||||
}
|
||||
|
||||
void ExpectSurface(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector<std::uint16_t>& before, std::uint32_t word, std::uint32_t wordMask, const std::string& what) {
|
||||
const auto stored = ReadBack(device, block);
|
||||
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) / 2u;
|
||||
const bool inside = x < ScissorWidth;
|
||||
const std::uint32_t expected = inside ? (before[index] & ~wordMask) | (word & wordMask) : before[index];
|
||||
if (stored[index] == expected) continue;
|
||||
char detail[192];
|
||||
std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) %s the scissor holds 0x%04x, expected 0x%04x (it held 0x%04x)", x, y, inside ? "inside" : "outside", stored[index], expected, before[index]);
|
||||
throw std::runtime_error(what + detail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::uint16_t> Fill(Block& block, std::uint32_t seed) {
|
||||
std::vector<std::uint16_t> words(block.bytes / 2u);
|
||||
for (std::size_t index = 0; index < words.size(); ++index) words[index] = static_cast<std::uint16_t>(((index + seed) * 0x9e3779b9u) >> 11u);
|
||||
std::memcpy(block.data, words.data(), block.bytes);
|
||||
return words;
|
||||
}
|
||||
|
||||
std::string Describe(std::uint32_t mask, std::array<float, 4> rgba) {
|
||||
char text[160];
|
||||
std::snprintf(text, sizeof(text), "CB_TARGET_MASK 0x%x write of (%.9g, %.9g, %.9g, %.9g)", mask, rgba[0], rgba[1], rgba[2], rgba[3]);
|
||||
return text;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (!TargetHasCapability(device->Target(), spv::CapabilityFragmentShaderPixelInterlockEXT)) {
|
||||
std::puts("skipped, the device lacks fragmentShaderPixelInterlock");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 2);
|
||||
Block block(layout.Bytes());
|
||||
const auto color = DecodeTarget(block);
|
||||
Require(AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 1u) == 0xf000u && AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 8u) == 0x000fu && AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 5u) == 0xf0f0u, "the export components did not map to the guest nibbles (X in 15..12, W in 3..0)");
|
||||
struct Case {
|
||||
std::uint32_t mask;
|
||||
std::array<float, 4> rgba;
|
||||
};
|
||||
const std::array<Case, 9> cases{{
|
||||
{0x1u, {1.0f, 0.0f, 0.0f, 0.0f}},
|
||||
{0x1u, {7.0f / 15.0f, 1.0f, 1.0f, 1.0f}},
|
||||
{0x5u, {0.2f, 0.4f, 0.6f, 0.8f}},
|
||||
{0x2u, {0.0f, 2.0f / 15.0f, 1.0f, 1.0f}},
|
||||
{0x8u, {1.0f, 1.0f, 1.0f, 11.0f / 15.0f}},
|
||||
{0x6u, {0.0f, 0.0f, 0.0f, 0.0f}},
|
||||
{0xeu, {0.0f, 1.0f, 0.5f, 0.25f}},
|
||||
{0xfu, {0.1f, 0.7f, 0.9f, 0.3f}},
|
||||
{0x1u, {0.0f, 0.0f, 0.0f, 0.0f}},
|
||||
}};
|
||||
auto before = Fill(block, 0);
|
||||
for (const auto& [mask, rgba] : cases) {
|
||||
const auto wordMask = Draw(*device, color, mask, Float32Export, Float32Program(rgba));
|
||||
ExpectSurface(*device, block, layout, before, Packed(rgba), mask == 0xfu ? 0xffffu : wordMask, "a 32_ABGR " + Describe(mask, rgba) + " into a tiled 4_4_4_4 unorm target");
|
||||
before = ReadBack(*device, block);
|
||||
}
|
||||
before = Fill(block, 77);
|
||||
const std::array<float, 4> halves{0.5f, 0.75f, 0.125f, 0.25f};
|
||||
const auto wordMask = Draw(*device, color, 0x9u, Float16Export, Float16Program(halves));
|
||||
ExpectSurface(*device, block, layout, before, Packed(halves), wordMask, "an FP16_ABGR " + Describe(0x9u, halves) + " into a tiled 4_4_4_4 unorm target");
|
||||
std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws merged into a %ux%u tiled target, the pixels outside the scissor kept\n", cases.size() + 1u, Width, Height);
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -217,6 +217,9 @@ private:
|
||||
append(key, value.positionLocation);
|
||||
append(key, value.sampleLocations);
|
||||
append(key, value.targetOutputMode);
|
||||
append(key, value.targetMerge.target);
|
||||
append(key, value.targetMerge.mask);
|
||||
append(key, value.targetMerge.samples);
|
||||
}
|
||||
|
||||
static void append(std::vector<std::uint64_t>& key, const ShaderVertexResourceDestination& value) {
|
||||
|
||||
@@ -355,6 +355,10 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) {
|
||||
writer.WriteU8(static_cast<std::uint8_t>(info.positionLocation));
|
||||
for (const std::uint8_t location : info.sampleLocations) writer.WriteU8(location);
|
||||
writer.WriteBool(info.stencilOperationValueExport);
|
||||
writer.WriteU8(info.targetMerge.target);
|
||||
writer.WriteU32(info.targetMerge.mask);
|
||||
writer.WriteU8(info.targetMerge.samples);
|
||||
for (const std::uint32_t word : info.targetMerge.descriptor) writer.WriteU32(word);
|
||||
}
|
||||
|
||||
ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) {
|
||||
@@ -423,6 +427,14 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) {
|
||||
for (std::uint8_t& location : info.sampleLocations) location = reader.ReadU8();
|
||||
}
|
||||
if (version >= 37u) info.stencilOperationValueExport = reader.ReadBool();
|
||||
if (version >= 40u) {
|
||||
info.targetMerge.target = reader.ReadU8();
|
||||
const auto mask = reader.ReadU32();
|
||||
if (mask > 0xffffu) throw std::runtime_error("invalid recompile request target merge mask");
|
||||
info.targetMerge.mask = static_cast<std::uint16_t>(mask);
|
||||
info.targetMerge.samples = reader.ReadU8();
|
||||
for (std::uint32_t& word : info.targetMerge.descriptor) word = reader.ReadU32();
|
||||
}
|
||||
if (version < 5u) {
|
||||
const auto input = [](PixelInput value, bool present) { return present ? PixelInputBit(value) : 0u; };
|
||||
info.inputAddr = input(PixelInput::PerspectiveSample, info.hasPerspectiveCenterVgpr && inputAddrOrCenterVgpr == 2u) | input(PixelInput::PerspectiveCenter, info.hasPerspectiveCenterVgpr) |
|
||||
@@ -827,7 +839,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const
|
||||
std::string buffer;
|
||||
Writer writer(buffer);
|
||||
writer.WriteU32(0x41505335u);
|
||||
writer.WriteU32(39u);
|
||||
writer.WriteU32(40u);
|
||||
writeShaderBinary(writer, request.shader);
|
||||
writeGuestContext(writer, request.context);
|
||||
writeSpirvTarget(writer, request.target);
|
||||
@@ -866,7 +878,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const
|
||||
Reader reader(decoded);
|
||||
if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature");
|
||||
const auto version = reader.ReadU32();
|
||||
if (version < 1u || version > 39u) throw std::runtime_error("unsupported recompile request serialization version");
|
||||
if (version < 1u || version > 40u) throw std::runtime_error("unsupported recompile request serialization version");
|
||||
DeserializedRequest result{};
|
||||
result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader);
|
||||
result.request.context = readGuestContext(reader, result, version);
|
||||
|
||||
+1
@@ -104,6 +104,7 @@ struct ImageResource {
|
||||
std::uint32_t emulatedCompare = 0;
|
||||
std::uint32_t table = NoTable;
|
||||
TableOperation tableOperation = TableOperation::Supported;
|
||||
bool targetMerge = false;
|
||||
|
||||
bool operator==(const ImageResource& other) const = default;
|
||||
};
|
||||
|
||||
@@ -195,6 +195,9 @@ struct ShaderPixelInputInfo {
|
||||
bool psIteratesSamples = false;
|
||||
PixelPositionLocation psPositionLocation = PixelPositionLocation::Center;
|
||||
std::array<std::uint8_t, 32> sampleLocations{};
|
||||
std::uint8_t mergeTarget = 0;
|
||||
std::uint16_t mergeMask = 0;
|
||||
std::uint8_t mergeSamples = 1;
|
||||
ShaderStageRuntime stage;
|
||||
|
||||
bool HasPositionInput() const {
|
||||
|
||||
@@ -1831,6 +1831,7 @@ std::shared_ptr<const ResolvedImageTables> ResourceMaterializer::ResolveImageTab
|
||||
}
|
||||
|
||||
std::vector<ImageResource> ResourceMaterializer::RuntimeImageModes(const ImageResource& image) {
|
||||
if (image.targetMerge) return {image};
|
||||
const bool table = image.table != NoTable;
|
||||
if (table && image.tableOperation == TableOperation::Unsupported) return {};
|
||||
std::vector<ImageResource> modes;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "Optimization/ShaderInfoCollector.hpp"
|
||||
#include "Optimization/ResourceMaterializer.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
@@ -519,6 +520,24 @@ std::uint16_t SamplerUseOf(IrOpcode opcode, std::uint32_t flags, IrShaderStage s
|
||||
return static_cast<std::uint16_t>(use);
|
||||
}
|
||||
|
||||
void AddTargetMerge(const ShaderPixelInputInfo& pixel, ShaderInfo& info) {
|
||||
if (info.runtimeImageModes.size() != info.images.size() || std::ranges::any_of(info.images, [](const ImageResource& image) { return image.targetMerge; })) {
|
||||
return Fail("color target merge image cannot join the prepared images");
|
||||
}
|
||||
ImageResource image;
|
||||
image.source = std::numeric_limits<std::uint32_t>::max();
|
||||
image.resourceClass = ImageResourceClass::Storage;
|
||||
image.numericClass = IrTextureNumericClass::Uint;
|
||||
image.dimension = RdnaImageDimension::Dim2D;
|
||||
image.read = true;
|
||||
image.written = true;
|
||||
image.fmaskCompatible = false;
|
||||
image.targetMerge = true;
|
||||
info.images.push_back(image);
|
||||
info.runtimeImageModes.push_back(ResourceMaterializer::RuntimeImageModes(image));
|
||||
AddInput(info, StageInputKind::FragCoord, 0, 4, "gl_FragCoord");
|
||||
}
|
||||
|
||||
void CollectSamplerUses(const IrProgram& program, ShaderInfo& info) {
|
||||
for (auto& sampler : info.samplers) {
|
||||
sampler.uses = 0;
|
||||
@@ -588,6 +607,7 @@ void ShaderInfoCollector::Collect(IrProgram& program, const ShaderStageInputInfo
|
||||
}
|
||||
CollectBuiltinInputs(program, next);
|
||||
CollectOutputs(program, inputInfo, next);
|
||||
if (program.Resources().stage == IrShaderStage::Pixel && inputInfo.pixel->mergeMask != 0u) AddTargetMerge(*inputInfo.pixel, next);
|
||||
CollectSamplerUses(program, next);
|
||||
program.Info() = std::move(next);
|
||||
program.Metadata().shaderInfoComplete = true;
|
||||
|
||||
@@ -959,7 +959,24 @@ std::shared_ptr<const SpecializedModule> specializeModule(const CompiledShaderAr
|
||||
return entry->module;
|
||||
}
|
||||
|
||||
RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot, bool registrationDefaults = false) {
|
||||
const ResourceSnapshot& mergeTargetImage(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot, bool registrationDefaults, ResourceSnapshot& merged) {
|
||||
const auto& images = variant.info.info.images;
|
||||
const auto image = std::ranges::find_if(images, [](const ImageResource& candidate) { return candidate.targetMerge; });
|
||||
if (image == images.end() || registrationDefaults) return snapshot;
|
||||
if (!request.context.pixel || request.context.pixel->targetMerge.mask == 0u) throw std::runtime_error("ShaderRecompiler: a color target merge variant was materialized without the merged target");
|
||||
if (image + 1 != images.end() || snapshot.images.size() + 1u != images.size()) throw std::runtime_error("ShaderRecompiler: the captured images do not precede the color target merge image");
|
||||
merged = snapshot;
|
||||
DescriptorValue descriptor;
|
||||
std::ranges::copy(request.context.pixel->targetMerge.descriptor, descriptor.dwords.begin());
|
||||
descriptor.dwordCount = 8u;
|
||||
descriptor.availableDwordMask = 0xffu;
|
||||
merged.images.push_back(descriptor);
|
||||
return merged;
|
||||
}
|
||||
|
||||
RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& captured, bool registrationDefaults = false) {
|
||||
ResourceSnapshot merged;
|
||||
const auto& snapshot = mergeTargetImage(variant, request, captured, registrationDefaults, merged);
|
||||
RecompileResult result;
|
||||
const auto& artifact = variant.artifact;
|
||||
static_cast<CompiledShaderArtifact&>(result) = {
|
||||
@@ -1371,6 +1388,11 @@ std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapsh
|
||||
mix(request.context.pixel->reversedBlend);
|
||||
mix(static_cast<std::uint64_t>(request.context.pixel->rasterPrimitive));
|
||||
mix(request.context.pixel->layerExported);
|
||||
const auto& merge = request.context.pixel->targetMerge;
|
||||
mix(merge.target);
|
||||
mix(merge.mask);
|
||||
mix(merge.samples);
|
||||
for (const auto word : merge.descriptor) mix(word);
|
||||
}
|
||||
if (request.context.vertex) {
|
||||
const auto& vertex = *request.context.vertex;
|
||||
|
||||
@@ -151,6 +151,15 @@ inline constexpr std::uint32_t One = 3u;
|
||||
|
||||
}
|
||||
|
||||
struct TargetMerge {
|
||||
std::uint8_t target = 0;
|
||||
std::uint16_t mask = 0;
|
||||
std::uint8_t samples = 1;
|
||||
std::array<std::uint32_t, 8> descriptor{};
|
||||
|
||||
bool operator==(const TargetMerge&) const = default;
|
||||
};
|
||||
|
||||
struct ShaderPixelStageInfo {
|
||||
std::uint32_t interpolatorCount;
|
||||
std::array<std::uint32_t, 32> interpolatorSettings;
|
||||
@@ -191,6 +200,7 @@ struct ShaderPixelStageInfo {
|
||||
bool iteratesSamples = false;
|
||||
PixelPositionLocation positionLocation = PixelPositionLocation::Center;
|
||||
std::array<std::uint8_t, 32> sampleLocations{};
|
||||
TargetMerge targetMerge{};
|
||||
};
|
||||
|
||||
struct ShaderVertexBufferResource {
|
||||
|
||||
@@ -140,7 +140,7 @@ constexpr std::size_t fieldCount() {
|
||||
|
||||
static_assert(fieldCount<ShaderInfo>() == 23, "ShaderInfo changed: update the info encoder");
|
||||
static_assert(fieldCount<BufferResource>() == 12, "BufferResource changed: update the info encoder");
|
||||
static_assert(fieldCount<ImageResource>() == 40, "ImageResource changed: update the info encoder");
|
||||
static_assert(fieldCount<ImageResource>() == 41, "ImageResource changed: update the info encoder");
|
||||
static_assert(fieldCount<SamplerResource>() == 8, "SamplerResource changed: update the info encoder");
|
||||
static_assert(fieldCount<SampledResourcePair>() == 3, "SampledResourcePair changed: update the info encoder");
|
||||
static_assert(fieldCount<StageInput>() == 5, "StageInput changed: update the info encoder");
|
||||
@@ -751,6 +751,7 @@ void encodeShaderInfo(Writer& writer, const ShaderInfo& info) {
|
||||
out.Value(image.emulatedCompare);
|
||||
out.Value(image.table);
|
||||
out.Value(image.tableOperation);
|
||||
out.Value(image.targetMerge);
|
||||
});
|
||||
writer.Value(!info.runtimeImageModes.empty());
|
||||
writer.List(info.samplers, [](Writer& out, const SamplerResource& sampler) {
|
||||
@@ -855,6 +856,7 @@ void decodeShaderInfo(Reader& reader, ShaderInfo& info) {
|
||||
in.Value(image.emulatedCompare);
|
||||
in.Value(image.table);
|
||||
in.Value(image.tableOperation);
|
||||
in.Value(image.targetMerge);
|
||||
});
|
||||
bool preparedImageModes = false;
|
||||
reader.Value(preparedImageModes);
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
namespace ShaderRecompiler::ShaderDiskCache {
|
||||
|
||||
inline constexpr std::uint32_t FormatVersion = 41;
|
||||
inline constexpr std::uint32_t FormatVersion = 42;
|
||||
|
||||
enum class LoadStatus {
|
||||
Loaded,
|
||||
|
||||
@@ -70,6 +70,7 @@ void DecorateTableNonUniform(SpirvEmitterState& state, std::uint32_t id);
|
||||
bool TablePointSampled(const ImageResource& image);
|
||||
bool TableModeEmitted(const ImageResource& mode);
|
||||
std::uint32_t StorageImageDescriptorPointer(SpirvEmitterState& state, std::uint32_t resource);
|
||||
std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord);
|
||||
void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t mipLod, std::uint32_t coord, std::uint32_t texel, std::uint32_t sample);
|
||||
std::uint32_t ExecutionModelForStage(IrShaderStage stage);
|
||||
std::uint32_t ConstantI32(SpirvEmitterState& state, std::int32_t value);
|
||||
|
||||
+1
@@ -21,6 +21,7 @@ void DecorateTableNonUniform(SpirvEmitterState& state, std::uint32_t id);
|
||||
bool TablePointSampled(const ImageResource& image);
|
||||
bool TableModeEmitted(const ImageResource& mode);
|
||||
std::uint32_t StorageImageDescriptorPointer(SpirvEmitterState& state, std::uint32_t resource);
|
||||
std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord);
|
||||
void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t mipLod, std::uint32_t coord, std::uint32_t texel, std::uint32_t sample);
|
||||
const RdnaImageDimensionInfo& RdnaImageDimensionInfoFor(RdnaImageDimension dimension);
|
||||
[[noreturn]] void ExitDescriptorBindingFailure(const SpirvEmitterState& state, DescriptorBindingKind kind, std::uint32_t resource, const char* reason);
|
||||
|
||||
@@ -233,6 +233,22 @@ std::uint32_t StorageImageDescriptorPointer(SpirvEmitterState& state, std::uint3
|
||||
return DescriptorElementPointer(state, pointerType, variable, arrayIndex, kind, resource, "storage image descriptor array was not emitted");
|
||||
}
|
||||
|
||||
std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord) {
|
||||
const auto& image = state.program.Info().images.at(resource);
|
||||
if (image.resourceClass != ImageResourceClass::Storage || image.table != NoTable || image.mipMode == ImageMipMode::DynamicStorage || image.atomic) {
|
||||
FailEmit("storage image read requested for an image that is not one plain storage descriptor");
|
||||
}
|
||||
if (std::ranges::find(state.supportedCapabilities, spv::CapabilityStorageImageReadWithoutFormat) == state.supportedCapabilities.end()) FailEmit("storage image reads without a format are unavailable on the target device");
|
||||
state.module.EmitCapability(spv::CapabilityStorageImageReadWithoutFormat);
|
||||
const auto imageType = ImageType(state, image);
|
||||
const auto descriptor = state.module.AllocateId();
|
||||
state.module.AddFunction(spv::OpLoad, imageType, descriptor, StorageImageDescriptorPointer(state, resource));
|
||||
const auto texel = state.module.AllocateId();
|
||||
const auto texelType = ImageVectorType(state, image.numericClass, 4u);
|
||||
state.module.AddFunction(spv::OpImageRead, texelType, texel, descriptor, coord);
|
||||
return texel;
|
||||
}
|
||||
|
||||
void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t mipLod, std::uint32_t coord, std::uint32_t texel, std::uint32_t sample) {
|
||||
const auto& image = state.runtimeImage != nullptr ? *state.runtimeImage : state.program.Info().images.at(resource);
|
||||
if (image.resourceClass != ImageResourceClass::Storage) {
|
||||
|
||||
@@ -150,7 +150,7 @@ namespace ShaderRecompiler
|
||||
}
|
||||
|
||||
bool OrderedPixelShader(const SpirvEmitterState& state) {
|
||||
return state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->psOrderedPixelShader;
|
||||
return state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && (state.inputInfo.pixel->psOrderedPixelShader || state.inputInfo.pixel->mergeMask != 0u);
|
||||
}
|
||||
|
||||
const SpirvInputBinding* SpirvInputBindingForParameter(const SpirvEmitterState& state, std::uint32_t location) {
|
||||
|
||||
@@ -383,12 +383,16 @@ void DefineModule(SpirvEmitterState& state) {
|
||||
state.module.EmitExtension("SPV_EXT_shader_stencil_export");
|
||||
state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeStencilRefReplacingEXT);
|
||||
}
|
||||
if (pixel.psEarlyZ && !pixel.psAlphaToCoverage && !pixel.QuadMasked()) {
|
||||
const bool merge = pixel.mergeMask != 0u;
|
||||
if (merge && (state.depthVariable != 0 || state.stencilRefVariable != 0 || pixel.psDepthExportEnable || pixel.psSampleMaskExportEnable || pixel.psStencilRefExportEnable || pixel.psAlphaToCoverage || pixel.QuadMasked() || (pixel.psPixelKillEnable && !pixel.psEarlyZ))) {
|
||||
throw std::runtime_error("a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that kills pixels under late tests, exports depth, stencil or coverage, or runs with a quad sample mask cannot use");
|
||||
}
|
||||
if ((pixel.psEarlyZ && !pixel.psAlphaToCoverage && !pixel.QuadMasked()) || merge) {
|
||||
state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests);
|
||||
}
|
||||
if (pixel.psOrderedPixelShader) {
|
||||
if (pixel.psOrderedPixelShader || merge) {
|
||||
if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast<std::uint32_t>(spv::CapabilityFragmentShaderPixelInterlockEXT)) == state.supportedCapabilities.end()) {
|
||||
throw std::runtime_error("a primitive-ordered pixel shader needs the fragmentShaderPixelInterlock feature, which the device lacks");
|
||||
throw std::runtime_error(merge ? "a partial write of a packed color target needs the fragmentShaderPixelInterlock feature, which the device lacks" : "a primitive-ordered pixel shader needs the fragmentShaderPixelInterlock feature, which the device lacks");
|
||||
}
|
||||
state.module.EmitCapability(spv::CapabilityFragmentShaderPixelInterlockEXT);
|
||||
state.module.EmitExtension("SPV_EXT_fragment_shader_interlock");
|
||||
|
||||
@@ -414,6 +414,28 @@ std::uint32_t SelectExportPacking(SpirvEmitterState& state, std::uint32_t target
|
||||
return result;
|
||||
}
|
||||
|
||||
void MergeColorTarget(SpirvEmitterState& state, std::uint32_t color) {
|
||||
const auto& pixel = PixelInfo(state);
|
||||
const auto& images = state.program.Info().images;
|
||||
const auto image = std::ranges::find_if(images, [](const ImageResource& candidate) { return candidate.targetMerge; });
|
||||
if (image == images.end()) FailEmit("the color target merge image was not collected");
|
||||
if (InputVariableForKind(state, StageInputKind::FragCoord) == 0u) FailEmit("the color target merge inputs were not declared");
|
||||
const auto resource = static_cast<std::uint32_t>(image - images.begin());
|
||||
const auto position = [&](std::uint32_t component) { return Unary(state, spv::OpConvertFToU, TypeU32(state), Unary(state, spv::OpBitcast, TypeF32(state), EmitBuiltinU32(state, StageInputKind::FragCoord, component))); };
|
||||
const auto x = position(0u);
|
||||
const auto y = position(1u);
|
||||
const auto coord = state.module.AllocateId();
|
||||
state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 2u), coord, x, y);
|
||||
const auto stored = state.module.AllocateId();
|
||||
state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), stored, EmitStorageImageRead(state, resource, coord), 0u);
|
||||
const auto word = PackExport(state, IrBufferFormat::Format4_4_4_4UNorm, color);
|
||||
const auto kept = EmitBinaryU32(state, spv::OpBitwiseAnd, stored, ConstantU32(state, ~static_cast<std::uint32_t>(pixel.mergeMask)));
|
||||
const auto merged = EmitOrU32(state, kept, EmitBinaryU32(state, spv::OpBitwiseAnd, word, ConstantU32(state, pixel.mergeMask)));
|
||||
const auto texel = state.module.AllocateId();
|
||||
state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4u), texel, merged, ConstantU32(state, 0u), ConstantU32(state, 0u), ConstantU32(state, 0u));
|
||||
EmitStorageImageWrite(state, resource, 0u, coord, texel, 0u);
|
||||
}
|
||||
|
||||
bool AlphaToCoverage(const SpirvEmitterState& state) {
|
||||
return state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->psAlphaToCoverage;
|
||||
}
|
||||
@@ -1847,6 +1869,10 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) {
|
||||
value = mapped;
|
||||
if (exp.index == 0u && !uintOutput && AlphaToCoverage(state)) StoreAlphaCoverage(state, value);
|
||||
const auto mode = MrtOutputMode(state, exp);
|
||||
if (PixelInfo(state).mergeMask != 0u && exp.index == PixelInfo(state).mergeTarget) {
|
||||
if (uintOutput || mode == 5u || mode == 6u) FailEmit("a partial write of a packed color target cannot take an integer or 16-bit normalized export");
|
||||
MergeColorTarget(state, value);
|
||||
}
|
||||
if (!uintOutput && mode != 5u && mode != 6u) value = SelectExportPacking(state, exp.index, value);
|
||||
}
|
||||
if (exp.kind == ExportTargetKind::Position) {
|
||||
|
||||
@@ -155,6 +155,10 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues
|
||||
pixelStorage.psIteratesSamples = pixel.iteratesSamples;
|
||||
pixelStorage.psPositionLocation = pixel.positionLocation;
|
||||
pixelStorage.sampleLocations = pixel.sampleLocations;
|
||||
if (pixel.targetMerge.mask != 0u && (pixel.targetMerge.target >= 8u || pixel.targetMerge.samples != 1u)) throw std::runtime_error("ShaderInputInfoBuilder: invalid color target merge");
|
||||
pixelStorage.mergeTarget = pixel.targetMerge.target;
|
||||
pixelStorage.mergeMask = pixel.targetMerge.mask;
|
||||
pixelStorage.mergeSamples = pixel.targetMerge.samples;
|
||||
ShaderStageInputInfo result;
|
||||
result.pixel = &pixelStorage;
|
||||
return result;
|
||||
|
||||
@@ -54,7 +54,8 @@ void InsertMeshBarycentricFlag(std::vector<std::uint8_t>& bytes) {
|
||||
|
||||
void InsertPixelDefaultsAfterFrontFace(std::vector<std::uint8_t>& bytes) {
|
||||
bytes.insert(bytes.begin() + 261u, {0x0fu, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0u, 0u, 0u, 0u});
|
||||
bytes.insert(bytes.begin() + 282u, 35u, 0u);
|
||||
bytes.insert(bytes.begin() + 282u, 73u, 0u);
|
||||
bytes[322] = 1u;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> DecodeBase64(std::string_view text) {
|
||||
@@ -214,7 +215,7 @@ int main() {
|
||||
Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u);
|
||||
Require(request.target.float16DenormPreserve == preserve);
|
||||
auto upgraded = bytes;
|
||||
upgraded[4] = 39u;
|
||||
upgraded[4] = 40u;
|
||||
InsertMeshBarycentricFlag(upgraded);
|
||||
if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u});
|
||||
if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded);
|
||||
@@ -254,7 +255,7 @@ int main() {
|
||||
layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data";
|
||||
}
|
||||
Require(layerRejected);
|
||||
layered[4] = 39u;
|
||||
layered[4] = 40u;
|
||||
InsertMeshBarycentricFlag(layered);
|
||||
layered.insert(layered.begin() + 259u, {0u, 0u});
|
||||
InsertPixelDefaultsAfterFrontFace(layered);
|
||||
@@ -280,7 +281,7 @@ int main() {
|
||||
Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle);
|
||||
Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u);
|
||||
Require(replay.request.target.float16DenormPreserve == preserve);
|
||||
bytes[4] = 39u;
|
||||
bytes[4] = 40u;
|
||||
InsertMeshBarycentricFlag(bytes);
|
||||
bytes.insert(bytes.begin() + 259u, {0u, 0u});
|
||||
InsertPixelDefaultsAfterFrontFace(bytes);
|
||||
@@ -410,7 +411,8 @@ int main() {
|
||||
if (fixture.pixel) main38.insert(main38.begin() + 259u, {0u, 0u});
|
||||
if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(main38);
|
||||
auto upgraded = main38;
|
||||
upgraded[4] = 39u;
|
||||
if (fixture.pixel) main38.erase(main38.begin() + 317u, main38.begin() + 355u);
|
||||
upgraded[4] = 40u;
|
||||
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, 0u);
|
||||
@@ -429,7 +431,7 @@ int main() {
|
||||
const auto replay25 = serializer.Deserialize(EncodeBase64(main25));
|
||||
Require(!fixture.pixel || (replay25.request.context.pixel->layerExported == layer && replay25.request.context.pixel->ditheredAlphaToCoverage == dither && replay25.request.context.pixel->reversedBlend == 0u));
|
||||
Require(replay25.request.target.float16DenormPreserve == preserve && (!fixture.tessellation || replay25.request.graphics->tessellation->primitiveGroupSize == 37u));
|
||||
main25[4] = 39u;
|
||||
main25[4] = 40u;
|
||||
InsertMeshBarycentricFlag(main25);
|
||||
if (fixture.pixel) {
|
||||
main25.insert(main25.begin() + 260u, 0u);
|
||||
@@ -451,9 +453,9 @@ int main() {
|
||||
const auto replay = serializer.Deserialize(encoded);
|
||||
Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve);
|
||||
auto bytes = DecodeBase64(encoded);
|
||||
Require(bytes.size() >= 8u && bytes[4] == 39u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u);
|
||||
Require(bytes.size() >= 8u && bytes[4] == 40u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u);
|
||||
auto future = bytes;
|
||||
future[4] = 40u;
|
||||
future[4] = 41u;
|
||||
bool futureRejected = false;
|
||||
try {
|
||||
static_cast<void>(serializer.Deserialize(EncodeBase64(future)));
|
||||
@@ -498,7 +500,7 @@ int main() {
|
||||
auto version35 = DecodeBase64(serializer.Serialize(request));
|
||||
const auto stencilOperationValue = std::search(version35.begin(), version35.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size();
|
||||
Require(stencilOperationValue < version35.end() && *stencilOperationValue == 0u);
|
||||
version35.erase(stencilOperationValue);
|
||||
version35.erase(stencilOperationValue, stencilOperationValue + 1 + 38);
|
||||
version35[4] = 35u;
|
||||
version35.pop_back();
|
||||
const auto replay35 = serializer.Deserialize(EncodeBase64(version35));
|
||||
|
||||
Reference in new issue
Block a user