Compare commits

...

5 commits

Author SHA1 Message Date
e4331fc703 [frontend] add sample shading slider
Signed-off-by: crueter <crueter@eden-emu.dev>
2025-08-13 15:40:19 +02:00
272d73baee [vk] Fix Sample Shading to ensure it is properly enabled
Even when sample shading was enabled via settings, the pipeline was
configured with minSampleShading = 0.0f, effectively disabling the
feature.

This patch sets minSampleShading = 1.0f when sample shading is enabled,
ensuring that per-sample shading is actually used as intended.
2025-08-13 15:40:19 +02:00
383fb23348
Revert image view usage flags regression introduced in 492d3856e8. (#241)
Maxwell format `VK_FORMAT_A8B8G8R8_SRGB_PACK32` does not support storage. However a `A8B8G8R8_UNORM` view is created for a image with that format which supports storage. The previous patch ignored image view format usage making it impossible for the pipeline to render to the texture.
This commit reverts the image usage override. However, there is still a mismatch between image format usage and image view format usage.

Reviewed-on: eden-emu/eden#241
Reviewed-by: Shinmegumi <shinmegumi@eden-emu.dev>
Co-authored-by: weakboson <weakboson@quantum-field.net>
Co-committed-by: weakboson <weakboson@quantum-field.net>
2025-08-13 15:35:57 +02:00
89d40c6302
[vk, texture_cache] MSAA ensure no more crash (#245)
> The shared_ptr<Image> capture ensures the temporary image outlives all queued GPU work (both the upload/download step and the MSAA compute copy). Without this, drivers read freed memory
> The temp image always has STORAGE & TRANSFER usage, matching what the compute MSAA path actually binds.
> Since this only a temp fix ontop of the previous commit, we only use the MSAA path for color; depth/stencil still use the original safe route.
> Should still be reworked though, as seen in the other MSAA commis that are open.

Reviewed-on: eden-emu/eden#245
Reviewed-by: crueter <crueter@eden-emu.dev>
Reviewed-by: Shinmegumi <shinmegumi@eden-emu.dev>
Co-authored-by: Maufeat <sahyno1996@gmail.com>
Co-committed-by: Maufeat <sahyno1996@gmail.com>
2025-08-13 01:42:56 +02:00
234e41193e
[desktop] Fix Default theme on Windows 10 (#246)
This fixes the default theme on Windows 10. Regression introduced in commit: [3f02d77](3f02d7713f)

Reviewed-on: eden-emu/eden#246
Reviewed-by: crueter <crueter@eden-emu.dev>
Co-authored-by: MaranBr <maranbr@outlook.com>
Co-committed-by: MaranBr <maranbr@outlook.com>
2025-08-12 20:01:55 +02:00
10 changed files with 125 additions and 63 deletions

View file

@ -39,6 +39,7 @@ enum class IntSetting(override val key: String) : AbstractIntSetting {
SOC_OVERLAY_POSITION("soc_overlay_position"),
MEMORY_LAYOUT("memory_layout_mode"),
FSR_SHARPENING_SLIDER("fsr_sharpening_slider"),
RENDERER_SAMPLE_SHADING_FRACTION("sample_shading_fraction"),
FAST_CPU_TIME("fast_cpu_time"),
CPU_TICKS("cpu_ticks"),
FAST_GPU_TIME("fast_gpu_time"),
@ -57,7 +58,7 @@ enum class IntSetting(override val key: String) : AbstractIntSetting {
OFFLINE_WEB_APPLET("offline_web_applet_mode"),
LOGIN_SHARE_APPLET("login_share_applet_mode"),
WIFI_WEB_AUTH_APPLET("wifi_web_auth_applet_mode"),
MY_PAGE_APPLET("my_page_applet_mode")
MY_PAGE_APPLET("my_page_applet_mode"),
;
override fun getInt(needsGlobal: Boolean): Int = NativeConfig.getInt(key, needsGlobal)

View file

@ -159,6 +159,14 @@ abstract class SettingsItem(
descriptionId = R.string.sample_shading_description
)
)
put(
SliderSetting(
IntSetting.RENDERER_SAMPLE_SHADING_FRACTION,
titleId = R.string.sample_shading_fraction,
descriptionId = R.string.sample_shading_fraction_description,
units = "%"
)
)
put(
SliderSetting(
ShortSetting.RENDERER_SPEED_LIMIT,

View file

@ -443,6 +443,7 @@ class SettingsFragmentPresenter(
add(BooleanSetting.RENDERER_PROVOKING_VERTEX.key)
add(BooleanSetting.RENDERER_DESCRIPTOR_INDEXING.key)
add(BooleanSetting.RENDERER_SAMPLE_SHADING.key)
add(IntSetting.RENDERER_SAMPLE_SHADING_FRACTION.key)
add(HeaderSetting(R.string.veil_renderer))
add(BooleanSetting.ENABLE_RAII.key)

View file

@ -82,6 +82,8 @@
<string name="descriptor_indexing_description">Improves texture and buffer handling, as well as the Maxwell translation layer. Supported by some Vulkan 1.1 GPUs and all Vulkan 1.2+ GPUs.</string>
<string name="sample_shading">Sample Shading</string>
<string name="sample_shading_description">Allows the fragment shader to execute per sample in a multi-sampled fragment instead once per fragment. Improves graphics quality at the cost of some performance. Only Vulkan 1.1+ devices support this extension.</string>
<string name="sample_shading_fraction">Sample Shading Fraction</string>
<string name="sample_shading_fraction_description">The intensity of the sample shading pass. Higher values improve quality more but also reduce performance to a greater extent.</string>
<string name="veil_renderer">Renderer</string>
<string name="enable_raii">RAII</string>

View file

@ -527,7 +527,17 @@ struct Values {
SwitchableSetting<bool> provoking_vertex{linkage, false, "provoking_vertex", Category::RendererExtensions};
SwitchableSetting<bool> descriptor_indexing{linkage, false, "descriptor_indexing", Category::RendererExtensions};
SwitchableSetting<bool> sample_shading{linkage, false, "sample_shading", Category::RendererExtensions};
SwitchableSetting<bool> sample_shading{linkage, false, "sample_shading", Category::RendererExtensions, Specialization::Paired};
SwitchableSetting<u32, true> sample_shading_fraction{linkage,
50,
0,
100,
"sample_shading_fraction",
Category::RendererExtensions,
Specialization::Scalar,
true,
false,
&sample_shading};
Setting<bool> renderer_debug{linkage, false, "debug", Category::RendererDebug};
Setting<bool> renderer_shader_feedback{linkage, false, "shader_feedback",

View file

@ -744,7 +744,7 @@ void GraphicsPipeline::MakePipeline(VkRenderPass render_pass) {
.flags = 0,
.rasterizationSamples = MaxwellToVK::MsaaMode(key.state.msaa_mode),
.sampleShadingEnable = Settings::values.sample_shading.GetValue() ? VK_TRUE : VK_FALSE,
.minSampleShading = 0.0f,
.minSampleShading = static_cast<float>(Settings::values.sample_shading_fraction.GetValue()) / 100.0f,
.pSampleMask = nullptr,
.alphaToCoverageEnable = key.state.alpha_to_coverage_enabled != 0 ? VK_TRUE : VK_FALSE,
.alphaToOneEnable = key.state.alpha_to_one_enabled != 0 ? VK_TRUE : VK_FALSE,

View file

@ -7,6 +7,7 @@
#include <algorithm>
#include <array>
#include <span>
#include <memory>
#include <vector>
#include <boost/container/small_vector.hpp>
@ -1538,7 +1539,7 @@ Image::Image(const VideoCommon::NullImageParams& params) : VideoCommon::ImageBas
Image::~Image() = default;
void Image::UploadMemory(VkBuffer buffer, VkDeviceSize offset,
std::span<const VideoCommon::BufferImageCopy> copies) {
std::span<const VideoCommon::BufferImageCopy> copies) {
// TODO: Move this to another API
const bool is_rescaled = True(flags & ImageFlagBits::Rescaled);
if (is_rescaled) {
@ -1546,70 +1547,89 @@ void Image::UploadMemory(VkBuffer buffer, VkDeviceSize offset,
}
// Handle MSAA upload if necessary
/* WARNING, TODO: This code uses some hacks, besides being fundamentally ugly
since tropic didn't want to touch it for a long time, so it needs a rewrite from someone better than me at vulkan.*/
if (info.num_samples > 1 && runtime->CanUploadMSAA()) {
// Only use MSAA copy pass for color formats
// TODO: Depth/stencil formats need special handling
if (aspect_mask == VK_IMAGE_ASPECT_COLOR_BIT) {
// Create a temporary non-MSAA image to upload the data first
ImageInfo temp_info = info;
temp_info.num_samples = 1;
/* WARNING, TODO: This code uses some hacks, besides being fundamentally ugly
since tropic didn't want to touch it for a long time, so it needs a rewrite from someone
better than me at vulkan. */
// CHANGE: Gate the MSAA path more strictly and only use it for color, when the pass and device
// support are available. Avoid running the MSAA path when prerequisites aren't met, preventing
// validation and runtime issues.
const bool wants_msaa_upload = info.num_samples > 1 &&
aspect_mask == VK_IMAGE_ASPECT_COLOR_BIT &&
runtime->CanUploadMSAA() && runtime->msaa_copy_pass != nullptr &&
runtime->device.IsStorageImageMultisampleSupported();
// Create image with same usage flags as the target image to avoid validation errors
VkImageCreateInfo image_ci = MakeImageCreateInfo(runtime->device, temp_info);
image_ci.usage = original_image.UsageFlags();
vk::Image temp_image = runtime->memory_allocator.CreateImage(image_ci);
if (wants_msaa_upload) {
// Create a temporary non-MSAA image to upload the data first
ImageInfo temp_info = info;
temp_info.num_samples = 1;
// Upload to the temporary non-MSAA image
scheduler->RequestOutsideRenderPassOperationContext();
auto vk_copies = TransformBufferImageCopies(copies, offset, aspect_mask);
const VkBuffer src_buffer = buffer;
const VkImage temp_vk_image = *temp_image;
const VkImageAspectFlags vk_aspect_mask = aspect_mask;
scheduler->Record([src_buffer, temp_vk_image, vk_aspect_mask, vk_copies](vk::CommandBuffer cmdbuf) {
CopyBufferToImage(cmdbuf, src_buffer, temp_vk_image, vk_aspect_mask, false, vk_copies);
});
// CHANGE: Build a fresh VkImageCreateInfo with robust usage flags for the temp image.
// Using the target image's usage as-is could miss STORAGE/TRANSFER bits and trigger validation errors.
VkImageCreateInfo image_ci = MakeImageCreateInfo(runtime->device, temp_info);
image_ci.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT;
// Use MSAACopyPass to convert from non-MSAA to MSAA
std::vector<VideoCommon::ImageCopy> image_copies;
for (const auto& copy : copies) {
VideoCommon::ImageCopy image_copy;
image_copy.src_offset = {0, 0, 0}; // Use zero offset for source
image_copy.dst_offset = copy.image_offset;
image_copy.src_subresource = copy.image_subresource;
image_copy.dst_subresource = copy.image_subresource;
image_copy.extent = copy.image_extent;
image_copies.push_back(image_copy);
}
// CHANGE: The previous stack-allocated wrapper was destroyed at function exit,
// which could destroy VkImage before the GPU used it.
auto temp_wrapper = std::make_shared<Image>(*runtime, temp_info, 0, 0);
temp_wrapper->original_image = runtime->memory_allocator.CreateImage(image_ci);
temp_wrapper->current_image = &Image::original_image;
temp_wrapper->aspect_mask = VK_IMAGE_ASPECT_COLOR_BIT;
temp_wrapper->initialized = true;
// wrapper image for the temporary image
Image temp_wrapper(*runtime, temp_info, 0, 0);
temp_wrapper.original_image = std::move(temp_image);
temp_wrapper.current_image = &Image::original_image;
temp_wrapper.aspect_mask = aspect_mask;
temp_wrapper.initialized = true;
// Use MSAACopyPass to convert from non-MSAA to MSAA
runtime->msaa_copy_pass->CopyImage(*this, temp_wrapper, image_copies, false);
std::exchange(initialized, true);
return;
}
// For depth/stencil formats, fall back to regular upload
} else {
// Regular non-MSAA upload
// Upload to the temporary non-MSAA image
scheduler->RequestOutsideRenderPassOperationContext();
auto vk_copies = TransformBufferImageCopies(copies, offset, aspect_mask);
auto vk_copies = TransformBufferImageCopies(copies, offset, temp_wrapper->aspect_mask);
const VkBuffer src_buffer = buffer;
const VkImage vk_image = *original_image;
const VkImageAspectFlags vk_aspect_mask = aspect_mask;
const bool is_initialized = std::exchange(initialized, true);
scheduler->Record([src_buffer, vk_image, vk_aspect_mask, is_initialized,
vk_copies](vk::CommandBuffer cmdbuf) {
CopyBufferToImage(cmdbuf, src_buffer, vk_image, vk_aspect_mask, is_initialized, vk_copies);
const VkImage temp_vk_image = *temp_wrapper->original_image;
const VkImageAspectFlags vk_aspect_mask = temp_wrapper->aspect_mask;
scheduler->Record([src_buffer, temp_vk_image, vk_aspect_mask, vk_copies,
// CHANGE: capture shared_ptr to pin lifetime through submission
keep = temp_wrapper](vk::CommandBuffer cmdbuf) {
CopyBufferToImage(cmdbuf, src_buffer, temp_vk_image, vk_aspect_mask, false, vk_copies);
});
// Use MSAACopyPass to convert from non-MSAA to MSAA
// CHANGE: only lifetime and usage were fixed.
std::vector<VideoCommon::ImageCopy> image_copies;
image_copies.reserve(copies.size());
for (const auto& copy : copies) {
VideoCommon::ImageCopy image_copy;
image_copy.src_offset = {0, 0, 0}; // Use zero offset for source
image_copy.dst_offset = copy.image_offset;
image_copy.src_subresource = copy.image_subresource;
image_copy.dst_subresource = copy.image_subresource;
image_copy.extent = copy.image_extent;
image_copies.push_back(image_copy);
}
runtime->msaa_copy_pass->CopyImage(*this, *temp_wrapper, image_copies,
/*msaa_to_non_msaa=*/false);
std::exchange(initialized, true);
// CHANGE: Add a no-op recording that captures temp_wrapper to ensure it stays alive
// at least until commands are submitted/recorded.
scheduler->Record([keep = std::move(temp_wrapper)](vk::CommandBuffer) {});
// CHANGE: Restore scaling before returning from the MSAA path.
if (is_rescaled) {
ScaleUp();
}
return;
}
// Regular non-MSAA upload (original behavior preserved)
scheduler->RequestOutsideRenderPassOperationContext();
auto vk_copies = TransformBufferImageCopies(copies, offset, aspect_mask);
const VkBuffer src_buffer = buffer;
const VkImage vk_image = *original_image;
const VkImageAspectFlags vk_aspect_mask = aspect_mask;
const bool is_initialized = std::exchange(initialized, true);
scheduler->Record([src_buffer, vk_image, vk_aspect_mask, is_initialized,
vk_copies](vk::CommandBuffer cmdbuf) {
CopyBufferToImage(cmdbuf, src_buffer, vk_image, vk_aspect_mask, is_initialized, vk_copies);
});
if (is_rescaled) {
ScaleUp();
}
@ -1994,10 +2014,15 @@ ImageView::ImageView(TextureCacheRuntime& runtime, const VideoCommon::ImageViewI
}
}
const auto format_info = MaxwellToVK::SurfaceFormat(*device, FormatType::Optimal, true, format);
if (ImageUsageFlags(format_info, format) != image.UsageFlags()) {
LOG_WARNING(Render_Vulkan,
"Image view format {} has different usage flags than image format {}", format,
image.info.format);
}
const VkImageViewUsageCreateInfo image_view_usage{
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO,
.pNext = nullptr,
.usage = image.UsageFlags(),
.usage = ImageUsageFlags(format_info, format),
};
const VkImageViewCreateInfo create_info{
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,

View file

@ -36,7 +36,16 @@ void ConfigureGraphicsExtensions::Setup(const ConfigurationShared::Builder& buil
for (auto setting :
Settings::values.linkage.by_category[Settings::Category::RendererExtensions]) {
ConfigurationShared::Widget* widget = builder.BuildWidget(setting, apply_funcs);
ConfigurationShared::Widget* widget = [&]() {
if (setting->Id() == Settings::values.sample_shading_fraction.Id()) {
// TODO(crueter): should support this natively perhaps?
return builder.BuildWidget(
setting, apply_funcs, ConfigurationShared::RequestType::Slider, true,
1.0f, nullptr, tr("%", "Sample Shading percentage (e.g. 50%)"));
} else {
return builder.BuildWidget(setting, apply_funcs);
}
}();
if (widget == nullptr) {
continue;

View file

@ -360,8 +360,10 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QWidget* parent)
tr("Improves texture & buffer handling and the Maxwell translation layer.\n"
"Some Vulkan 1.1+ and all 1.2+ devices support this extension."));
INSERT(Settings, sample_shading, QString(), QString());
INSERT(Settings,
sample_shading,
sample_shading_fraction,
tr("Sample Shading"),
tr("Allows the fragment shader to execute per sample in a multi-sampled fragment "
"instead once per fragment. Improves graphics quality at the cost of some performance.\n"

View file

@ -444,7 +444,6 @@ GMainWindow::GMainWindow(bool has_broken_vulkan)
ui->setupUi(this);
statusBar()->hide();
// Check dark mode before a theme is loaded
startup_icon_theme = QIcon::themeName();
// fallback can only be set once, colorful theme icons are okay on both light/dark
QIcon::setFallbackThemeName(QStringLiteral("colorful"));
@ -5475,6 +5474,10 @@ void GMainWindow::UpdateUITheme() {
current_theme = default_theme;
}
#ifdef _WIN32
QIcon::setThemeName(current_theme);
AdjustLinkColor();
#else
if (current_theme == QStringLiteral("default") || current_theme == QStringLiteral("colorful")) {
QIcon::setThemeName(current_theme == QStringLiteral("colorful") ? current_theme
: startup_icon_theme);
@ -5487,6 +5490,7 @@ void GMainWindow::UpdateUITheme() {
QIcon::setThemeSearchPaths(QStringList(QStringLiteral(":/icons")));
AdjustLinkColor();
}
#endif
if (current_theme != default_theme) {
QString theme_uri{QStringLiteral(":%1/style.qss").arg(current_theme)};