diff --git a/src/freedreno/vulkan/meson.build b/src/freedreno/vulkan/meson.build
index 4495cbe0495..d8c17e6df8d 100644
--- a/src/freedreno/vulkan/meson.build
+++ b/src/freedreno/vulkan/meson.build
@@ -38,6 +38,7 @@ libtu_files = files(
'tu_image.cc',
'tu_knl.cc',
'tu_lrz.cc',
+ 'tu_nir_lower_demote_samples.cc',
'tu_nir_lower_multiview.cc',
'tu_nir_lower_ray_query.cc',
'tu_pass.cc',
diff --git a/src/freedreno/vulkan/tu_device.cc b/src/freedreno/vulkan/tu_device.cc
index f260e794d19..ba66d092496 100644
--- a/src/freedreno/vulkan/tu_device.cc
+++ b/src/freedreno/vulkan/tu_device.cc
@@ -1830,6 +1830,7 @@ static const driOptionDescription tu_dri_options[] = {
DRI_CONF_TU_USE_TEX_COORD_ROUND_NEAREST_EVEN_MODE(false)
DRI_CONF_TU_IGNORE_FRAG_DEPTH_DIRECTION(false)
DRI_CONF_TU_ENABLE_SOFTFLOAT32(false)
+ DRI_CONF_TU_EMULATE_ALPHA_TO_COVERAGE(false)
DRI_CONF_SECTION_END
};
@@ -1860,6 +1861,8 @@ tu_init_dri_options(struct tu_instance *instance)
driQueryOptionb(&instance->dri_options, "tu_ignore_frag_depth_direction");
instance->enable_softfloat32 =
driQueryOptionb(&instance->dri_options, "tu_enable_softfloat32");
+ instance->emulate_alpha_to_coverage =
+ driQueryOptionb(&instance->dri_options, "tu_emulate_alpha_to_coverage");
}
static uint32_t instance_count = 0;
diff --git a/src/freedreno/vulkan/tu_device.h b/src/freedreno/vulkan/tu_device.h
index 592519e97b7..9b58475ba0d 100644
--- a/src/freedreno/vulkan/tu_device.h
+++ b/src/freedreno/vulkan/tu_device.h
@@ -229,6 +229,12 @@ struct tu_instance
* However we don't want native Vulkan apps using this.
*/
bool enable_softfloat32;
+
+ /* The hardware implementation of alpha-to-coverage gives visually poor
+ * results for many games. Set this option to enable it in the shader
+ * instead.
+ */
+ bool emulate_alpha_to_coverage;
};
VK_DEFINE_HANDLE_CASTS(tu_instance, vk.base, VkInstance,
VK_OBJECT_TYPE_INSTANCE)
diff --git a/src/freedreno/vulkan/tu_nir_lower_demote_samples.cc b/src/freedreno/vulkan/tu_nir_lower_demote_samples.cc
new file mode 100644
index 00000000000..4f6e470aa9f
--- /dev/null
+++ b/src/freedreno/vulkan/tu_nir_lower_demote_samples.cc
@@ -0,0 +1,88 @@
+/*
+ * Copyright © 2025 Valve Corporation
+ * SPDX-License-Identifier: MIT
+ */
+
+#include "tu_shader.h"
+#include "nir/nir_builder.h"
+
+/* Lower demote_samples to a write to gl_SampleMask. Take into account
+ * existing writes to gl_SampleMask.
+ */
+
+bool
+tu_nir_lower_demote_samples(nir_shader *nir)
+{
+ nir_function_impl *entrypoint = nir_shader_get_entrypoint(nir);
+ nir_variable *sample_mask = NULL;
+
+ nir_builder _b = nir_builder_create(entrypoint), *b = &_b;
+
+ bool progress = false;
+
+ uint32_t sample_mask_driver_location = ~0;
+ nir_foreach_block (block, entrypoint) {
+ nir_foreach_instr_safe (instr, block) {
+ if (instr->type != nir_instr_type_intrinsic)
+ continue;
+
+ nir_intrinsic_instr *intrin = nir_instr_as_intrinsic(instr);
+ if (intrin->intrinsic == nir_intrinsic_store_output) {
+ nir_io_semantics sem = nir_intrinsic_io_semantics(intrin);
+ if (sem.location != FRAG_RESULT_SAMPLE_MASK)
+ continue;
+ } else if (intrin->intrinsic != nir_intrinsic_demote_samples) {
+ continue;
+ }
+
+ if (!sample_mask) {
+ sample_mask = nir_local_variable_create(entrypoint,
+ glsl_uint_type(),
+ "sample_mask");
+ /* Initialize sample_mask to ~0 (all samples) */
+ b->cursor = nir_before_impl(entrypoint);
+ nir_store_var(b, sample_mask, nir_imm_int(b, ~0), 0x1);
+ }
+
+ b->cursor = nir_before_instr(instr);
+
+ if (intrin->intrinsic == nir_intrinsic_demote_samples) {
+ /* For each demote_samples, remove the samples from sample_mask */
+ nir_def *to_demote = intrin->src[0].ssa;
+
+ nir_store_var(b, sample_mask,
+ nir_iand(b, nir_load_var(b, sample_mask),
+ nir_inot(b, to_demote)), 0x1);
+ } else if (intrin->intrinsic == nir_intrinsic_store_output) {
+ /* If there is an existing write to SampleMask, AND it with
+ * sample_mask and remove it.
+ */
+ nir_def *old_mask = intrin->src[0].ssa;
+ nir_store_var(b, sample_mask,
+ nir_iand(b, nir_load_var(b, sample_mask),
+ old_mask), 0x1);
+ sample_mask_driver_location = nir_intrinsic_base(intrin);
+ }
+
+ nir_instr_remove(instr);
+ progress = true;
+ }
+ }
+
+ if (progress) {
+ if (sample_mask_driver_location == ~0)
+ sample_mask_driver_location = nir->num_outputs++;
+
+ /* Finally, at the end insert a write to SampleMask. */
+ b->cursor = nir_after_impl(entrypoint);
+ nir_store_output(b, nir_load_var(b, sample_mask),
+ nir_imm_int(b, 0),
+ .base = sample_mask_driver_location,
+ .io_semantics = {
+ .location = FRAG_RESULT_SAMPLE_MASK
+ });
+ }
+
+ return nir_progress(progress, entrypoint, nir_metadata_control_flow);
+}
+
diff --git a/src/freedreno/vulkan/tu_pipeline.cc b/src/freedreno/vulkan/tu_pipeline.cc
index f3f7507e2da..b20b4bf4cff 100644
--- a/src/freedreno/vulkan/tu_pipeline.cc
+++ b/src/freedreno/vulkan/tu_pipeline.cc
@@ -1850,6 +1850,18 @@ tu_pipeline_builder_compile_shaders(struct tu_pipeline_builder *builder,
VK_GRAPHICS_PIPELINE_LIBRARY_FRAGMENT_SHADER_BIT_EXT) {
keys[MESA_SHADER_FRAGMENT].custom_resolve =
builder->graphics_state.rp->custom_resolve;
+
+ if (builder->device->physical_device->instance->emulate_alpha_to_coverage) {
+ keys[MESA_SHADER_FRAGMENT].emulate_alpha_to_coverage = true;
+
+ /* Don't emulate if we know it won't be enabled. */
+ if (builder->graphics_state.ms &&
+ !BITSET_TEST(builder->graphics_state.dynamic,
+ MESA_VK_DYNAMIC_MS_ALPHA_TO_COVERAGE_ENABLE)) {
+ if (!builder->graphics_state.ms->alpha_to_coverage_enable)
+ keys[MESA_SHADER_FRAGMENT].emulate_alpha_to_coverage = false;
+ }
+ }
}
if (builder->create_flags &
@@ -3278,6 +3290,8 @@ tu6_emit_blend(struct tu_cs *cs,
{
bool rop_reads_dst = cb->logic_op_enable && tu_logic_op_reads_dst((VkLogicOp)cb->logic_op);
enum a3xx_rop_code rop = tu6_rop((VkLogicOp)cb->logic_op);
+ if (cs->device->physical_device->instance->emulate_alpha_to_coverage)
+ alpha_to_coverage_enable = false;
uint32_t blend_enable_mask = 0;
for (unsigned i = 0; i < cb->attachment_count; i++) {
diff --git a/src/freedreno/vulkan/tu_shader.cc b/src/freedreno/vulkan/tu_shader.cc
index b04f6f919b5..c06619b56ff 100644
--- a/src/freedreno/vulkan/tu_shader.cc
+++ b/src/freedreno/vulkan/tu_shader.cc
@@ -1533,6 +1533,20 @@ tu_nir_lower_view_to_zero(nir_shader *shader)
lower_view_to_zero, NULL);
}
+static bool
+lower_alpha_to_coverage(nir_shader *shader)
+{
+ nir_builder b = nir_builder_create(nir_shader_get_entrypoint(shader));
+ b.cursor = nir_before_cf_list(&nir_shader_get_entrypoint(shader)->body);
+ nir_def *a2c_enabled =
+ nir_ine_imm(&b, nir_load_alpha_to_coverage_enable_ir3(&b), 0);
+
+ NIR_PASS(_, shader, nir_lower_alpha_to_coverage, false, a2c_enabled);
+ NIR_PASS(_, shader, tu_nir_lower_demote_samples);
+
+ return true;
+}
+
static void
shared_type_info(const struct glsl_type *type, unsigned *size, unsigned *align)
{
@@ -3073,6 +3087,9 @@ tu_shader_create(struct tu_device *dev,
ir3_nir_lower_io(nir);
+ if (key->emulate_alpha_to_coverage)
+ lower_alpha_to_coverage(nir);
+
struct ir3_const_allocations const_allocs = {};
NIR_PASS(_, nir, tu_lower_io, dev, shader, layout,
key->read_only_input_attachments, key->dynamic_renderpass,
diff --git a/src/freedreno/vulkan/tu_shader.h b/src/freedreno/vulkan/tu_shader.h
index 23b653c4e9c..02f23aa1b21 100644
--- a/src/freedreno/vulkan/tu_shader.h
+++ b/src/freedreno/vulkan/tu_shader.h
@@ -129,6 +129,7 @@ struct tu_shader_key {
bool robust_uniform_access2;
bool lower_view_index_to_device_index;
bool custom_resolve;
+ bool emulate_alpha_to_coverage;
enum ir3_wavesize_option api_wavesize, real_wavesize;
};
@@ -143,6 +144,9 @@ tu_nir_lower_multiview(nir_shader *nir, uint32_t mask, struct tu_device *dev);
bool
tu_nir_lower_ray_queries(nir_shader *nir);
+bool
+tu_nir_lower_demote_samples(nir_shader *nir);
+
nir_shader *
tu_spirv_to_nir(struct tu_device *dev,
void *mem_ctx,
diff --git a/src/util/00-mesa-defaults.conf b/src/util/00-mesa-defaults.conf
index 51453f6aaaa..fe39dc305eb 100644
--- a/src/util/00-mesa-defaults.conf
+++ b/src/util/00-mesa-defaults.conf
@@ -1388,6 +1388,16 @@ TODO: document the other workarounds.
default, the latter is used through this option.
-->
+
+
+
@@ -1406,6 +1416,9 @@ TODO: document the other workarounds.
+
+
+
diff --git a/src/util/driconf.h b/src/util/driconf.h
index 5e2b723efed..7d4f990c56f 100644
--- a/src/util/driconf.h
+++ b/src/util/driconf.h
@@ -688,6 +688,10 @@
DRI_CONF_OPT_B(tu_enable_softfloat32, def, \
"Enable softfloat emulation for float32 denormals")
+#define DRI_CONF_TU_EMULATE_ALPHA_TO_COVERAGE(def) \
+ DRI_CONF_OPT_B(tu_emulate_alpha_to_coverage, def, \
+ "Enable emulation of alpha-to-coverage")
+
/**
* \brief Honeykrisp specific configuration options
*/