render: use native clears for stencil fill shaders

This commit is contained in:
nmzik
2026-09-05 12:36:13 +04:00
parent 6a187ac67e
commit bec7c510c2
10 changed files with 457 additions and 115 deletions
+26 -14
View File
@@ -1408,24 +1408,40 @@ bool TextureCache::ClearImageFromBuffer(CommandBuffer& command, uint64_t address
if (!selected) {
return false;
}
auto& image = m_slot_images[selected];
vk::ClearColorValue color_clear {};
float depth_clear = 0.0f;
uint8_t stencil_clear = 0;
auto& image = m_slot_images[selected];
vk::ClearValue clear {};
if (aspect == vk::ImageAspectFlagBits::eColor) {
if (!DecodePackedColorClear(image.info.pixel_format, packed_clear, color_clear)) {
if (!DecodePackedColorClear(image.info.pixel_format, packed_clear, clear.color)) {
return false;
}
} else {
uint8_t stencil_clear = 0;
if ((aspect == vk::ImageAspectFlagBits::eDepth &&
!DecodePackedDepthClear(image.info.pixel_format, packed_clear, depth_clear)) ||
!DecodePackedDepthClear(image.info.pixel_format, packed_clear, clear.depthStencil.depth)) ||
(aspect == vk::ImageAspectFlagBits::eStencil &&
!DecodePackedStencilClear(packed_clear, stencil_clear))) {
return false;
}
clear.depthStencil.stencil = stencil_clear;
}
ClearImage(command, selected, {aspect, 0, image.info.resources.levels, 0, image.backing.layers},
clear);
return true;
}
void TextureCache::ClearImage(CommandBuffer& command, ImageId id,
const vk::ImageSubresourceRange& range, const vk::ClearValue& clear) {
auto& image = m_slot_images[id];
const auto aspects = image.info.IsDepth() ? ImageViewOps::DepthAspectMask(image.backing.format)
: vk::ImageAspectFlagBits::eColor;
EXIT_IF(command.IsInvalid() || image.depth_id || !range.aspectMask ||
range.levelCount == 0 || range.baseMipLevel >= image.info.resources.levels ||
range.levelCount > image.info.resources.levels - range.baseMipLevel ||
range.layerCount == 0 || range.baseArrayLayer >= image.backing.layers ||
range.layerCount > image.backing.layers - range.baseArrayLayer ||
(range.aspectMask & aspects) != range.aspectMask);
if (image.IsBufferModified() || image.IsCpuDirty()) {
InitializeImage(selected);
InitializeImage(id);
if (image.info.samples == 1 && (image.IsBufferModified() || image.IsCpuDirty())) {
EXIT("TextureCache: image clear retained guest ownership\n");
}
@@ -1433,18 +1449,14 @@ bool TextureCache::ClearImageFromBuffer(CommandBuffer& command, uint64_t address
command.EndRendering();
image.Transit(vk::ImageLayout::eTransferDstOptimal, vk::AccessFlagBits2::eTransferWrite, {},
command.Handle());
const vk::ImageSubresourceRange range {aspect, 0, VK_REMAINING_MIP_LEVELS, 0,
image.backing.layers};
if (aspect == vk::ImageAspectFlagBits::eColor) {
if (range.aspectMask == vk::ImageAspectFlagBits::eColor) {
command.Handle().clearColorImage(image.backing.image, vk::ImageLayout::eTransferDstOptimal,
&color_clear, 1, &range);
&clear.color, 1, &range);
} else {
const vk::ClearDepthStencilValue clear {depth_clear, stencil_clear};
command.Handle().clearDepthStencilImage(
image.backing.image, vk::ImageLayout::eTransferDstOptimal, &clear, 1, &range);
image.backing.image, vk::ImageLayout::eTransferDstOptimal, &clear.depthStencil, 1, &range);
}
CommitGpuWrite(image);
return true;
}
void TextureCache::InvalidateMemory(uint64_t address, uint64_t size) {
+3
View File
@@ -147,6 +147,9 @@ private:
uint64_t destination_size, DownloadPlan plan);
void DownloadDepth(Image& image, Buffer& destination, uint64_t destination_offset);
void CommitGpuWrite(Image& image);
// Caller holds m_lock. Clears only the selected aspects and subresources.
void ClearImage(CommandBuffer& command, ImageId id, const vk::ImageSubresourceRange& range,
const vk::ClearValue& clear);
void PrepareImageCopy(Image& image);
void RefreshCopySource(ImageId id);
[[nodiscard]] bool CopyD16(Image& destination, Image& source);
+3
View File
@@ -204,6 +204,9 @@ private:
void ResetBindings();
[[nodiscard]] bool TryConsumeComputeMetaClear(const ShaderComputeInputInfo& input,
const CommandBuffer& buffer);
[[nodiscard]] bool TryConsumeComputeImageClear(const ShaderComputeInputInfo& input,
CommandBuffer& command, uint32_t group_x,
uint32_t group_y, uint32_t group_z, uint32_t mode);
RenderContext& m_context;
std::vector<ImageId> m_bound_images;
@@ -30,6 +30,7 @@
#include <cmath>
#include <cstring>
#include <limits>
#include <mutex>
#include <span>
#include <unordered_map>
#include <vector>
@@ -44,6 +45,19 @@ static uint64_t BufferDescriptorSize(const ShaderBufferResource& descriptor) {
return stride == 0 ? records : records * stride;
}
static bool FillSourcesDisjoint(std::span<const ShaderRecompiler::IR::DescriptorValue> sources,
GuestRange destination, uint32_t output_buffer = UINT32_MAX) {
for (uint32_t i = 0; i < sources.size(); ++i) {
if (i == output_buffer) continue;
const auto source = DecodeNativeDescriptor<ShaderBufferResource>(sources[i]);
const auto bytes = BufferDescriptorSize(source);
if (source.Base48() > UINT64_MAX - bytes ||
(source.Base48() < destination.End() &&
destination.address < source.Base48() + bytes)) return false;
}
return true;
}
bool RenderExecutor::TryConsumeComputeMetaClear(const ShaderComputeInputInfo& input,
const CommandBuffer& buffer) {
const auto& program = *input.stage.program;
@@ -83,27 +97,28 @@ bool ResolveComputeImageClear(const ShaderComputeInputInfo& input, uint32_t grou
uint64_t& resolved_size) {
const auto& program = *input.stage.program;
const auto& resources = input.stage.resources;
const auto& fill = resources.buffer_fill;
if (fill.element_size == 0 || fill.element_size > 16 || fill.element_size % 4 != 0 ||
fill.buffer >= resources.buffers.size() ||
const auto& fill = resources.uniform_fill;
if (fill.kind != ShaderRecompiler::IR::UniformFillKind::Buffer || fill.words == 0 ||
fill.words > 4 || fill.resource >= resources.buffers.size() ||
resources.buffers.size() != program.info.buffers.size()) {
return false;
}
const auto& resource = program.info.buffers[fill.buffer];
const auto& raw = resources.buffers[fill.buffer];
const auto element_size = fill.words * sizeof(uint32_t);
const auto& resource = program.info.buffers[fill.resource];
const auto& raw = resources.buffers[fill.resource];
const auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(raw);
constexpr std::array formats {
Prospero::BufferFormat::k32UInt, Prospero::BufferFormat::k32_32UInt,
Prospero::BufferFormat::k32_32_32UInt, Prospero::BufferFormat::k32_32_32_32UInt};
if (!resource.formatted || !resource.written || resource.read || resource.atomic ||
resource.scalar || descriptor.Stride() != fill.element_size ||
descriptor.Format() != formats[fill.element_size / 4 - 1] || descriptor.SwizzleEnabled() ||
resource.scalar || descriptor.Stride() != element_size ||
descriptor.Format() != formats[fill.words - 1] || descriptor.SwizzleEnabled() ||
descriptor.IndexStride() != 0 || descriptor.AddTid() ||
resource.packed_stride != descriptor.PackedStride() || raw.dword_count != 4 ||
descriptor.Base48() == 0) {
return false;
}
if (input.threads_num[0] == 0 || input.threads_num[0] != fill.group_stride ||
if (input.threads_num[0] == 0 || input.threads_num[0] != fill.group_stride[0] ||
input.threads_num[1] != 1 || input.threads_num[2] != 1 || group_x == 0 || group_y != 1 ||
group_z != 1 || mode != (input.dispatch_thread_dimensions ? 0x61u : 0x41u)) {
return false;
@@ -119,25 +134,71 @@ bool ResolveComputeImageClear(const ShaderComputeInputInfo& input, uint32_t grou
input.dispatch_threads_num[1] != 1 || input.dispatch_threads_num[2] != 1))) {
return false;
}
for (uint32_t i = 0; i < resources.buffers.size(); ++i) {
if (i == fill.buffer) continue;
const auto source = DecodeNativeDescriptor<ShaderBufferResource>(resources.buffers[i]);
const auto bytes = BufferDescriptorSize(source);
if (source.Base48() > UINT64_MAX - bytes ||
(source.Base48() < descriptor.Base48() + size &&
descriptor.Base48() < source.Base48() + bytes)) {
return false;
}
}
if (!FillSourcesDisjoint(resources.buffers, {descriptor.Base48(), size}, fill.resource))
return false;
resolved_descriptor = descriptor;
resolved_clear = fill.value;
resolved_size = size;
return true;
}
static bool TryConsumeComputeImageClear(const ShaderComputeInputInfo& input, CommandBuffer& command,
uint32_t group_x, uint32_t group_y, uint32_t group_z,
uint32_t mode) {
bool RenderExecutor::TryConsumeComputeImageClear(const ShaderComputeInputInfo& input,
CommandBuffer& command, uint32_t group_x,
uint32_t group_y, uint32_t group_z, uint32_t mode) {
const auto& program = *input.stage.program;
const auto& resources = input.stage.resources;
const auto& fill = resources.uniform_fill;
auto& cache = command.GetContext().GetTextureCache();
if (fill.kind == ShaderRecompiler::IR::UniformFillKind::Image) {
if (mode != 0x41u || input.dispatch_thread_dimensions || fill.words != 1 || fill.value > 255 ||
resources.images.size() != 1 || program.info.images.size() != 1 || fill.resource != 0 ||
resources.images[0].dword_count != 8 || input.threads_num[2] != 1 ||
fill.group_stride[2] != 1) return false;
const auto descriptor = DecodeNativeDescriptor<ShaderTextureResource>(resources.images[0]);
const auto& resource = program.info.images[0];
if (descriptor.IsNull() || descriptor.Format() != Prospero::BufferFormat::k8UInt ||
descriptor.Type() != Prospero::ImageType::kColor2DArray ||
descriptor.MetaCompress() || descriptor.WriteCompress() ||
descriptor.BaseLevel() > descriptor.LastLevel() ||
descriptor.BaseLevel() > descriptor.MaxMip() ||
descriptor.BaseArray5() > descriptor.Depth() ||
descriptor.DstSelX() != 4 || (resource.shader_swizzle & 7u) != 4 ||
resource.numeric_class != Prospero::TextureNumericClass::Uint ||
resource.dimension != ShaderRecompiler::Decoder::ImageDimension::Dim2DArray ||
resource.read || resource.atomic || !resource.written ||
resource.mip_mode != ShaderRecompiler::IR::ImageMipMode::None) return false;
const std::array extents {
std::max(1u, (descriptor.Width5() + 1u) >> descriptor.BaseLevel()),
std::max(1u, (descriptor.Height5() + 1u) >> descriptor.BaseLevel()),
descriptor.Depth() - descriptor.BaseArray5() + 1u};
const std::array groups {group_x, group_y, group_z};
for (uint32_t axis = 0; axis < 3; ++axis) {
const uint64_t threads = input.threads_num[axis];
// Guest image writes outside the descriptor dimensions are discarded. Only the
// final workgroup may extend beyond the selected image view.
if (threads == 0 || threads != fill.group_stride[axis] ||
groups[axis] != (extents[axis] + threads - 1) / threads ||
groups[axis] * threads > UINT32_MAX) return false;
}
const auto binding = ResolveTexture(resource, resources.images[0]);
const auto& destination = binding.desc.info.data;
if (!FillSourcesDisjoint(resources.buffers, destination)) return false;
std::scoped_lock lock {cache.m_lock};
const auto& image = cache.GetImage(binding.image_id);
const auto& view = binding.desc.view_info;
if (image.backing.format != vk::Format::eD32SfloatS8Uint || image.info.samples != 1 ||
image.info.stencil != destination || view.base_level >= image.backing.mip_levels ||
view.base_layer >= image.backing.layers || view.layer_count != extents[2] ||
view.layer_count > image.backing.layers - view.base_layer ||
std::max(1u, image.info.extent.width >> view.base_level) != extents[0] ||
std::max(1u, image.info.extent.height >> view.base_level) != extents[1]) return false;
const vk::ImageSubresourceRange range {vk::ImageAspectFlagBits::eStencil, view.base_level,
1, view.base_layer, view.layer_count};
vk::ClearValue clear {};
clear.depthStencil = vk::ClearDepthStencilValue {0.0f, fill.value};
cache.ClearImage(command, binding.image_id, range, clear);
return true;
}
ShaderBufferResource descriptor;
uint32_t packed_clear = 0;
uint64_t size = 0;
@@ -145,7 +206,6 @@ static bool TryConsumeComputeImageClear(const ShaderComputeInputInfo& input, Com
size)) {
return false;
}
auto& cache = command.GetContext().GetTextureCache();
if (!cache.ClearImageFromBuffer(command, descriptor.Base48(), size, packed_clear)) {
// Recognized metadata-fill shaders access DCC as an ordinary storage buffer and may run
// before the render target is bound. TryConsumeDccFill either consumes registered state
@@ -16,13 +16,16 @@ struct DescriptorValue {
}
};
struct BufferFill {
uint32_t buffer = 0;
uint32_t group_stride = 0;
uint32_t element_size = 0;
uint32_t value = 0;
enum class UniformFillKind { None, Buffer, Image };
bool operator==(const BufferFill&) const = default;
struct UniformFill {
UniformFillKind kind = UniformFillKind::None;
uint32_t resource = 0;
std::array<uint32_t, 3> group_stride {};
uint32_t words = 0;
uint32_t value = 0;
bool operator==(const UniformFill&) const = default;
};
struct ResourceSnapshot {
@@ -31,7 +34,7 @@ struct ResourceSnapshot {
std::vector<DescriptorValue> samplers;
std::vector<uint32_t> flattened_srt;
std::vector<uint32_t> user_data;
BufferFill buffer_fill;
UniformFill uniform_fill;
};
} // namespace Libs::Graphics::ShaderRecompiler::IR
+3 -3
View File
@@ -492,8 +492,8 @@ struct CompiledShaderInfo {
BindingLayout bindings;
};
struct BufferFillPlan {
BufferFill fill;
struct UniformFillPlan {
UniformFill fill;
std::array<Value, 4> values;
};
@@ -523,7 +523,7 @@ struct ResourcePlan {
bool srt_plan_complete = false;
bool resource_tracking_complete = false;
ShaderInfo info;
BufferFillPlan buffer_fill;
UniformFillPlan uniform_fill;
};
struct Program: ResourcePlan {
@@ -300,16 +300,16 @@ static bool MaterializeSnapshot(const ResourcePlan& program, const SrtRuntime& r
}
auto& next = snapshot.resources;
const auto& fill = program.buffer_fill;
const auto words = fill.fill.element_size / sizeof(uint32_t);
const auto& fill = program.uniform_fill;
const auto words = fill.fill.words;
std::array<uint32_t, 4> stored {};
if (words != 0 &&
EvaluateUniformValues(program, std::span(fill.values).first(words), runtime,
std::span(stored).first(words)) &&
std::all_of(stored.begin(), stored.begin() + words,
[&](uint32_t value) { return value == stored[0]; })) {
next.buffer_fill = fill.fill;
next.buffer_fill.value = stored[0];
next.uniform_fill = fill.fill;
next.uniform_fill.value = stored[0];
}
auto cursor = values.begin();
next.buffers.assign(cursor, cursor + program.info.buffers.size());
@@ -724,9 +724,10 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program) {
return blocks;
}
// Nonnegative affine coefficients for constant, local X and workgroup X. Reject modular
// Nonnegative affine coefficients for constant, local and workgroup coordinates. Reject modular
// arithmetic that could wrap; runtime coverage also bounds the largest invocation index.
static std::optional<std::array<uint64_t, 3>> FillIndex(Value value, uint32_t depth = 0) {
static std::optional<std::array<uint64_t, 3>> FillIndex(Value value, uint32_t axis,
uint32_t depth = 0) {
value = value.Resolve();
if (depth > 32 || value.GetType() != Type::U32) {
return {};
@@ -739,7 +740,7 @@ static std::optional<std::array<uint64_t, 3>> FillIndex(Value value, uint32_t de
return {};
}
const auto op = inst->GetOpcode();
if (op == ValueOpcode::GetBuiltin && inst->Arg(1) == Value(0u)) {
if (op == ValueOpcode::GetBuiltin && inst->Arg(1) == Value(axis)) {
if (inst->Arg(0) == Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId))) {
return std::array<uint64_t, 3> {0, 1, 0};
}
@@ -751,8 +752,8 @@ static std::optional<std::array<uint64_t, 3>> FillIndex(Value value, uint32_t de
op != ValueOpcode::ShiftLeftLogical32) {
return {};
}
auto left = FillIndex(inst->Arg(0), depth + 1);
auto right = FillIndex(inst->Arg(1), depth + 1);
auto left = FillIndex(inst->Arg(0), axis, depth + 1);
auto right = FillIndex(inst->Arg(1), axis, depth + 1);
if (!left || !right) {
return {};
}
@@ -774,10 +775,10 @@ static std::optional<std::array<uint64_t, 3>> FillIndex(Value value, uint32_t de
return left;
}
static BufferFillPlan AnalyzeBufferFill(const Program& program) {
static UniformFillPlan AnalyzeUniformFill(const Program& program) {
if (program.stage != ShaderType::Compute || program.blocks.empty() ||
program.blocks.size() != program.block_info.size() || program.info.uses_dma ||
!program.info.images.empty() || !program.info.samplers.empty()) {
!program.info.samplers.empty()) {
return {};
}
std::unordered_set<uint32_t> visited;
@@ -788,7 +789,8 @@ static BufferFillPlan AnalyzeBufferFill(const Program& program) {
for (const auto& inst: *program.blocks[index]) {
if (AddressOpcodeInfoOf(inst.GetOpcode()).access != AddressAccess::None) return {};
if (!inst.MayHaveSideEffects()) continue;
if (store != nullptr || BufferAccessOf(inst.GetOpcode()) != BufferAccess::Write)
if (store != nullptr || (BufferAccessOf(inst.GetOpcode()) != BufferAccess::Write &&
inst.GetOpcode() != ValueOpcode::ImageWrite))
return {};
store = &inst;
}
@@ -800,36 +802,64 @@ static BufferFillPlan AnalyzeBufferFill(const Program& program) {
index = static_cast<uint32_t>(next - program.block_info.begin());
}
if (store == nullptr || visited.size() != program.blocks.size()) return {};
const auto op = store->GetOpcode();
constexpr std::array stores {ValueOpcode::StoreBufferU32, ValueOpcode::StoreBufferU32x2,
ValueOpcode::StoreBufferU32x3, ValueOpcode::StoreBufferU32x4};
const auto store_op = std::ranges::find(stores, op);
if (store_op == stores.end() || store->Arg(2).Resolve() != Value(0u) ||
store->Arg(3).Resolve() != Value(0u) || store->Arg(5).Resolve() != Value(true))
return {};
const auto& memory = program.memory_info.at(store->Flags<MemoryFlags>().index);
if (!memory.formatted || memory.typed || !memory.idxen || memory.offen || memory.offset != 0 ||
memory.data_bits != 32 ||
memory.data_dwords != static_cast<uint32_t>(store_op - stores.begin() + 1))
return {};
const auto address = FillIndex(store->Arg(1));
if (!address || (*address)[0] != 0 || (*address)[1] != 1 || (*address)[2] == 0) return {};
for (const auto& buffer: program.info.buffers) {
if (buffer.read && (!buffer.scalar || buffer.written)) return {};
}
BufferFillPlan result;
result.fill = {memory.resource, static_cast<uint32_t>((*address)[2]), memory.data_dwords * 4,
0};
const auto data = store->Arg(4).Resolve();
const auto& memory = program.memory_info.at(store->Flags<MemoryFlags>().index);
UniformFillPlan result;
result.fill.resource = memory.resource;
Value data;
if (store->GetOpcode() == ValueOpcode::ImageWrite) {
if (program.info.images.size() != 1 || memory.dmask != 1 || memory.data_bits != 32 ||
memory.image_has_mip || memory.image_sample_flags != 0 || memory.image_r128 ||
memory.image_dimension != Decoder::ImageDimension::Dim2DArray ||
store->Arg(3).Resolve() != Value(true)) return {};
const auto& image = program.info.images[memory.resource];
if (image.read || image.atomic || image.mip_mode != ImageMipMode::None) return {};
const auto* address = store->Arg(1).ResolveInstruction();
if (address == nullptr || address->GetOpcode() != ValueOpcode::MakeImageAddress) return {};
for (uint32_t axis = 0; axis < 3; ++axis) {
const auto index = FillIndex(address->Arg(axis), axis);
if (!index || (*index)[0] != 0 || (*index)[1] != (axis < 2 ? 1u : 0u) ||
(*index)[2] == 0 || (axis == 2 && (*index)[2] != 1)) return {};
result.fill.group_stride[axis] = static_cast<uint32_t>((*index)[2]);
}
const auto* values = store->Arg(2).ResolveInstruction();
if (values == nullptr || values->GetOpcode() != ValueOpcode::CompositeConstructU32x4)
return {};
result.fill.kind = UniformFillKind::Image;
result.fill.words = 1;
data = values->Arg(0);
} else {
if (!program.info.images.empty()) return {};
const auto op = store->GetOpcode();
constexpr std::array stores {ValueOpcode::StoreBufferU32, ValueOpcode::StoreBufferU32x2,
ValueOpcode::StoreBufferU32x3, ValueOpcode::StoreBufferU32x4};
const auto store_op = std::ranges::find(stores, op);
if (store_op == stores.end() || store->Arg(2).Resolve() != Value(0u) ||
store->Arg(3).Resolve() != Value(0u) || store->Arg(5).Resolve() != Value(true))
return {};
if (!memory.formatted || memory.typed || !memory.idxen || memory.offen || memory.offset != 0 ||
memory.data_bits != 32 ||
memory.data_dwords != static_cast<uint32_t>(store_op - stores.begin() + 1))
return {};
const auto address = FillIndex(store->Arg(1), 0);
if (!address || (*address)[0] != 0 || (*address)[1] != 1 || (*address)[2] == 0) return {};
result.fill.kind = UniformFillKind::Buffer;
result.fill.group_stride[0] = static_cast<uint32_t>((*address)[2]);
result.fill.words = memory.data_dwords;
data = store->Arg(4);
}
data = data.Resolve();
const auto* vector = data.TryInstruction();
constexpr std::array composites {ValueOpcode::CompositeConstructU32x2,
ValueOpcode::CompositeConstructU32x3,
ValueOpcode::CompositeConstructU32x4};
for (uint32_t i = 0; i < memory.data_dwords; ++i) {
if (memory.data_dwords > 1 &&
(vector == nullptr || vector->GetOpcode() != composites[memory.data_dwords - 2]))
for (uint32_t i = 0; i < result.fill.words; ++i) {
if (result.fill.words > 1 &&
(vector == nullptr || vector->GetOpcode() != composites[result.fill.words - 2]))
return {};
const auto word = memory.data_dwords == 1 ? data : vector->Arg(i);
const auto word = result.fill.words == 1 ? data : vector->Arg(i);
if (word.GetType() != Type::U32 || !UniformIntegerValue(program, word)) return {};
result.values[i] = word;
}
@@ -895,9 +925,9 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
for (auto& block: plan.control_flow) {
block.condition = Clone(block.condition);
}
plan.buffer_fill = AnalyzeBufferFill(program);
for (uint32_t i = 0; i < plan.buffer_fill.fill.element_size / sizeof(uint32_t); ++i) {
plan.buffer_fill.values[i] = Clone(plan.buffer_fill.values[i]);
plan.uniform_fill = AnalyzeUniformFill(program);
for (uint32_t i = 0; i < plan.uniform_fill.fill.words; ++i) {
plan.uniform_fill.values[i] = Clone(plan.uniform_fill.values[i]);
}
plan.materialization_sources.reserve(plan.info.buffers.size() + plan.info.images.size() +
plan.info.samplers.size());