Revert some wip changes
This commit is contained in:
parent
808276b48a
commit
b695ca5a2a
17 changed files with 45 additions and 1135 deletions
|
@ -18,7 +18,6 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
|
||||||
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"),
|
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"),
|
||||||
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"),
|
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"),
|
||||||
RENDERER_DEBUG("debug"),
|
RENDERER_DEBUG("debug"),
|
||||||
RENDERER_ENHANCED_SHADER_BUILDING("use_enhanced_shader_building"),
|
|
||||||
PICTURE_IN_PICTURE("picture_in_picture"),
|
PICTURE_IN_PICTURE("picture_in_picture"),
|
||||||
USE_CUSTOM_RTC("custom_rtc_enabled"),
|
USE_CUSTOM_RTC("custom_rtc_enabled"),
|
||||||
BLACK_BACKGROUNDS("black_backgrounds"),
|
BLACK_BACKGROUNDS("black_backgrounds"),
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||||
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
@ -644,21 +643,11 @@ struct Values {
|
||||||
|
|
||||||
// Add-Ons
|
// Add-Ons
|
||||||
std::map<u64, std::vector<std::string>> disabled_addons;
|
std::map<u64, std::vector<std::string>> disabled_addons;
|
||||||
|
|
||||||
// Renderer Advanced Settings
|
|
||||||
SwitchableSetting<bool> use_enhanced_shader_building{linkage, false, "Enhanced Shader Building",
|
|
||||||
Category::RendererAdvanced};
|
|
||||||
|
|
||||||
// Add a new setting for shader compilation priority
|
|
||||||
SwitchableSetting<int> shader_compilation_priority{linkage, 0, "Shader Compilation Priority",
|
|
||||||
Category::RendererAdvanced};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
extern Values values;
|
extern Values values;
|
||||||
|
|
||||||
void UpdateGPUAccuracy();
|
void UpdateGPUAccuracy();
|
||||||
// boold isGPULevelNormal();
|
|
||||||
// TODO: ZEP
|
|
||||||
bool IsGPULevelExtreme();
|
bool IsGPULevelExtreme();
|
||||||
bool IsGPULevelHigh();
|
bool IsGPULevelHigh();
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project
|
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project
|
||||||
# SPDX-FileCopyrightText: 2025 Citron Emulator Project
|
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
add_subdirectory(host_shaders)
|
add_subdirectory(host_shaders)
|
||||||
|
@ -246,8 +245,6 @@ add_library(video_core STATIC
|
||||||
renderer_vulkan/vk_turbo_mode.h
|
renderer_vulkan/vk_turbo_mode.h
|
||||||
renderer_vulkan/vk_update_descriptor.cpp
|
renderer_vulkan/vk_update_descriptor.cpp
|
||||||
renderer_vulkan/vk_update_descriptor.h
|
renderer_vulkan/vk_update_descriptor.h
|
||||||
renderer_vulkan/vk_texture_manager.cpp
|
|
||||||
renderer_vulkan/vk_texture_manager.h
|
|
||||||
shader_cache.cpp
|
shader_cache.cpp
|
||||||
shader_cache.h
|
shader_cache.h
|
||||||
shader_environment.cpp
|
shader_environment.cpp
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||||
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <chrono>
|
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
#include "common/settings.h" // for enum class Settings::ShaderBackend
|
#include "common/settings.h" // for enum class Settings::ShaderBackend
|
||||||
#include "common/thread_worker.h"
|
#include "common/thread_worker.h"
|
||||||
|
@ -237,68 +234,26 @@ GraphicsPipeline::GraphicsPipeline(const Device& device, TextureCache& texture_c
|
||||||
auto func{[this, sources_ = std::move(sources), sources_spirv_ = std::move(sources_spirv),
|
auto func{[this, sources_ = std::move(sources), sources_spirv_ = std::move(sources_spirv),
|
||||||
shader_notify, backend, in_parallel,
|
shader_notify, backend, in_parallel,
|
||||||
force_context_flush](ShaderContext::Context*) mutable {
|
force_context_flush](ShaderContext::Context*) mutable {
|
||||||
// Track time for shader compilation for possible performance tuning
|
|
||||||
const auto start_time = std::chrono::high_resolution_clock::now();
|
|
||||||
|
|
||||||
// Prepare compilation steps for all shader stages
|
|
||||||
std::vector<std::function<void()>> compilation_steps;
|
|
||||||
compilation_steps.reserve(5); // Maximum number of shader stages
|
|
||||||
|
|
||||||
// Prepare all compilation steps first to better distribute work
|
|
||||||
for (size_t stage = 0; stage < 5; ++stage) {
|
for (size_t stage = 0; stage < 5; ++stage) {
|
||||||
switch (backend) {
|
switch (backend) {
|
||||||
case Settings::ShaderBackend::Glsl:
|
case Settings::ShaderBackend::Glsl:
|
||||||
if (!sources_[stage].empty()) {
|
if (!sources_[stage].empty()) {
|
||||||
compilation_steps.emplace_back([this, stage, source = sources_[stage]]() {
|
source_programs[stage] = CreateProgram(sources_[stage], Stage(stage));
|
||||||
source_programs[stage] = CreateProgram(source, Stage(stage));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Settings::ShaderBackend::Glasm:
|
case Settings::ShaderBackend::Glasm:
|
||||||
if (!sources_[stage].empty()) {
|
if (!sources_[stage].empty()) {
|
||||||
compilation_steps.emplace_back([this, stage, source = sources_[stage]]() {
|
assembly_programs[stage] =
|
||||||
assembly_programs[stage] = CompileProgram(source, AssemblyStage(stage));
|
CompileProgram(sources_[stage], AssemblyStage(stage));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Settings::ShaderBackend::SpirV:
|
case Settings::ShaderBackend::SpirV:
|
||||||
if (!sources_spirv_[stage].empty()) {
|
if (!sources_spirv_[stage].empty()) {
|
||||||
compilation_steps.emplace_back([this, stage, source = sources_spirv_[stage]]() {
|
source_programs[stage] = CreateProgram(sources_spirv_[stage], Stage(stage));
|
||||||
source_programs[stage] = CreateProgram(source, Stage(stage));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're running in parallel, use high-priority execution for vertex and fragment shaders
|
|
||||||
// as these are typically needed first by the renderer
|
|
||||||
if (in_parallel && compilation_steps.size() > 1) {
|
|
||||||
// Execute vertex (0) and fragment (4) shaders first if they exist
|
|
||||||
for (size_t priority_stage : {0, 4}) {
|
|
||||||
for (size_t i = 0; i < compilation_steps.size(); ++i) {
|
|
||||||
if ((i == priority_stage || (priority_stage == 0 && i <= 1)) && i < compilation_steps.size()) {
|
|
||||||
compilation_steps[i]();
|
|
||||||
compilation_steps[i] = [](){}; // Mark as executed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute all remaining compilation steps
|
|
||||||
for (auto& step : compilation_steps) {
|
|
||||||
step(); // Will do nothing for already executed steps
|
|
||||||
}
|
|
||||||
|
|
||||||
// Performance measurement for possible logging or optimization
|
|
||||||
const auto end_time = std::chrono::high_resolution_clock::now();
|
|
||||||
const auto compilation_time = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
||||||
end_time - start_time).count();
|
|
||||||
|
|
||||||
if (compilation_time > 50) { // Only log slow compilations
|
|
||||||
LOG_DEBUG(Render_OpenGL, "Shader compilation took {}ms", compilation_time);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (force_context_flush || in_parallel) {
|
if (force_context_flush || in_parallel) {
|
||||||
std::scoped_lock lock{built_mutex};
|
std::scoped_lock lock{built_mutex};
|
||||||
built_fence.Create();
|
built_fence.Create();
|
||||||
|
@ -668,41 +623,15 @@ void GraphicsPipeline::WaitForBuild() {
|
||||||
is_built = true;
|
is_built = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GraphicsPipeline::IsBuilt() const noexcept {
|
bool GraphicsPipeline::IsBuilt() noexcept {
|
||||||
if (is_built) {
|
if (is_built) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!built_fence.handle) {
|
if (built_fence.handle == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
is_built = built_fence.IsSignaled();
|
||||||
// Check if the async build has finished by polling the fence
|
return is_built;
|
||||||
const GLsync sync = built_fence.handle;
|
|
||||||
const GLuint result = glClientWaitSync(sync, 0, 0);
|
|
||||||
if (result == GL_ALREADY_SIGNALED || result == GL_CONDITION_SATISFIED) {
|
|
||||||
// Mark this as mutable even though we're in a const method - this is
|
|
||||||
// essentially a cached value update which is acceptable
|
|
||||||
const_cast<GraphicsPipeline*>(this)->is_built = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For better performance tracking, capture time spent waiting for shaders
|
|
||||||
static thread_local std::chrono::high_resolution_clock::time_point last_shader_wait_log;
|
|
||||||
static thread_local u32 shader_wait_count = 0;
|
|
||||||
|
|
||||||
auto now = std::chrono::high_resolution_clock::now();
|
|
||||||
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
|
|
||||||
now - last_shader_wait_log).count();
|
|
||||||
|
|
||||||
// Log shader compilation status periodically to help diagnose performance issues
|
|
||||||
if (elapsed >= 5) { // Log every 5 seconds
|
|
||||||
shader_wait_count++;
|
|
||||||
LOG_DEBUG(Render_OpenGL, "Waiting for async shader compilation... (count={})",
|
|
||||||
shader_wait_count);
|
|
||||||
last_shader_wait_log = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace OpenGL
|
} // namespace OpenGL
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||||
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
@ -103,7 +102,7 @@ public:
|
||||||
return uses_local_memory;
|
return uses_local_memory;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool IsBuilt() const noexcept;
|
[[nodiscard]] bool IsBuilt() noexcept;
|
||||||
|
|
||||||
template <typename Spec>
|
template <typename Spec>
|
||||||
static auto MakeConfigureSpecFunc() {
|
static auto MakeConfigureSpecFunc() {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
@ -609,33 +608,9 @@ std::unique_ptr<ComputePipeline> ShaderCache::CreateComputePipeline(
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<ShaderWorker> ShaderCache::CreateWorkers() const {
|
std::unique_ptr<ShaderWorker> ShaderCache::CreateWorkers() const {
|
||||||
// Calculate optimal number of workers based on available CPU cores
|
return std::make_unique<ShaderWorker>(std::max(std::thread::hardware_concurrency(), 2U) - 1,
|
||||||
// Leave at least 1 core for main thread and other operations
|
"GlShaderBuilder",
|
||||||
// Use more cores for more parallelism in shader compilation
|
[this] { return Context{emu_window}; });
|
||||||
const u32 num_worker_threads = std::max(std::thread::hardware_concurrency(), 2U);
|
|
||||||
const u32 optimal_workers = num_worker_threads <= 3 ?
|
|
||||||
num_worker_threads - 1 : // On dual/quad core, leave 1 core free
|
|
||||||
num_worker_threads - 2; // On 6+ core systems, leave 2 cores free for other tasks
|
|
||||||
|
|
||||||
auto worker = std::make_unique<ShaderWorker>(
|
|
||||||
optimal_workers,
|
|
||||||
"GlShaderBuilder",
|
|
||||||
[this] {
|
|
||||||
auto context = Context{emu_window};
|
|
||||||
|
|
||||||
// Apply thread priority based on settings
|
|
||||||
// This allows users to control how aggressive shader compilation is
|
|
||||||
const int priority = Settings::values.shader_compilation_priority.GetValue();
|
|
||||||
if (priority != 0) {
|
|
||||||
Common::SetCurrentThreadPriority(
|
|
||||||
priority > 0 ? Common::ThreadPriority::High : Common::ThreadPriority::Low);
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return worker;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace OpenGL
|
} // namespace OpenGL
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
@ -127,8 +126,6 @@ RendererVulkan::RendererVulkan(Core::Frontend::EmuWindow& emu_window,
|
||||||
rasterizer(render_window, gpu, device_memory, device, memory_allocator, state_tracker,
|
rasterizer(render_window, gpu, device_memory, device, memory_allocator, state_tracker,
|
||||||
scheduler),
|
scheduler),
|
||||||
hybrid_memory(std::make_unique<HybridMemory>(device, memory_allocator)),
|
hybrid_memory(std::make_unique<HybridMemory>(device, memory_allocator)),
|
||||||
texture_manager(device, memory_allocator),
|
|
||||||
shader_manager(device),
|
|
||||||
applet_frame() {
|
applet_frame() {
|
||||||
if (Settings::values.renderer_force_max_clock.GetValue() && device.ShouldBoostClocks()) {
|
if (Settings::values.renderer_force_max_clock.GetValue() && device.ShouldBoostClocks()) {
|
||||||
turbo_mode.emplace(instance, dld);
|
turbo_mode.emplace(instance, dld);
|
||||||
|
@ -178,41 +175,7 @@ RendererVulkan::RendererVulkan(Core::Frontend::EmuWindow& emu_window,
|
||||||
LOG_INFO(Render_Vulkan, "Fault-managed memory not supported on this platform");
|
LOG_INFO(Render_Vulkan, "Fault-managed memory not supported on this platform");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize enhanced shader compilation system
|
|
||||||
shader_manager.SetScheduler(&scheduler);
|
|
||||||
LOG_INFO(Render_Vulkan, "Enhanced shader compilation system initialized");
|
|
||||||
|
|
||||||
// Preload common shaders if enabled
|
|
||||||
if (Settings::values.use_asynchronous_shaders.GetValue()) {
|
|
||||||
// Use a simple shader directory path - can be updated to match Citron's actual path structure
|
|
||||||
const std::string shader_dir = "./shaders";
|
|
||||||
std::vector<std::string> common_shaders;
|
|
||||||
|
|
||||||
// Add paths to common shaders that should be preloaded
|
|
||||||
// These will be compiled in parallel for faster startup
|
|
||||||
try {
|
|
||||||
if (std::filesystem::exists(shader_dir)) {
|
|
||||||
for (const auto& entry : std::filesystem::directory_iterator(shader_dir)) {
|
|
||||||
if (entry.is_regular_file() && entry.path().extension() == ".spv") {
|
|
||||||
common_shaders.push_back(entry.path().string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!common_shaders.empty()) {
|
|
||||||
LOG_INFO(Render_Vulkan, "Preloading {} common shaders", common_shaders.size());
|
|
||||||
shader_manager.PreloadShaders(common_shaders);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOG_INFO(Render_Vulkan, "Shader directory not found at {}", shader_dir);
|
|
||||||
}
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
LOG_ERROR(Render_Vulkan, "Error during shader preloading: {}", e.what());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Report();
|
Report();
|
||||||
InitializePlatformSpecific();
|
|
||||||
} catch (const vk::Exception& exception) {
|
} catch (const vk::Exception& exception) {
|
||||||
LOG_ERROR(Render_Vulkan, "Vulkan initialization failed with error: {}", exception.what());
|
LOG_ERROR(Render_Vulkan, "Vulkan initialization failed with error: {}", exception.what());
|
||||||
throw std::runtime_error{fmt::format("Vulkan initialization error {}", exception.what())};
|
throw std::runtime_error{fmt::format("Vulkan initialization error {}", exception.what())};
|
||||||
|
@ -517,154 +480,4 @@ void RendererVulkan::RenderAppletCaptureLayer(
|
||||||
CaptureFormat);
|
CaptureFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RendererVulkan::FixMSAADepthStencil(VkCommandBuffer cmd_buffer, const Framebuffer& framebuffer) {
|
|
||||||
if (framebuffer.Samples() == VK_SAMPLE_COUNT_1_BIT) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the scheduler's command buffer wrapper
|
|
||||||
scheduler.Record([&](vk::CommandBuffer cmdbuf) {
|
|
||||||
// Find the depth/stencil image in the framebuffer's attachments
|
|
||||||
for (u32 i = 0; i < framebuffer.NumImages(); ++i) {
|
|
||||||
if (framebuffer.HasAspectDepthBit() && (framebuffer.ImageRanges()[i].aspectMask & VK_IMAGE_ASPECT_DEPTH_BIT)) {
|
|
||||||
VkImageMemoryBarrier barrier{
|
|
||||||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
|
|
||||||
.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
|
|
||||||
.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT,
|
|
||||||
.oldLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
|
|
||||||
.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
|
||||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
|
||||||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
|
||||||
.image = framebuffer.Images()[i],
|
|
||||||
.subresourceRange = framebuffer.ImageRanges()[i]
|
|
||||||
};
|
|
||||||
|
|
||||||
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
|
|
||||||
VK_PIPELINE_STAGE_TRANSFER_BIT,
|
|
||||||
0, nullptr, nullptr, barrier);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void RendererVulkan::ResolveMSAA(VkCommandBuffer cmd_buffer, const Framebuffer& framebuffer) {
|
|
||||||
if (framebuffer.Samples() == VK_SAMPLE_COUNT_1_BIT) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the scheduler's command buffer wrapper
|
|
||||||
scheduler.Record([&](vk::CommandBuffer cmdbuf) {
|
|
||||||
// Find color attachments
|
|
||||||
for (u32 i = 0; i < framebuffer.NumColorBuffers(); ++i) {
|
|
||||||
if (framebuffer.HasAspectColorBit(i)) {
|
|
||||||
VkImageResolve resolve_region{
|
|
||||||
.srcSubresource{
|
|
||||||
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
|
|
||||||
.mipLevel = 0,
|
|
||||||
.baseArrayLayer = 0,
|
|
||||||
.layerCount = 1,
|
|
||||||
},
|
|
||||||
.srcOffset = {0, 0, 0},
|
|
||||||
.dstSubresource{
|
|
||||||
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
|
|
||||||
.mipLevel = 0,
|
|
||||||
.baseArrayLayer = 0,
|
|
||||||
.layerCount = 1,
|
|
||||||
},
|
|
||||||
.dstOffset = {0, 0, 0},
|
|
||||||
.extent{
|
|
||||||
.width = framebuffer.RenderArea().width,
|
|
||||||
.height = framebuffer.RenderArea().height,
|
|
||||||
.depth = 1
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
cmdbuf.ResolveImage(
|
|
||||||
framebuffer.Images()[i], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
|
||||||
framebuffer.Images()[i], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
||||||
resolve_region
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RendererVulkan::HandleVulkanError(VkResult result, const std::string& operation) {
|
|
||||||
if (result == VK_SUCCESS) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result == VK_ERROR_DEVICE_LOST) {
|
|
||||||
LOG_CRITICAL(Render_Vulkan, "Vulkan device lost during {}", operation);
|
|
||||||
RecoverFromError();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY || result == VK_ERROR_OUT_OF_HOST_MEMORY) {
|
|
||||||
LOG_CRITICAL(Render_Vulkan, "Vulkan out of memory during {}", operation);
|
|
||||||
// Potential recovery: clear caches, reduce workload
|
|
||||||
texture_manager.CleanupTextureCache();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_ERROR(Render_Vulkan, "Vulkan error during {}: {}", operation, result);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RendererVulkan::RecoverFromError() {
|
|
||||||
LOG_INFO(Render_Vulkan, "Attempting to recover from Vulkan error");
|
|
||||||
|
|
||||||
// Wait for device to finish operations
|
|
||||||
void(device.GetLogical().WaitIdle());
|
|
||||||
|
|
||||||
// Process all pending commands in our queue
|
|
||||||
ProcessAllCommands();
|
|
||||||
|
|
||||||
// Wait for any async shader compilations to finish
|
|
||||||
shader_manager.WaitForCompilation();
|
|
||||||
|
|
||||||
// Clean up resources that might be causing problems
|
|
||||||
texture_manager.CleanupTextureCache();
|
|
||||||
|
|
||||||
// Reset command buffers and pipelines
|
|
||||||
scheduler.Flush();
|
|
||||||
|
|
||||||
LOG_INFO(Render_Vulkan, "Recovery attempt completed");
|
|
||||||
}
|
|
||||||
|
|
||||||
void RendererVulkan::InitializePlatformSpecific() {
|
|
||||||
LOG_INFO(Render_Vulkan, "Initializing platform-specific Vulkan components");
|
|
||||||
|
|
||||||
#if defined(_WIN32) || defined(_WIN64)
|
|
||||||
LOG_INFO(Render_Vulkan, "Initializing Vulkan for Windows");
|
|
||||||
// Windows-specific initialization
|
|
||||||
#elif defined(__linux__)
|
|
||||||
LOG_INFO(Render_Vulkan, "Initializing Vulkan for Linux");
|
|
||||||
// Linux-specific initialization
|
|
||||||
#elif defined(__ANDROID__)
|
|
||||||
LOG_INFO(Render_Vulkan, "Initializing Vulkan for Android");
|
|
||||||
// Android-specific initialization
|
|
||||||
#else
|
|
||||||
LOG_INFO(Render_Vulkan, "Platform-specific Vulkan initialization not implemented for this platform");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Create a compute buffer using the HybridMemory system if enabled
|
|
||||||
if (Settings::values.use_gpu_memory_manager.GetValue()) {
|
|
||||||
try {
|
|
||||||
// Create a small compute buffer for testing
|
|
||||||
const VkDeviceSize buffer_size = 1 * 1024 * 1024; // 1 MB
|
|
||||||
ComputeBuffer compute_buffer = hybrid_memory->CreateComputeBuffer(
|
|
||||||
buffer_size,
|
|
||||||
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT |
|
|
||||||
VK_BUFFER_USAGE_TRANSFER_DST_BIT,
|
|
||||||
MemoryUsage::DeviceLocal);
|
|
||||||
|
|
||||||
LOG_INFO(Render_Vulkan, "Successfully created compute buffer using HybridMemory");
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
LOG_ERROR(Render_Vulkan, "Failed to create compute buffer: {}", e.what());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Vulkan
|
} // namespace Vulkan
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
@ -7,7 +6,6 @@
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <variant>
|
#include <variant>
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
#include "common/dynamic_library.h"
|
#include "common/dynamic_library.h"
|
||||||
#include "video_core/host1x/gpu_device_memory_manager.h"
|
#include "video_core/host1x/gpu_device_memory_manager.h"
|
||||||
|
@ -19,8 +17,6 @@
|
||||||
#include "video_core/renderer_vulkan/vk_state_tracker.h"
|
#include "video_core/renderer_vulkan/vk_state_tracker.h"
|
||||||
#include "video_core/renderer_vulkan/vk_swapchain.h"
|
#include "video_core/renderer_vulkan/vk_swapchain.h"
|
||||||
#include "video_core/renderer_vulkan/vk_turbo_mode.h"
|
#include "video_core/renderer_vulkan/vk_turbo_mode.h"
|
||||||
#include "video_core/renderer_vulkan/vk_texture_manager.h"
|
|
||||||
#include "video_core/renderer_vulkan/vk_shader_util.h"
|
|
||||||
#include "video_core/vulkan_common/vulkan_device.h"
|
#include "video_core/vulkan_common/vulkan_device.h"
|
||||||
#include "video_core/vulkan_common/vulkan_memory_allocator.h"
|
#include "video_core/vulkan_common/vulkan_memory_allocator.h"
|
||||||
#include "video_core/vulkan_common/hybrid_memory.h"
|
#include "video_core/vulkan_common/hybrid_memory.h"
|
||||||
|
@ -58,9 +54,6 @@ public:
|
||||||
return device.GetDriverName();
|
return device.GetDriverName();
|
||||||
}
|
}
|
||||||
|
|
||||||
void FixMSAADepthStencil(VkCommandBuffer cmd_buffer, const Framebuffer& framebuffer);
|
|
||||||
void ResolveMSAA(VkCommandBuffer cmd_buffer, const Framebuffer& framebuffer);
|
|
||||||
|
|
||||||
// Enhanced platform-specific initialization
|
// Enhanced platform-specific initialization
|
||||||
void InitializePlatformSpecific();
|
void InitializePlatformSpecific();
|
||||||
|
|
||||||
|
@ -77,10 +70,6 @@ private:
|
||||||
void RenderScreenshot(std::span<const Tegra::FramebufferConfig> framebuffers);
|
void RenderScreenshot(std::span<const Tegra::FramebufferConfig> framebuffers);
|
||||||
void RenderAppletCaptureLayer(std::span<const Tegra::FramebufferConfig> framebuffers);
|
void RenderAppletCaptureLayer(std::span<const Tegra::FramebufferConfig> framebuffers);
|
||||||
|
|
||||||
// Enhanced error handling
|
|
||||||
bool HandleVulkanError(VkResult result, const std::string& operation);
|
|
||||||
void RecoverFromError();
|
|
||||||
|
|
||||||
Tegra::MaxwellDeviceMemoryManager& device_memory;
|
Tegra::MaxwellDeviceMemoryManager& device_memory;
|
||||||
Tegra::GPU& gpu;
|
Tegra::GPU& gpu;
|
||||||
|
|
||||||
|
@ -106,10 +95,6 @@ private:
|
||||||
// HybridMemory for advanced memory management
|
// HybridMemory for advanced memory management
|
||||||
std::unique_ptr<HybridMemory> hybrid_memory;
|
std::unique_ptr<HybridMemory> hybrid_memory;
|
||||||
|
|
||||||
// Enhanced texture and shader management
|
|
||||||
TextureManager texture_manager;
|
|
||||||
ShaderManager shader_manager;
|
|
||||||
|
|
||||||
Frame applet_frame;
|
Frame applet_frame;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||||
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <chrono>
|
|
||||||
|
|
||||||
#include <boost/container/small_vector.hpp>
|
#include <boost/container/small_vector.hpp>
|
||||||
|
|
||||||
|
@ -39,23 +37,10 @@ ComputePipeline::ComputePipeline(const Device& device_, vk::PipelineCache& pipel
|
||||||
if (shader_notify) {
|
if (shader_notify) {
|
||||||
shader_notify->MarkShaderBuilding();
|
shader_notify->MarkShaderBuilding();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track compilation start time for performance metrics
|
|
||||||
const auto start_time = std::chrono::high_resolution_clock::now();
|
|
||||||
|
|
||||||
std::copy_n(info.constant_buffer_used_sizes.begin(), uniform_buffer_sizes.size(),
|
std::copy_n(info.constant_buffer_used_sizes.begin(), uniform_buffer_sizes.size(),
|
||||||
uniform_buffer_sizes.begin());
|
uniform_buffer_sizes.begin());
|
||||||
|
|
||||||
auto func{[this, &descriptor_pool, shader_notify, pipeline_statistics, start_time] {
|
|
||||||
// Simplify the high priority determination - we can't use workgroup_size
|
|
||||||
// because it doesn't exist, so use a simpler heuristic
|
|
||||||
const bool is_high_priority = false; // Default to false until we can find a better criterion
|
|
||||||
|
|
||||||
if (is_high_priority) {
|
|
||||||
// Increase thread priority for small compute shaders that are likely part of critical path
|
|
||||||
Common::SetCurrentThreadPriority(Common::ThreadPriority::High);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
auto func{[this, &descriptor_pool, shader_notify, pipeline_statistics] {
|
||||||
DescriptorLayoutBuilder builder{device};
|
DescriptorLayoutBuilder builder{device};
|
||||||
builder.Add(info, VK_SHADER_STAGE_COMPUTE_BIT);
|
builder.Add(info, VK_SHADER_STAGE_COMPUTE_BIT);
|
||||||
|
|
||||||
|
@ -64,11 +49,15 @@ ComputePipeline::ComputePipeline(const Device& device_, vk::PipelineCache& pipel
|
||||||
descriptor_update_template =
|
descriptor_update_template =
|
||||||
builder.CreateTemplate(*descriptor_set_layout, *pipeline_layout, false);
|
builder.CreateTemplate(*descriptor_set_layout, *pipeline_layout, false);
|
||||||
descriptor_allocator = descriptor_pool.Allocator(*descriptor_set_layout, info);
|
descriptor_allocator = descriptor_pool.Allocator(*descriptor_set_layout, info);
|
||||||
|
const VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT subgroup_size_ci{
|
||||||
|
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO_EXT,
|
||||||
|
.pNext = nullptr,
|
||||||
|
.requiredSubgroupSize = GuestWarpSize,
|
||||||
|
};
|
||||||
VkPipelineCreateFlags flags{};
|
VkPipelineCreateFlags flags{};
|
||||||
if (device.IsKhrPipelineExecutablePropertiesEnabled()) {
|
if (device.IsKhrPipelineExecutablePropertiesEnabled()) {
|
||||||
flags |= VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR;
|
flags |= VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR;
|
||||||
}
|
}
|
||||||
|
|
||||||
pipeline = device.GetLogical().CreateComputePipeline(
|
pipeline = device.GetLogical().CreateComputePipeline(
|
||||||
{
|
{
|
||||||
.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
|
.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
|
||||||
|
@ -76,7 +65,8 @@ ComputePipeline::ComputePipeline(const Device& device_, vk::PipelineCache& pipel
|
||||||
.flags = flags,
|
.flags = flags,
|
||||||
.stage{
|
.stage{
|
||||||
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
|
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
|
||||||
.pNext = nullptr,
|
.pNext =
|
||||||
|
device.IsExtSubgroupSizeControlSupported() ? &subgroup_size_ci : nullptr,
|
||||||
.flags = 0,
|
.flags = 0,
|
||||||
.stage = VK_SHADER_STAGE_COMPUTE_BIT,
|
.stage = VK_SHADER_STAGE_COMPUTE_BIT,
|
||||||
.module = *spv_module,
|
.module = *spv_module,
|
||||||
|
@ -89,15 +79,6 @@ ComputePipeline::ComputePipeline(const Device& device_, vk::PipelineCache& pipel
|
||||||
},
|
},
|
||||||
*pipeline_cache);
|
*pipeline_cache);
|
||||||
|
|
||||||
// Performance measurement
|
|
||||||
const auto end_time = std::chrono::high_resolution_clock::now();
|
|
||||||
const auto compilation_time = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
||||||
end_time - start_time).count();
|
|
||||||
|
|
||||||
if (compilation_time > 50) { // Only log slow compilations
|
|
||||||
LOG_DEBUG(Render_Vulkan, "Compiled compute shader in {}ms", compilation_time);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pipeline_statistics) {
|
if (pipeline_statistics) {
|
||||||
pipeline_statistics->Collect(*pipeline);
|
pipeline_statistics->Collect(*pipeline);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||||
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
@ -260,16 +259,7 @@ GraphicsPipeline::GraphicsPipeline(
|
||||||
std::ranges::copy(info->constant_buffer_used_sizes, uniform_buffer_sizes[stage].begin());
|
std::ranges::copy(info->constant_buffer_used_sizes, uniform_buffer_sizes[stage].begin());
|
||||||
num_textures += Shader::NumDescriptors(info->texture_descriptors);
|
num_textures += Shader::NumDescriptors(info->texture_descriptors);
|
||||||
}
|
}
|
||||||
|
auto func{[this, shader_notify, &render_pass_cache, &descriptor_pool, pipeline_statistics] {
|
||||||
// Track compilation start time for performance metrics
|
|
||||||
const auto start_time = std::chrono::high_resolution_clock::now();
|
|
||||||
|
|
||||||
auto func{[this, shader_notify, &render_pass_cache, &descriptor_pool, pipeline_statistics, start_time] {
|
|
||||||
// Use enhanced shader compilation if enabled in settings
|
|
||||||
if (Settings::values.use_enhanced_shader_building.GetValue()) {
|
|
||||||
Common::SetCurrentThreadPriority(Common::ThreadPriority::High);
|
|
||||||
}
|
|
||||||
|
|
||||||
DescriptorLayoutBuilder builder{MakeBuilder(device, stage_infos)};
|
DescriptorLayoutBuilder builder{MakeBuilder(device, stage_infos)};
|
||||||
uses_push_descriptor = builder.CanUsePushDescriptor();
|
uses_push_descriptor = builder.CanUsePushDescriptor();
|
||||||
descriptor_set_layout = builder.CreateDescriptorSetLayout(uses_push_descriptor);
|
descriptor_set_layout = builder.CreateDescriptorSetLayout(uses_push_descriptor);
|
||||||
|
@ -284,17 +274,6 @@ GraphicsPipeline::GraphicsPipeline(
|
||||||
const VkRenderPass render_pass{render_pass_cache.Get(MakeRenderPassKey(key.state))};
|
const VkRenderPass render_pass{render_pass_cache.Get(MakeRenderPassKey(key.state))};
|
||||||
Validate();
|
Validate();
|
||||||
MakePipeline(render_pass);
|
MakePipeline(render_pass);
|
||||||
|
|
||||||
// Performance measurement
|
|
||||||
const auto end_time = std::chrono::high_resolution_clock::now();
|
|
||||||
const auto compilation_time = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
||||||
end_time - start_time).count();
|
|
||||||
|
|
||||||
// Log shader compilation time for slow shaders to help diagnose performance issues
|
|
||||||
if (compilation_time > 100) { // Only log very slow compilations
|
|
||||||
LOG_DEBUG(Render_Vulkan, "Compiled graphics pipeline in {}ms", compilation_time);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pipeline_statistics) {
|
if (pipeline_statistics) {
|
||||||
pipeline_statistics->Collect(*pipeline);
|
pipeline_statistics->Collect(*pipeline);
|
||||||
}
|
}
|
||||||
|
@ -333,9 +312,6 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
|
||||||
const auto& regs{maxwell3d->regs};
|
const auto& regs{maxwell3d->regs};
|
||||||
const bool via_header_index{regs.sampler_binding == Maxwell::SamplerBinding::ViaHeaderBinding};
|
const bool via_header_index{regs.sampler_binding == Maxwell::SamplerBinding::ViaHeaderBinding};
|
||||||
const auto config_stage{[&](size_t stage) LAMBDA_FORCEINLINE {
|
const auto config_stage{[&](size_t stage) LAMBDA_FORCEINLINE {
|
||||||
// Get the constant buffer information from Maxwell's state
|
|
||||||
const auto& cbufs = maxwell3d->state.shader_stages[stage].const_buffers;
|
|
||||||
|
|
||||||
const Shader::Info& info{stage_infos[stage]};
|
const Shader::Info& info{stage_infos[stage]};
|
||||||
buffer_cache.UnbindGraphicsStorageBuffers(stage);
|
buffer_cache.UnbindGraphicsStorageBuffers(stage);
|
||||||
if constexpr (Spec::has_storage_buffers) {
|
if constexpr (Spec::has_storage_buffers) {
|
||||||
|
@ -347,7 +323,7 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
|
||||||
++ssbo_index;
|
++ssbo_index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const auto& cbufs{maxwell3d->state.shader_stages[stage].const_buffers};
|
||||||
const auto read_handle{[&](const auto& desc, u32 index) {
|
const auto read_handle{[&](const auto& desc, u32 index) {
|
||||||
ASSERT(cbufs[desc.cbuf_index].enabled);
|
ASSERT(cbufs[desc.cbuf_index].enabled);
|
||||||
const u32 index_offset{index << desc.size_shift};
|
const u32 index_offset{index << desc.size_shift};
|
||||||
|
@ -369,7 +345,6 @@ void GraphicsPipeline::ConfigureImpl(bool is_indexed) {
|
||||||
}
|
}
|
||||||
return TexturePair(gpu_memory->Read<u32>(addr), via_header_index);
|
return TexturePair(gpu_memory->Read<u32>(addr), via_header_index);
|
||||||
}};
|
}};
|
||||||
|
|
||||||
const auto add_image{[&](const auto& desc, bool blacklist) LAMBDA_FORCEINLINE {
|
const auto add_image{[&](const auto& desc, bool blacklist) LAMBDA_FORCEINLINE {
|
||||||
for (u32 index = 0; index < desc.count; ++index) {
|
for (u32 index = 0; index < desc.count; ++index) {
|
||||||
const auto handle{read_handle(desc, index)};
|
const auto handle{read_handle(desc, index)};
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||||
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
@ -266,42 +265,18 @@ Shader::RuntimeInfo MakeRuntimeInfo(std::span<const Shader::IR::Program> program
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t GetTotalPipelineWorkers() {
|
size_t GetTotalPipelineWorkers() {
|
||||||
const size_t num_cores = std::max<size_t>(static_cast<size_t>(std::thread::hardware_concurrency()), 2ULL);
|
const size_t max_core_threads =
|
||||||
|
std::max<size_t>(static_cast<size_t>(std::thread::hardware_concurrency()), 2ULL) - 1ULL;
|
||||||
// Calculate optimal number of workers based on available CPU cores
|
|
||||||
size_t optimal_workers;
|
|
||||||
|
|
||||||
#ifdef ANDROID
|
#ifdef ANDROID
|
||||||
// Mobile devices need more conservative threading to avoid thermal issues
|
// Leave at least a few cores free in android
|
||||||
// Leave more cores free on Android for system processes and other apps
|
constexpr size_t free_cores = 3ULL;
|
||||||
constexpr size_t min_free_cores = 3ULL;
|
if (max_core_threads <= free_cores) {
|
||||||
if (num_cores <= min_free_cores + 1) {
|
return 1ULL;
|
||||||
return 1ULL; // At least one worker
|
|
||||||
}
|
}
|
||||||
optimal_workers = num_cores - min_free_cores;
|
return max_core_threads - free_cores;
|
||||||
#else
|
#else
|
||||||
// Desktop systems can use more aggressive threading
|
return max_core_threads;
|
||||||
if (num_cores <= 3) {
|
|
||||||
optimal_workers = num_cores - 1; // Dual/triple core: leave 1 core free
|
|
||||||
} else if (num_cores <= 6) {
|
|
||||||
optimal_workers = num_cores - 2; // Quad/hex core: leave 2 cores free
|
|
||||||
} else {
|
|
||||||
// For 8+ core systems, use more workers but still leave some cores for other tasks
|
|
||||||
optimal_workers = num_cores - (num_cores / 4); // Leave ~25% of cores free
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Apply threading priority via shader_compilation_priority setting if enabled
|
|
||||||
const int priority = Settings::values.shader_compilation_priority.GetValue();
|
|
||||||
if (priority > 0) {
|
|
||||||
// High priority - use more cores for shader compilation
|
|
||||||
optimal_workers = std::min(optimal_workers + 1, num_cores - 1);
|
|
||||||
} else if (priority < 0) {
|
|
||||||
// Low priority - use fewer cores for shader compilation
|
|
||||||
optimal_workers = (optimal_workers >= 2) ? optimal_workers - 1 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return optimal_workers;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // Anonymous namespace
|
} // Anonymous namespace
|
||||||
|
@ -619,35 +594,14 @@ GraphicsPipeline* PipelineCache::BuiltPipeline(GraphicsPipeline* pipeline) const
|
||||||
if (pipeline->IsBuilt()) {
|
if (pipeline->IsBuilt()) {
|
||||||
return pipeline;
|
return pipeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!use_asynchronous_shaders) {
|
if (!use_asynchronous_shaders) {
|
||||||
return pipeline;
|
return pipeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advanced heuristics for smarter async shader compilation
|
|
||||||
|
|
||||||
// Track stutter metrics for better debugging and performance tuning
|
|
||||||
static thread_local u32 async_shader_count = 0;
|
|
||||||
static thread_local std::chrono::high_resolution_clock::time_point last_async_shader_log;
|
|
||||||
auto now = std::chrono::high_resolution_clock::now();
|
|
||||||
|
|
||||||
// Simplify UI shader detection since we don't have access to clear_buffers
|
|
||||||
const bool is_ui_shader = !maxwell3d->regs.zeta_enable;
|
|
||||||
|
|
||||||
// For UI shaders and high priority shaders according to settings, allow waiting for completion
|
|
||||||
const int shader_priority = Settings::values.shader_compilation_priority.GetValue();
|
|
||||||
if ((is_ui_shader && shader_priority >= 0) || shader_priority > 1) {
|
|
||||||
// For UI/menu elements and critical visuals, let's wait for the shader to compile
|
|
||||||
// but only if high shader priority
|
|
||||||
return pipeline;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If something is using depth, we can assume that games are not rendering anything which
|
// If something is using depth, we can assume that games are not rendering anything which
|
||||||
// will be used one time.
|
// will be used one time.
|
||||||
if (maxwell3d->regs.zeta_enable) {
|
if (maxwell3d->regs.zeta_enable) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If games are using a small index count, we can assume these are full screen quads.
|
// If games are using a small index count, we can assume these are full screen quads.
|
||||||
// Usually these shaders are only used once for building textures so we can assume they
|
// Usually these shaders are only used once for building textures so we can assume they
|
||||||
// can't be built async
|
// can't be built async
|
||||||
|
@ -655,23 +609,6 @@ GraphicsPipeline* PipelineCache::BuiltPipeline(GraphicsPipeline* pipeline) const
|
||||||
if (draw_state.index_buffer.count <= 6 || draw_state.vertex_buffer.count <= 6) {
|
if (draw_state.index_buffer.count <= 6 || draw_state.vertex_buffer.count <= 6) {
|
||||||
return pipeline;
|
return pipeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track and log async shader statistics periodically
|
|
||||||
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
|
|
||||||
now - last_async_shader_log).count();
|
|
||||||
|
|
||||||
if (elapsed >= 10) { // Log every 10 seconds
|
|
||||||
async_shader_count = 0;
|
|
||||||
last_async_shader_log = now;
|
|
||||||
}
|
|
||||||
async_shader_count++;
|
|
||||||
|
|
||||||
// Log less frequently to avoid spamming log
|
|
||||||
if (async_shader_count % 100 == 1) {
|
|
||||||
LOG_DEBUG(Render_Vulkan, "Async shader compilation in progress (count={})",
|
|
||||||
async_shader_count);
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,141 +1,15 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <thread>
|
|
||||||
#include <filesystem>
|
|
||||||
#include <fstream>
|
|
||||||
#include <vector>
|
|
||||||
#include <atomic>
|
|
||||||
#include <queue>
|
|
||||||
#include <condition_variable>
|
|
||||||
#include <future>
|
|
||||||
#include <chrono>
|
|
||||||
#include <unordered_set>
|
|
||||||
|
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "common/logging/log.h"
|
|
||||||
#include "video_core/renderer_vulkan/vk_shader_util.h"
|
#include "video_core/renderer_vulkan/vk_shader_util.h"
|
||||||
#include "video_core/renderer_vulkan/vk_scheduler.h"
|
|
||||||
#include "video_core/vulkan_common/vulkan_device.h"
|
#include "video_core/vulkan_common/vulkan_device.h"
|
||||||
#include "video_core/vulkan_common/vulkan_wrapper.h"
|
#include "video_core/vulkan_common/vulkan_wrapper.h"
|
||||||
|
|
||||||
#define SHADER_CACHE_DIR "./shader_cache"
|
|
||||||
|
|
||||||
namespace Vulkan {
|
namespace Vulkan {
|
||||||
|
|
||||||
// Global command submission queue for asynchronous operations
|
|
||||||
std::mutex commandQueueMutex;
|
|
||||||
std::queue<std::function<void()>> commandQueue;
|
|
||||||
std::condition_variable commandQueueCondition;
|
|
||||||
std::atomic<bool> isCommandQueueActive{true};
|
|
||||||
std::thread commandQueueThread;
|
|
||||||
|
|
||||||
// Pointer to Citron's scheduler for integration
|
|
||||||
Scheduler* globalScheduler = nullptr;
|
|
||||||
|
|
||||||
// Command queue worker thread (multi-threaded command recording)
|
|
||||||
void CommandQueueWorker() {
|
|
||||||
while (isCommandQueueActive.load()) {
|
|
||||||
std::function<void()> command;
|
|
||||||
{
|
|
||||||
std::unique_lock<std::mutex> lock(commandQueueMutex);
|
|
||||||
if (commandQueue.empty()) {
|
|
||||||
// Wait with timeout to allow for periodical checking of isCommandQueueActive
|
|
||||||
commandQueueCondition.wait_for(lock, std::chrono::milliseconds(100),
|
|
||||||
[]{ return !commandQueue.empty() || !isCommandQueueActive.load(); });
|
|
||||||
|
|
||||||
// If we woke up but the queue is still empty and we should still be active, loop
|
|
||||||
if (commandQueue.empty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
command = commandQueue.front();
|
|
||||||
commandQueue.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute the command
|
|
||||||
if (command) {
|
|
||||||
command();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the command queue system
|
|
||||||
void InitializeCommandQueue() {
|
|
||||||
if (!commandQueueThread.joinable()) {
|
|
||||||
isCommandQueueActive.store(true);
|
|
||||||
commandQueueThread = std::thread(CommandQueueWorker);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutdown the command queue system
|
|
||||||
void ShutdownCommandQueue() {
|
|
||||||
isCommandQueueActive.store(false);
|
|
||||||
commandQueueCondition.notify_all();
|
|
||||||
|
|
||||||
if (commandQueueThread.joinable()) {
|
|
||||||
commandQueueThread.join();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submit a command to the queue for asynchronous execution
|
|
||||||
void SubmitCommandToQueue(std::function<void()> command) {
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(commandQueueMutex);
|
|
||||||
commandQueue.push(command);
|
|
||||||
}
|
|
||||||
commandQueueCondition.notify_one();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the global scheduler reference for command integration
|
|
||||||
void SetGlobalScheduler(Scheduler* scheduler) {
|
|
||||||
globalScheduler = scheduler;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submit a Vulkan command to the existing Citron scheduler
|
|
||||||
void SubmitToScheduler(std::function<void(vk::CommandBuffer)> command) {
|
|
||||||
if (globalScheduler) {
|
|
||||||
globalScheduler->Record(std::move(command));
|
|
||||||
} else {
|
|
||||||
LOG_WARNING(Render_Vulkan, "Trying to submit to scheduler but no scheduler is set");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush the Citron scheduler - use when needing to ensure commands are executed
|
|
||||||
u64 FlushScheduler(VkSemaphore signal_semaphore, VkSemaphore wait_semaphore) {
|
|
||||||
if (globalScheduler) {
|
|
||||||
return globalScheduler->Flush(signal_semaphore, wait_semaphore);
|
|
||||||
} else {
|
|
||||||
LOG_WARNING(Render_Vulkan, "Trying to flush scheduler but no scheduler is set");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process both command queue and scheduler commands
|
|
||||||
void ProcessAllCommands() {
|
|
||||||
// Process our command queue first
|
|
||||||
{
|
|
||||||
std::unique_lock<std::mutex> lock(commandQueueMutex);
|
|
||||||
while (!commandQueue.empty()) {
|
|
||||||
auto command = commandQueue.front();
|
|
||||||
commandQueue.pop();
|
|
||||||
lock.unlock();
|
|
||||||
|
|
||||||
command();
|
|
||||||
|
|
||||||
lock.lock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then flush the scheduler if it exists
|
|
||||||
if (globalScheduler) {
|
|
||||||
globalScheduler->Flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vk::ShaderModule BuildShader(const Device& device, std::span<const u32> code) {
|
vk::ShaderModule BuildShader(const Device& device, std::span<const u32> code) {
|
||||||
return device.GetLogical().CreateShaderModule({
|
return device.GetLogical().CreateShaderModule({
|
||||||
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
|
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
|
||||||
|
@ -146,368 +20,4 @@ vk::ShaderModule BuildShader(const Device& device, std::span<const u32> code) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IsShaderValid(VkShaderModule shader_module) {
|
|
||||||
// TODO: validate the shader by checking if it's null
|
|
||||||
// or by examining SPIR-V data for correctness [ZEP]
|
|
||||||
return shader_module != VK_NULL_HANDLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Atomic flag for tracking shader compilation status
|
|
||||||
std::atomic<bool> compilingShader(false);
|
|
||||||
|
|
||||||
void AsyncCompileShader(const Device& device, const std::string& shader_path,
|
|
||||||
std::function<void(VkShaderModule)> callback) {
|
|
||||||
LOG_INFO(Render_Vulkan, "Asynchronously compiling shader: {}", shader_path);
|
|
||||||
|
|
||||||
// Create shader cache directory if it doesn't exist
|
|
||||||
if (!std::filesystem::exists(SHADER_CACHE_DIR)) {
|
|
||||||
std::filesystem::create_directory(SHADER_CACHE_DIR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use atomic flag to prevent duplicate compilations of the same shader
|
|
||||||
if (compilingShader.exchange(true)) {
|
|
||||||
LOG_WARNING(Render_Vulkan, "Shader compilation already in progress, skipping: {}", shader_path);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use actual threading for async compilation
|
|
||||||
std::thread([device_ptr = &device, shader_path, callback = std::move(callback)]() mutable {
|
|
||||||
auto startTime = std::chrono::high_resolution_clock::now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
std::vector<u32> spir_v;
|
|
||||||
bool success = false;
|
|
||||||
|
|
||||||
// Check if the file exists and attempt to read it
|
|
||||||
if (std::filesystem::exists(shader_path)) {
|
|
||||||
std::ifstream shader_file(shader_path, std::ios::binary);
|
|
||||||
if (shader_file) {
|
|
||||||
shader_file.seekg(0, std::ios::end);
|
|
||||||
size_t file_size = static_cast<size_t>(shader_file.tellg());
|
|
||||||
shader_file.seekg(0, std::ios::beg);
|
|
||||||
|
|
||||||
spir_v.resize(file_size / sizeof(u32));
|
|
||||||
if (shader_file.read(reinterpret_cast<char*>(spir_v.data()), file_size)) {
|
|
||||||
success = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
vk::ShaderModule shader = BuildShader(*device_ptr, spir_v);
|
|
||||||
if (IsShaderValid(*shader)) {
|
|
||||||
// Cache the compiled shader to disk for faster loading next time
|
|
||||||
std::string cache_path = std::string(SHADER_CACHE_DIR) + "/" +
|
|
||||||
std::filesystem::path(shader_path).filename().string() + ".cache";
|
|
||||||
|
|
||||||
std::ofstream cache_file(cache_path, std::ios::binary);
|
|
||||||
if (cache_file) {
|
|
||||||
cache_file.write(reinterpret_cast<const char*>(spir_v.data()),
|
|
||||||
spir_v.size() * sizeof(u32));
|
|
||||||
}
|
|
||||||
|
|
||||||
auto endTime = std::chrono::high_resolution_clock::now();
|
|
||||||
std::chrono::duration<double> duration = endTime - startTime;
|
|
||||||
LOG_INFO(Render_Vulkan, "Shader compiled in {:.2f} seconds: {}",
|
|
||||||
duration.count(), shader_path);
|
|
||||||
|
|
||||||
// Store the module pointer for the callback
|
|
||||||
VkShaderModule raw_module = *shader;
|
|
||||||
|
|
||||||
// Submit callback to main thread via command queue for thread safety
|
|
||||||
SubmitCommandToQueue([callback = std::move(callback), raw_module]() {
|
|
||||||
callback(raw_module);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
LOG_ERROR(Render_Vulkan, "Shader validation failed: {}", shader_path);
|
|
||||||
SubmitCommandToQueue([callback = std::move(callback)]() {
|
|
||||||
callback(VK_NULL_HANDLE);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOG_ERROR(Render_Vulkan, "Failed to read shader file: {}", shader_path);
|
|
||||||
SubmitCommandToQueue([callback = std::move(callback)]() {
|
|
||||||
callback(VK_NULL_HANDLE);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
LOG_ERROR(Render_Vulkan, "Error compiling shader: {}", e.what());
|
|
||||||
SubmitCommandToQueue([callback = std::move(callback)]() {
|
|
||||||
callback(VK_NULL_HANDLE);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Release the compilation flag
|
|
||||||
compilingShader.store(false);
|
|
||||||
}).detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
ShaderManager::ShaderManager(const Device& device_) : device(device_) {
|
|
||||||
// Initialize command queue system
|
|
||||||
InitializeCommandQueue();
|
|
||||||
}
|
|
||||||
|
|
||||||
ShaderManager::~ShaderManager() {
|
|
||||||
// Wait for any pending compilations to finish
|
|
||||||
WaitForCompilation();
|
|
||||||
|
|
||||||
// Clean up shader modules
|
|
||||||
std::lock_guard<std::mutex> lock(shader_mutex);
|
|
||||||
shader_cache.clear();
|
|
||||||
|
|
||||||
// Shutdown command queue
|
|
||||||
ShutdownCommandQueue();
|
|
||||||
}
|
|
||||||
|
|
||||||
VkShaderModule ShaderManager::GetShaderModule(const std::string& shader_path) {
|
|
||||||
// Check in-memory cache first
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(shader_mutex);
|
|
||||||
auto it = shader_cache.find(shader_path);
|
|
||||||
if (it != shader_cache.end()) {
|
|
||||||
return *it->second;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize the path to avoid filesystem issues
|
|
||||||
std::string normalized_path = shader_path;
|
|
||||||
std::replace(normalized_path.begin(), normalized_path.end(), '\\', '/');
|
|
||||||
|
|
||||||
// Check if shader exists
|
|
||||||
if (!std::filesystem::exists(normalized_path)) {
|
|
||||||
LOG_WARNING(Render_Vulkan, "Shader file does not exist: {}", normalized_path);
|
|
||||||
return VK_NULL_HANDLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if shader is available in disk cache first
|
|
||||||
const std::string filename = std::filesystem::path(normalized_path).filename().string();
|
|
||||||
std::string cache_path = std::string(SHADER_CACHE_DIR) + "/" + filename + ".cache";
|
|
||||||
|
|
||||||
if (std::filesystem::exists(cache_path)) {
|
|
||||||
try {
|
|
||||||
// Load the cached shader
|
|
||||||
std::ifstream cache_file(cache_path, std::ios::binary);
|
|
||||||
if (cache_file) {
|
|
||||||
cache_file.seekg(0, std::ios::end);
|
|
||||||
size_t file_size = static_cast<size_t>(cache_file.tellg());
|
|
||||||
|
|
||||||
if (file_size > 0 && file_size % sizeof(u32) == 0) {
|
|
||||||
cache_file.seekg(0, std::ios::beg);
|
|
||||||
std::vector<u32> spir_v;
|
|
||||||
spir_v.resize(file_size / sizeof(u32));
|
|
||||||
|
|
||||||
if (cache_file.read(reinterpret_cast<char*>(spir_v.data()), file_size)) {
|
|
||||||
vk::ShaderModule shader = BuildShader(device, spir_v);
|
|
||||||
if (IsShaderValid(*shader)) {
|
|
||||||
// Store in memory cache
|
|
||||||
std::lock_guard<std::mutex> lock(shader_mutex);
|
|
||||||
shader_cache[normalized_path] = std::move(shader);
|
|
||||||
LOG_INFO(Render_Vulkan, "Loaded shader from cache: {}", normalized_path);
|
|
||||||
return *shader_cache[normalized_path];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
LOG_WARNING(Render_Vulkan, "Failed to load shader from cache: {}", e.what());
|
|
||||||
// Continue to load from original file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to load the shader directly if cache load failed
|
|
||||||
if (LoadShader(normalized_path)) {
|
|
||||||
std::lock_guard<std::mutex> lock(shader_mutex);
|
|
||||||
return *shader_cache[normalized_path];
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_ERROR(Render_Vulkan, "Failed to load shader: {}", normalized_path);
|
|
||||||
return VK_NULL_HANDLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShaderManager::ReloadShader(const std::string& shader_path) {
|
|
||||||
LOG_INFO(Render_Vulkan, "Reloading shader: {}", shader_path);
|
|
||||||
|
|
||||||
// Remove the old shader from cache
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(shader_mutex);
|
|
||||||
shader_cache.erase(shader_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the shader again
|
|
||||||
LoadShader(shader_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ShaderManager::LoadShader(const std::string& shader_path) {
|
|
||||||
LOG_INFO(Render_Vulkan, "Loading shader from: {}", shader_path);
|
|
||||||
|
|
||||||
if (!std::filesystem::exists(shader_path)) {
|
|
||||||
LOG_ERROR(Render_Vulkan, "Shader file does not exist: {}", shader_path);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
std::vector<u32> spir_v;
|
|
||||||
std::ifstream shader_file(shader_path, std::ios::binary);
|
|
||||||
|
|
||||||
if (!shader_file.is_open()) {
|
|
||||||
LOG_ERROR(Render_Vulkan, "Failed to open shader file: {}", shader_path);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
shader_file.seekg(0, std::ios::end);
|
|
||||||
const size_t file_size = static_cast<size_t>(shader_file.tellg());
|
|
||||||
|
|
||||||
if (file_size == 0 || file_size % sizeof(u32) != 0) {
|
|
||||||
LOG_ERROR(Render_Vulkan, "Invalid shader file size ({}): {}", file_size, shader_path);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
shader_file.seekg(0, std::ios::beg);
|
|
||||||
spir_v.resize(file_size / sizeof(u32));
|
|
||||||
|
|
||||||
if (!shader_file.read(reinterpret_cast<char*>(spir_v.data()), file_size)) {
|
|
||||||
LOG_ERROR(Render_Vulkan, "Failed to read shader data: {}", shader_path);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
vk::ShaderModule shader = BuildShader(device, spir_v);
|
|
||||||
if (!IsShaderValid(*shader)) {
|
|
||||||
LOG_ERROR(Render_Vulkan, "Created shader module is invalid: {}", shader_path);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store in memory cache
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(shader_mutex);
|
|
||||||
shader_cache[shader_path] = std::move(shader);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also store in disk cache for future use
|
|
||||||
try {
|
|
||||||
if (!std::filesystem::exists(SHADER_CACHE_DIR)) {
|
|
||||||
std::filesystem::create_directory(SHADER_CACHE_DIR);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string cache_path = std::string(SHADER_CACHE_DIR) + "/" +
|
|
||||||
std::filesystem::path(shader_path).filename().string() + ".cache";
|
|
||||||
|
|
||||||
std::ofstream cache_file(cache_path, std::ios::binary);
|
|
||||||
if (cache_file.is_open()) {
|
|
||||||
cache_file.write(reinterpret_cast<const char*>(spir_v.data()),
|
|
||||||
spir_v.size() * sizeof(u32));
|
|
||||||
|
|
||||||
if (!cache_file) {
|
|
||||||
LOG_WARNING(Render_Vulkan, "Failed to write shader cache: {}", cache_path);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOG_WARNING(Render_Vulkan, "Failed to create shader cache file: {}", cache_path);
|
|
||||||
}
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
LOG_WARNING(Render_Vulkan, "Error writing shader cache: {}", e.what());
|
|
||||||
// Continue even if disk cache fails
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
LOG_ERROR(Render_Vulkan, "Error loading shader: {}", e.what());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShaderManager::WaitForCompilation() {
|
|
||||||
// Wait until no shader is being compiled
|
|
||||||
while (compilingShader.load()) {
|
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process any pending commands in the queue
|
|
||||||
std::unique_lock<std::mutex> lock(commandQueueMutex);
|
|
||||||
while (!commandQueue.empty()) {
|
|
||||||
auto command = commandQueue.front();
|
|
||||||
commandQueue.pop();
|
|
||||||
lock.unlock();
|
|
||||||
|
|
||||||
command();
|
|
||||||
|
|
||||||
lock.lock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Integrate with Citron's scheduler for shader operations
|
|
||||||
void ShaderManager::SetScheduler(Scheduler* scheduler) {
|
|
||||||
SetGlobalScheduler(scheduler);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load multiple shaders in parallel
|
|
||||||
void ShaderManager::PreloadShaders(const std::vector<std::string>& shader_paths) {
|
|
||||||
if (shader_paths.empty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_INFO(Render_Vulkan, "Preloading {} shaders", shader_paths.size());
|
|
||||||
|
|
||||||
// Track shaders that need to be loaded
|
|
||||||
std::unordered_set<std::string> shaders_to_load;
|
|
||||||
|
|
||||||
// First check which shaders are not already cached
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(shader_mutex);
|
|
||||||
for (const auto& path : shader_paths) {
|
|
||||||
if (shader_cache.find(path) == shader_cache.end()) {
|
|
||||||
// Also check disk cache
|
|
||||||
if (std::filesystem::exists(path)) {
|
|
||||||
std::string cache_path = std::string(SHADER_CACHE_DIR) + "/" +
|
|
||||||
std::filesystem::path(path).filename().string() + ".cache";
|
|
||||||
if (!std::filesystem::exists(cache_path)) {
|
|
||||||
shaders_to_load.insert(path);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOG_WARNING(Render_Vulkan, "Shader file not found: {}", path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shaders_to_load.empty()) {
|
|
||||||
LOG_INFO(Render_Vulkan, "All shaders already cached, no preloading needed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_INFO(Render_Vulkan, "Found {} shaders that need preloading", shaders_to_load.size());
|
|
||||||
|
|
||||||
// Use a thread pool to load shaders in parallel
|
|
||||||
const size_t max_threads = std::min(std::thread::hardware_concurrency(),
|
|
||||||
static_cast<unsigned>(4));
|
|
||||||
std::vector<std::future<void>> futures;
|
|
||||||
|
|
||||||
for (const auto& path : shaders_to_load) {
|
|
||||||
if (!std::filesystem::exists(path)) {
|
|
||||||
LOG_WARNING(Render_Vulkan, "Skipping non-existent shader: {}", path);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto future = std::async(std::launch::async, [this, path]() {
|
|
||||||
try {
|
|
||||||
this->LoadShader(path);
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
LOG_ERROR(Render_Vulkan, "Error loading shader {}: {}", path, e.what());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
futures.push_back(std::move(future));
|
|
||||||
|
|
||||||
// Limit max parallel threads
|
|
||||||
if (futures.size() >= max_threads) {
|
|
||||||
futures.front().wait();
|
|
||||||
futures.erase(futures.begin());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for remaining shaders to load
|
|
||||||
for (auto& future : futures) {
|
|
||||||
future.wait();
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_INFO(Render_Vulkan, "Finished preloading shaders");
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Vulkan
|
} // namespace Vulkan
|
||||||
|
|
|
@ -1,16 +1,9 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <span>
|
#include <span>
|
||||||
#include <string>
|
|
||||||
#include <unordered_map>
|
|
||||||
#include <mutex>
|
|
||||||
#include <atomic>
|
|
||||||
#include <functional>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "video_core/vulkan_common/vulkan_wrapper.h"
|
#include "video_core/vulkan_common/vulkan_wrapper.h"
|
||||||
|
@ -18,48 +11,7 @@
|
||||||
namespace Vulkan {
|
namespace Vulkan {
|
||||||
|
|
||||||
class Device;
|
class Device;
|
||||||
class Scheduler;
|
|
||||||
|
|
||||||
// Command queue system for asynchronous operations
|
|
||||||
void InitializeCommandQueue();
|
|
||||||
void ShutdownCommandQueue();
|
|
||||||
void SubmitCommandToQueue(std::function<void()> command);
|
|
||||||
void CommandQueueWorker();
|
|
||||||
|
|
||||||
// Scheduler integration functions
|
|
||||||
void SetGlobalScheduler(Scheduler* scheduler);
|
|
||||||
void SubmitToScheduler(std::function<void(vk::CommandBuffer)> command);
|
|
||||||
u64 FlushScheduler(VkSemaphore signal_semaphore = nullptr, VkSemaphore wait_semaphore = nullptr);
|
|
||||||
void ProcessAllCommands();
|
|
||||||
|
|
||||||
vk::ShaderModule BuildShader(const Device& device, std::span<const u32> code);
|
vk::ShaderModule BuildShader(const Device& device, std::span<const u32> code);
|
||||||
|
|
||||||
// Enhanced shader functionality
|
|
||||||
bool IsShaderValid(VkShaderModule shader_module);
|
|
||||||
|
|
||||||
void AsyncCompileShader(const Device& device, const std::string& shader_path,
|
|
||||||
std::function<void(VkShaderModule)> callback);
|
|
||||||
|
|
||||||
class ShaderManager {
|
|
||||||
public:
|
|
||||||
explicit ShaderManager(const Device& device);
|
|
||||||
~ShaderManager();
|
|
||||||
|
|
||||||
VkShaderModule GetShaderModule(const std::string& shader_path);
|
|
||||||
void ReloadShader(const std::string& shader_path);
|
|
||||||
bool LoadShader(const std::string& shader_path);
|
|
||||||
void WaitForCompilation();
|
|
||||||
|
|
||||||
// Batch process multiple shaders in parallel
|
|
||||||
void PreloadShaders(const std::vector<std::string>& shader_paths);
|
|
||||||
|
|
||||||
// Integrate with Citron's scheduler
|
|
||||||
void SetScheduler(Scheduler* scheduler);
|
|
||||||
|
|
||||||
private:
|
|
||||||
const Device& device;
|
|
||||||
std::mutex shader_mutex;
|
|
||||||
std::unordered_map<std::string, vk::ShaderModule> shader_cache;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Vulkan
|
} // namespace Vulkan
|
||||||
|
|
|
@ -30,10 +30,6 @@
|
||||||
|
|
||||||
namespace Vulkan {
|
namespace Vulkan {
|
||||||
|
|
||||||
// TextureCacheManager implementations to fix linker errors
|
|
||||||
TextureCacheManager::TextureCacheManager() = default;
|
|
||||||
TextureCacheManager::~TextureCacheManager() = default;
|
|
||||||
|
|
||||||
using Tegra::Engines::Fermi2D;
|
using Tegra::Engines::Fermi2D;
|
||||||
using Tegra::Texture::SwizzleSource;
|
using Tegra::Texture::SwizzleSource;
|
||||||
using Tegra::Texture::TextureMipmapFilter;
|
using Tegra::Texture::TextureMipmapFilter;
|
||||||
|
|
|
@ -5,10 +5,6 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <span>
|
#include <span>
|
||||||
#include <mutex>
|
|
||||||
#include <atomic>
|
|
||||||
#include <string>
|
|
||||||
#include <unordered_map>
|
|
||||||
|
|
||||||
#include "video_core/texture_cache/texture_cache_base.h"
|
#include "video_core/texture_cache/texture_cache_base.h"
|
||||||
|
|
||||||
|
@ -41,22 +37,6 @@ class RenderPassCache;
|
||||||
class StagingBufferPool;
|
class StagingBufferPool;
|
||||||
class Scheduler;
|
class Scheduler;
|
||||||
|
|
||||||
// Enhanced texture management for better error handling and thread safety
|
|
||||||
class TextureCacheManager {
|
|
||||||
public:
|
|
||||||
explicit TextureCacheManager();
|
|
||||||
~TextureCacheManager();
|
|
||||||
|
|
||||||
VkImage GetTextureFromCache(const std::string& texture_path);
|
|
||||||
void ReloadTexture(const std::string& texture_path);
|
|
||||||
bool IsTextureLoadedCorrectly(VkImage texture);
|
|
||||||
void HandleTextureCache();
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::mutex texture_mutex;
|
|
||||||
std::unordered_map<std::string, VkImage> texture_cache;
|
|
||||||
};
|
|
||||||
|
|
||||||
class TextureCacheRuntime {
|
class TextureCacheRuntime {
|
||||||
public:
|
public:
|
||||||
explicit TextureCacheRuntime(const Device& device_, Scheduler& scheduler_,
|
explicit TextureCacheRuntime(const Device& device_, Scheduler& scheduler_,
|
||||||
|
@ -137,10 +117,6 @@ public:
|
||||||
|
|
||||||
VkFormat GetSupportedFormat(VkFormat requested_format, VkFormatFeatureFlags required_features) const;
|
VkFormat GetSupportedFormat(VkFormat requested_format, VkFormatFeatureFlags required_features) const;
|
||||||
|
|
||||||
// Enhanced texture error handling
|
|
||||||
bool IsTextureLoadedCorrectly(VkImage texture);
|
|
||||||
void HandleTextureError(const std::string& texture_path);
|
|
||||||
|
|
||||||
const Device& device;
|
const Device& device;
|
||||||
Scheduler& scheduler;
|
Scheduler& scheduler;
|
||||||
MemoryAllocator& memory_allocator;
|
MemoryAllocator& memory_allocator;
|
||||||
|
@ -152,9 +128,6 @@ public:
|
||||||
const Settings::ResolutionScalingInfo& resolution;
|
const Settings::ResolutionScalingInfo& resolution;
|
||||||
std::array<std::vector<VkFormat>, VideoCore::Surface::MaxPixelFormat> view_formats;
|
std::array<std::vector<VkFormat>, VideoCore::Surface::MaxPixelFormat> view_formats;
|
||||||
|
|
||||||
// Enhanced texture management
|
|
||||||
TextureCacheManager texture_cache_manager;
|
|
||||||
|
|
||||||
static constexpr size_t indexing_slots = 8 * sizeof(size_t);
|
static constexpr size_t indexing_slots = 8 * sizeof(size_t);
|
||||||
std::array<vk::Buffer, indexing_slots> buffers{};
|
std::array<vk::Buffer, indexing_slots> buffers{};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <mutex>
|
|
||||||
#include <string>
|
|
||||||
#include <unordered_map>
|
|
||||||
#include <functional>
|
|
||||||
#include <atomic>
|
|
||||||
#include <optional>
|
|
||||||
|
|
||||||
#include "video_core/vulkan_common/vulkan_wrapper.h"
|
|
||||||
|
|
||||||
namespace Vulkan {
|
|
||||||
|
|
||||||
class Device;
|
|
||||||
class MemoryAllocator;
|
|
||||||
|
|
||||||
// Enhanced texture manager for better error handling and thread safety
|
|
||||||
class TextureManager {
|
|
||||||
public:
|
|
||||||
explicit TextureManager(const Device& device, MemoryAllocator& memory_allocator);
|
|
||||||
~TextureManager();
|
|
||||||
|
|
||||||
// Get a texture from the cache, loading it if necessary
|
|
||||||
VkImage GetTexture(const std::string& texture_path);
|
|
||||||
|
|
||||||
// Force a texture to reload from disk
|
|
||||||
void ReloadTexture(const std::string& texture_path);
|
|
||||||
|
|
||||||
// Check if a texture is loaded correctly
|
|
||||||
bool IsTextureLoadedCorrectly(VkImage texture);
|
|
||||||
|
|
||||||
// Remove old textures from the cache
|
|
||||||
void CleanupTextureCache();
|
|
||||||
|
|
||||||
// Handle texture rendering, with automatic reload if needed
|
|
||||||
void HandleTextureRendering(const std::string& texture_path,
|
|
||||||
std::function<void(VkImage)> render_callback);
|
|
||||||
|
|
||||||
private:
|
|
||||||
// Load a texture from disk and create a Vulkan image
|
|
||||||
vk::Image LoadTexture(const std::string& texture_path);
|
|
||||||
|
|
||||||
// Create a default texture to use in case of errors
|
|
||||||
vk::Image CreateDefaultTexture();
|
|
||||||
|
|
||||||
const Device& device;
|
|
||||||
MemoryAllocator& memory_allocator;
|
|
||||||
std::mutex texture_mutex;
|
|
||||||
std::unordered_map<std::string, vk::Image> texture_cache;
|
|
||||||
std::optional<vk::Image> default_texture;
|
|
||||||
VkFormat texture_format = VK_FORMAT_B8G8R8A8_SRGB;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace Vulkan
|
|
|
@ -140,10 +140,6 @@ public:
|
||||||
return (flags & property_flags) == flags && (type_mask & shifted_memory_type) != 0;
|
return (flags & property_flags) == flags && (type_mask & shifted_memory_type) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool IsEmpty() const noexcept {
|
|
||||||
return commits.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
[[nodiscard]] static constexpr u32 ShiftType(u32 type) {
|
[[nodiscard]] static constexpr u32 ShiftType(u32 type) {
|
||||||
return 1U << type;
|
return 1U << type;
|
||||||
|
@ -288,78 +284,39 @@ MemoryCommit MemoryAllocator::Commit(const VkMemoryRequirements& requirements, M
|
||||||
const u32 type_mask = requirements.memoryTypeBits;
|
const u32 type_mask = requirements.memoryTypeBits;
|
||||||
const VkMemoryPropertyFlags usage_flags = MemoryUsagePropertyFlags(usage);
|
const VkMemoryPropertyFlags usage_flags = MemoryUsagePropertyFlags(usage);
|
||||||
const VkMemoryPropertyFlags flags = MemoryPropertyFlags(type_mask, usage_flags);
|
const VkMemoryPropertyFlags flags = MemoryPropertyFlags(type_mask, usage_flags);
|
||||||
|
|
||||||
// First attempt
|
|
||||||
if (std::optional<MemoryCommit> commit = TryCommit(requirements, flags)) {
|
if (std::optional<MemoryCommit> commit = TryCommit(requirements, flags)) {
|
||||||
return std::move(*commit);
|
return std::move(*commit);
|
||||||
}
|
}
|
||||||
|
// Commit has failed, allocate more memory.
|
||||||
// Commit has failed, allocate more memory
|
|
||||||
const u64 chunk_size = AllocationChunkSize(requirements.size);
|
const u64 chunk_size = AllocationChunkSize(requirements.size);
|
||||||
if (TryAllocMemory(flags, type_mask, chunk_size)) {
|
if (!TryAllocMemory(flags, type_mask, chunk_size)) {
|
||||||
return TryCommit(requirements, flags).value();
|
// TODO(Rodrigo): Handle out of memory situations in some way like flushing to guest memory.
|
||||||
|
throw vk::Exception(VK_ERROR_OUT_OF_DEVICE_MEMORY);
|
||||||
}
|
}
|
||||||
|
// Commit again, this time it won't fail since there's a fresh allocation above.
|
||||||
// Memory allocation failed - try to recover by releasing empty allocations
|
// If it does, there's a bug.
|
||||||
for (auto it = allocations.begin(); it != allocations.end();) {
|
return TryCommit(requirements, flags).value();
|
||||||
if ((*it)->IsEmpty()) {
|
|
||||||
it = allocations.erase(it);
|
|
||||||
} else {
|
|
||||||
++it;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try allocating again after cleanup
|
|
||||||
if (TryAllocMemory(flags, type_mask, chunk_size)) {
|
|
||||||
return TryCommit(requirements, flags).value();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If still failing, try with non-device-local memory as a last resort
|
|
||||||
if (flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) {
|
|
||||||
const VkMemoryPropertyFlags fallback_flags = flags & ~VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
|
|
||||||
if (TryAllocMemory(fallback_flags, type_mask, chunk_size)) {
|
|
||||||
if (auto commit = TryCommit(requirements, fallback_flags)) {
|
|
||||||
LOG_WARNING(Render_Vulkan, "Falling back to non-device-local memory due to OOM");
|
|
||||||
return std::move(*commit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_CRITICAL(Render_Vulkan, "Vulkan memory allocation failed - out of device memory");
|
|
||||||
throw vk::Exception(VK_ERROR_OUT_OF_DEVICE_MEMORY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MemoryAllocator::TryAllocMemory(VkMemoryPropertyFlags flags, u32 type_mask, u64 size) {
|
bool MemoryAllocator::TryAllocMemory(VkMemoryPropertyFlags flags, u32 type_mask, u64 size) {
|
||||||
const auto type_opt = FindType(flags, type_mask);
|
const u32 type = FindType(flags, type_mask).value();
|
||||||
if (!type_opt) {
|
|
||||||
if ((flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0) {
|
|
||||||
// Try to allocate non device local memory
|
|
||||||
return TryAllocMemory(flags & ~VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, type_mask, size);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const u64 aligned_size = (device.GetDriverID() == VK_DRIVER_ID_QUALCOMM_PROPRIETARY) ?
|
|
||||||
Common::AlignUp(size, 4096) : // Adreno requires 4KB alignment
|
|
||||||
size; // Others (NVIDIA, AMD, Intel, etc)
|
|
||||||
|
|
||||||
vk::DeviceMemory memory = device.GetLogical().TryAllocateMemory({
|
vk::DeviceMemory memory = device.GetLogical().TryAllocateMemory({
|
||||||
.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
|
.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
|
||||||
.pNext = nullptr,
|
.pNext = nullptr,
|
||||||
.allocationSize = aligned_size,
|
.allocationSize = size,
|
||||||
.memoryTypeIndex = *type_opt,
|
.memoryTypeIndex = type,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!memory) {
|
if (!memory) {
|
||||||
if ((flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0) {
|
if ((flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0) {
|
||||||
// Try to allocate non device local memory
|
// Try to allocate non device local memory
|
||||||
return TryAllocMemory(flags & ~VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, type_mask, size);
|
return TryAllocMemory(flags & ~VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, type_mask, size);
|
||||||
|
} else {
|
||||||
|
// RIP
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
allocations.push_back(
|
allocations.push_back(
|
||||||
std::make_unique<MemoryAllocation>(this, std::move(memory), flags, aligned_size, *type_opt));
|
std::make_unique<MemoryAllocation>(this, std::move(memory), flags, size, type));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue