fix(Vulkan): Work around a driver issue causing graphical corruption, other minor changes (#11204) b8b0d3e01c

This change primarily works around a driver issue that was causing visual corruption on some newer Adreno-based devices.
There are other minor changes as well (displaying the driver version from the bootstrapping code, setting a minimum requirement of Vulkan 1.1 in the renderer)

Co-authored-by: Josh Jersild <joshua@rive.app>
This commit is contained in:
JoshJRive
2025-12-03 20:16:59 +00:00
parent 94ada07808
commit 7aa95ac536
13 changed files with 181 additions and 49 deletions

View File

@@ -1 +1 @@
f38d54d8e466ed5cb2ba1246bc79510ceaa5e6cf
b8b0d3e01cb5b85000a1b2f19ab73c3c836154d9

View File

@@ -71,12 +71,10 @@ public:
void hotloadShaders(rive::Span<const uint32_t> spirvData);
private:
RenderContextVulkanImpl(rcp<VulkanContext>,
const VkPhysicalDeviceProperties&,
const ContextOptions&);
RenderContextVulkanImpl(rcp<VulkanContext>, const ContextOptions&);
// Called outside the constructor so we can use virtual methods.
void initGPUObjects(ShaderCompilationMode, uint32_t vendorID);
void initGPUObjects(ShaderCompilationMode);
void prepareToFlush(uint64_t nextFrameNumber,
uint64_t safeFrameNumber) override;
@@ -232,6 +230,7 @@ private:
std::unique_ptr<TessellatePipeline> m_tessellatePipeline;
rcp<vkutil::Buffer> m_tessSpanIndexBuffer;
rcp<vkutil::Texture2D> m_tessTexture;
rcp<vkutil::Texture2D> m_tesselationSyncIssueWorkaroundTexture;
rcp<vkutil::Framebuffer> m_tessTextureFramebuffer;
// Renders feathers to the atlas.

View File

@@ -31,7 +31,7 @@ struct VulkanFeatures
bool fragmentShaderPixelInterlock = false;
// Indicates a nonconformant driver, like MoltenVK.
bool VK_KHR_portability_subset;
bool VK_KHR_portability_subset = false;
};
// Wraps a VkDevice, function dispatch table, and VMA library instance.

View File

@@ -47,10 +47,10 @@ public:
#ifndef NDEBUG
.desiredValidationType =
options.enableVulkanCoreValidationLayers
? VulkanValidationType::Core
? VulkanValidationType::core
: (options.enableVulkanSynchronizationValidationLayers
? VulkanValidationType::Synchronization
: VulkanValidationType::None),
? VulkanValidationType::synchronization
: VulkanValidationType::none),
.wantDebugCallbacks = !options.disableDebugCallbacks,
#endif
});

View File

@@ -73,6 +73,9 @@ private:
std::string deviceName;
VkPhysicalDeviceType deviceType;
uint32_t deviceAPIVersion;
uint32_t driverVersionMajor;
uint32_t driverVersionMinor;
uint32_t driverVersionPatch;
};
static FindDeviceResult findCompatiblePhysicalDevice(

View File

@@ -7,19 +7,19 @@ namespace rive_vkb
{
enum class VulkanValidationType
{
None,
Core,
Synchronization,
none,
core,
synchronization,
};
#ifndef NDEBUG
constexpr bool RIVE_DEFAULT_VULKAN_DEBUG_PREFERENCE = true;
constexpr VulkanValidationType RIVE_DEFAULT_VULKAN_VALIDATION_TYPE =
VulkanValidationType::Core;
VulkanValidationType::core;
#else
constexpr bool RIVE_DEFAULT_VULKAN_DEBUG_PREFERENCE = false;
constexpr VulkanValidationType RIVE_DEFAULT_VULKAN_VALIDATION_TYPE =
VulkanValidationType::None;
VulkanValidationType::none;
#endif
class VulkanLibrary;

View File

@@ -3,9 +3,11 @@
*/
#include <string>
#include <sstream>
#include "rive/renderer/vulkan/vkutil.hpp"
#include "rive_vk_bootstrap/vulkan_device.hpp"
#include "rive_vk_bootstrap/vulkan_instance.hpp"
#include "shaders/constants.glsl"
#include "logging.hpp"
#include "vulkan_library.hpp"
@@ -30,6 +32,36 @@ static const char* physicalDeviceTypeName(VkPhysicalDeviceType type)
}
}
void unpackDriverVersion(const VkPhysicalDeviceProperties& props,
uint32_t* majorOut,
uint32_t* minorOut,
uint32_t* patchOut)
{
if (props.vendorID == VULKAN_VENDOR_NVIDIA)
{
// NVidia uses 10|8|8|6 encoding for driver version. We'll ignore the
// fourth version section.
*majorOut = props.driverVersion >> 22;
*minorOut = (props.driverVersion >> 14) & 0xff;
*patchOut = (props.driverVersion >> 6) & 0xff;
}
#ifdef _WIN32
else if (props.vendorID == VULKAN_VENDOR_INTEL)
{
*majorOut = props.driverVersion >> 14;
*minorOut = props.driverVersion & 0x3fff;
*patchOut = 0;
}
#endif
else
{
// Everything else seems to use the standard Vulkan encoding.
*majorOut = VK_API_VERSION_MAJOR(props.driverVersion);
*minorOut = VK_API_VERSION_MINOR(props.driverVersion);
*patchOut = VK_API_VERSION_PATCH(props.driverVersion);
}
}
VulkanDevice::VulkanDevice(VulkanInstance& instance, const Options& opts)
{
assert(!opts.headless ||
@@ -220,12 +252,15 @@ VulkanDevice::VulkanDevice(VulkanInstance& instance, const Options& opts)
LOAD_MEMBER_INSTANCE_FUNC(vkGetPhysicalDeviceSurfaceCapabilitiesKHR,
instance);
printf("==== Vulkan %i.%i.%i GPU (%s): %s [ ",
printf("==== Vulkan %i.%i.%i GPU (%s): %s (driver %i.%i.%i) [ ",
VK_API_VERSION_MAJOR(m_riveVulkanFeatures.apiVersion),
VK_API_VERSION_MINOR(m_riveVulkanFeatures.apiVersion),
VK_API_VERSION_PATCH(m_riveVulkanFeatures.apiVersion),
physicalDeviceTypeName(findResult.deviceType),
findResult.deviceName.c_str());
findResult.deviceName.c_str(),
findResult.driverVersionMajor,
findResult.driverVersionMinor,
findResult.driverVersionPatch);
struct CommaSeparator
{
const char* m_separator = "";
@@ -382,11 +417,16 @@ VulkanDevice::FindDeviceResult VulkanDevice::findCompatiblePhysicalDevice(
if (strstr(props.deviceName, nameFilter) != nullptr)
{
uint32_t major, minor, patch;
unpackDriverVersion(props, &major, &minor, &patch);
matchResult = {
.physicalDevice = device,
.deviceName = props.deviceName,
.deviceType = props.deviceType,
.deviceAPIVersion = props.apiVersion,
.driverVersionMajor = major,
.driverVersionMinor = minor,
.driverVersionPatch = patch,
};
matchedDeviceNames.push_back(std::string{props.deviceName});
}
@@ -444,11 +484,17 @@ VulkanDevice::FindDeviceResult VulkanDevice::findCompatiblePhysicalDevice(
if (!onlyAcceptDesiredType ||
props.deviceType == desiredDeviceType)
{
uint32_t major, minor, patch;
unpackDriverVersion(props, &major, &minor, &patch);
return {
.physicalDevice = device,
.deviceName = props.deviceName,
.deviceType = props.deviceType,
.deviceAPIVersion = props.apiVersion,
.driverVersionMajor = major,
.driverVersionMinor = minor,
.driverVersionPatch = patch,
};
}
}

View File

@@ -168,23 +168,26 @@ VulkanInstance::VulkanInstance(const Options& opts)
#endif
bool useFallbackDebugCallbacks = false;
if (validationType != VulkanValidationType::None)
if (validationType != VulkanValidationType::none)
{
if (!add_layer_if_supported("VK_LAYER_KHRONOS_validation"))
{
LOG_ERROR_LINE("WARNING: Validation layers are not supported. "
"Creating context without validation layers.\n");
validationType = VulkanValidationType::None;
validationType = VulkanValidationType::none;
}
else
else if (validationType != VulkanValidationType::core)
{
// If we're doing any other type of validation, add the settings
// extension. (RenderDoc does not work when this extension is,
// enabled, which is why we don't always enable it)
extensions.push_back(VK_EXT_LAYER_SETTINGS_EXTENSION_NAME);
}
}
if (enableDebugCallbacks)
{
if (validationType != VulkanValidationType::None)
if (validationType != VulkanValidationType::none)
{
// If we are enabling validation layers, VK_EXT_debug_utils willl
// come along with that, even though it currently isn't in the
@@ -258,7 +261,7 @@ VulkanInstance::VulkanInstance(const Options& opts)
.pSettings = settingsForSyncValidate,
};
if (validationType == VulkanValidationType::Synchronization)
if (validationType == VulkanValidationType::synchronization)
{
validationLayerSettingsForSyncValidation.pNext = createInfo.pNext;
createInfo.pNext = &validationLayerSettingsForSyncValidation;

View File

@@ -50,7 +50,6 @@ static VkFilter vk_filter(rive::ImageFilter option)
PipelineManagerVulkan::PipelineManagerVulkan(rcp<VulkanContext> vk,
ShaderCompilationMode mode,
uint32_t vendorID,
VkImageView nullTextureView) :
Super(mode),
m_vk(std::move(vk)),
@@ -58,8 +57,7 @@ PipelineManagerVulkan::PipelineManagerVulkan(rcp<VulkanContext> vk,
VK_FORMAT_R32_SFLOAT,
VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT)
? VK_FORMAT_R32_SFLOAT
: VK_FORMAT_R16_SFLOAT),
m_vendorID(vendorID)
: VK_FORMAT_R16_SFLOAT)
{
// Create the immutable samplers.
VkSamplerCreateInfo linearSamplerCreateInfo = {

View File

@@ -17,7 +17,6 @@ class PipelineManagerVulkan : public AsyncPipelineManager<DrawPipelineVulkan>
public:
PipelineManagerVulkan(rcp<VulkanContext>,
ShaderCompilationMode,
uint32_t vendorID,
VkImageView nullTextureView);
~PipelineManagerVulkan();
@@ -31,7 +30,11 @@ public:
InterlockMode,
DrawPipelineLayoutVulkan::Options);
uint32_t vendorID() const { return m_vendorID; }
uint32_t vendorID() const
{
return m_vk->physicalDeviceProperties().vendorID;
}
VkFormat atlasFormat() const { return m_atlasFormat; }
VulkanContext* vulkanContext() const { return m_vk.get(); }
@@ -110,7 +113,6 @@ private:
rcp<VulkanContext> m_vk;
VkFormat m_atlasFormat;
uint32_t m_vendorID;
// Samplers.
VkSampler m_linearSampler;

View File

@@ -674,7 +674,6 @@ private:
RenderContextVulkanImpl::RenderContextVulkanImpl(
rcp<VulkanContext> vk,
const VkPhysicalDeviceProperties& physicalDeviceProps,
const ContextOptions& contextOptions) :
m_vk(std::move(vk)),
m_flushUniformBufferPool(m_vk, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT),
@@ -688,6 +687,8 @@ RenderContextVulkanImpl::RenderContextVulkanImpl(
m_triangleBufferPool(m_vk, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT),
m_descriptorSetPoolPool(make_rcp<DescriptorSetPoolPool>(m_vk))
{
const auto& physicalDeviceProps = m_vk->physicalDeviceProperties();
m_platformFeatures.supportsRasterOrderingMode =
!contextOptions.forceAtomicMode &&
m_vk->features.rasterizationOrderColorAttachmentAccess;
@@ -787,8 +788,7 @@ RenderContextVulkanImpl::RenderContextVulkanImpl(
}
void RenderContextVulkanImpl::initGPUObjects(
ShaderCompilationMode shaderCompilationMode,
uint32_t vendorID)
ShaderCompilationMode shaderCompilationMode)
{
// Bound when there is not an image paint.
constexpr static uint8_t black[] = {0, 0, 0, 1};
@@ -800,10 +800,28 @@ void RenderContextVulkanImpl::initGPUObjects(
"null image texture");
m_nullImageTexture->scheduleUpload(black, sizeof(black));
if (strstr(m_vk->physicalDeviceProperties().deviceName, "Adreno (TM) 8") !=
nullptr)
{
// The Adreno 8s (at least on the Galaxy S25) have a strange
// synchronization issue around our tesselation texture, where the
// barriers appear to not work properly (leading to tesselation texture
// corruption, even across frames).
// We can do a blit to a 1x1 texture, however, which seems to make the
// synchronization play nice.
m_tesselationSyncIssueWorkaroundTexture = m_vk->makeTexture2D(
{
.format = VK_FORMAT_R8G8B8A8_UINT,
.extent = {1, 1},
.usage = VK_IMAGE_USAGE_SAMPLED_BIT |
VK_IMAGE_USAGE_TRANSFER_DST_BIT,
},
"tesselation sync bug workaround texture");
}
m_pipelineManager = std::make_unique<PipelineManagerVulkan>(
m_vk,
shaderCompilationMode,
vendorID,
m_nullImageTexture->vkImageView());
// The pipelines reference our vulkan objects. Delete them first.
@@ -955,12 +973,21 @@ void RenderContextVulkanImpl::resizeTessellationTexture(uint32_t width,
width = std::max(width, 1u);
height = std::max(height, 1u);
VkImageUsageFlags usage =
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
// If we are doing the Adreno synchronization workaround we also need the
// TRANSFER_SRC bit
if (m_tesselationSyncIssueWorkaroundTexture != nullptr)
{
usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
}
m_tessTexture = m_vk->makeTexture2D(
{
.format = VK_FORMAT_R32G32B32A32_UINT,
.extent = {width, height},
.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT |
VK_IMAGE_USAGE_SAMPLED_BIT,
.usage = usage,
},
"tesselation texture");
@@ -1746,6 +1773,54 @@ void RenderContextVulkanImpl::flush(const FlushDescriptor& desc)
// VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL.
m_tessTexture->lastAccess().layout =
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
if (m_tesselationSyncIssueWorkaroundTexture != nullptr)
{
// On the Adreno 8xx series drivers we've encountered, there is some
// sort of synchronization issue with the tesselation texture that
// causes the barriers to not work, and it ends up being corrupted.
// However, if we first just blit it to an offscreen texture (just a
// 1x1 texture), the render corruption goes away.
m_tessTexture->barrier(
commandBuffer,
{
.pipelineStages = VK_PIPELINE_STAGE_TRANSFER_BIT,
.accessMask = VK_ACCESS_TRANSFER_READ_BIT,
.layout = VK_IMAGE_LAYOUT_GENERAL,
});
// We need this transition but really only one time (if we haven't
// done so already)
if (m_tesselationSyncIssueWorkaroundTexture->lastAccess().layout !=
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
{
m_tesselationSyncIssueWorkaroundTexture->barrier(
commandBuffer,
{
.pipelineStages = VK_PIPELINE_STAGE_TRANSFER_BIT,
.accessMask = VK_ACCESS_TRANSFER_WRITE_BIT,
.layout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
},
vkutil::ImageAccessAction::invalidateContents);
}
m_vk->blitSubRect(
commandBuffer,
m_tessTexture->vkImage(),
VK_IMAGE_LAYOUT_GENERAL,
m_tesselationSyncIssueWorkaroundTexture->vkImage(),
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
IAABB::MakeWH(
m_tesselationSyncIssueWorkaroundTexture->width(),
m_tesselationSyncIssueWorkaroundTexture->height()));
// NOTE: Technically there should be a barrier after this blit to
// prevent a write-after-write hazard. However, we don't use this
// texture at all and thus don't care if we overwrite it, so there
// is intentionally no barrier here...but you will get a failure on
// this texture if you enable synchronization validation on a device
// with the workaround enabled.
}
}
// Ensure the tessellation texture has finished rendering before the path
@@ -2909,22 +2984,28 @@ std::unique_ptr<RenderContext> RenderContextVulkanImpl::MakeContext(
device,
features,
pfnvkGetInstanceProcAddr);
VkPhysicalDeviceProperties physicalDeviceProps;
vk->GetPhysicalDeviceProperties(vk->physicalDevice, &physicalDeviceProps);
if (vk->physicalDeviceProperties().apiVersion < VK_API_VERSION_1_1)
{
fprintf(
stderr,
"ERROR: Rive Vulkan renderer requires a driver that supports at least Vulkan 1.1.\n");
return nullptr;
}
std::unique_ptr<RenderContextVulkanImpl> impl(
new RenderContextVulkanImpl(std::move(vk),
physicalDeviceProps,
contextOptions));
new RenderContextVulkanImpl(std::move(vk), contextOptions));
if (contextOptions.forceAtomicMode &&
!impl->platformFeatures().supportsClockwiseAtomicMode)
{
fprintf(stderr,
"ERROR: Requested \"atomic\" mode but Vulkan does not support "
"fragmentStoresAndAtomics on this platform.\n");
fprintf(
stderr,
"ERROR: Requested \"atomic\" mode but Vulkan does not support fragmentStoresAndAtomics on this platform.\n");
return nullptr;
}
impl->initGPUObjects(contextOptions.shaderCompilationMode,
physicalDeviceProps.vendorID);
impl->initGPUObjects(contextOptions.shaderCompilationMode);
return std::make_unique<RenderContext>(std::move(impl));
}
} // namespace rive::gpu

View File

@@ -65,10 +65,10 @@ public:
#ifndef NDEBUG
.desiredValidationType =
m_backendParams.disableValidationLayers
? VulkanValidationType::None
? VulkanValidationType::none
: (m_backendParams.wantVulkanSynchronizationValidation
? VulkanValidationType::Synchronization
: VulkanValidationType::Core),
? VulkanValidationType::synchronization
: VulkanValidationType::core),
.wantDebugCallbacks = !m_backendParams.disableValidationLayers,
#endif
});

View File

@@ -38,10 +38,10 @@ public:
#ifndef NDEBUG
.desiredValidationType =
m_backendParams.disableValidationLayers
? VulkanValidationType::None
? VulkanValidationType::none
: (m_backendParams.wantVulkanSynchronizationValidation
? VulkanValidationType::Synchronization
: VulkanValidationType::Core),
? VulkanValidationType::synchronization
: VulkanValidationType::core),
.wantDebugCallbacks = !m_backendParams.disableDebugCallbacks,
#endif
});