diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h
index 014b4a318e..f0c253dffb 100644
--- a/src/video_core/buffer_cache/buffer_cache.h
+++ b/src/video_core/buffer_cache/buffer_cache.h
@@ -806,6 +806,58 @@ void BufferCache
::UpdateVertexBufferSlot(u32 index, const Binding& binding) {
template
void BufferCache::BindHostVertexBuffers() {
+ bool needs_vertex_input_refresh = false;
+ bool use_vertex_input_dynamic_state = false;
+ bool use_dynamic_stride = false;
+ u32 max_dynamic_stride = 0;
+ std::array min_dynamic_stride{};
+ if constexpr (!IS_OPENGL) {
+ use_vertex_input_dynamic_state = runtime.UsesVertexInputDynamicState();
+ if (use_vertex_input_dynamic_state) {
+ for (u32 index = 0; index < NUM_VERTEX_BUFFERS; ++index) {
+ if (maxwell3d->dirty.flags[Dirty::VertexBuffer0 + index]) {
+ needs_vertex_input_refresh = true;
+ break;
+ }
+ }
+ }
+
+ use_dynamic_stride = runtime.UsesDynamicVertexBindingStride();
+ if (use_dynamic_stride) {
+ max_dynamic_stride = runtime.GetMaxVertexInputBindingStride();
+ for (const auto& attribute : maxwell3d->regs.vertex_attrib_format) {
+ if (attribute.constant != 0) {
+ continue;
+ }
+ const u32 binding = attribute.buffer;
+ if (binding >= NUM_VERTEX_BUFFERS) {
+ continue;
+ }
+ const u32 extent = attribute.offset + attribute.SizeInBytes();
+ min_dynamic_stride[binding] = (std::max)(min_dynamic_stride[binding], extent);
+ }
+ }
+ }
+
+ const auto sanitize_stride = [&](u32 binding, u32 stride) -> u32 {
+ if constexpr (IS_OPENGL) {
+ return stride;
+ } else {
+ if (!use_dynamic_stride || stride == 0) {
+ return stride;
+ }
+ const u32 min_stride = min_dynamic_stride[binding];
+ const u32 required_stride = (std::max)(stride, min_stride);
+ if (required_stride <= max_dynamic_stride) {
+ return required_stride;
+ }
+ if (min_stride > max_dynamic_stride) {
+ return 0;
+ }
+ return max_dynamic_stride;
+ }
+ };
+
#ifdef ANDROID
const bool use_optimized_vertex_buffers = Settings::values.use_optimized_vertex_buffers.GetValue();
#else
@@ -838,7 +890,8 @@ void BufferCache::BindHostVertexBuffers() {
continue;
}
flags[Dirty::VertexBuffer0 + index] = false;
- const u32 stride = maxwell3d->regs.vertex_streams[index].stride;
+ const u32 stride =
+ sanitize_stride(index, maxwell3d->regs.vertex_streams[index].stride);
const u32 offset = buffer.Offset(binding.device_addr);
buffer.MarkUsage(offset, binding.size);
if (!bindings.buffers.empty() && index != last_index + 1) {
@@ -881,7 +934,8 @@ void BufferCache
::BindHostVertexBuffers() {
const Binding& binding = channel_state->vertex_buffers[index];
Buffer& buffer = slot_buffers[binding.buffer_id];
- const u32 stride = maxwell3d->regs.vertex_streams[index].stride;
+ const u32 stride =
+ sanitize_stride(index, maxwell3d->regs.vertex_streams[index].stride);
const u32 offset = buffer.Offset(binding.device_addr);
buffer.MarkUsage(offset, binding.size);
@@ -893,6 +947,12 @@ void BufferCache
::BindHostVertexBuffers() {
runtime.BindVertexBuffers(host_bindings);
}
}
+
+ if constexpr (!IS_OPENGL) {
+ if (needs_vertex_input_refresh) {
+ runtime.NotifyVertexInputBindingChange();
+ }
+ }
}
template
diff --git a/src/video_core/renderer_vulkan/vk_buffer_cache.h b/src/video_core/renderer_vulkan/vk_buffer_cache.h
index c8dd68e3dc..9f19f802f3 100644
--- a/src/video_core/renderer_vulkan/vk_buffer_cache.h
+++ b/src/video_core/renderer_vulkan/vk_buffer_cache.h
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
@@ -161,6 +161,28 @@ public:
return max_dynamic_storage_buffers;
}
+ bool UsesDynamicVertexBindingStride() const {
+ return device.IsExtExtendedDynamicStateSupported() && !use_vertex_input_dynamic_state;
+ }
+
+ bool UsesVertexInputDynamicState() const {
+ return use_vertex_input_dynamic_state;
+ }
+
+ void NotifyVertexInputBindingChange() {
+ needs_vertex_input_refresh = true;
+ }
+
+ bool ConsumeVertexInputBindingChange() {
+ const bool refresh = needs_vertex_input_refresh;
+ needs_vertex_input_refresh = false;
+ return refresh;
+ }
+
+ u32 GetMaxVertexInputBindingStride() const {
+ return device.GetMaxVertexInputBindingStride();
+ }
+
private:
void BindBuffer(VkBuffer buffer, u32 offset, u32 size) {
guest_descriptor_queue.AddBuffer(buffer, offset, size);
@@ -184,6 +206,7 @@ private:
QuadIndexedPass quad_index_pass;
bool use_vertex_input_dynamic_state = false;
+ bool needs_vertex_input_refresh = false;
bool limit_dynamic_storage_buffers = false;
u32 max_dynamic_storage_buffers = (std::numeric_limits::max)();
};
diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h
index 1a41e50a36..e3e2737e3b 100644
--- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h
+++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h
@@ -82,6 +82,9 @@ public:
const std::array& infos);
bool HasDynamicVertexInput() const noexcept { return key.state.dynamic_vertex_input; }
+ bool UsesVertexAttribute(size_t index) const noexcept {
+ return index < Maxwell::NumVertexAttributes && stage_infos[0].loads.Generic(index);
+ }
bool SupportsAlphaToCoverage() const noexcept {
return fragment_has_color0_output;
}
diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
index ad431c9ac5..108e6d649e 100644
--- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp
+++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
@@ -1014,20 +1014,6 @@ void RasterizerVulkan::UpdateDynamicStates() {
// EDS3 Enables: LogicOpEnable, DepthClamp, LineStipple, ConservativeRaster
if (device.IsExtExtendedDynamicState3EnablesSupported()) {
- using namespace Tegra::Engines;
- // AMD Workaround: LogicOp incompatible with float render targets
- if (device.GetDriverID() == VkDriverIdKHR::VK_DRIVER_ID_AMD_OPEN_SOURCE ||
- device.GetDriverID() == VkDriverIdKHR::VK_DRIVER_ID_AMD_PROPRIETARY) {
- const auto has_float = std::any_of(
- regs.vertex_attrib_format.begin(), regs.vertex_attrib_format.end(),
- [](const auto& attrib) {
- return attrib.type == Maxwell3D::Regs::VertexAttribute::Type::Float;
- }
- );
- if (regs.logic_op.enable) {
- regs.logic_op.enable = static_cast(!has_float);
- }
- }
UpdateLogicOpEnable(regs);
UpdateDepthClampEnable(regs);
UpdateLineRasterizationMode(regs);
@@ -1045,7 +1031,7 @@ void RasterizerVulkan::UpdateDynamicStates() {
// Vertex Input Dynamic State: Independent from EDS levels
if (device.IsExtVertexInputDynamicStateSupported()) {
if (auto* gp = pipeline_cache.CurrentGraphicsPipeline(); gp && gp->HasDynamicVertexInput()) {
- UpdateVertexInput(regs);
+ UpdateVertexInput(regs, *gp);
}
}
}
@@ -1725,9 +1711,11 @@ void RasterizerVulkan::UpdateStencilTestEnable(Tegra::Engines::Maxwell3D::Regs&
});
}
-void RasterizerVulkan::UpdateVertexInput(Tegra::Engines::Maxwell3D::Regs& regs) {
+void RasterizerVulkan::UpdateVertexInput(Tegra::Engines::Maxwell3D::Regs& regs,
+ const GraphicsPipeline& pipeline) {
auto& dirty{maxwell3d->dirty.flags};
- if (!dirty[Dirty::VertexInput]) {
+ const bool force_vertex_input_refresh = buffer_cache_runtime.ConsumeVertexInputBindingChange();
+ if (!dirty[Dirty::VertexInput] && !force_vertex_input_refresh) {
return;
}
dirty[Dirty::VertexInput] = false;
@@ -1758,6 +1746,9 @@ void RasterizerVulkan::UpdateVertexInput(Tegra::Engines::Maxwell3D::Regs& regs)
if (attribute.constant) {
continue;
}
+ if (!pipeline.UsesVertexAttribute(index)) {
+ continue;
+ }
const size_t binding{attribute.buffer};
if (binding >= max_vertex_bindings) {
diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.h b/src/video_core/renderer_vulkan/vk_rasterizer.h
index b689c6b660..b3bf6c8b7c 100644
--- a/src/video_core/renderer_vulkan/vk_rasterizer.h
+++ b/src/video_core/renderer_vulkan/vk_rasterizer.h
@@ -191,7 +191,8 @@ private:
void UpdateLogicOp(Tegra::Engines::Maxwell3D::Regs& regs);
void UpdateBlending(Tegra::Engines::Maxwell3D::Regs& regs);
- void UpdateVertexInput(Tegra::Engines::Maxwell3D::Regs& regs);
+ void UpdateVertexInput(Tegra::Engines::Maxwell3D::Regs& regs,
+ const GraphicsPipeline& pipeline);
Tegra::GPU& gpu;
Tegra::MaxwellDeviceMemoryManager& device_memory;
diff --git a/src/video_core/renderer_vulkan/vk_scheduler.cpp b/src/video_core/renderer_vulkan/vk_scheduler.cpp
index 226619d8d6..f974505b2d 100644
--- a/src/video_core/renderer_vulkan/vk_scheduler.cpp
+++ b/src/video_core/renderer_vulkan/vk_scheduler.cpp
@@ -153,11 +153,17 @@ void Scheduler::RequestOutsideRenderPassOperationContext() {
}
bool Scheduler::UpdateGraphicsPipeline(GraphicsPipeline* pipeline) {
+ const auto consume_eds_refresh = [this] {
+ if (!state.needs_state_enable_refresh) {
+ return;
+ }
+ state_tracker.InvalidateExtendedDynamicStateFlags();
+ state.needs_state_enable_refresh = false;
+ };
+
if (state.graphics_pipeline == pipeline) {
- if (pipeline && pipeline->UsesExtendedDynamicState() &&
- state.needs_state_enable_refresh) {
- state_tracker.InvalidateStateEnableFlag();
- state.needs_state_enable_refresh = false;
+ if (pipeline && pipeline->UsesExtendedDynamicState()) {
+ consume_eds_refresh();
}
return false;
}
@@ -170,9 +176,8 @@ bool Scheduler::UpdateGraphicsPipeline(GraphicsPipeline* pipeline) {
if (!pipeline->UsesExtendedDynamicState()) {
state.needs_state_enable_refresh = true;
- } else if (state.needs_state_enable_refresh) {
- state_tracker.InvalidateStateEnableFlag();
- state.needs_state_enable_refresh = false;
+ } else {
+ consume_eds_refresh();
}
return true;
diff --git a/src/video_core/renderer_vulkan/vk_state_tracker.h b/src/video_core/renderer_vulkan/vk_state_tracker.h
index 74bae9e181..f7c61ed7ef 100644
--- a/src/video_core/renderer_vulkan/vk_state_tracker.h
+++ b/src/video_core/renderer_vulkan/vk_state_tracker.h
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
@@ -101,6 +101,37 @@ public:
(*flags)[Dirty::StateEnable] = true;
}
+ void InvalidateExtendedDynamicStateFlags() {
+ InvalidateStateEnableFlag();
+
+ (*flags)[Dirty::CullMode] = true;
+ (*flags)[Dirty::DepthCompareOp] = true;
+ (*flags)[Dirty::FrontFace] = true;
+ (*flags)[Dirty::StencilOp] = true;
+ (*flags)[Dirty::DepthBoundsEnable] = true;
+ (*flags)[Dirty::DepthTestEnable] = true;
+ (*flags)[Dirty::DepthWriteEnable] = true;
+ (*flags)[Dirty::StencilTestEnable] = true;
+
+ (*flags)[Dirty::PrimitiveRestartEnable] = true;
+ (*flags)[Dirty::RasterizerDiscardEnable] = true;
+ (*flags)[Dirty::DepthBiasEnable] = true;
+ (*flags)[Dirty::LogicOp] = true;
+
+ (*flags)[Dirty::LogicOpEnable] = true;
+ (*flags)[Dirty::DepthClampEnable] = true;
+ (*flags)[Dirty::LineRasterizationMode] = true;
+ (*flags)[Dirty::LineStippleEnable] = true;
+ (*flags)[Dirty::ConservativeRasterizationMode] = true;
+ (*flags)[Dirty::AlphaToCoverageEnable] = true;
+ (*flags)[Dirty::AlphaToOneEnable] = true;
+
+ (*flags)[Dirty::Blending] = true;
+ (*flags)[Dirty::ColorMask] = true;
+ (*flags)[Dirty::BlendEnable] = true;
+ (*flags)[Dirty::BlendEquations] = true;
+ }
+
bool TouchViewports() {
const bool dirty_viewports = Exchange(Dirty::Viewports, false);
const bool rescale_viewports = Exchange(VideoCommon::Dirty::RescaleViewports, false);
diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h
index 8914ef0eb3..7777f945f4 100644
--- a/src/video_core/vulkan_common/vulkan_device.h
+++ b/src/video_core/vulkan_common/vulkan_device.h
@@ -783,6 +783,10 @@ public:
return properties.properties.limits.maxVertexInputBindings;
}
+ u32 GetMaxVertexInputBindingStride() const {
+ return properties.properties.limits.maxVertexInputBindingStride;
+ }
+
u32 GetMaxViewports() const {
return properties.properties.limits.maxViewports;
}