shader: sample packed normalized images with one bilinear gather

This commit is contained in:
nmzik
2026-10-12 23:51:22 +02:00
parent c11254a996
commit 56738aa4e1
7 changed files with 225 additions and 24 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ constexpr FormatInfo kFormatInfo[] = {
{BufferFormat::k16_16UInt, 4, 0, 4, true, true},
{BufferFormat::k16_16SInt, 4, 0, 4, true, false, true},
{BufferFormat::k16_16Float, 4, 0, 4, true, false},
{BufferFormat::k11_11_10UNorm, 4, 0, 0, false, false},
{BufferFormat::k11_11_10UNorm, 4, 0, 0, true, false},
{BufferFormat::k11_11_10UInt, 4, 0, 4, true, true},
{BufferFormat::k11_11_10Float, 4, 0, 4, true, false},
{BufferFormat::k10_11_11Float, 4, 0, 4, true, false},
@@ -568,7 +568,8 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
const bool shader_conversion =
surface_format.conversion_format != Prospero::BufferFormat::kInvalid;
const bool sampled_numeric_class =
storage || resource.numeric_class == Prospero::SampledTextureNumericClass(format);
storage || resource.numeric_class ==
Prospero::SampledTextureNumericClass(Prospero::RemapTextureFormat(format));
if (!storage && resource.resource_class == ShaderRecompiler::IR::ImageResourceClass::Sampled &&
!sampled_numeric_class) {
EXIT("sampled image numeric class mismatch: numeric=%u format=%u addr=0x%016" PRIx64 "\n",
@@ -385,12 +385,20 @@ Format::BufferFormatInfo ImageConversionFormat(const EmitterState& state,
info.component_count > 4u);
EXIT_IF(info.type != Format::ComponentType::Uscaled &&
((info.type != Format::ComponentType::Uint &&
!(info.type == Format::ComponentType::Unorm &&
image.resource_class == IR::ImageResourceClass::Storage)) || !info.packed_bitfield ||
info.type != Format::ComponentType::Unorm) || !info.packed_bitfield ||
info.byte_size != sizeof(uint32_t)));
return info;
}
uint32_t ExtractPackedImageComponent(EmitterState& state, const Format::BufferFormatInfo& info,
uint32_t packed, uint32_t type, uint32_t component) {
const auto result = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitFieldUExtract, type, result, packed,
ConstantU32(state, info.component_bit_offset[component]),
ConstantU32(state, info.component_bits[component]));
return result;
}
uint32_t ImageGatherSource(const EmitterState& state, const IR::MemoryInfo& mem) {
const auto component = ImageGatherComponent(mem.dmask);
const auto info = ImageConversionFormat(state, mem);
@@ -414,13 +422,11 @@ uint32_t UnpackImageTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint
}
uint32_t components[4] {};
for (uint32_t component = 0; component < info.component_count; component++) {
components[component] = ctx.state.builder.AllocateId();
if (info.packed_bitfield) {
ctx.state.builder.AddFunction(spv::OpBitFieldUExtract, scalar_type,
components[component], packed,
ConstantU32(ctx.state, info.component_bit_offset[component]),
ConstantU32(ctx.state, info.component_bits[component]));
components[component] =
ExtractPackedImageComponent(ctx.state, info, packed, scalar_type, component);
} else {
components[component] = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpCompositeExtract, scalar_type,
components[component], texel, component);
// UNorm backing preserves the guest's filtering; scaling precedes swizzle constants.
@@ -480,21 +486,74 @@ uint32_t UnpackImageGather(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uin
return Binary(ctx.state, spv::OpFMul, vector_type, gathered, scales);
}
uint32_t values[4] {};
for (uint32_t lane = 0; lane < 4u; lane++) {
const auto packed = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), packed, gathered,
lane);
values[lane] = ctx.state.builder.AllocateId();
const auto physical = (selector - 4u) % info.component_count;
ctx.state.builder.AddFunction(spv::OpBitFieldUExtract, TypeU32(ctx.state), values[lane],
packed,
ConstantU32(ctx.state, info.component_bit_offset[physical]),
ConstantU32(ctx.state, info.component_bits[physical]));
return ExtractPackedImageComponent(ctx.state, info, gathered, TypeU32Vector(ctx.state, 4),
(selector - 4u) % info.component_count);
}
uint32_t SamplePackedUnormLz(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t coord) {
auto& state = ctx.state;
const auto& image = state.program.info.images[mem.resource];
const auto info = ImageConversionFormat(state, mem);
const auto sampled = MakeSampledImage(state, mem.resource,
LoadSamplerDescriptor(state, mem.sampler));
const auto gathered = state.builder.AllocateId();
state.builder.AddFunction(spv::OpImageGather, TypeU32Vector(state, 4), gathered, sampled,
coord, ConstantU32(state, 0u));
state.builder.RequireCapability(spv::CapabilityImageQuery);
const auto size = state.builder.AllocateId();
state.builder.AddFunction(spv::OpImageQuerySizeLod, TypeU32Vector(state, 2), size,
LoadImageDescriptor(state, mem.resource), ConstantU32(state, 0u));
const auto size_f32 = Unary(state, spv::OpConvertUToF, TypeF32Vector(state, 2), size);
const auto half = ConstantF32Value(state, 0.5f);
const auto halves = state.builder.Constant(spv::OpConstantComposite,
TypeF32Vector(state, 2), half, half);
const auto fractions = state.builder.AllocateId();
state.builder.AddFunction(
spv::OpExtInst, TypeF32Vector(state, 2), fractions, GlslStd450(state), GLSLstd450Fract,
Binary(state, spv::OpFSub, TypeF32Vector(state, 2),
Binary(state, spv::OpFMul, TypeF32Vector(state, 2), coord, size_f32), halves));
uint32_t fraction[2];
uint32_t complement[2];
for (uint32_t axis = 0; axis < 2u; axis++) {
fraction[axis] = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeExtract, TypeF32(state), fraction[axis],
fractions, axis);
complement[axis] = Binary(state, spv::OpFSub, TypeF32(state),
ConstantF32Value(state, 1.0f), fraction[axis]);
}
const auto result = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(ctx.state, 4), result,
values[0], values[1], values[2], values[3]);
// Gather order is (x0, y1), (x1, y1), (x1, y0), (x0, y0).
const auto weights = state.builder.AllocateId();
state.builder.AddFunction(
spv::OpCompositeConstruct, TypeF32Vector(state, 4), weights,
Binary(state, spv::OpFMul, TypeF32(state), complement[0], fraction[1]),
Binary(state, spv::OpFMul, TypeF32(state), fraction[0], fraction[1]),
Binary(state, spv::OpFMul, TypeF32(state), fraction[0], complement[1]),
Binary(state, spv::OpFMul, TypeF32(state), complement[0], complement[1]));
uint32_t decoded[4] {};
uint32_t selected[4] {ZeroF32(state), ZeroF32(state), ZeroF32(state), ZeroF32(state)};
for (uint32_t component = 0; component < 4u; component++) {
if ((mem.dmask & (1u << component)) == 0u) continue;
const auto selector = (image.shader_swizzle >> (component * 3u)) & 7u;
if (selector == 1u) {
selected[component] = ConstantF32Value(state, 1.0f);
} else if (selector >= 4u) {
const auto physical = (selector - 4u) % info.component_count;
if (decoded[physical] == 0u) {
const auto texels = Unary(
state, spv::OpConvertUToF, TypeF32Vector(state, 4),
ExtractPackedImageComponent(state, info, gathered,
TypeU32Vector(state, 4), physical));
decoded[physical] = Binary(
state, spv::OpFMul, TypeF32(state),
Binary(state, spv::OpDot, TypeF32(state), texels, weights),
ConstantF32Value(state, 1.0f / ((1u << info.component_bits[physical]) - 1u)));
}
selected[component] = decoded[physical];
}
}
const auto result = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 4), result,
selected[0], selected[1], selected[2], selected[3]);
return result;
}
@@ -700,6 +759,22 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
ctx.Define(inst, QueryDimensions(ctx, mem, *address));
return;
}
if (image.resource_class == IR::ImageResourceClass::Sampled &&
image.conversion_format == Prospero::BufferFormat::k11_11_10UNorm) {
if (op != IR::ValueOpcode::ImageSampleRaw ||
image.dimension != ImageDimension::Dim2D ||
mem.image_dimension != ImageDimension::Dim2D || mem.data_bits != 32u ||
mem.image_sample_flags != Decoder::ImageSampleFlagLevelZero ||
image.indirect_root != IR::ImageResource::NoIndirectImage ||
!state.program.info.samplers.at(mem.sampler).indirect_resources.empty()) {
ctx.Fail(inst, "packed UNorm sampling requires a direct 2D sample_lz");
return;
}
const auto coord = CoordF32(ctx, mem, *address, Layout(mem).coord, 2u);
ctx.Define(inst, ResultVector(ctx, SamplePackedUnormLz(ctx, mem, coord),
Prospero::TextureNumericClass::Float, false, mem));
return;
}
if (op == IR::ValueOpcode::ImageQueryLod) {
state.builder.RequireCapability(spv::CapabilityImageQuery);
const auto dimension = image.dimension;
@@ -351,6 +351,9 @@ void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
if (a16) {
inst.image_sample_flags |= ImageSampleFlagA16;
}
if ((sample != nullptr || gather != nullptr) && (word0 & (1u << 12u)) != 0u) {
inst.image_sample_flags |= ImageSampleFlagUnnormalized;
}
inst.image_dimension = dimension;
inst.image_r128 = r128;
inst.image_nsa_dwords = nsa_dwords;
@@ -762,6 +762,7 @@ enum ImageSampleFlag : uint32_t {
ImageSampleFlagCd = 1u << 8u,
ImageSampleFlagGatherHorizontal = 1u << 9u,
ImageSampleFlagAdjust = 1u << 10u,
ImageSampleFlagUnnormalized = 1u << 11u,
};
enum class ImageDimension : uint32_t {
@@ -105,6 +105,40 @@ Prospero::BufferFormat ImageConversionFormat(Prospero::BufferFormat format) {
: Prospero::BufferFormat::kInvalid;
}
bool ValidatePackedUnormSampling(const ResourcePlan& program, const ResourceSnapshot& snapshot,
uint32_t image_index) {
ShaderTextureResource texture;
std::copy_n(snapshot.images[image_index].dwords.begin(), std::size(texture.fields),
texture.fields);
if (texture.Type() != Prospero::ImageType::kColor2D ||
texture.BaseLevel() != texture.LastLevel() || texture.MinLod() != 0u ||
texture.CornerSample()) {
return SpecializationFail("packed UNorm sampling requires a centered, single-mip 2D image");
}
for (const auto& pair: program.info.sampled_pairs) {
if (pair.image != image_index) continue;
if (pair.sampler >= snapshot.samplers.size() ||
!program.info.samplers[pair.sampler].indirect_resources.empty()) {
return SpecializationFail("packed UNorm sampling requires a direct sampler");
}
ShaderSamplerResource sampler;
std::copy_n(snapshot.samplers[pair.sampler].dwords.begin(), std::size(sampler.fields),
sampler.fields);
const auto linear = static_cast<uint32_t>(Prospero::SamplerFilter::kBilinear);
const auto clamp = static_cast<uint32_t>(Prospero::SamplerClampMode::kClampLastTexel);
if (sampler.XyMagFilter() != linear || sampler.XyMinFilter() != linear ||
sampler.ClampX() != clamp || sampler.ClampY() != clamp ||
sampler.ForceUnormCoords() || sampler.MaxAnisoRatio() != 0u ||
sampler.FilterMode() != 0u || sampler.MinLod() != 0u || sampler.ForceSrgb() ||
sampler.TruncCoord() || (sampler.fields[0] & (1u << 19u)) != 0u) { // MC_COORD_TRUNC
return SpecializationFail(fmt::format(
"packed UNorm image {} requires normalized bilinear clamp sampling (sampler {})",
image_index, pair.sampler));
}
}
return true;
}
enum class SamplerClass : uint8_t { Float, Integer };
SamplerClass ClassifySampler(const ImageResource& image) {
@@ -653,7 +687,13 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
const bool raw_sint_storage = storage && format == Prospero::BufferFormat::k32SInt &&
base.written && (base.atomic ? !base.atomic64 : !base.read);
image.numeric_class = Prospero::SampledTextureNumericClass(
storage ? Prospero::RemapTextureFormat(format) : format);
Prospero::RemapTextureFormat(format));
if (!storage && format == Prospero::BufferFormat::k11_11_10UNorm) {
if (image.indirect_root != ImageResource::NoIndirectImage) {
return SpecializationFail("packed UNorm sampling requires a direct image");
}
if (!ValidatePackedUnormSampling(program, snapshot, i)) return false;
}
if (storage) {
if ((!raw_sint_storage && image.numeric_class == Prospero::TextureNumericClass::Sint) ||
image.numeric_class == Prospero::TextureNumericClass::Unsupported) {
+81
View File
@@ -34625,6 +34625,83 @@ TestCase ImageSamplePackedUintConvertsSampleAndGather() {
return test;
}
template <bool sparse> TestCase ImageSamplePackedUnormBilinear() {
using O = ShaderOpcode;
namespace D = ShaderRecompiler::Decoder;
const std::vector<u32> unnormalized = {EncodeMimg0(0x27, 0x7) | (1u << 12u),
EncodeMimg1(0, 20, 0, 2)};
D::Instruction decoded;
D::DecodeInstruction(unnormalized, 0, decoded);
Require("ImageSamplePackedUnormBilinear", "coordinate mode",
decoded.image_sample_flags ==
(D::ImageSampleFlagLevelZero | D::ImageSampleFlagUnnormalized),
"unnormalized instruction coordinates lost their sampling restriction");
constexpr u32 channels[4][3] = {{0, 0, 0}, {2047, 0, 512},
{0, 2047, 1023}, {2047, 2047, 256}};
constexpr float coordinates[][2] = {{0.5f, 0.5f}, {0.375f, 0.625f},
{-0.25f, 1.25f}, {0.25f, 0.25f},
{0.75f, 0.25f}, {0.0f, 0.5f}};
TestCase test;
test.name = sparse ? "ImageSamplePackedUnormBilinearSparse" : "ImageSamplePackedUnormBilinear";
test.image_width = test.image_height = 2;
test.sampled_image_format = vk::Format::eR32Uint;
test.sampled_image_dwords_per_pixel = 1;
for (const auto &pixel : channels) {
test.sampled_image_rgba.push_back(pixel[0] | (pixel[1] << 11u) | (pixel[2] << 22u));
}
test.user_data = MakeSampledTextureData(Prospero::BufferFormat::k11_11_10UNorm);
test.user_data[1] |= 1u << 30u;
test.user_data[2] = 1u << 14u;
test.user_data[8] = 0x92u;
test.user_data[9] = 0x00fff000u;
test.user_data[10] = 0x0a500000u; // Captured linear guest sampler; gather uses integer texels.
test.has_user_data = true;
test.image_descriptor_swizzle = DstSel(6, 4, 5, 1);
test.expected_force_point_sampler = true;
test.expected_float_tolerance = 0.000005f;
auto &code = test.code;
code.push_back(EncodeSop1(0x04, 12, 126));
for (const auto &uv : coordinates) {
AppendVMovLiteral(&code, 20, std::bit_cast<u32>(uv[0]));
AppendVMovLiteral(&code, 21, std::bit_cast<u32>(uv[1]));
code.push_back(EncodeMimg0(0x27, sparse ? 0x5 : 0xf));
code.push_back(EncodeMimg1(0, 20, 0, 2));
const float x = uv[0] * 2.0f - 0.5f, y = uv[1] * 2.0f - 0.5f;
const int ix = static_cast<int>(std::floor(x)), iy = static_cast<int>(std::floor(y));
const float fx = x - std::floor(x), fy = y - std::floor(y);
float rgba[4] = {0, 0, 0, 1};
for (u32 channel = 0; channel < 3; ++channel) {
for (int dy = 0; dy < 2; ++dy) for (int dx = 0; dx < 2; ++dx) {
const auto pixel = std::clamp(iy + dy, 0, 1) * 2 + std::clamp(ix + dx, 0, 1);
rgba[channel] += channels[pixel][channel] / (channel == 2 ? 1023.0f : 2047.0f) *
(dx ? fx : 1.0f - fx) * (dy ? fy : 1.0f - fy);
}
}
constexpr u32 swizzle[4] = {2, 0, 1, 3};
u32 reg = 0;
for (u32 component = 0; component < 4; ++component) {
if (sparse && component != 0 && component != 2) continue;
AppendStoreVgpr(&code, reg++, test.expected.size());
test.expected.push_back(std::bit_cast<u32>(rgba[swizzle[component]]));
}
}
AppendVMovLiteral(&code, 0, 0x3f400000u);
code.push_back(EncodeSop1(0x04, 126, InlineU32(0)));
code.push_back(EncodeMimg0(0x27, 0x1));
code.push_back(EncodeMimg1(0, 20, 0, 2));
code.push_back(EncodeSop1(0x04, 126, 12));
AppendStoreVgpr(&code, 0, test.expected.size());
test.expected.push_back(0x3f400000u);
AppendEnd(&code);
test.user_data[50] = test.expected.size() * sizeof(u32);
test.user_data[51] = 3u << 28u;
test.opcodes = {O::V_MOV_B32, O::S_MOV_B64, O::IMAGE_SAMPLE,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.required_spirv = {"OpImageGather", "OpBitFieldUExtract", "OpConvertUToF"};
test.forbidden_spirv = {"OpImageFetch", "OpImageSample", "OpImageRead", "OpImageWrite"};
return test;
}
template <bool rg, bool snorm = false> TestCase ImageSamplePacked8() {
using O = ShaderOpcode;
TestCase test;
@@ -38570,6 +38647,8 @@ std::vector<TestCase> MakeCases() {
AddCase(ImageLoadR32SintUsesSignedSampledImage);
AddCase(ImageLoadPackedUintUnpacksAndSwizzles);
AddCase(ImageSamplePackedUintConvertsSampleAndGather);
AddCase(ImageSamplePackedUnormBilinear<false>);
AddCase(ImageSamplePackedUnormBilinear<true>);
AddCase(ImageSamplePacked8<false>);
AddCase(ImageSamplePacked8<true>);
AddCase(ImageSamplePacked8<false, true>);
@@ -44197,6 +44276,8 @@ int main(int argc, char **argv) {
vulkan.CheckBgra16Readback();
RunCase(&vulkan, ImageLoadPackedUintUnpacksAndSwizzles());
RunCase(&vulkan, ImageSamplePackedUintConvertsSampleAndGather());
RunCase(&vulkan, ImageSamplePackedUnormBilinear<false>());
RunCase(&vulkan, ImageSamplePackedUnormBilinear<true>());
RunCase(&vulkan, ImageStorePackedUintSaturatesChannels());
RunCase(&vulkan, ImageStorePackedUintHonorsSparseDmask());
RunCase(&vulkan, ImageStorePackedUnorm<false>());