shader: validate texture mip ranges and channel selectors

This commit is contained in:
nmzik
2026-10-15 17:58:14 +02:00
parent 2caf2f7e4e
commit bcf13e101e
4 changed files with 81 additions and 28 deletions
@@ -31,18 +31,6 @@ namespace ImageViewOps {
ImageViewOps::IsFormatDepthCompatible(view_format); ImageViewOps::IsFormatDepthCompatible(view_format);
} }
[[nodiscard]] inline bool IsValidImageSwizzle(uint32_t swizzle,
uint32_t component_count = 4) noexcept {
if ((swizzle & ~0xfffu) != 0) {
return false;
}
for (uint32_t channel = 0; channel < 4; channel++) {
const auto selector = GetDstSel(swizzle, channel);
if (selector > 1 && (selector < 4 || selector >= 4 + component_count)) return false;
}
return true;
}
[[noreturn]] inline void UnsupportedColorView(const char* usage, vk::Format image_format, [[noreturn]] inline void UnsupportedColorView(const char* usage, vk::Format image_format,
vk::Format view_format, uint32_t swizzle) noexcept { vk::Format view_format, uint32_t swizzle) noexcept {
EXIT("unsupported %s color image view: image_format=%d view_format=%d swizzle=0x%03x\n", usage, EXIT("unsupported %s color image view: image_format=%d view_format=%d swizzle=0x%03x\n", usage,
@@ -87,6 +87,7 @@ bool ValidImageDescriptor(const DescriptorValue& descriptor, bool r128 = false)
type != Prospero::ImageType::kColor2DMsaa) { type != Prospero::ImageType::kColor2DMsaa) {
return false; return false;
} }
if (!IsValidImageSwizzle(words[3] & 0xfffu)) return false;
const bool array = type == Prospero::ImageType::kColor1DArray || const bool array = type == Prospero::ImageType::kColor1DArray ||
type == Prospero::ImageType::kColor2DArray || type == Prospero::ImageType::kColor2DArray ||
type == Prospero::ImageType::kColor2DMsaaArray || type == Prospero::ImageType::kColor2DMsaaArray ||
@@ -94,15 +95,15 @@ bool ValidImageDescriptor(const DescriptorValue& descriptor, bool r128 = false)
if (array && ((words[4] >> 16u) & 0x1fffu) > (words[4] & 0x1fffu)) { if (array && ((words[4] >> 16u) & 0x1fffu) > (words[4] & 0x1fffu)) {
return false; return false;
} }
const auto base_level = (words[3] >> 12u) & 0xfu;
const auto last_level = (words[3] >> 16u) & 0xfu;
if (type == Prospero::ImageType::kColor2DMsaa || if (type == Prospero::ImageType::kColor2DMsaa ||
type == Prospero::ImageType::kColor2DMsaaArray) { type == Prospero::ImageType::kColor2DMsaaArray) {
const auto base_level = (descriptor.dwords[3] >> 12u) & 0xfu;
const auto fragments = (descriptor.dwords[3] >> 16u) & 0xfu;
const auto max_mip = (descriptor.dwords[5] >> 4u) & 0xfu; const auto max_mip = (descriptor.dwords[5] >> 4u) & 0xfu;
return base_level == 0 && fragments >= 1 && fragments <= 3 && return base_level == 0 && last_level >= 1 && last_level <= 3 &&
(r128 || max_mip == fragments); (r128 || max_mip == last_level);
} }
return true; return base_level <= last_level;
} }
uint32_t DescriptorImageSwizzle(const DescriptorValue& descriptor) { uint32_t DescriptorImageSwizzle(const DescriptorValue& descriptor) {
@@ -167,7 +168,7 @@ uint32_t ImageMipCount(const ImageResource& image, const DescriptorValue& descri
} }
const auto base = (descriptor.dwords[3] >> 12u) & 0xfu; const auto base = (descriptor.dwords[3] >> 12u) & 0xfu;
const auto last = (descriptor.dwords[3] >> 16u) & 0xfu; const auto last = (descriptor.dwords[3] >> 16u) & 0xfu;
return base <= last ? last - base + 1u : 0u; return last - base + 1u;
} }
bool DecodeBufferDescriptor(const DescriptorValue& descriptor, ShaderBufferResource& result) { bool DecodeBufferDescriptor(const DescriptorValue& descriptor, ShaderBufferResource& result) {
@@ -831,10 +832,6 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
return SpecializationFail(fmt::format("image resource {} has an invalid class", i)); return SpecializationFail(fmt::format("image resource {} has an invalid class", i));
} }
image.mip_count = ImageMipCount(base, descriptor); image.mip_count = ImageMipCount(base, descriptor);
if (image.mip_count == 0u) {
return SpecializationFail(
fmt::format("image descriptor {} has an invalid mip range", i));
}
if (NullImageDescriptor(descriptor)) { if (NullImageDescriptor(descriptor)) {
image.numeric_class = base.atomic ? Prospero::TextureNumericClass::Uint image.numeric_class = base.atomic ? Prospero::TextureNumericClass::Uint
: Prospero::TextureNumericClass::Float; : Prospero::TextureNumericClass::Float;
+12
View File
@@ -66,6 +66,18 @@ inline uint8_t GetDstSel(uint32_t swizzle, uint32_t channel) {
return (swizzle >> (channel * 3u)) & 0x7u; return (swizzle >> (channel * 3u)) & 0x7u;
} }
[[nodiscard]] inline bool IsValidImageSwizzle(uint32_t swizzle,
uint32_t component_count = 4) noexcept {
if ((swizzle & ~0xfffu) != 0) {
return false;
}
for (uint32_t channel = 0; channel < 4; channel++) {
const auto selector = GetDstSel(swizzle, channel);
if (selector > 1 && (selector < 4 || selector >= 4 + component_count)) return false;
}
return true;
}
struct ShaderClipSpaceTransform { struct ShaderClipSpaceTransform {
float scale[2] = {}; float scale[2] = {};
float offset[2] = {}; float offset[2] = {};
+62 -6
View File
@@ -336,9 +336,9 @@ void TestInvariantIndirectImageMaterialization(bool nested) {
}; };
put(16u, 0u); put(16u, 0u);
put(64u, 1u); put(64u, 1u);
// Captured sampler/T# overlap: valid-looking format149 at an unreachable DWORD start. // A structurally valid but unsupported format at an unreachable DWORD start.
const std::array<uint32_t, 8> unreachable{ const std::array<uint32_t, 8> unreachable{
0x00fff000u, 0x09500000u, 0u, 0x900672b0u, 0u, 0u, 0u, 0u}; 0x00fff000u, 0x09500000u, 0u, 0x900003acu, 0u, 0u, 0u, 0u};
std::copy(unreachable.begin(), unreachable.end(), memory.words.begin() + 0x1064u / 4u); std::copy(unreachable.begin(), unreachable.end(), memory.words.begin() + 0x1064u / 4u);
SrtRuntime runtime{.user_data = data, .userdata = &memory, SrtRuntime runtime{.user_data = data, .userdata = &memory,
.read_specialization_memory = ReadLinearTestMemory}; .read_specialization_memory = ReadLinearTestMemory};
@@ -472,6 +472,21 @@ void TestBoundedImageViewEligibility() {
snapshot.images[3].dwords == descriptor(0x24u, Type::kColor2D), snapshot.images[3].dwords == descriptor(0x24u, Type::kColor2D),
"typed table eligibility changed byte offsets or discarded compatible views"); "typed table eligibility changed byte offsets or discarded compatible views");
// CS127daf PC0x163c: an overlapping heap tuple is not a valid texture view.
const std::array<uint32_t, 8> invalid_view{
0x00fff000u, 0x09503fccu, 0u, 0x900672c0u, 0u, 0u, 0u, 0u};
std::copy(invalid_view.begin(), invalid_view.end(), memory.words.begin() + 0x1040u / 4u);
for (const bool r128 : {false, true}) {
plan.info.images[0].r128 = r128;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.images.size() == (r128 ? 3u : 4u) &&
snapshot.flattened_srt[specialization.images[0].indirect_mapping_offset + 17u] == 0u &&
snapshot.images[1].dwords == descriptor(0x20u, Type::kColor2D) &&
snapshot.images.back().dwords == descriptor(0x24u, Type::kColor2D),
"invalid heap texture view survived normalization or removed valid neighbors");
}
plan.info.images[0].r128 = false;
// An explicitly selected descriptor is not an unrelated entry in a broad heap. // An explicitly selected descriptor is not an unrelated entry in a broad heap.
const auto image_source = plan.info.images[0].source; const auto image_source = plan.info.images[0].source;
std::vector<uint32_t> selected; std::vector<uint32_t> selected;
@@ -3186,6 +3201,41 @@ void TestImageDescriptorFields() {
snapshot.images[0].dwords == user_data, snapshot.images[0].dwords == user_data,
"valid texture descriptor was rejected"); "valid texture descriptor was rejected");
const auto valid_descriptor = user_data; const auto valid_descriptor = user_data;
for (const auto mode : {ImageMipMode::None, ImageMipMode::Dynamic}) {
plan.info.images[0].mip_mode = mode;
for (const auto [base, last] : {std::pair{0u, 0u}, {1u, 1u}, {2u, 3u},
{15u, 15u}, {7u, 6u}}) {
user_data = valid_descriptor;
user_data[3] |= (base << 12u) | (last << 16u);
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
(base <= last ? snapshot.images[0].dwords == user_data : is_null()),
"texture view mip-range validity depended on the image instruction");
}
}
plan.info.images[0].mip_mode = ImageMipMode::None;
for (uint32_t shift = 0u; shift < 12u; shift += 3u) {
for (uint32_t channel = 0u; channel < 8u; ++channel) {
user_data = valid_descriptor;
user_data[3] = (user_data[3] & ~(7u << shift)) | (channel << shift);
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
(channel == 2u || channel == 3u ? is_null()
: snapshot.images[0].dwords == user_data),
"texture view accepted an undefined channel selector or rejected a defined one");
}
}
for (uint32_t fragments = 0u; fragments <= 4u; ++fragments) {
user_data = valid_descriptor;
user_data[3] = (user_data[3] & 0xfffu) | (fragments << 16u) |
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2DMsaa) << 28u);
user_data[5] = fragments << 4u;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
(fragments >= 1u && fragments <= 3u
? snapshot.images[0].dwords == user_data : is_null()),
"MSAA fragment encoding was confused with a mip range");
user_data[3] |= 1u << 12u;
Check(MaterializeResources(plan, runtime, snapshot, specialization) && is_null(),
"MSAA view accepted a nonzero base mip");
}
// Keep RESOURCE_LEVEL set in this valid texture descriptor. // Keep RESOURCE_LEVEL set in this valid texture descriptor.
user_data = {0x0208a200u, 0xca900000u, 0x800fc00fu, 0x90960facu, user_data = {0x0208a200u, 0xca900000u, 0x800fc00fu, 0x90960facu,
0u, 0x60u, 0u, 0u}; 0u, 0x60u, 0u, 0u};
@@ -3454,7 +3504,7 @@ void TestCompactScalarImageTable() {
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u); (static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
if (!compact) at[5] = 0x00700000u; if (!compact) at[5] = 0x00700000u;
} }
const std::array poison{0x00fff000u, 0x09500000u, 0u, 0x900672b0u}; const std::array poison{0x00fff000u, 0x09500000u, 0u, 0x900003acu};
if (member == 0x16cu) if (member == 0x16cu)
std::copy(poison.begin(), poison.end(), memory.words.begin() + (0x1000u + 0x17cu) / 4u); std::copy(poison.begin(), poison.end(), memory.words.begin() + (0x1000u + 0x17cu) / 4u);
memory.fail_address = 0x2000u + bytes; // Last descriptor ends at the readable boundary. memory.fail_address = 0x2000u + bytes; // Last descriptor ends at the readable boundary.
@@ -5181,9 +5231,15 @@ void TestDynamicStorageMipTracking() {
changed_user_data[3] = changed_user_data[3] =
(changed_user_data[3] & ~((0xfu << 12u) | (0xfu << 16u))) | (changed_user_data[3] & ~((0xfu << 12u) | (0xfu << 16u))) |
(4u << 12u) | (3u << 16u); (4u << 12u) | (3u << 16u);
Check(!MaterializeResources(resource_plan, {.user_data = changed_user_data}, Check(MaterializeResources(resource_plan, {.user_data = changed_user_data},
changed_snapshot, changed_specialization), changed_snapshot, changed_specialization) &&
"an inverted dynamic storage mip range was accepted"); changed_snapshot.images.size() == 2u &&
std::ranges::all_of(changed_snapshot.images, [](const auto& image) {
return std::ranges::all_of(image.dwords, [](uint32_t word) { return word == 0u; });
}) && changed_specialization.images[0].mip_count == 1u &&
changed_specialization.images[1].mip_count == 1u &&
changed_specialization != specialization,
"an inverted mip range was not normalized consistently for plain and dynamic views");
} }
void TestSrtFlatteningAndRuntimeMemoization() { void TestSrtFlatteningAndRuntimeMemoization() {