Compare commits

...

35 Commits

Author SHA1 Message Date
Joshua Vandaële
c762b7a210
Merge 553a75c996 into 52fcdde485 2025-06-07 21:52:35 +00:00
JMC47
52fcdde485
Merge pull request #13386 from iwubcode/resource_manager_system
VideoCommon: add resource manager, tracks resources to load assets in optimal way and manage memory
2025-06-07 15:51:34 -04:00
JMC47
5eb61024c6
Merge pull request #13740 from JoshuaVandaele/bsod-fix-i-hope
Host: Clean up device handle in all cases
2025-06-07 12:27:49 -04:00
Admiral H. Curtiss
a27b845514
Merge pull request #13710 from TryTwo/UI_Sliders_Update
Advanced Panel convert QSliders into ConfigSliders
2025-06-07 17:39:40 +02:00
Admiral H. Curtiss
1b1ca019a4
Merge pull request #13724 from SuperSamus/gamelist-properties-noduplicates
GameList: Prevent opening Properties multiple times for the same game
2025-06-07 17:24:50 +02:00
Joshua Vandaële
241834709b
Host: Clean up device handle 2025-06-07 16:55:45 +02:00
JosJuice
185b080f03
Merge pull request #13142 from JosJuice/fifo-window-starting-stopping
DolphinQt: Call FIFOPlayerWindow::UpdateControls for Starting/Stopping
2025-06-07 10:43:19 +02:00
iwubcode
c3d3b81533 VideoCommon: remove 'GetByteSizeInMemory()' from custom asset, it is not needed anymore 2025-06-06 23:03:02 -05:00
iwubcode
774a84a953 VideoCommon: avoid race conditions with asset load/unload by moving the lock to the entire function, favor atomics for the memory/time getters 2025-06-06 23:03:02 -05:00
iwubcode
b3f50c969e VideoCommon: rename m_bytes_loaded in asset library to bytes_loaded 2025-06-06 23:03:02 -05:00
iwubcode
3b83907b88 VideoCommon: update CustomAsset's load time to be before the load occurs (this prevents issues where the load time might be incorrectly inflated by long load operations)
Co-authored-by: Jordan Woyak <jordan.woyak@gmail.com>
2025-06-06 23:03:02 -05:00
iwubcode
d940d62cae VideoCommon: watch texture pack folder for texture reloads (from dynamic input textures) 2025-06-06 23:03:02 -05:00
iwubcode
7afa9e6c6f VideoCommon: use CustomResourceManager in the texture cache and hook up to our hires textures 2025-06-06 23:03:02 -05:00
iwubcode
12d178a8df VideoCommon: initialize and shutdown the CustomResourceManager when the video thread initializes and shuts down 2025-06-06 23:03:02 -05:00
iwubcode
f910c1d934 Core: add CustomResourceManager to System 2025-06-06 23:03:02 -05:00
iwubcode
70abcb2030 VideoCommon: add resource manager and new asset loader; the resource manager uses a least recently used cache to determine which assets get priority for loading. Additionally, if the system is low on memory, assets will be purged with the less requested assets being the first to go. The loader is multithreaded now and loads assets as quickly as possible as long as memory is available
Co-authored-by: Jordan Woyak <jordan.woyak@gmail.com>
2025-06-06 23:03:02 -05:00
iwubcode
d8ea31ca46 VideoCommon: rename GameTextureAsset into TextureAsset and make it only contain CustomTextureData. Move validation and load logic to individual functions 2025-06-06 23:03:02 -05:00
iwubcode
2ae43324cb VideoCommon: move AssetMap to a types header file, so it can be pulled in without the DirectFilesystemAssetLibrary dependencies, the header will be expanded later 2025-06-06 23:03:02 -05:00
iwubcode
7d59c2743d Common: Add class 'FilesystemWatcher' that is used to watch paths and receive callbacks about filesystem level events for anything under that path 2025-06-06 23:03:02 -05:00
iwubcode
8113399b68 Externals: add watcher, a library used to watch a filesystem location for changes 2025-06-06 23:03:02 -05:00
iwubcode
bafe78203d VideoCommon: remove 'GetLastAssetWriteTime' and switch to a steady_clock for asset times 2025-06-06 23:03:02 -05:00
iwubcode
15f125ebee VideoCommon: change asset loading to return the number of bytes loaded instead of a pass/fail 2025-06-06 23:03:02 -05:00
iwubcode
316740daed VideoCommon: add 'Unload' functionality to CustomAsset 2025-06-06 23:03:02 -05:00
Jordan Woyak
903eafcf65
Merge pull request #13714 from Dentomologist/dolphinqt_remove_redundant_window_hints
DolphinQt: Remove redundant window hints
2025-06-06 22:13:01 -05:00
Dentomologist
2a7e8a4003 DolphinQt: Remove redundant window hints
Remove window hints clearing the flag Qt::WindowContextHelpButtonHint,
which is already off by default in Qt 6.

In Qt 5 this flag was set by default for QDialogs, and on Windows put a
? button in the corner of the title bar allowing users to activate Qt's
QWhatsThis help system for a given widget. Since we don't set that text
the ? button was useless and so we hid it manually.
2025-06-06 19:35:13 -07:00
iwubcode
9ec69b5925 VideoCommon: add a handle to custom asset, this is an id that is only relevant for a particular game session but is slightly faster as a numeric value for lookups than the traditional asset id 2025-06-06 19:20:25 -05:00
iwubcode
d7de49ccf6 Core / VideoCommon: Remove original custom asset loader 2025-06-06 19:20:25 -05:00
JMC47
5ec5db9240
Merge pull request #13392 from jordan-woyak/frame-pacing-accurate-time
CoreTiming: Add setting to pursue accurate overall emulation runtime
2025-06-06 19:17:44 -04:00
JMC47
974c56d3c5
Merge pull request #13731 from iwubcode/pixel_shader_gen_fix_complete_initialize
VideoCommon: ensure pixel shader gen input structure is completely initialized
2025-06-06 19:08:57 -04:00
iwubcode
ae26b38fc0 VideoCommon: fix pixel shader gen error about structure not being fully initialized 2025-06-05 01:37:45 -05:00
Martino Fontana
2de9122b5f GameList: Prevent opening Properties multiple times for the same game 2025-06-04 23:11:09 +02:00
TryTwo
a6a5e201b6 Qt Advanced Panel: Convert QSliders into ConfigSliders.
Part of the refactor into the config system.
2025-05-26 19:52:30 -07:00
Jordan Woyak
ec29d120b5 CoreTiming: Add a setting to pursue accurate emulation time. 2025-05-02 15:23:43 -05:00
Joshua Vandaële
553a75c996
ScmRevGen: Determine the commits ahead using upstream if possible 2025-04-07 13:28:22 +02:00
JosJuice
d48e6e25ad DolphinQt: Call FIFOPlayerWindow::UpdateControls for Starting/Stopping
Follow-up for
https://github.com/dolphin-emu/dolphin/pull/12918#discussion_r1785153070.
2024-10-20 13:38:24 +02:00
97 changed files with 1535 additions and 1117 deletions

3
.gitmodules vendored
View File

@ -84,6 +84,9 @@
[submodule "Externals/Vulkan-Headers"]
path = Externals/Vulkan-Headers
url = https://github.com/KhronosGroup/Vulkan-Headers.git
[submodule "Externals/watcher/watcher"]
path = Externals/watcher/watcher
url = https://github.com/e-dant/watcher.git
[submodule "Externals/SFML/SFML"]
path = Externals/SFML/SFML
url = https://github.com/SFML/SFML.git

View File

@ -2,6 +2,18 @@ cmake_minimum_required(VERSION 3.13)
# for revision info
if(GIT_FOUND)
# Determine whether upstream/master exists
execute_process(WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} ls-remote --heads upstream master
RESULT_VARIABLE UPSTREAM_RESULT
OUTPUT_VARIABLE UPSTREAM_OUTPUT
OUTPUT_STRIP_TRAILING_WHITESPACE)
if(UPSTREAM_RESULT EQUAL 0 AND UPSTREAM_OUTPUT)
set(BASE_BRANCH "upstream/master")
else()
set(BASE_BRANCH "master")
endif()
# defines DOLPHIN_WC_REVISION
execute_process(WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
OUTPUT_VARIABLE DOLPHIN_WC_REVISION
@ -19,7 +31,7 @@ if(GIT_FOUND)
OUTPUT_VARIABLE DOLPHIN_WC_BRANCH
OUTPUT_STRIP_TRAILING_WHITESPACE)
# defines DOLPHIN_WC_COMMITS_AHEAD_MASTER
execute_process(WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-list --count HEAD ^master
execute_process(WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-list --count HEAD ^${BASE_BRANCH}
OUTPUT_VARIABLE DOLPHIN_WC_COMMITS_AHEAD_MASTER
OUTPUT_STRIP_TRAILING_WHITESPACE)

View File

@ -784,6 +784,8 @@ if (USE_RETRO_ACHIEVEMENTS)
add_subdirectory(Externals/rcheevos)
endif()
add_subdirectory(Externals/watcher)
########################################
# Pre-build events: Define configuration variables and write SCM info header
#

4
Externals/watcher/CMakeLists.txt vendored Normal file
View File

@ -0,0 +1,4 @@
add_library(watcher INTERFACE IMPORTED GLOBAL)
set_target_properties(watcher PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_LIST_DIR}/watcher/include
)

1
Externals/watcher/watcher vendored Submodule

@ -0,0 +1 @@
Subproject commit b03bdcfc11549df595b77239cefe2643943a3e2f

View File

@ -64,6 +64,8 @@ add_library(common
FatFsUtil.h
FileSearch.cpp
FileSearch.h
FilesystemWatcher.cpp
FilesystemWatcher.h
FileUtil.cpp
FileUtil.h
FixedSizeQueue.h
@ -184,6 +186,7 @@ PRIVATE
FatFs
Iconv::Iconv
spng::spng
watcher
${VTUNE_LIBRARIES}
)

View File

@ -0,0 +1,67 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Common/FilesystemWatcher.h"
#include <wtr/watcher.hpp>
#include "Common/Logging/Log.h"
#include "Common/StringUtil.h"
namespace Common
{
FilesystemWatcher::FilesystemWatcher() = default;
FilesystemWatcher::~FilesystemWatcher() = default;
void FilesystemWatcher::Watch(const std::string& path)
{
const auto [iter, inserted] = m_watched_paths.try_emplace(path, nullptr);
if (inserted)
{
iter->second = std::make_unique<wtr::watch>(path, [this](wtr::event e) {
const auto watched_path = PathToString(e.path_name);
if (e.path_type == wtr::event::path_type::watcher)
{
if (watched_path.starts_with('e'))
ERROR_LOG_FMT(COMMON, "Filesystem watcher: '{}'", watched_path);
else if (watched_path.starts_with('w'))
WARN_LOG_FMT(COMMON, "Filesystem watcher: '{}'", watched_path);
return;
}
if (e.effect_type == wtr::event::effect_type::create)
{
const auto path = WithUnifiedPathSeparators(watched_path);
PathAdded(path);
}
else if (e.effect_type == wtr::event::effect_type::modify)
{
const auto path = WithUnifiedPathSeparators(watched_path);
PathModified(path);
}
else if (e.effect_type == wtr::event::effect_type::rename)
{
if (!e.associated)
{
WARN_LOG_FMT(COMMON, "Rename on path '{}' seen without association!", watched_path);
return;
}
const auto old_path = WithUnifiedPathSeparators(watched_path);
const auto new_path = WithUnifiedPathSeparators(PathToString(e.associated->path_name));
PathRenamed(old_path, new_path);
}
else if (e.effect_type == wtr::event::effect_type::destroy)
{
const auto path = WithUnifiedPathSeparators(watched_path);
PathDeleted(path);
}
});
}
}
void FilesystemWatcher::Unwatch(const std::string& path)
{
m_watched_paths.erase(path);
}
} // namespace Common

View File

@ -0,0 +1,47 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <map>
#include <memory>
#include <string>
#include <string_view>
namespace wtr
{
inline namespace watcher
{
class watch;
}
} // namespace wtr
namespace Common
{
// A class that can watch a path and receive callbacks
// when files or directories underneath that path receive events
class FilesystemWatcher
{
public:
FilesystemWatcher();
virtual ~FilesystemWatcher();
void Watch(const std::string& path);
void Unwatch(const std::string& path);
private:
// A new file or folder was added to one of the watched paths
virtual void PathAdded(std::string_view path) {}
// A file or folder was modified in one of the watched paths
virtual void PathModified(std::string_view path) {}
// A file or folder was renamed in one of the watched paths
virtual void PathRenamed(std::string_view old_path, std::string_view new_path) {}
// A file or folder was deleted in one of the watched paths
virtual void PathDeleted(std::string_view path) {}
std::map<std::string, std::unique_ptr<wtr::watch>> m_watched_paths;
};
} // namespace Common

View File

@ -5,9 +5,18 @@ var outfile = "./scmrev.h";
var cmd_revision = " rev-parse HEAD";
var cmd_describe = " describe --always --long --dirty";
var cmd_branch = " rev-parse --abbrev-ref HEAD";
var cmd_commits_ahead = " rev-list --count HEAD ^master";
var cmd_get_tag = " describe --exact-match HEAD";
function RemoteBranchExists(remote) {
var cmd = gitexe + " ls-remote --heads " + remote + " master";
try {
var output = wshShell.Exec(cmd).StdOut.ReadLine();
return output.trim() !== "";
} catch (e) {
return false;
}
}
function GetGitExe()
{
try
@ -92,8 +101,12 @@ function GetFileContents(f)
}
}
// get info from git
var gitexe = GetGitExe();
var base_branch = RemoteBranchExists("upstream") ? "upstream/master" : "master";
var cmd_commits_ahead = " rev-list --count HEAD ^" + base_branch;
// get info from git
var revision = GetFirstStdOutLine(gitexe + cmd_revision);
var describe = GetFirstStdOutLine(gitexe + cmd_describe);
var branch = GetFirstStdOutLine(gitexe + cmd_branch);

View File

@ -45,6 +45,7 @@ const Info<bool> MAIN_ACCURATE_CPU_CACHE{{System::Main, "Core", "AccurateCPUCach
const Info<bool> MAIN_DSP_HLE{{System::Main, "Core", "DSPHLE"}, true};
const Info<int> MAIN_MAX_FALLBACK{{System::Main, "Core", "MaxFallback"}, 100};
const Info<int> MAIN_TIMING_VARIANCE{{System::Main, "Core", "TimingVariance"}, 40};
const Info<bool> MAIN_CORRECT_TIME_DRIFT{{System::Main, "Core", "CorrectTimeDrift"}, false};
const Info<bool> MAIN_CPU_THREAD{{System::Main, "Core", "CPUThread"}, true};
const Info<bool> MAIN_SYNC_ON_SKIP_IDLE{{System::Main, "Core", "SyncOnSkipIdle"}, true};
const Info<std::string> MAIN_DEFAULT_ISO{{System::Main, "Core", "DefaultISO"}, ""};

View File

@ -63,6 +63,7 @@ extern const Info<bool> MAIN_ACCURATE_CPU_CACHE;
extern const Info<bool> MAIN_DSP_HLE;
extern const Info<int> MAIN_MAX_FALLBACK;
extern const Info<int> MAIN_TIMING_VARIANCE;
extern const Info<bool> MAIN_CORRECT_TIME_DRIFT;
extern const Info<bool> MAIN_CPU_THREAD;
extern const Info<bool> MAIN_SYNC_ON_SKIP_IDLE;
extern const Info<std::string> MAIN_DEFAULT_ISO;

View File

@ -82,7 +82,6 @@
#include "InputCommon/ControllerInterface/ControllerInterface.h"
#include "InputCommon/GCAdapter.h"
#include "VideoCommon/Assets/CustomAssetLoader.h"
#include "VideoCommon/AsyncRequests.h"
#include "VideoCommon/Fifo.h"
#include "VideoCommon/FrameDumper.h"
@ -528,9 +527,6 @@ static void EmuThread(Core::System& system, std::unique_ptr<BootParameters> boot
FreeLook::LoadInputConfig();
system.GetCustomAssetLoader().Init();
Common::ScopeGuard asset_loader_guard([&system] { system.GetCustomAssetLoader().Shutdown(); });
system.GetMovie().Init(*boot);
Common::ScopeGuard movie_guard([&system] { system.GetMovie().Shutdown(); });

View File

@ -105,10 +105,20 @@ void CoreTimingManager::Init()
m_last_oc_factor = m_config_oc_factor;
m_globals.last_OC_factor_inverted = m_config_oc_inv_factor;
m_on_state_changed_handle = Core::AddOnStateChangedCallback([this](Core::State state) {
if (state == Core::State::Running)
{
// We don't want Throttle to attempt catch-up for all the time lost while paused.
ResetThrottle(GetTicks());
}
});
}
void CoreTimingManager::Shutdown()
{
Core::RemoveOnStateChangedCallback(&m_on_state_changed_handle);
std::lock_guard lk(m_ts_write_lock);
MoveEvents();
ClearPendingEvents();
@ -131,6 +141,8 @@ void CoreTimingManager::RefreshConfig()
m_max_variance = std::chrono::duration_cast<DT>(DT_ms(Config::Get(Config::MAIN_TIMING_VARIANCE)));
m_correct_time_drift = Config::Get(Config::MAIN_CORRECT_TIME_DRIFT);
if (AchievementManager::GetInstance().IsHardcoreModeActive() &&
Config::Get(Config::MAIN_EMULATION_SPEED) < 1.0f &&
Config::Get(Config::MAIN_EMULATION_SPEED) > 0.0f)
@ -428,7 +440,9 @@ void CoreTimingManager::Throttle(const s64 target_cycle)
const TimePoint time = Clock::now();
const TimePoint min_target = time - m_max_fallback;
if (target_time < min_target)
// "Correct Time Drift" setting prevents timing relaxing.
if (!m_correct_time_drift && target_time < min_target)
{
// Core is running too slow.. i.e. CPU bottleneck.
const DT adjustment = min_target - target_time;

View File

@ -211,6 +211,7 @@ private:
DT m_max_fallback = {};
DT m_max_variance = {};
bool m_correct_time_drift = false;
double m_emulation_speed = 1.0;
bool IsSpeedUnlimited() const;
@ -225,6 +226,8 @@ private:
std::atomic_bool m_use_precision_timer = false;
Common::PrecisionTimer m_precision_cpu_timer;
Common::PrecisionTimer m_precision_gpu_timer;
int m_on_state_changed_handle;
};
} // namespace CoreTiming

View File

@ -78,8 +78,8 @@ std::string USBHost::GetDeviceNameFromVIDPID(u16 vid, u16 pid)
libusb_get_string_descriptor_ascii(handle, desc.iProduct, buffer, sizeof(buffer)) > 0)
{
device_name = reinterpret_cast<char*>(buffer);
libusb_close(handle);
}
libusb_close(handle);
}
return false;
}

View File

@ -33,7 +33,7 @@
#include "IOS/USB/Emulated/Infinity.h"
#include "IOS/USB/Emulated/Skylanders/Skylander.h"
#include "IOS/USB/USBScanner.h"
#include "VideoCommon/Assets/CustomAssetLoader.h"
#include "VideoCommon/Assets/CustomResourceManager.h"
#include "VideoCommon/CommandProcessor.h"
#include "VideoCommon/Fifo.h"
#include "VideoCommon/GeometryShaderManager.h"
@ -96,7 +96,7 @@ struct System::Impl
VideoInterface::VideoInterfaceManager m_video_interface;
Interpreter m_interpreter;
JitInterface m_jit_interface;
VideoCommon::CustomAssetLoader m_custom_asset_loader;
VideoCommon::CustomResourceManager m_custom_resource_manager;
FifoPlayer m_fifo_player;
FifoRecorder m_fifo_recorder;
Movie::MovieManager m_movie;
@ -335,8 +335,8 @@ VideoInterface::VideoInterfaceManager& System::GetVideoInterface() const
return m_impl->m_video_interface;
}
VideoCommon::CustomAssetLoader& System::GetCustomAssetLoader() const
VideoCommon::CustomResourceManager& System::GetCustomResourceManager() const
{
return m_impl->m_custom_asset_loader;
return m_impl->m_custom_resource_manager;
}
} // namespace Core

View File

@ -108,8 +108,8 @@ class SystemTimersManager;
}
namespace VideoCommon
{
class CustomAssetLoader;
}
class CustomResourceManager;
} // namespace VideoCommon
namespace VideoInterface
{
class VideoInterfaceManager;
@ -197,7 +197,7 @@ public:
VertexShaderManager& GetVertexShaderManager() const;
XFStateManager& GetXFStateManager() const;
VideoInterface::VideoInterfaceManager& GetVideoInterface() const;
VideoCommon::CustomAssetLoader& GetCustomAssetLoader() const;
VideoCommon::CustomResourceManager& GetCustomResourceManager() const;
private:
System();

View File

@ -59,6 +59,7 @@
<ClInclude Include="Common\Event.h" />
<ClInclude Include="Common\FatFsUtil.h" />
<ClInclude Include="Common\FileSearch.h" />
<ClInclude Include="Common\FilesystemWatcher.h" />
<ClInclude Include="Common\FileUtil.h" />
<ClInclude Include="Common\FixedSizeQueue.h" />
<ClInclude Include="Common\Flag.h" />
@ -669,12 +670,16 @@
<ClInclude Include="VideoCommon\Assets\CustomAsset.h" />
<ClInclude Include="VideoCommon\Assets\CustomAssetLibrary.h" />
<ClInclude Include="VideoCommon\Assets\CustomAssetLoader.h" />
<ClInclude Include="VideoCommon\Assets\CustomResourceManager.h" />
<ClInclude Include="VideoCommon\Assets\CustomTextureData.h" />
<ClInclude Include="VideoCommon\Assets\DirectFilesystemAssetLibrary.h" />
<ClInclude Include="VideoCommon\Assets\MaterialAsset.h" />
<ClInclude Include="VideoCommon\Assets\MeshAsset.h" />
<ClInclude Include="VideoCommon\Assets\ShaderAsset.h" />
<ClInclude Include="VideoCommon\Assets\TextureAsset.h" />
<ClInclude Include="VideoCommon\Assets\TextureAssetUtils.h" />
<ClInclude Include="VideoCommon\Assets\Types.h" />
<ClInclude Include="VideoCommon\Assets\WatchableFilesystemAssetLibrary.h" />
<ClInclude Include="VideoCommon\AsyncRequests.h" />
<ClInclude Include="VideoCommon\AsyncShaderCompiler.h" />
<ClInclude Include="VideoCommon\BoundingBox.h" />
@ -814,6 +819,7 @@
<ClCompile Include="Common\ENet.cpp" />
<ClCompile Include="Common\FatFsUtil.cpp" />
<ClCompile Include="Common\FileSearch.cpp" />
<ClCompile Include="Common\FilesystemWatcher.cpp" />
<ClCompile Include="Common\FileUtil.cpp" />
<ClCompile Include="Common\FloatUtils.cpp" />
<ClCompile Include="Common\GekkoDisassembler.cpp" />
@ -1320,14 +1326,15 @@
<ClCompile Include="VideoCommon\AbstractStagingTexture.cpp" />
<ClCompile Include="VideoCommon\AbstractTexture.cpp" />
<ClCompile Include="VideoCommon\Assets\CustomAsset.cpp" />
<ClCompile Include="VideoCommon\Assets\CustomAssetLibrary.cpp" />
<ClCompile Include="VideoCommon\Assets\CustomAssetLoader.cpp" />
<ClCompile Include="VideoCommon\Assets\CustomResourceManager.cpp" />
<ClCompile Include="VideoCommon\Assets\CustomTextureData.cpp" />
<ClCompile Include="VideoCommon\Assets\DirectFilesystemAssetLibrary.cpp" />
<ClCompile Include="VideoCommon\Assets\MaterialAsset.cpp" />
<ClCompile Include="VideoCommon\Assets\MeshAsset.cpp" />
<ClCompile Include="VideoCommon\Assets\ShaderAsset.cpp" />
<ClCompile Include="VideoCommon\Assets\TextureAsset.cpp" />
<ClCompile Include="VideoCommon\Assets\TextureAssetUtils.cpp" />
<ClCompile Include="VideoCommon\AsyncRequests.cpp" />
<ClCompile Include="VideoCommon\AsyncShaderCompiler.cpp" />
<ClCompile Include="VideoCommon\BoundingBox.cpp" />

View File

@ -15,7 +15,6 @@
AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent)
{
setWindowTitle(tr("About Dolphin"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
QString branch_str = QString::fromStdString(Common::GetScmBranchStr());
const int commits_ahead = Common::GetScmCommitsAheadMaster();

View File

@ -27,7 +27,6 @@
AchievementsWindow::AchievementsWindow(QWidget* parent) : QDialog(parent)
{
setWindowTitle(tr("Achievements"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
CreateMainLayout();
ConnectWidgets();

View File

@ -26,7 +26,6 @@ CheatsManager::CheatsManager(Core::System& system, QWidget* parent)
: QDialog(parent), m_system(system)
{
setWindowTitle(tr("Cheats Manager"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
connect(&Settings::Instance(), &Settings::EmulationStateChanged, this,
&CheatsManager::OnStateChanged);

View File

@ -23,7 +23,6 @@
CheatCodeEditor::CheatCodeEditor(QWidget* parent) : QDialog(parent)
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setWindowTitle(tr("Cheat Code Editor"));
CreateWidgets();

View File

@ -80,6 +80,36 @@ void ConfigSlider::OnConfigChanged()
}
}
ConfigSliderU32::ConfigSliderU32(u32 minimum, u32 maximum, const Config::Info<u32>& setting,
u32 scale)
: ConfigSliderU32(minimum, maximum, setting, nullptr, scale)
{
}
ConfigSliderU32::ConfigSliderU32(u32 minimum, u32 maximum, const Config::Info<u32>& setting,
Config::Layer* layer, u32 scale)
: ConfigControl(Qt::Horizontal, setting.GetLocation(), layer), m_setting(setting),
m_scale(scale)
{
setMinimum(minimum);
setMaximum(maximum);
setValue(ReadValue(setting));
OnConfigChanged();
connect(this, &ConfigSliderU32::valueChanged, this, &ConfigSliderU32::Update);
}
void ConfigSliderU32::Update(u32 value)
{
SaveValue(m_setting, value * m_scale);
}
void ConfigSliderU32::OnConfigChanged()
{
setValue(ReadValue(m_setting) / m_scale);
}
ConfigSliderLabel::ConfigSliderLabel(const QString& text, ConfigSlider* slider)
: QLabel(text), m_slider(QPointer<ConfigSlider>(slider))
{

View File

@ -11,6 +11,7 @@
#include "DolphinQt/Config/ConfigControls/ConfigControl.h"
#include "DolphinQt/Config/ToolTipControls/ToolTipSlider.h"
#include "Common/CommonTypes.h"
#include "Common/Config/ConfigInfo.h"
class ConfigSlider final : public ConfigControl<ToolTipSlider>
@ -38,6 +39,25 @@ private:
std::vector<int> m_tick_values;
};
class ConfigSliderU32 final : public ConfigControl<ToolTipSlider>
{
Q_OBJECT
public:
ConfigSliderU32(u32 minimum, u32 maximum, const Config::Info<u32>& setting, u32 scale = 1);
ConfigSliderU32(u32 minimum, u32 maximum, const Config::Info<u32>& setting, Config ::Layer* layer,
u32 scale = 1);
void Update(u32 value);
protected:
void OnConfigChanged() override;
private:
const Config::Info<u32> m_setting;
u32 m_scale = 1;
};
class ConfigSliderLabel final : public QLabel
{
Q_OBJECT

View File

@ -17,7 +17,6 @@ ControllerInterfaceWindow::ControllerInterfaceWindow(QWidget* parent) : QDialog(
CreateMainLayout();
setWindowTitle(tr("Alternate Input Sources"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
}
void ControllerInterfaceWindow::CreateMainLayout()

View File

@ -29,7 +29,6 @@ DualShockUDPClientAddServerDialog::DualShockUDPClientAddServerDialog(QWidget* pa
void DualShockUDPClientAddServerDialog::CreateWidgets()
{
setWindowTitle(tr("Add New DSU Server"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
m_main_layout = new QGridLayout;

View File

@ -17,7 +17,6 @@ FreeLookWindow::FreeLookWindow(QWidget* parent) : QDialog(parent)
ConnectWidgets();
setWindowTitle(tr("Free Look Settings"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
}
void FreeLookWindow::CreateMainLayout()

View File

@ -22,7 +22,6 @@
ColorCorrectionConfigWindow::ColorCorrectionConfigWindow(QWidget* parent) : QDialog(parent)
{
setWindowTitle(tr("Color Correction Configuration"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
Create();
ConnectWidgets();

View File

@ -28,7 +28,6 @@ GraphicsWindow::GraphicsWindow(MainWindow* parent) : QDialog(parent), m_main_win
CreateMainLayout();
setWindowTitle(tr("Graphics"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
OnBackendChanged(QString::fromStdString(Config::Get(Config::MAIN_GFX_BACKEND)));

View File

@ -42,7 +42,6 @@ PostProcessingConfigWindow::PostProcessingConfigWindow(EnhancementsWidget* paren
}
setWindowTitle(tr("Post-Processing Shader Configuration"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
PopulateGroups();
Create();

View File

@ -17,8 +17,6 @@
GCPadWiiUConfigDialog::GCPadWiiUConfigDialog(int port, QWidget* parent)
: QDialog(parent), m_port{port}
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
CreateLayout();
LoadSettings();

View File

@ -268,7 +268,6 @@ IOWindow::IOWindow(MappingWindow* window, ControllerEmu::EmulatedController* con
connect(&Settings::Instance(), &Settings::ConfigChanged, this, &IOWindow::ConfigChanged);
setWindowTitle(type == IOWindow::Type::Input ? tr("Configure Input") : tr("Configure Output"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
ConfigChanged();

View File

@ -65,7 +65,6 @@ MappingWindow::MappingWindow(QWidget* parent, Type type, int port_num)
: QDialog(parent), m_port(port_num)
{
setWindowTitle(tr("Port %1").arg(port_num + 1));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
CreateDevicesLayout();
CreateProfilesLayout();

View File

@ -38,7 +38,6 @@ NewPatchDialog::NewPatchDialog(QWidget* parent, PatchEngine::Patch& patch)
: QDialog(parent), m_patch(patch)
{
setWindowTitle(tr("Patch Editor"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
CreateWidgets();
ConnectWidgets();

View File

@ -25,7 +25,7 @@
#include "UICommon/GameFile.h"
PropertiesDialog::PropertiesDialog(QWidget* parent, const UICommon::GameFile& game)
: StackedSettingsWindow{parent}
: StackedSettingsWindow{parent}, m_filepath(game.GetFilePath())
{
setWindowTitle(QStringLiteral("%1: %2 - %3")
.arg(QString::fromStdString(game.GetFileName()),

View File

@ -17,6 +17,7 @@ class PropertiesDialog final : public StackedSettingsWindow
Q_OBJECT
public:
explicit PropertiesDialog(QWidget* parent, const UICommon::GameFile& game);
const std::string& GetFilePath() const { return m_filepath; }
signals:
void OpenGeneralSettings();
@ -24,4 +25,7 @@ signals:
#ifdef USE_RETRO_ACHIEVEMENTS
void OpenAchievementSettings();
#endif // USE_RETRO_ACHIEVEMENTS
private:
const std::string m_filepath;
};

View File

@ -24,8 +24,6 @@
StackedSettingsWindow::StackedSettingsWindow(QWidget* parent) : QDialog{parent}
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
// This eliminates the ugly line between the title bar and window contents with KDE Plasma.
setStyleSheet(QStringLiteral("QDialog { border: none; }"));

View File

@ -42,7 +42,6 @@ ConvertDialog::ConvertDialog(QList<std::shared_ptr<const UICommon::GameFile>> fi
ASSERT(!m_files.empty());
setWindowTitle(tr("Convert"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
QGridLayout* grid_layout = new QGridLayout;
grid_layout->setColumnStretch(1, 1);

View File

@ -41,7 +41,6 @@ QString HtmlFormatErrorLine(const Common::GekkoAssembler::AssemblerError& err)
AssembleInstructionDialog::AssembleInstructionDialog(QWidget* parent, u32 address, u32 value)
: QDialog(parent), m_code(value), m_address(address)
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setWindowModality(Qt::WindowModal);
setWindowTitle(tr("Instruction"));

View File

@ -200,7 +200,7 @@ BranchWatchDialog::BranchWatchDialog(Core::System& system, Core::BranchWatch& br
: QDialog(parent), m_system(system), m_branch_watch(branch_watch), m_code_widget(code_widget)
{
setWindowTitle(tr("Branch Watch Tool"));
setWindowFlags((windowFlags() | Qt::WindowMinMaxButtonsHint) & ~Qt::WindowContextHelpButtonHint);
setWindowFlags(windowFlags() | Qt::WindowMinMaxButtonsHint);
// Branch Watch Table
m_table_view = new QTableView(nullptr);

View File

@ -24,7 +24,6 @@
BreakpointDialog::BreakpointDialog(BreakpointWidget* parent)
: QDialog(parent), m_parent(parent), m_open_mode(OpenMode::New)
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setWindowTitle(tr("New Breakpoint"));
CreateWidgets();
ConnectWidgets();
@ -36,7 +35,6 @@ BreakpointDialog::BreakpointDialog(BreakpointWidget* parent)
BreakpointDialog::BreakpointDialog(BreakpointWidget* parent, const TBreakPoint* breakpoint)
: QDialog(parent), m_parent(parent), m_open_mode(OpenMode::EditBreakPoint)
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setWindowTitle(tr("Edit Breakpoint"));
CreateWidgets();
ConnectWidgets();
@ -56,7 +54,6 @@ BreakpointDialog::BreakpointDialog(BreakpointWidget* parent, const TBreakPoint*
BreakpointDialog::BreakpointDialog(BreakpointWidget* parent, const TMemCheck* memcheck)
: QDialog(parent), m_parent(parent), m_open_mode(OpenMode::EditMemCheck)
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setWindowTitle(tr("Edit Breakpoint"));
CreateWidgets();

View File

@ -14,7 +14,6 @@
PatchInstructionDialog::PatchInstructionDialog(QWidget* parent, u32 address, u32 value)
: QDialog(parent), m_address(address)
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setWindowModality(Qt::WindowModal);
setWindowTitle(tr("Instruction"));

View File

@ -21,7 +21,6 @@ DiscordJoinRequestDialog::DiscordJoinRequestDialog(QWidget* parent, const std::s
: QDialog(parent), m_user_id(id), m_close_timestamp(std::time(nullptr) + s_max_lifetime_seconds)
{
setWindowTitle(tr("Request to Join Your Party"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
QPixmap avatar_pixmap;

View File

@ -59,10 +59,18 @@ FIFOPlayerWindow::FIFOPlayerWindow(FifoPlayer& fifo_player, FifoRecorder& fifo_r
});
connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, [this](Core::State state) {
// We don't want to trigger OnEmulationStarted when going from Paused to Running,
// and nothing in UpdateControls treats Paused and Running differently
if (state == Core::State::Paused)
state = Core::State::Running;
// Skip redundant updates
if (state == m_emu_state)
return;
if (state == Core::State::Running && m_emu_state != Core::State::Paused)
UpdateControls();
if (state == Core::State::Running)
OnEmulationStarted();
else if (state == Core::State::Uninitialized)
OnEmulationStopped();
@ -266,8 +274,6 @@ void FIFOPlayerWindow::StopRecording()
void FIFOPlayerWindow::OnEmulationStarted()
{
UpdateControls();
if (m_fifo_player.GetFile())
OnFIFOLoaded();
}
@ -278,7 +284,6 @@ void FIFOPlayerWindow::OnEmulationStopped()
if (m_fifo_recorder.IsRecording())
StopRecording();
UpdateControls();
// When emulation stops, switch away from the analyzer tab, as it no longer shows anything useful
m_tab_widget->setCurrentWidget(m_main_widget);
m_analyzer->Update();

View File

@ -60,7 +60,6 @@ GCMemcardCreateNewDialog::GCMemcardCreateNewDialog(QWidget* parent) : QDialog(pa
});
setWindowTitle(tr("Create New Memory Card"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
}
GCMemcardCreateNewDialog::~GCMemcardCreateNewDialog() = default;

View File

@ -98,7 +98,6 @@ GCMemcardManager::GCMemcardManager(QWidget* parent) : QDialog(parent)
resize(650, 500);
setWindowTitle(tr("GameCube Memory Card Manager"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
}
GCMemcardManager::~GCMemcardManager() = default;

View File

@ -566,6 +566,15 @@ void GameList::OpenProperties()
if (!game)
return;
auto property_windows = this->findChildren<PropertiesDialog*>();
auto it =
std::ranges::find(property_windows, game->GetFilePath(), &PropertiesDialog::GetFilePath);
if (it != property_windows.end())
{
(*it)->raise();
return;
}
PropertiesDialog* properties = new PropertiesDialog(this, *game);
connect(properties, &PropertiesDialog::OpenGeneralSettings, this, &GameList::OpenGeneralSettings);

View File

@ -27,7 +27,6 @@ NANDRepairDialog::NANDRepairDialog(const WiiUtils::NANDCheckResult& result, QWid
: QDialog(parent)
{
setWindowTitle(tr("NAND Check"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setWindowIcon(Resources::GetAppIcon());
QVBoxLayout* main_layout = new QVBoxLayout();

View File

@ -30,7 +30,6 @@ bool NKitWarningDialog::ShowUnlessDisabled(QWidget* parent)
NKitWarningDialog::NKitWarningDialog(QWidget* parent) : QDialog(parent)
{
setWindowTitle(tr("NKit Warning"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setWindowIcon(Resources::GetAppIcon());
QVBoxLayout* main_layout = new QVBoxLayout;

View File

@ -45,7 +45,6 @@ ChunkedProgressDialog::ChunkedProgressDialog(QWidget* parent) : QDialog(parent)
CreateWidgets();
ConnectWidgets();
setWindowTitle(tr("Data Transfer"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
}
void ChunkedProgressDialog::CreateWidgets()

View File

@ -14,7 +14,6 @@
GameListDialog::GameListDialog(const GameListModel& game_list_model, QWidget* parent)
: QDialog(parent), m_game_list_model(game_list_model)
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setWindowTitle(tr("Select a game"));
CreateWidgets();

View File

@ -32,7 +32,6 @@
NetPlayBrowser::NetPlayBrowser(QWidget* parent) : QDialog(parent)
{
setWindowTitle(tr("NetPlay Session Browser"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
CreateWidgets();
RestoreSettings();
@ -297,7 +296,6 @@ void NetPlayBrowser::accept()
{
QInputDialog dialog(this);
dialog.setWindowFlags(dialog.windowFlags() & ~Qt::WindowContextHelpButtonHint);
dialog.setWindowTitle(tr("Enter password"));
dialog.setLabelText(tr("This session requires a password:"));
dialog.setWindowModality(Qt::WindowModal);

View File

@ -97,8 +97,6 @@ NetPlayDialog::NetPlayDialog(const GameListModel& game_list_model,
: QDialog(parent), m_game_list_model(game_list_model),
m_start_game_callback(std::move(start_game_callback))
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setWindowTitle(tr("NetPlay"));
setWindowIcon(Resources::GetAppIcon());

View File

@ -32,7 +32,6 @@ NetPlaySetupDialog::NetPlaySetupDialog(const GameListModel& game_list_model, QWi
: QDialog(parent), m_game_list_model(game_list_model)
{
setWindowTitle(tr("NetPlay Setup"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
CreateMainLayout();

View File

@ -17,7 +17,6 @@
PadMappingDialog::PadMappingDialog(QWidget* parent) : QDialog(parent)
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setWindowTitle(tr("Assign Controllers"));
CreateWidgets();

View File

@ -26,7 +26,6 @@ public:
ParallelProgressDialog(Args&&... args) : m_dialog{std::forward<Args>(args)...}
{
setParent(m_dialog.parent());
m_dialog.setWindowFlags(m_dialog.windowFlags() & ~Qt::WindowContextHelpButtonHint);
ConnectSignalsAndSlots();
}

View File

@ -24,7 +24,6 @@ ResourcePackManager::ResourcePackManager(QWidget* widget) : QDialog(widget)
RepopulateTable();
setWindowTitle(tr("Resource Pack Manager"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
resize(QSize(900, 600));
}

View File

@ -46,7 +46,6 @@ RiivolutionBootWidget::RiivolutionBootWidget(std::string game_id, std::optional<
m_base_game_path(std::move(base_game_path))
{
setWindowTitle(tr("Start with Riivolution Patches"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
CreateWidgets();
ConnectWidgets();

View File

@ -12,7 +12,6 @@
#include <QLabel>
#include <QRadioButton>
#include <QSignalBlocker>
#include <QSlider>
#include <QVBoxLayout>
#include <cmath>
@ -25,6 +24,8 @@
#include "Core/System.h"
#include "DolphinQt/Config/ConfigControls/ConfigBool.h"
#include "DolphinQt/Config/ConfigControls/ConfigFloatSlider.h"
#include "DolphinQt/Config/ConfigControls/ConfigSlider.h"
#include "DolphinQt/QtUtils/QtUtils.h"
#include "DolphinQt/QtUtils/SignalBlocking.h"
#include "DolphinQt/Settings.h"
@ -89,6 +90,18 @@ void AdvancedPane::CreateLayout()
"needed.<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>"));
cpu_options_group_layout->addWidget(m_accurate_cpu_cache_checkbox);
auto* const timing_group = new QGroupBox(tr("Timing"));
main_layout->addWidget(timing_group);
auto* timing_group_layout = new QVBoxLayout{timing_group};
auto* const correct_time_drift =
new ConfigBool{tr("Correct Time Drift"), Config::MAIN_CORRECT_TIME_DRIFT};
correct_time_drift->SetDescription(
tr("Allow the emulated console to run fast after stutters,"
"<br>pursuing accurate overall elapsed time unless paused or speed-adjusted."
"<br><br>This may be useful for internet play."
"<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>"));
timing_group_layout->addWidget(correct_time_drift);
auto* clock_override = new QGroupBox(tr("Clock Override"));
auto* clock_override_layout = new QVBoxLayout();
clock_override->setLayout(clock_override_layout);
@ -103,12 +116,24 @@ void AdvancedPane::CreateLayout()
cpu_clock_override_slider_layout->setContentsMargins(0, 0, 0, 0);
clock_override_layout->addLayout(cpu_clock_override_slider_layout);
m_cpu_clock_override_slider = new QSlider(Qt::Horizontal);
m_cpu_clock_override_slider->setRange(1, 400);
m_cpu_clock_override_slider = new ConfigFloatSlider(0.01f, 4.0f, Config::MAIN_OVERCLOCK, 0.01f);
cpu_clock_override_slider_layout->addWidget(m_cpu_clock_override_slider);
m_cpu_clock_override_slider_label = new QLabel();
cpu_clock_override_slider_layout->addWidget(m_cpu_clock_override_slider_label);
m_cpu_label = new QLabel();
cpu_clock_override_slider_layout->addWidget(m_cpu_label);
std::function<void()> cpu_text = [this]() {
const float multi = Config::Get(Config::MAIN_OVERCLOCK);
const int percent = static_cast<int>(std::round(multi * 100.f));
const int core_clock =
Core::System::GetInstance().GetSystemTimers().GetTicksPerSecond() / std::pow(10, 6);
const int clock = static_cast<int>(std::round(multi * core_clock));
m_cpu_label->setText(tr("%1% (%2 MHz)").arg(QString::number(percent), QString::number(clock)));
};
cpu_text();
connect(m_cpu_clock_override_slider, &QSlider::valueChanged, this,
[this, cpu_text]() { cpu_text(); });
m_cpu_clock_override_checkbox->SetDescription(
tr("Adjusts the emulated CPU's clock rate.<br><br>"
@ -135,12 +160,25 @@ void AdvancedPane::CreateLayout()
vi_rate_override_slider_layout->setContentsMargins(0, 0, 0, 0);
vi_rate_override_layout->addLayout(vi_rate_override_slider_layout);
m_vi_rate_override_slider = new QSlider(Qt::Horizontal);
m_vi_rate_override_slider->setRange(1, 500);
m_vi_rate_override_slider = new ConfigFloatSlider(0.01f, 5.0f, Config::MAIN_VI_OVERCLOCK, 0.01f);
vi_rate_override_slider_layout->addWidget(m_vi_rate_override_slider);
m_vi_rate_override_slider_label = new QLabel();
vi_rate_override_slider_layout->addWidget(m_vi_rate_override_slider_label);
m_vi_label = new QLabel();
vi_rate_override_slider_layout->addWidget(m_vi_label);
std::function<void()> vi_text = [this]() {
const int percent =
static_cast<int>(std::round(Config::Get(Config::MAIN_VI_OVERCLOCK) * 100.f));
float vps =
static_cast<float>(Core::System::GetInstance().GetVideoInterface().GetTargetRefreshRate());
if (vps == 0.0f || !Config::Get(Config::MAIN_VI_OVERCLOCK_ENABLE))
vps = 59.94f * Config::Get(Config::MAIN_VI_OVERCLOCK);
m_vi_label->setText(
tr("%1% (%2 VPS)").arg(QString::number(percent), QString::number(vps, 'f', 2)));
};
vi_text();
connect(m_vi_rate_override_slider, &QSlider::valueChanged, this,
[this, vi_text]() { vi_text(); });
m_vi_rate_override_checkbox->SetDescription(
tr("Adjusts the VBI frequency. Also adjusts the emulated CPU's "
@ -167,27 +205,34 @@ void AdvancedPane::CreateLayout()
mem1_override_slider_layout->setContentsMargins(0, 0, 0, 0);
ram_override_layout->addLayout(mem1_override_slider_layout);
m_mem1_override_slider = new QSlider(Qt::Horizontal);
m_mem1_override_slider->setRange(24, 64);
m_mem1_override_slider = new ConfigSliderU32(24, 64, Config::MAIN_MEM1_SIZE, 0x100000);
mem1_override_slider_layout->addWidget(m_mem1_override_slider);
m_mem1_override_slider_label = new QLabel();
mem1_override_slider_layout->addWidget(m_mem1_override_slider_label);
m_mem1_label =
new QLabel(tr("%1 MB (MEM1)").arg(QString::number(m_mem1_override_slider->value())));
mem1_override_slider_layout->addWidget(m_mem1_label);
connect(m_mem1_override_slider, &QSlider::valueChanged, this, [this](int value) {
m_mem1_label->setText(tr("%1 MB (MEM1)").arg(QString::number(value)));
});
auto* mem2_override_slider_layout = new QHBoxLayout();
mem2_override_slider_layout->setContentsMargins(0, 0, 0, 0);
ram_override_layout->addLayout(mem2_override_slider_layout);
m_mem2_override_slider = new QSlider(Qt::Horizontal);
m_mem2_override_slider->setRange(64, 128);
m_mem2_override_slider = new ConfigSliderU32(64, 128, Config::MAIN_MEM2_SIZE, 0x100000);
mem2_override_slider_layout->addWidget(m_mem2_override_slider);
m_mem2_override_slider_label = new QLabel();
mem2_override_slider_layout->addWidget(m_mem2_override_slider_label);
m_mem2_label =
new QLabel(tr("%1 MB (MEM2)").arg(QString::number(m_mem2_override_slider->value())));
mem2_override_slider_layout->addWidget(m_mem2_label);
connect(m_mem2_override_slider, &QSlider::valueChanged, this, [this](int value) {
m_mem2_label->setText(tr("%1 MB (MEM2)").arg(QString::number(value)));
});
m_ram_override_checkbox->SetDescription(
tr("Adjusts the amount of RAM in the emulated console.<br><br>"
"<b>WARNING</b>: Enabling this will completely break many games.<br>Only a small number "
"<b>WARNING</b>: Enabling this will completely break many games.<br>Only a small "
"number "
"of games can benefit from this."
"<br><br><dolphin_emphasis>If unsure, leave this unchecked.</dolphin_emphasis>"));
@ -227,36 +272,12 @@ void AdvancedPane::ConnectLayout()
Config::SetBaseOrCurrent(Config::MAIN_CPU_CORE, cpu_cores[index]);
});
connect(m_cpu_clock_override_slider, &QSlider::valueChanged, [this](int oc_factor) {
const float factor = m_cpu_clock_override_slider->value() / 100.f;
Config::SetBaseOrCurrent(Config::MAIN_OVERCLOCK, factor);
Update();
});
connect(m_vi_rate_override_slider, &QSlider::valueChanged, [this](int oc_factor) {
const float factor = m_vi_rate_override_slider->value() / 100.f;
Config::SetBaseOrCurrent(Config::MAIN_VI_OVERCLOCK, factor);
Update();
});
m_ram_override_checkbox->setChecked(Config::Get(Config::MAIN_RAM_OVERRIDE_ENABLE));
connect(m_ram_override_checkbox, &QCheckBox::toggled, [this](bool enable_ram_override) {
Config::SetBaseOrCurrent(Config::MAIN_RAM_OVERRIDE_ENABLE, enable_ram_override);
Update();
});
connect(m_mem1_override_slider, &QSlider::valueChanged, [this](int slider_value) {
const u32 mem1_size = m_mem1_override_slider->value() * 0x100000;
Config::SetBaseOrCurrent(Config::MAIN_MEM1_SIZE, mem1_size);
Update();
});
connect(m_mem2_override_slider, &QSlider::valueChanged, [this](int slider_value) {
const u32 mem2_size = m_mem2_override_slider->value() * 0x100000;
Config::SetBaseOrCurrent(Config::MAIN_MEM2_SIZE, mem2_size);
Update();
});
connect(m_custom_rtc_datetime, &QDateTimeEdit::dateTimeChanged, [this](QDateTime date_time) {
Config::SetBaseOrCurrent(Config::MAIN_CUSTOM_RTC_VALUE,
static_cast<u32>(date_time.toSecsSinceEpoch()));
@ -295,21 +316,7 @@ void AdvancedPane::Update()
}
m_cpu_clock_override_slider->setEnabled(enable_cpu_clock_override_widgets);
m_cpu_clock_override_slider_label->setEnabled(enable_cpu_clock_override_widgets);
{
const QSignalBlocker blocker(m_cpu_clock_override_slider);
m_cpu_clock_override_slider->setValue(
static_cast<int>(std::round(Config::Get(Config::MAIN_OVERCLOCK) * 100.f)));
}
m_cpu_clock_override_slider_label->setText([] {
int core_clock =
Core::System::GetInstance().GetSystemTimers().GetTicksPerSecond() / std::pow(10, 6);
int percent = static_cast<int>(std::round(Config::Get(Config::MAIN_OVERCLOCK) * 100.f));
int clock = static_cast<int>(std::round(Config::Get(Config::MAIN_OVERCLOCK) * core_clock));
return tr("%1% (%2 MHz)").arg(QString::number(percent), QString::number(clock));
}());
m_cpu_label->setEnabled(enable_cpu_clock_override_widgets);
QFont vi_bf = font();
vi_bf.setBold(Config::GetActiveLayerForConfig(Config::MAIN_VI_OVERCLOCK_ENABLE) !=
@ -318,53 +325,16 @@ void AdvancedPane::Update()
m_vi_rate_override_checkbox->setChecked(enable_vi_rate_override_widgets);
m_vi_rate_override_slider->setEnabled(enable_vi_rate_override_widgets);
m_vi_rate_override_slider_label->setEnabled(enable_vi_rate_override_widgets);
{
const QSignalBlocker blocker(m_vi_rate_override_slider);
m_vi_rate_override_slider->setValue(
static_cast<int>(std::round(Config::Get(Config::MAIN_VI_OVERCLOCK) * 100.f)));
}
m_vi_rate_override_slider_label->setText([] {
int percent = static_cast<int>(std::round(Config::Get(Config::MAIN_VI_OVERCLOCK) * 100.f));
float vps =
static_cast<float>(Core::System::GetInstance().GetVideoInterface().GetTargetRefreshRate());
if (vps == 0.0f || !Config::Get(Config::MAIN_VI_OVERCLOCK_ENABLE))
vps = 59.94f * Config::Get(Config::MAIN_VI_OVERCLOCK);
return tr("%1% (%2 VPS)").arg(QString::number(percent), QString::number(vps, 'f', 2));
}());
m_vi_label->setEnabled(enable_vi_rate_override_widgets);
m_ram_override_checkbox->setEnabled(is_uninitialized);
SignalBlocking(m_ram_override_checkbox)->setChecked(enable_ram_override_widgets);
m_mem1_override_slider->setEnabled(enable_ram_override_widgets && is_uninitialized);
m_mem1_override_slider_label->setEnabled(enable_ram_override_widgets && is_uninitialized);
{
const QSignalBlocker blocker(m_mem1_override_slider);
const u32 mem1_size = Config::Get(Config::MAIN_MEM1_SIZE) / 0x100000;
m_mem1_override_slider->setValue(mem1_size);
}
m_mem1_override_slider_label->setText([] {
const u32 mem1_size = Config::Get(Config::MAIN_MEM1_SIZE) / 0x100000;
return tr("%1 MB (MEM1)").arg(QString::number(mem1_size));
}());
m_mem1_label->setEnabled(enable_ram_override_widgets && is_uninitialized);
m_mem2_override_slider->setEnabled(enable_ram_override_widgets && is_uninitialized);
m_mem2_override_slider_label->setEnabled(enable_ram_override_widgets && is_uninitialized);
{
const QSignalBlocker blocker(m_mem2_override_slider);
const u32 mem2_size = Config::Get(Config::MAIN_MEM2_SIZE) / 0x100000;
m_mem2_override_slider->setValue(mem2_size);
}
m_mem2_override_slider_label->setText([] {
const u32 mem2_size = Config::Get(Config::MAIN_MEM2_SIZE) / 0x100000;
return tr("%1 MB (MEM2)").arg(QString::number(mem2_size));
}());
m_mem2_label->setEnabled(enable_ram_override_widgets && is_uninitialized);
m_custom_rtc_checkbox->setEnabled(is_uninitialized);
SignalBlocking(m_custom_rtc_checkbox)->setChecked(Config::Get(Config::MAIN_CUSTOM_RTC_ENABLE));

View File

@ -8,6 +8,9 @@
#include <QWidget>
class ConfigBool;
class ConfigFloatSlider;
class ConfigSlider;
class ConfigSliderU32;
class QCheckBox;
class QComboBox;
class QLabel;
@ -36,19 +39,19 @@ private:
ConfigBool* m_pause_on_panic_checkbox;
ConfigBool* m_accurate_cpu_cache_checkbox;
ConfigBool* m_cpu_clock_override_checkbox;
QSlider* m_cpu_clock_override_slider;
QLabel* m_cpu_clock_override_slider_label;
ConfigFloatSlider* m_cpu_clock_override_slider;
QLabel* m_cpu_label;
ConfigBool* m_vi_rate_override_checkbox;
QSlider* m_vi_rate_override_slider;
QLabel* m_vi_rate_override_slider_label;
ConfigFloatSlider* m_vi_rate_override_slider;
QLabel* m_vi_label;
ConfigBool* m_custom_rtc_checkbox;
QDateTimeEdit* m_custom_rtc_datetime;
ConfigBool* m_ram_override_checkbox;
QSlider* m_mem1_override_slider;
QLabel* m_mem1_override_slider_label;
QSlider* m_mem2_override_slider;
QLabel* m_mem2_override_slider_label;
ConfigSliderU32* m_mem1_override_slider;
QLabel* m_mem1_label;
ConfigSliderU32* m_mem2_override_slider;
QLabel* m_mem2_label;
};

View File

@ -95,7 +95,6 @@ void BroadbandAdapterSettingsDialog::InitControls()
}
setWindowTitle(window_title);
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
m_address_input = new QLineEdit(current_address);
m_address_input->setPlaceholderText(address_placeholder);

View File

@ -42,7 +42,6 @@ USBDeviceAddToWhitelistDialog::USBDeviceAddToWhitelistDialog(QWidget* parent) :
void USBDeviceAddToWhitelistDialog::InitControls()
{
setWindowTitle(tr("Add New USB Device"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
m_whitelist_buttonbox = new QDialogButtonBox();
auto* add_button = new QPushButton(tr("Add"));

View File

@ -45,7 +45,6 @@ ControllerEmu::InputOverrideFunction InputOverrider::GetInputOverrideFunction()
TASInputWindow::TASInputWindow(QWidget* parent) : QDialog(parent)
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
setWindowIcon(Resources::GetAppIcon());
QGridLayout* settings_layout = new QGridLayout;

View File

@ -59,7 +59,6 @@ void Updater::OnUpdateAvailable(const NewVersionInformation& info)
QDialog* dialog = new QDialog(m_parent);
dialog->setAttribute(Qt::WA_DeleteOnClose, true);
dialog->setWindowTitle(tr("Update available"));
dialog->setWindowFlags(dialog->windowFlags() & ~Qt::WindowContextHelpButtonHint);
auto* label = new QLabel(
tr("<h2>A new version of Dolphin is available!</h2>Dolphin %1 is available for "

View File

@ -96,7 +96,6 @@ static WiiUtils::UpdateResult ShowProgress(QWidget* parent, Callable function, A
UpdateProgressDialog dialog{parent};
dialog.setLabelText(QObject::tr("Preparing to update...\nThis can take a while."));
dialog.setWindowTitle(QObject::tr("Updating"));
dialog.setWindowFlags(dialog.windowFlags() & ~Qt::WindowContextHelpButtonHint);
// QProgressDialog doesn't set its minimum size correctly.
dialog.setMinimumSize(360, 150);

View File

@ -3,46 +3,54 @@
#include "VideoCommon/Assets/CustomAsset.h"
#include <utility>
namespace VideoCommon
{
CustomAsset::CustomAsset(std::shared_ptr<CustomAssetLibrary> library,
const CustomAssetLibrary::AssetID& asset_id)
: m_owning_library(std::move(library)), m_asset_id(asset_id)
const CustomAssetLibrary::AssetID& asset_id, u64 asset_handle)
: m_owning_library(std::move(library)), m_asset_id(asset_id), m_handle(asset_handle)
{
}
bool CustomAsset::Load()
{
const auto load_information = LoadImpl(m_asset_id);
if (load_information.m_bytes_loaded > 0)
{
std::lock_guard lk(m_info_lock);
m_bytes_loaded = load_information.m_bytes_loaded;
m_last_loaded_time = load_information.m_load_time;
}
return load_information.m_bytes_loaded != 0;
}
CustomAssetLibrary::TimeType CustomAsset::GetLastWriteTime() const
{
return m_owning_library->GetLastAssetWriteTime(m_asset_id);
}
const CustomAssetLibrary::TimeType& CustomAsset::GetLastLoadedTime() const
std::size_t CustomAsset::Load()
{
std::lock_guard lk(m_info_lock);
// The load time needs to come from before the data is actually read.
// Using a time point from after the read marks the asset as more up-to-date than it actually is,
// and has potential to race (and not be updated) if a change happens immediately after load.
const auto load_time = ClockType::now();
const auto load_information = LoadImpl(m_asset_id);
if (load_information.bytes_loaded > 0)
{
m_bytes_loaded = load_information.bytes_loaded;
m_last_loaded_time = load_time;
return m_bytes_loaded;
}
return 0;
}
std::size_t CustomAsset::Unload()
{
std::lock_guard lk(m_info_lock);
UnloadImpl();
return std::exchange(m_bytes_loaded, 0);
}
CustomAsset::TimeType CustomAsset::GetLastLoadedTime() const
{
return m_last_loaded_time;
}
std::size_t CustomAsset::GetHandle() const
{
return m_handle;
}
const CustomAssetLibrary::AssetID& CustomAsset::GetAssetId() const
{
return m_asset_id;
}
std::size_t CustomAsset::GetByteSizeInMemory() const
{
std::lock_guard lk(m_info_lock);
return m_bytes_loaded;
}
} // namespace VideoCommon

View File

@ -6,9 +6,9 @@
#include "Common/CommonTypes.h"
#include "VideoCommon/Assets/CustomAssetLibrary.h"
#include <atomic>
#include <memory>
#include <mutex>
#include <optional>
namespace VideoCommon
{
@ -17,42 +17,47 @@ namespace VideoCommon
class CustomAsset
{
public:
using ClockType = std::chrono::steady_clock;
using TimeType = ClockType::time_point;
CustomAsset(std::shared_ptr<CustomAssetLibrary> library,
const CustomAssetLibrary::AssetID& asset_id);
const CustomAssetLibrary::AssetID& asset_id, u64 session_id);
virtual ~CustomAsset() = default;
CustomAsset(const CustomAsset&) = delete;
CustomAsset(CustomAsset&&) = delete;
CustomAsset& operator=(const CustomAsset&) = delete;
CustomAsset& operator=(CustomAsset&&) = delete;
// Loads the asset from the library returning a pass/fail result
bool Load();
// Loads the asset from the library returning the number of bytes loaded
std::size_t Load();
// Queries the last time the asset was modified or standard epoch time
// if the asset hasn't been modified yet
// Note: not thread safe, expected to be called by the loader
CustomAssetLibrary::TimeType GetLastWriteTime() const;
// Unloads the asset data, resets the bytes loaded and
// returns the number of bytes unloaded
std::size_t Unload();
// Returns the time that the data was last loaded
const CustomAssetLibrary::TimeType& GetLastLoadedTime() const;
TimeType GetLastLoadedTime() const;
// Returns an id that uniquely identifies this asset
const CustomAssetLibrary::AssetID& GetAssetId() const;
// A rough estimate of how much space this asset
// will take in memroy
std::size_t GetByteSizeInMemory() const;
// Returns an id that is unique to this game session
// This is a faster form to hash and can be used
// as an index
std::size_t GetHandle() const;
protected:
const std::shared_ptr<CustomAssetLibrary> m_owning_library;
private:
virtual CustomAssetLibrary::LoadInfo LoadImpl(const CustomAssetLibrary::AssetID& asset_id) = 0;
virtual void UnloadImpl() = 0;
CustomAssetLibrary::AssetID m_asset_id;
std::size_t m_handle;
mutable std::mutex m_info_lock;
std::size_t m_bytes_loaded = 0;
CustomAssetLibrary::TimeType m_last_loaded_time = {};
std::atomic<TimeType> m_last_loaded_time = {};
};
// An abstract class that is expected to
@ -83,6 +88,14 @@ protected:
bool m_loaded = false;
mutable std::mutex m_data_lock;
std::shared_ptr<UnderlyingType> m_data;
private:
void UnloadImpl() override
{
std::lock_guard lk(m_data_lock);
m_loaded = false;
m_data.reset();
}
};
// A helper struct that contains
@ -96,7 +109,7 @@ template <typename AssetType>
struct CachedAsset
{
std::shared_ptr<AssetType> m_asset;
VideoCommon::CustomAssetLibrary::TimeType m_cached_write_time;
CustomAsset::TimeType m_cached_write_time;
};
} // namespace VideoCommon

View File

@ -1,85 +0,0 @@
// Copyright 2023 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "VideoCommon/Assets/CustomAssetLibrary.h"
#include <algorithm>
#include "Common/Logging/Log.h"
#include "VideoCommon/Assets/TextureAsset.h"
namespace VideoCommon
{
CustomAssetLibrary::LoadInfo CustomAssetLibrary::LoadGameTexture(const AssetID& asset_id,
TextureData* data)
{
const auto load_info = LoadTexture(asset_id, data);
if (load_info.m_bytes_loaded == 0)
return {};
if (data->m_type != TextureData::Type::Type_Texture2D)
{
ERROR_LOG_FMT(
VIDEO,
"Custom asset '{}' is not a valid game texture, it is expected to be a 2d texture "
"but was a '{}'.",
asset_id, data->m_type);
return {};
}
// Note: 'LoadTexture()' ensures we have a level loaded
for (std::size_t slice_index = 0; slice_index < data->m_texture.m_slices.size(); slice_index++)
{
auto& slice = data->m_texture.m_slices[slice_index];
const auto& first_mip = slice.m_levels[0];
// Verify that each mip level is the correct size (divide by 2 each time).
u32 current_mip_width = first_mip.width;
u32 current_mip_height = first_mip.height;
for (u32 mip_level = 1; mip_level < static_cast<u32>(slice.m_levels.size()); mip_level++)
{
if (current_mip_width != 1 || current_mip_height != 1)
{
current_mip_width = std::max(current_mip_width / 2, 1u);
current_mip_height = std::max(current_mip_height / 2, 1u);
const VideoCommon::CustomTextureData::ArraySlice::Level& level = slice.m_levels[mip_level];
if (current_mip_width == level.width && current_mip_height == level.height)
continue;
ERROR_LOG_FMT(VIDEO,
"Invalid custom game texture size {}x{} for texture asset {}. Slice {} with "
"mipmap level {} "
"must be {}x{}.",
level.width, level.height, asset_id, slice_index, mip_level,
current_mip_width, current_mip_height);
}
else
{
// It is invalid to have more than a single 1x1 mipmap.
ERROR_LOG_FMT(
VIDEO,
"Custom game texture {} has too many 1x1 mipmaps for slice {}. Skipping extra levels.",
asset_id, slice_index);
}
// Drop this mip level and any others after it.
while (slice.m_levels.size() > mip_level)
slice.m_levels.pop_back();
}
// All levels have to have the same format.
if (std::ranges::any_of(slice.m_levels,
[&first_mip](const auto& l) { return l.format != first_mip.format; }))
{
ERROR_LOG_FMT(
VIDEO, "Custom game texture {} has inconsistent formats across mip levels for slice {}.",
asset_id, slice_index);
return {};
}
}
return load_info;
}
} // namespace VideoCommon

View File

@ -10,10 +10,11 @@
namespace VideoCommon
{
class CustomTextureData;
struct MaterialData;
struct MeshData;
struct PixelShaderData;
struct TextureData;
struct TextureAndSamplerData;
// This class provides functionality to load
// specific data (like textures). Where this data
@ -21,28 +22,21 @@ struct TextureData;
class CustomAssetLibrary
{
public:
using TimeType = std::chrono::system_clock::time_point;
// The AssetID is a unique identifier for a particular asset
using AssetID = std::string;
struct LoadInfo
{
std::size_t m_bytes_loaded = 0;
TimeType m_load_time = {};
std::size_t bytes_loaded = 0;
};
virtual ~CustomAssetLibrary() = default;
// Loads a texture with a sampler and type, if there are no levels, bytes loaded will be empty
virtual LoadInfo LoadTexture(const AssetID& asset_id, TextureAndSamplerData* data) = 0;
// Loads a texture, if there are no levels, bytes loaded will be empty
virtual LoadInfo LoadTexture(const AssetID& asset_id, TextureData* data) = 0;
// Gets the last write time for a given asset id
virtual TimeType GetLastAssetWriteTime(const AssetID& asset_id) const = 0;
// Loads a texture as a game texture, providing additional checks like confirming
// each mip level size is correct and that the format is consistent across the data
LoadInfo LoadGameTexture(const AssetID& asset_id, TextureData* data);
virtual LoadInfo LoadTexture(const AssetID& asset_id, CustomTextureData* data) = 0;
// Loads a pixel shader
virtual LoadInfo LoadPixelShader(const AssetID& asset_id, PixelShaderData* data) = 0;

View File

@ -1,108 +1,157 @@
// Copyright 2023 Dolphin Emulator Project
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "VideoCommon/Assets/CustomAssetLoader.h"
#include "Common/MemoryUtil.h"
#include "VideoCommon/Assets/CustomAssetLibrary.h"
#include <fmt/format.h>
#include "Common/Logging/Log.h"
#include "Common/Thread.h"
#include "UICommon/UICommon.h"
namespace VideoCommon
{
void CustomAssetLoader::Init()
void CustomAssetLoader::Initialize()
{
m_asset_monitor_thread_shutdown.Clear();
const size_t sys_mem = Common::MemPhysical();
const size_t recommended_min_mem = 2 * size_t(1024 * 1024 * 1024);
// keep 2GB memory for system stability if system RAM is 4GB+ - use half of memory in other cases
m_max_memory_available =
(sys_mem / 2 < recommended_min_mem) ? (sys_mem / 2) : (sys_mem - recommended_min_mem);
m_asset_monitor_thread = std::thread([this]() {
Common::SetCurrentThreadName("Asset monitor");
while (true)
{
if (m_asset_monitor_thread_shutdown.IsSet())
{
break;
}
std::this_thread::sleep_for(TIME_BETWEEN_ASSET_MONITOR_CHECKS);
std::lock_guard lk(m_asset_load_lock);
for (auto& [asset_id, asset_to_monitor] : m_assets_to_monitor)
{
if (auto ptr = asset_to_monitor.lock())
{
const auto write_time = ptr->GetLastWriteTime();
if (write_time > ptr->GetLastLoadedTime())
{
(void)ptr->Load();
}
}
}
}
});
m_asset_load_thread.Reset("Custom Asset Loader", [this](std::weak_ptr<CustomAsset> asset) {
if (auto ptr = asset.lock())
{
if (m_memory_exceeded)
return;
if (ptr->Load())
{
std::lock_guard lk(m_asset_load_lock);
const std::size_t asset_memory_size = ptr->GetByteSizeInMemory();
m_total_bytes_loaded += asset_memory_size;
m_assets_to_monitor.try_emplace(ptr->GetAssetId(), ptr);
if (m_total_bytes_loaded > m_max_memory_available)
{
ERROR_LOG_FMT(VIDEO,
"Asset memory exceeded with asset '{}', future assets won't load until "
"memory is available.",
ptr->GetAssetId());
m_memory_exceeded = true;
}
}
}
});
ResizeWorkerThreads(2);
}
void CustomAssetLoader::Shutdown()
{
m_asset_load_thread.StopAndCancel();
m_asset_monitor_thread_shutdown.Set();
m_asset_monitor_thread.join();
m_assets_to_monitor.clear();
m_total_bytes_loaded = 0;
Reset(false);
}
std::shared_ptr<GameTextureAsset>
CustomAssetLoader::LoadGameTexture(const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr<CustomAssetLibrary> library)
bool CustomAssetLoader::StartWorkerThreads(u32 num_worker_threads)
{
return LoadOrCreateAsset<GameTextureAsset>(asset_id, m_game_textures, std::move(library));
for (u32 i = 0; i < num_worker_threads; i++)
{
m_worker_threads.emplace_back(&CustomAssetLoader::WorkerThreadRun, this, i);
}
return HasWorkerThreads();
}
std::shared_ptr<PixelShaderAsset>
CustomAssetLoader::LoadPixelShader(const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr<CustomAssetLibrary> library)
bool CustomAssetLoader::ResizeWorkerThreads(u32 num_worker_threads)
{
return LoadOrCreateAsset<PixelShaderAsset>(asset_id, m_pixel_shaders, std::move(library));
if (m_worker_threads.size() == num_worker_threads)
return true;
StopWorkerThreads();
return StartWorkerThreads(num_worker_threads);
}
std::shared_ptr<MaterialAsset>
CustomAssetLoader::LoadMaterial(const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr<CustomAssetLibrary> library)
bool CustomAssetLoader::HasWorkerThreads() const
{
return LoadOrCreateAsset<MaterialAsset>(asset_id, m_materials, std::move(library));
return !m_worker_threads.empty();
}
std::shared_ptr<MeshAsset> CustomAssetLoader::LoadMesh(const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr<CustomAssetLibrary> library)
void CustomAssetLoader::StopWorkerThreads()
{
return LoadOrCreateAsset<MeshAsset>(asset_id, m_meshes, std::move(library));
if (!HasWorkerThreads())
return;
// Signal worker threads to stop, and wake all of them.
{
std::lock_guard guard(m_assets_to_load_lock);
m_exit_flag.Set();
m_worker_thread_wake.notify_all();
}
// Wait for worker threads to exit.
for (std::thread& thr : m_worker_threads)
thr.join();
m_worker_threads.clear();
m_exit_flag.Clear();
}
void CustomAssetLoader::WorkerThreadRun(u32 thread_index)
{
Common::SetCurrentThreadName(fmt::format("Asset Loader {}", thread_index).c_str());
std::unique_lock load_lock(m_assets_to_load_lock);
while (true)
{
m_worker_thread_wake.wait(load_lock,
[&] { return !m_assets_to_load.empty() || m_exit_flag.IsSet(); });
if (m_exit_flag.IsSet())
return;
// If more memory than allowed has already been loaded, we will load nothing more
// until the next ScheduleAssetsToLoad from Manager.
if (m_change_in_memory > m_allowed_memory)
{
m_assets_to_load.clear();
continue;
}
auto* const item = m_assets_to_load.front();
m_assets_to_load.pop_front();
// Make sure another thread isn't loading this handle.
if (!m_handles_in_progress.insert(item->GetHandle()).second)
continue;
load_lock.unlock();
// Unload previously loaded asset.
m_change_in_memory -= item->Unload();
const std::size_t bytes_loaded = item->Load();
m_change_in_memory += s64(bytes_loaded);
load_lock.lock();
{
INFO_LOG_FMT(VIDEO, "CustomAssetLoader thread {} loaded: {} ({})", thread_index,
item->GetAssetId(), UICommon::FormatSize(bytes_loaded));
std::lock_guard lk{m_assets_loaded_lock};
m_asset_handles_loaded.emplace_back(item->GetHandle(), bytes_loaded > 0);
// Make sure no other threads try to re-process this item.
// Manager will take the handles and re-ScheduleAssetsToLoad based on timestamps if needed.
std::erase(m_assets_to_load, item);
}
m_handles_in_progress.erase(item->GetHandle());
}
}
auto CustomAssetLoader::TakeLoadResults() -> LoadResults
{
std::lock_guard guard(m_assets_loaded_lock);
return {std::move(m_asset_handles_loaded), m_change_in_memory.exchange(0)};
}
void CustomAssetLoader::ScheduleAssetsToLoad(std::list<CustomAsset*> assets_to_load,
u64 allowed_memory)
{
if (assets_to_load.empty()) [[unlikely]]
return;
// There's new assets to process, notify worker threads
std::lock_guard guard(m_assets_to_load_lock);
m_allowed_memory = allowed_memory;
m_assets_to_load = std::move(assets_to_load);
m_worker_thread_wake.notify_all();
}
void CustomAssetLoader::Reset(bool restart_worker_threads)
{
const std::size_t worker_thread_count = m_worker_threads.size();
StopWorkerThreads();
m_assets_to_load.clear();
m_asset_handles_loaded.clear();
m_allowed_memory = 0;
m_change_in_memory = 0;
if (restart_worker_threads)
{
StartWorkerThreads(static_cast<u32>(worker_thread_count));
}
}
} // namespace VideoCommon

View File

@ -1,27 +1,24 @@
// Copyright 2023 Dolphin Emulator Project
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <chrono>
#include <map>
#include <memory>
#include <atomic>
#include <condition_variable>
#include <list>
#include <mutex>
#include <set>
#include <thread>
#include <vector>
#include "Common/Flag.h"
#include "Common/Logging/Log.h"
#include "Common/WorkQueueThread.h"
#include "VideoCommon/Assets/CustomAsset.h"
#include "VideoCommon/Assets/MaterialAsset.h"
#include "VideoCommon/Assets/MeshAsset.h"
#include "VideoCommon/Assets/ShaderAsset.h"
#include "VideoCommon/Assets/TextureAsset.h"
namespace VideoCommon
{
// This class is responsible for loading data asynchronously when requested
// and watches that data asynchronously reloading it if it changes
// This class takes any number of assets
// and loads them across a configurable
// thread pool
class CustomAssetLoader
{
public:
@ -32,77 +29,54 @@ public:
CustomAssetLoader& operator=(const CustomAssetLoader&) = delete;
CustomAssetLoader& operator=(CustomAssetLoader&&) = delete;
void Init();
void Initialize();
void Shutdown();
// The following Load* functions will load or create an asset associated
// with the given asset id
// Loads happen asynchronously where the data will be set now or in the future
// Callees are expected to query the underlying data with 'GetData()'
// from the 'CustomLoadableAsset' class to determine if the data is ready for use
std::shared_ptr<GameTextureAsset> LoadGameTexture(const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr<CustomAssetLibrary> library);
using AssetHandle = std::pair<std::size_t, bool>;
struct LoadResults
std::shared_ptr<PixelShaderAsset> LoadPixelShader(const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr<CustomAssetLibrary> library);
{
std::vector<AssetHandle> asset_handles;
s64 change_in_memory;
};
std::shared_ptr<MaterialAsset> LoadMaterial(const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr<CustomAssetLibrary> library);
// Returns a vector of loaded asset handle / loaded result pairs
// and the change in memory.
LoadResults TakeLoadResults();
std::shared_ptr<MeshAsset> LoadMesh(const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr<CustomAssetLibrary> library);
// Schedule assets to load on the worker threads
// and set how much memory is available for loading these additional assets.
void ScheduleAssetsToLoad(std::list<CustomAsset*> assets_to_load, u64 allowed_memory);
void Reset(bool restart_worker_threads = true);
private:
// TODO C++20: use a 'derived_from' concept against 'CustomAsset' when available
template <typename AssetType>
std::shared_ptr<AssetType>
LoadOrCreateAsset(const CustomAssetLibrary::AssetID& asset_id,
std::map<CustomAssetLibrary::AssetID, std::weak_ptr<AssetType>>& asset_map,
std::shared_ptr<CustomAssetLibrary> library)
{
auto [it, inserted] = asset_map.try_emplace(asset_id);
if (!inserted)
{
auto shared = it->second.lock();
if (shared)
return shared;
}
std::shared_ptr<AssetType> ptr(new AssetType(std::move(library), asset_id), [&](AssetType* a) {
{
std::lock_guard lk(m_asset_load_lock);
m_total_bytes_loaded -= a->GetByteSizeInMemory();
m_assets_to_monitor.erase(a->GetAssetId());
if (m_max_memory_available >= m_total_bytes_loaded && m_memory_exceeded)
{
INFO_LOG_FMT(VIDEO, "Asset memory went below limit, new assets can begin loading.");
m_memory_exceeded = false;
}
}
delete a;
});
it->second = ptr;
m_asset_load_thread.Push(it->second);
return ptr;
}
bool StartWorkerThreads(u32 num_worker_threads);
bool ResizeWorkerThreads(u32 num_worker_threads);
bool HasWorkerThreads() const;
void StopWorkerThreads();
static constexpr auto TIME_BETWEEN_ASSET_MONITOR_CHECKS = std::chrono::milliseconds{500};
void WorkerThreadRun(u32 thread_index);
std::map<CustomAssetLibrary::AssetID, std::weak_ptr<GameTextureAsset>> m_game_textures;
std::map<CustomAssetLibrary::AssetID, std::weak_ptr<PixelShaderAsset>> m_pixel_shaders;
std::map<CustomAssetLibrary::AssetID, std::weak_ptr<MaterialAsset>> m_materials;
std::map<CustomAssetLibrary::AssetID, std::weak_ptr<MeshAsset>> m_meshes;
std::thread m_asset_monitor_thread;
Common::Flag m_asset_monitor_thread_shutdown;
Common::Flag m_exit_flag;
std::size_t m_total_bytes_loaded = 0;
std::size_t m_max_memory_available = 0;
std::atomic_bool m_memory_exceeded = false;
std::vector<std::thread> m_worker_threads;
std::map<CustomAssetLibrary::AssetID, std::weak_ptr<CustomAsset>> m_assets_to_monitor;
std::mutex m_assets_to_load_lock;
std::list<CustomAsset*> m_assets_to_load;
// Use a recursive mutex to handle the scenario where an asset goes out of scope while
// iterating over the assets to monitor which calls the lock above in 'LoadOrCreateAsset'
std::recursive_mutex m_asset_load_lock;
Common::WorkQueueThread<std::weak_ptr<CustomAsset>> m_asset_load_thread;
std::condition_variable m_worker_thread_wake;
std::vector<AssetHandle> m_asset_handles_loaded;
// Memory available to load new assets.
s64 m_allowed_memory = 0;
// Change in memory from just-loaded/unloaded asset results yet to be taken by the Manager.
std::atomic<s64> m_change_in_memory = 0;
std::mutex m_assets_loaded_lock;
std::set<std::size_t> m_handles_in_progress;
};
} // namespace VideoCommon

View File

@ -0,0 +1,229 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "VideoCommon/Assets/CustomResourceManager.h"
#include "Common/Logging/Log.h"
#include "Common/MemoryUtil.h"
#include "UICommon/UICommon.h"
#include "VideoCommon/Assets/CustomAsset.h"
#include "VideoCommon/Assets/TextureAsset.h"
#include "VideoCommon/VideoEvents.h"
namespace VideoCommon
{
void CustomResourceManager::Initialize()
{
// Use half of available system memory but leave at least 2GiB unused for system stability.
constexpr size_t must_keep_unused = 2 * size_t(1024 * 1024 * 1024);
const size_t sys_mem = Common::MemPhysical();
const size_t keep_unused_mem = std::max(sys_mem / 2, std::min(sys_mem, must_keep_unused));
m_max_ram_available = sys_mem - keep_unused_mem;
if (m_max_ram_available == 0)
ERROR_LOG_FMT(VIDEO, "Not enough system memory for custom resources.");
m_asset_loader.Initialize();
m_xfb_event =
AfterFrameEvent::Register([this](Core::System&) { XFBTriggered(); }, "CustomResourceManager");
}
void CustomResourceManager::Shutdown()
{
Reset();
m_asset_loader.Shutdown();
}
void CustomResourceManager::Reset()
{
m_asset_loader.Reset(true);
m_active_assets = {};
m_pending_assets = {};
m_asset_handle_to_data.clear();
m_asset_id_to_handle.clear();
m_texture_data_asset_cache.clear();
m_dirty_assets.clear();
m_ram_used = 0;
}
void CustomResourceManager::MarkAssetDirty(const CustomAssetLibrary::AssetID& asset_id)
{
std::lock_guard guard(m_dirty_mutex);
m_dirty_assets.insert(asset_id);
}
CustomResourceManager::TextureTimePair CustomResourceManager::GetTextureDataFromAsset(
const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr<VideoCommon::CustomAssetLibrary> library)
{
auto& resource = m_texture_data_asset_cache[asset_id];
if (resource.asset_data != nullptr &&
resource.asset_data->load_status == AssetData::LoadStatus::ResourceDataAvailable)
{
m_active_assets.MakeAssetHighestPriority(resource.asset->GetHandle(), resource.asset);
return {resource.texture_data, resource.asset->GetLastLoadedTime()};
}
// If there is an error, don't try and load again until the error is fixed
if (resource.asset_data != nullptr && resource.asset_data->has_load_error)
return {};
LoadTextureDataAsset(asset_id, std::move(library), &resource);
m_active_assets.MakeAssetHighestPriority(resource.asset->GetHandle(), resource.asset);
return {};
}
void CustomResourceManager::LoadTextureDataAsset(
const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr<VideoCommon::CustomAssetLibrary> library, InternalTextureDataResource* resource)
{
if (!resource->asset)
{
resource->asset =
CreateAsset<TextureAsset>(asset_id, AssetData::AssetType::TextureData, std::move(library));
resource->asset_data = &m_asset_handle_to_data[resource->asset->GetHandle()];
}
auto texture_data = resource->asset->GetData();
if (!texture_data || resource->asset_data->load_status == AssetData::LoadStatus::PendingReload)
{
// Tell the system we are still interested in loading this asset
const auto asset_handle = resource->asset->GetHandle();
m_pending_assets.MakeAssetHighestPriority(asset_handle,
m_asset_handle_to_data[asset_handle].asset.get());
}
else if (resource->asset_data->load_status == AssetData::LoadStatus::LoadFinished)
{
resource->texture_data = std::move(texture_data);
resource->asset_data->load_status = AssetData::LoadStatus::ResourceDataAvailable;
}
}
void CustomResourceManager::XFBTriggered()
{
ProcessDirtyAssets();
ProcessLoadedAssets();
if (m_ram_used > m_max_ram_available)
{
RemoveAssetsUntilBelowMemoryLimit();
}
if (m_pending_assets.IsEmpty())
return;
if (m_ram_used > m_max_ram_available)
return;
const u64 allowed_memory = m_max_ram_available - m_ram_used;
m_asset_loader.ScheduleAssetsToLoad(m_pending_assets.Elements(), allowed_memory);
}
void CustomResourceManager::ProcessDirtyAssets()
{
decltype(m_dirty_assets) dirty_assets;
if (const auto lk = std::unique_lock{m_dirty_mutex, std::try_to_lock})
std::swap(dirty_assets, m_dirty_assets);
const auto now = CustomAsset::ClockType::now();
for (const auto& asset_id : dirty_assets)
{
if (const auto it = m_asset_id_to_handle.find(asset_id); it != m_asset_id_to_handle.end())
{
const auto asset_handle = it->second;
AssetData& asset_data = m_asset_handle_to_data[asset_handle];
asset_data.load_status = AssetData::LoadStatus::PendingReload;
asset_data.load_request_time = now;
// Asset was reloaded, clear any errors we might have
asset_data.has_load_error = false;
m_pending_assets.InsertAsset(it->second, asset_data.asset.get());
DEBUG_LOG_FMT(VIDEO, "Dirty asset pending reload: {}", asset_data.asset->GetAssetId());
}
}
}
void CustomResourceManager::ProcessLoadedAssets()
{
const auto load_results = m_asset_loader.TakeLoadResults();
// Update the ram with the change in memory from the loader
//
// Note: Assets with outstanding reload requests will have
// two copies in memory temporarily (the old data stored in
// the asset shared_ptr that the resource manager owns, and
// the new data loaded from the loader in the asset's shared_ptr)
// This temporary duplication will not be reflected in the
// resource manager's ram used
m_ram_used += load_results.change_in_memory;
for (const auto& [handle, load_successful] : load_results.asset_handles)
{
AssetData& asset_data = m_asset_handle_to_data[handle];
// If we have a reload request that is newer than our loaded time
// we need to wait for another reload.
if (asset_data.load_request_time > asset_data.asset->GetLastLoadedTime())
continue;
m_pending_assets.RemoveAsset(handle);
asset_data.load_request_time = {};
if (!load_successful)
{
asset_data.has_load_error = true;
}
else
{
m_active_assets.InsertAsset(handle, asset_data.asset.get());
asset_data.load_status = AssetData::LoadStatus::LoadFinished;
}
}
}
void CustomResourceManager::RemoveAssetsUntilBelowMemoryLimit()
{
const u64 threshold_ram = m_max_ram_available * 8 / 10;
if (m_ram_used > threshold_ram)
{
INFO_LOG_FMT(VIDEO, "Memory usage over threshold: {}", UICommon::FormatSize(m_ram_used));
}
// Clear out least recently used resources until
// we get safely in our threshold
while (m_ram_used > threshold_ram && m_active_assets.Size() > 0)
{
auto* const asset = m_active_assets.RemoveLowestPriorityAsset();
AssetData& asset_data = m_asset_handle_to_data[asset->GetHandle()];
// Remove the resource manager's cached entry with its asset data
if (asset_data.type == AssetData::AssetType::TextureData)
{
m_texture_data_asset_cache.erase(asset->GetAssetId());
}
// Remove the asset's copy
const std::size_t bytes_unloaded = asset_data.asset->Unload();
m_ram_used -= bytes_unloaded;
asset_data.load_status = AssetData::LoadStatus::Unloaded;
asset_data.load_request_time = {};
INFO_LOG_FMT(VIDEO, "Unloading asset: {} ({})", asset_data.asset->GetAssetId(),
UICommon::FormatSize(bytes_unloaded));
}
}
} // namespace VideoCommon

View File

@ -0,0 +1,215 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <list>
#include <map>
#include <memory>
#include <set>
#include <vector>
#include "Common/CommonTypes.h"
#include "Common/HookableEvent.h"
#include "VideoCommon/Assets/CustomAsset.h"
#include "VideoCommon/Assets/CustomAssetLibrary.h"
#include "VideoCommon/Assets/CustomAssetLoader.h"
#include "VideoCommon/Assets/CustomTextureData.h"
namespace VideoCommon
{
class TextureAsset;
// The resource manager manages custom resources (textures, shaders, meshes)
// called assets. These assets are loaded using a priority system,
// where assets requested more often gets loaded first. This system
// also tracks memory usage and if memory usage goes over a calculated limit,
// then assets will be purged with older assets being targeted first.
class CustomResourceManager
{
public:
void Initialize();
void Shutdown();
void Reset();
// Request that an asset be reloaded
void MarkAssetDirty(const CustomAssetLibrary::AssetID& asset_id);
void XFBTriggered();
using TextureTimePair = std::pair<std::shared_ptr<CustomTextureData>, CustomAsset::TimeType>;
// Returns a pair with the custom texture data and the time it was last loaded
// Callees are not expected to hold onto the shared_ptr as that will prevent
// the resource manager from being able to properly release data
TextureTimePair GetTextureDataFromAsset(const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr<VideoCommon::CustomAssetLibrary> library);
private:
// A generic interface to describe an assets' type
// and load state
struct AssetData
{
std::unique_ptr<CustomAsset> asset;
CustomAsset::TimeType load_request_time = {};
bool has_load_error = false;
enum class AssetType
{
TextureData
};
AssetType type;
enum class LoadStatus
{
PendingReload,
LoadFinished,
ResourceDataAvailable,
Unloaded,
};
LoadStatus load_status = LoadStatus::PendingReload;
};
// A structure to represent some raw texture data
// (this data hasn't hit the GPU yet, used for custom textures)
struct InternalTextureDataResource
{
AssetData* asset_data = nullptr;
VideoCommon::TextureAsset* asset = nullptr;
std::shared_ptr<CustomTextureData> texture_data;
};
void LoadTextureDataAsset(const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr<VideoCommon::CustomAssetLibrary> library,
InternalTextureDataResource* resource);
void ProcessDirtyAssets();
void ProcessLoadedAssets();
void RemoveAssetsUntilBelowMemoryLimit();
template <typename T>
T* CreateAsset(const CustomAssetLibrary::AssetID& asset_id, AssetData::AssetType asset_type,
std::shared_ptr<VideoCommon::CustomAssetLibrary> library)
{
const auto [it, added] =
m_asset_id_to_handle.try_emplace(asset_id, m_asset_handle_to_data.size());
if (added)
{
AssetData asset_data;
asset_data.asset = std::make_unique<T>(library, asset_id, it->second);
asset_data.type = asset_type;
asset_data.load_request_time = {};
asset_data.has_load_error = false;
m_asset_handle_to_data.insert_or_assign(it->second, std::move(asset_data));
}
auto& asset_data_from_handle = m_asset_handle_to_data[it->second];
asset_data_from_handle.load_status = AssetData::LoadStatus::PendingReload;
return static_cast<T*>(asset_data_from_handle.asset.get());
}
// Maintains a priority-sorted list of assets.
// Used to figure out which assets to load or unload first.
// Most recently used assets get marked with highest priority.
class AssetPriorityQueue
{
public:
const auto& Elements() const { return m_assets; }
// Inserts or moves the asset to the top of the queue.
void MakeAssetHighestPriority(u64 asset_handle, CustomAsset* asset)
{
RemoveAsset(asset_handle);
m_assets.push_front(asset);
// See CreateAsset for how a handle gets defined
if (asset_handle >= m_iterator_lookup.size())
m_iterator_lookup.resize(asset_handle + 1, m_assets.end());
m_iterator_lookup[asset_handle] = m_assets.begin();
}
// Inserts an asset at lowest priority or
// does nothing if asset is already in the queue.
void InsertAsset(u64 asset_handle, CustomAsset* asset)
{
if (asset_handle >= m_iterator_lookup.size())
m_iterator_lookup.resize(asset_handle + 1, m_assets.end());
if (m_iterator_lookup[asset_handle] == m_assets.end())
{
m_assets.push_back(asset);
m_iterator_lookup[asset_handle] = std::prev(m_assets.end());
}
}
CustomAsset* RemoveLowestPriorityAsset()
{
if (m_assets.empty()) [[unlikely]]
return nullptr;
auto* const ret = m_assets.back();
if (ret != nullptr)
{
m_iterator_lookup[ret->GetHandle()] = m_assets.end();
}
m_assets.pop_back();
return ret;
}
void RemoveAsset(u64 asset_handle)
{
if (asset_handle >= m_iterator_lookup.size())
return;
const auto iter = m_iterator_lookup[asset_handle];
if (iter != m_assets.end())
{
m_assets.erase(iter);
m_iterator_lookup[asset_handle] = m_assets.end();
}
}
bool IsEmpty() const { return m_assets.empty(); }
std::size_t Size() const { return m_assets.size(); }
private:
std::list<CustomAsset*> m_assets;
// Handle-to-iterator lookup for fast access.
// Grows as needed on insert.
std::vector<decltype(m_assets)::iterator> m_iterator_lookup;
};
// Assets that are currently active in memory, in order of most recently used by the game.
AssetPriorityQueue m_active_assets;
// Assets that need to be loaded.
// e.g. Because the game tried to use them or because they changed on disk.
// Ordered by most recently used.
AssetPriorityQueue m_pending_assets;
std::map<std::size_t, AssetData> m_asset_handle_to_data;
std::map<CustomAssetLibrary::AssetID, std::size_t> m_asset_id_to_handle;
// Memory used by currently "loaded" assets.
u64 m_ram_used = 0;
// A calculated amount of memory to avoid exceeding.
u64 m_max_ram_available = 0;
std::map<CustomAssetLibrary::AssetID, InternalTextureDataResource> m_texture_data_asset_cache;
std::mutex m_dirty_mutex;
std::set<CustomAssetLibrary::AssetID> m_dirty_assets;
CustomAssetLoader m_asset_loader;
Common::EventHook m_xfb_event;
};
} // namespace VideoCommon

View File

@ -13,30 +13,19 @@
#include "Common/JsonUtil.h"
#include "Common/Logging/Log.h"
#include "Common/StringUtil.h"
#include "Core/System.h"
#include "VideoCommon/Assets/CustomResourceManager.h"
#include "VideoCommon/Assets/MaterialAsset.h"
#include "VideoCommon/Assets/MeshAsset.h"
#include "VideoCommon/Assets/ShaderAsset.h"
#include "VideoCommon/Assets/TextureAsset.h"
#include "VideoCommon/Assets/TextureAssetUtils.h"
#include "VideoCommon/RenderState.h"
namespace VideoCommon
{
namespace
{
std::chrono::system_clock::time_point FileTimeToSysTime(std::filesystem::file_time_type file_time)
{
#ifdef _WIN32
return std::chrono::clock_cast<std::chrono::system_clock>(file_time);
#else
// Note: all compilers should switch to chrono::clock_cast
// once it is available for use
const auto system_time_now = std::chrono::system_clock::now();
const auto file_time_now = decltype(file_time)::clock::now();
return std::chrono::time_point_cast<std::chrono::system_clock::duration>(
file_time - file_time_now + system_time_now);
#endif
}
std::size_t GetAssetSize(const CustomTextureData& data)
{
std::size_t total = 0;
@ -50,30 +39,6 @@ std::size_t GetAssetSize(const CustomTextureData& data)
return total;
}
} // namespace
CustomAssetLibrary::TimeType
DirectFilesystemAssetLibrary::GetLastAssetWriteTime(const AssetID& asset_id) const
{
std::lock_guard lk(m_lock);
if (auto iter = m_assetid_to_asset_map_path.find(asset_id);
iter != m_assetid_to_asset_map_path.end())
{
const auto& asset_map_path = iter->second;
CustomAssetLibrary::TimeType max_entry;
for (const auto& [key, value] : asset_map_path)
{
std::error_code ec;
const auto tp = std::filesystem::last_write_time(value, ec);
if (ec)
continue;
auto tp_sys = FileTimeToSysTime(tp);
if (tp_sys > max_entry)
max_entry = tp_sys;
}
return max_entry;
}
return {};
}
CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadPixelShader(const AssetID& asset_id,
PixelShaderData* data)
@ -158,7 +123,7 @@ CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadPixelShader(const
if (!PixelShaderData::FromJson(asset_id, root_obj, data))
return {};
return LoadInfo{approx_mem_size, GetLastAssetWriteTime(asset_id)};
return LoadInfo{approx_mem_size};
}
CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadMaterial(const AssetID& asset_id,
@ -216,7 +181,7 @@ CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadMaterial(const As
return {};
}
return LoadInfo{metadata_size, GetLastAssetWriteTime(asset_id)};
return LoadInfo{metadata_size};
}
CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadMesh(const AssetID& asset_id,
@ -311,11 +276,41 @@ CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadMesh(const AssetI
if (!MeshData::FromJson(asset_id, root_obj, data))
return {};
return LoadInfo{approx_mem_size, GetLastAssetWriteTime(asset_id)};
return LoadInfo{approx_mem_size};
}
CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadTexture(const AssetID& asset_id,
TextureData* data)
CustomTextureData* data)
{
const auto asset_map = GetAssetMapForID(asset_id);
if (asset_map.empty())
{
ERROR_LOG_FMT(VIDEO, "Asset '{}' error - raw texture expected to have one or two files mapped!",
asset_id);
return {};
}
const auto texture_path = asset_map.find("texture");
if (texture_path == asset_map.end())
{
ERROR_LOG_FMT(VIDEO, "Asset '{}' expected to have a texture entry mapped!", asset_id);
return {};
}
if (!LoadTextureDataFromFile(asset_id, texture_path->second,
TextureAndSamplerData::Type::Type_Texture2D, data))
{
return {};
}
if (!PurgeInvalidMipsFromTextureData(asset_id, data))
return {};
return LoadInfo{GetAssetSize(*data)};
}
CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadTexture(const AssetID& asset_id,
TextureAndSamplerData* data)
{
const auto asset_map = GetAssetMapForID(asset_id);
@ -368,7 +363,7 @@ CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadTexture(const Ass
}
const auto& root_obj = root.get<picojson::object>();
if (!TextureData::FromJson(asset_id, root_obj, data))
if (!TextureAndSamplerData::FromJson(asset_id, root_obj, data))
{
return {};
}
@ -376,128 +371,62 @@ CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadTexture(const Ass
else
{
data->m_sampler = RenderState::GetLinearSamplerState();
data->m_type = TextureData::Type::Type_Texture2D;
data->m_type = TextureAndSamplerData::Type::Type_Texture2D;
}
auto ext = PathToString(texture_path->second.extension());
Common::ToLower(&ext);
if (ext == ".dds")
{
if (!LoadDDSTexture(&data->m_texture, PathToString(texture_path->second)))
{
ERROR_LOG_FMT(VIDEO, "Asset '{}' error - could not load dds texture!", asset_id);
return {};
}
if (!LoadTextureDataFromFile(asset_id, texture_path->second, data->m_type, &data->m_texture))
return {};
if (!PurgeInvalidMipsFromTextureData(asset_id, &data->m_texture))
return {};
if (data->m_texture.m_slices.empty()) [[unlikely]]
data->m_texture.m_slices.push_back({});
if (!LoadMips(texture_path->second, &data->m_texture.m_slices[0]))
return {};
return LoadInfo{GetAssetSize(data->m_texture) + metadata_size, GetLastAssetWriteTime(asset_id)};
}
else if (ext == ".png")
{
// PNG could support more complicated texture types in the future
// but for now just error
if (data->m_type != TextureData::Type::Type_Texture2D)
{
ERROR_LOG_FMT(VIDEO, "Asset '{}' error - PNG is not supported for texture type '{}'!",
asset_id, data->m_type);
return {};
}
// If we have no slices, create one
if (data->m_texture.m_slices.empty())
data->m_texture.m_slices.push_back({});
auto& slice = data->m_texture.m_slices[0];
// If we have no levels, create one to pass into LoadPNGTexture
if (slice.m_levels.empty())
slice.m_levels.push_back({});
if (!LoadPNGTexture(&slice.m_levels[0], PathToString(texture_path->second)))
{
ERROR_LOG_FMT(VIDEO, "Asset '{}' error - could not load png texture!", asset_id);
return {};
}
if (!LoadMips(texture_path->second, &slice))
return {};
return LoadInfo{GetAssetSize(data->m_texture) + metadata_size, GetLastAssetWriteTime(asset_id)};
}
ERROR_LOG_FMT(VIDEO, "Asset '{}' error - extension '{}' unknown!", asset_id, ext);
return {};
return LoadInfo{GetAssetSize(data->m_texture) + metadata_size};
}
void DirectFilesystemAssetLibrary::SetAssetIDMapData(const AssetID& asset_id,
AssetMap asset_path_map)
VideoCommon::Assets::AssetMap asset_path_map)
{
std::lock_guard lk(m_lock);
m_assetid_to_asset_map_path[asset_id] = std::move(asset_path_map);
}
bool DirectFilesystemAssetLibrary::LoadMips(const std::filesystem::path& asset_path,
CustomTextureData::ArraySlice* data)
{
if (!data) [[unlikely]]
return false;
std::string path;
std::string filename;
std::string extension;
SplitPath(PathToString(asset_path), &path, &filename, &extension);
std::string extension_lower = extension;
Common::ToLower(&extension_lower);
// Load additional mip levels
for (u32 mip_level = static_cast<u32>(data->m_levels.size());; mip_level++)
VideoCommon::Assets::AssetMap previous_asset_map;
{
const auto mip_level_filename = filename + fmt::format("_mip{}", mip_level);
const auto full_path = path + mip_level_filename + extension;
if (!File::Exists(full_path))
return true;
VideoCommon::CustomTextureData::ArraySlice::Level level;
if (extension_lower == ".dds")
{
if (!LoadDDSTexture(&level, full_path, mip_level))
{
ERROR_LOG_FMT(VIDEO, "Custom mipmap '{}' failed to load", mip_level_filename);
return false;
}
}
else if (extension_lower == ".png")
{
if (!LoadPNGTexture(&level, full_path))
{
ERROR_LOG_FMT(VIDEO, "Custom mipmap '{}' failed to load", mip_level_filename);
return false;
}
}
else
{
ERROR_LOG_FMT(VIDEO, "Custom mipmap '{}' has unsupported extension", mip_level_filename);
return false;
}
data->m_levels.push_back(std::move(level));
std::lock_guard lk(m_asset_map_lock);
previous_asset_map = m_asset_id_to_asset_map_path[asset_id];
}
return true;
{
std::lock_guard lk(m_path_map_lock);
for (const auto& [name, path] : previous_asset_map)
{
m_path_to_asset_id.erase(PathToString(path));
}
for (const auto& [name, path] : asset_path_map)
{
m_path_to_asset_id[PathToString(path)] = asset_id;
}
}
{
std::lock_guard lk(m_asset_map_lock);
m_asset_id_to_asset_map_path[asset_id] = std::move(asset_path_map);
}
}
DirectFilesystemAssetLibrary::AssetMap
void DirectFilesystemAssetLibrary::PathModified(std::string_view path)
{
std::lock_guard lk(m_path_map_lock);
if (const auto iter = m_path_to_asset_id.find(path); iter != m_path_to_asset_id.end())
{
auto& system = Core::System::GetInstance();
auto& resource_manager = system.GetCustomResourceManager();
resource_manager.MarkAssetDirty(iter->second);
}
}
VideoCommon::Assets::AssetMap
DirectFilesystemAssetLibrary::GetAssetMapForID(const AssetID& asset_id) const
{
std::lock_guard lk(m_lock);
if (auto iter = m_assetid_to_asset_map_path.find(asset_id);
iter != m_assetid_to_asset_map_path.end())
std::lock_guard lk(m_asset_map_lock);
if (auto iter = m_asset_id_to_asset_map_path.find(asset_id);
iter != m_asset_id_to_asset_map_path.end())
{
return iter->second;
}

View File

@ -8,39 +8,39 @@
#include <mutex>
#include <string>
#include "VideoCommon/Assets/CustomAssetLibrary.h"
#include "VideoCommon/Assets/CustomTextureData.h"
#include "VideoCommon/Assets/TextureAsset.h"
#include "VideoCommon/Assets/Types.h"
#include "VideoCommon/Assets/WatchableFilesystemAssetLibrary.h"
namespace VideoCommon
{
// This class implements 'CustomAssetLibrary' and loads any assets
// directly from the filesystem
class DirectFilesystemAssetLibrary final : public CustomAssetLibrary
class DirectFilesystemAssetLibrary final : public WatchableFilesystemAssetLibrary
{
public:
using AssetMap = std::map<std::string, std::filesystem::path>;
LoadInfo LoadTexture(const AssetID& asset_id, TextureData* data) override;
LoadInfo LoadTexture(const AssetID& asset_id, TextureAndSamplerData* data) override;
LoadInfo LoadTexture(const AssetID& asset_id, CustomTextureData* data) override;
LoadInfo LoadPixelShader(const AssetID& asset_id, PixelShaderData* data) override;
LoadInfo LoadMaterial(const AssetID& asset_id, MaterialData* data) override;
LoadInfo LoadMesh(const AssetID& asset_id, MeshData* data) override;
// Gets the latest time from amongst all the files in the asset map
TimeType GetLastAssetWriteTime(const AssetID& asset_id) const override;
// Assigns the asset id to a map of files, how this map is read is dependent on the data
// For instance, a raw texture would expect the map to have a single entry and load that
// file as the asset. But a model file data might have its data spread across multiple files
void SetAssetIDMapData(const AssetID& asset_id, AssetMap asset_path_map);
void SetAssetIDMapData(const AssetID& asset_id, Assets::AssetMap asset_path_map);
private:
// Loads additional mip levels into the texture structure until _mip<N> texture is not found
bool LoadMips(const std::filesystem::path& asset_path, CustomTextureData::ArraySlice* data);
void PathModified(std::string_view path) override;
// Gets the asset map given an asset id
AssetMap GetAssetMapForID(const AssetID& asset_id) const;
Assets::AssetMap GetAssetMapForID(const AssetID& asset_id) const;
mutable std::mutex m_lock;
std::map<AssetID, std::map<std::string, std::filesystem::path>> m_assetid_to_asset_map_path;
mutable std::mutex m_asset_map_lock;
std::map<AssetID, Assets::AssetMap> m_asset_id_to_asset_map_path;
mutable std::mutex m_path_map_lock;
std::map<std::string, AssetID, std::less<>> m_path_to_asset_id;
};
} // namespace VideoCommon

View File

@ -384,7 +384,7 @@ CustomAssetLibrary::LoadInfo MaterialAsset::LoadImpl(const CustomAssetLibrary::A
{
auto potential_data = std::make_shared<MaterialData>();
const auto loaded_info = m_owning_library->LoadMaterial(asset_id, potential_data.get());
if (loaded_info.m_bytes_loaded == 0)
if (loaded_info.bytes_loaded == 0)
return {};
{
std::lock_guard lk(m_data_lock);

View File

@ -651,7 +651,7 @@ CustomAssetLibrary::LoadInfo MeshAsset::LoadImpl(const CustomAssetLibrary::Asset
{
auto potential_data = std::make_shared<MeshData>();
const auto loaded_info = m_owning_library->LoadMesh(asset_id, potential_data.get());
if (loaded_info.m_bytes_loaded == 0)
if (loaded_info.bytes_loaded == 0)
return {};
{
std::lock_guard lk(m_data_lock);

View File

@ -439,7 +439,7 @@ CustomAssetLibrary::LoadInfo PixelShaderAsset::LoadImpl(const CustomAssetLibrary
{
auto potential_data = std::make_shared<PixelShaderData>();
const auto loaded_info = m_owning_library->LoadPixelShader(asset_id, potential_data.get());
if (loaded_info.m_bytes_loaded == 0)
if (loaded_info.bytes_loaded == 0)
return {};
{
std::lock_guard lk(m_data_lock);

View File

@ -153,8 +153,8 @@ bool ParseSampler(const VideoCommon::CustomAssetLibrary::AssetID& asset_id,
return true;
}
} // namespace
bool TextureData::FromJson(const CustomAssetLibrary::AssetID& asset_id,
const picojson::object& json, TextureData* data)
bool TextureAndSamplerData::FromJson(const CustomAssetLibrary::AssetID& asset_id,
const picojson::object& json, TextureAndSamplerData* data)
{
const auto type_iter = json.find("type");
if (type_iter == json.end())
@ -176,7 +176,7 @@ bool TextureData::FromJson(const CustomAssetLibrary::AssetID& asset_id,
if (type == "texture2d")
{
data->m_type = TextureData::Type::Type_Texture2D;
data->m_type = TextureAndSamplerData::Type::Type_Texture2D;
if (!ParseSampler(asset_id, json, &data->m_sampler))
{
@ -185,7 +185,7 @@ bool TextureData::FromJson(const CustomAssetLibrary::AssetID& asset_id,
}
else if (type == "texturecube")
{
data->m_type = TextureData::Type::Type_TextureCube;
data->m_type = TextureAndSamplerData::Type::Type_TextureCube;
}
else
{
@ -199,7 +199,7 @@ bool TextureData::FromJson(const CustomAssetLibrary::AssetID& asset_id,
return true;
}
void TextureData::ToJson(picojson::object* obj, const TextureData& data)
void TextureAndSamplerData::ToJson(picojson::object* obj, const TextureAndSamplerData& data)
{
if (!obj) [[unlikely]]
return;
@ -207,13 +207,13 @@ void TextureData::ToJson(picojson::object* obj, const TextureData& data)
auto& json_obj = *obj;
switch (data.m_type)
{
case TextureData::Type::Type_Texture2D:
case TextureAndSamplerData::Type::Type_Texture2D:
json_obj.emplace("type", "texture2d");
break;
case TextureData::Type::Type_TextureCube:
case TextureAndSamplerData::Type::Type_TextureCube:
json_obj.emplace("type", "texturecube");
break;
case TextureData::Type::Type_Undefined:
case TextureAndSamplerData::Type::Type_Undefined:
break;
};
@ -254,11 +254,11 @@ void TextureData::ToJson(picojson::object* obj, const TextureData& data)
json_obj.emplace("filter_mode", filter_mode);
}
CustomAssetLibrary::LoadInfo GameTextureAsset::LoadImpl(const CustomAssetLibrary::AssetID& asset_id)
CustomAssetLibrary::LoadInfo TextureAsset::LoadImpl(const CustomAssetLibrary::AssetID& asset_id)
{
auto potential_data = std::make_shared<TextureData>();
const auto loaded_info = m_owning_library->LoadGameTexture(asset_id, potential_data.get());
if (loaded_info.m_bytes_loaded == 0)
auto potential_data = std::make_shared<CustomTextureData>();
const auto loaded_info = m_owning_library->LoadTexture(asset_id, potential_data.get());
if (loaded_info.bytes_loaded == 0)
return {};
{
std::lock_guard lk(m_data_lock);
@ -267,75 +267,4 @@ CustomAssetLibrary::LoadInfo GameTextureAsset::LoadImpl(const CustomAssetLibrary
}
return loaded_info;
}
bool GameTextureAsset::Validate(u32 native_width, u32 native_height) const
{
std::lock_guard lk(m_data_lock);
if (!m_loaded)
{
ERROR_LOG_FMT(VIDEO,
"Game texture can't be validated for asset '{}' because it is not loaded yet.",
GetAssetId());
return false;
}
if (m_data->m_texture.m_slices.empty())
{
ERROR_LOG_FMT(VIDEO,
"Game texture can't be validated for asset '{}' because no data was available.",
GetAssetId());
return false;
}
if (m_data->m_texture.m_slices.size() > 1)
{
ERROR_LOG_FMT(
VIDEO,
"Game texture can't be validated for asset '{}' because it has more slices than expected.",
GetAssetId());
return false;
}
const auto& slice = m_data->m_texture.m_slices[0];
if (slice.m_levels.empty())
{
ERROR_LOG_FMT(
VIDEO,
"Game texture can't be validated for asset '{}' because first slice has no data available.",
GetAssetId());
return false;
}
// Verify that the aspect ratio of the texture hasn't changed, as this could have
// side-effects.
const VideoCommon::CustomTextureData::ArraySlice::Level& first_mip = slice.m_levels[0];
if (first_mip.width * native_height != first_mip.height * native_width)
{
// Note: this feels like this should return an error but
// for legacy reasons this is only a notice that something *could*
// go wrong
WARN_LOG_FMT(
VIDEO,
"Invalid custom texture size {}x{} for game texture asset '{}'. The aspect differs "
"from the native size {}x{}.",
first_mip.width, first_mip.height, GetAssetId(), native_width, native_height);
}
// Same deal if the custom texture isn't a multiple of the native size.
if (native_width != 0 && native_height != 0 &&
(first_mip.width % native_width || first_mip.height % native_height))
{
// Note: this feels like this should return an error but
// for legacy reasons this is only a notice that something *could*
// go wrong
WARN_LOG_FMT(
VIDEO,
"Invalid custom texture size {}x{} for game texture asset '{}'. Please use an integer "
"upscaling factor based on the native size {}x{}.",
first_mip.width, first_mip.height, GetAssetId(), native_width, native_height);
}
return true;
}
} // namespace VideoCommon

View File

@ -13,11 +13,11 @@
namespace VideoCommon
{
struct TextureData
struct TextureAndSamplerData
{
static bool FromJson(const CustomAssetLibrary::AssetID& asset_id, const picojson::object& json,
TextureData* data);
static void ToJson(picojson::object* obj, const TextureData& data);
TextureAndSamplerData* data);
static void ToJson(picojson::object* obj, const TextureAndSamplerData& data);
enum class Type
{
Type_Undefined,
@ -30,23 +30,19 @@ struct TextureData
SamplerState m_sampler;
};
class GameTextureAsset final : public CustomLoadableAsset<TextureData>
class TextureAsset final : public CustomLoadableAsset<CustomTextureData>
{
public:
using CustomLoadableAsset::CustomLoadableAsset;
// Validates that the game texture matches the native dimensions provided
// Callees are expected to call this once the data is loaded
bool Validate(u32 native_width, u32 native_height) const;
private:
CustomAssetLibrary::LoadInfo LoadImpl(const CustomAssetLibrary::AssetID& asset_id) override;
};
} // namespace VideoCommon
template <>
struct fmt::formatter<VideoCommon::TextureData::Type>
: EnumFormatter<VideoCommon::TextureData::Type::Type_Max>
struct fmt::formatter<VideoCommon::TextureAndSamplerData::Type>
: EnumFormatter<VideoCommon::TextureAndSamplerData::Type::Type_Max>
{
constexpr formatter() : EnumFormatter({"Undefined", "Texture2D", "TextureCube"}) {}
};

View File

@ -0,0 +1,244 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "VideoCommon/Assets/TextureAssetUtils.h"
#include <algorithm>
#include "Common/FileUtil.h"
#include "Common/Logging/Log.h"
#include "Common/StringUtil.h"
namespace VideoCommon
{
namespace
{
// Loads additional mip levels into the texture structure until _mip<N> texture is not found
bool LoadMips(const std::filesystem::path& asset_path, CustomTextureData::ArraySlice* data)
{
if (!data) [[unlikely]]
return false;
std::string path;
std::string filename;
std::string extension;
SplitPath(PathToString(asset_path), &path, &filename, &extension);
std::string extension_lower = extension;
Common::ToLower(&extension_lower);
// Load additional mip levels
for (u32 mip_level = static_cast<u32>(data->m_levels.size());; mip_level++)
{
const auto mip_level_filename = filename + fmt::format("_mip{}", mip_level);
const auto full_path = path + mip_level_filename + extension;
if (!File::Exists(full_path))
return true;
VideoCommon::CustomTextureData::ArraySlice::Level level;
if (extension_lower == ".dds")
{
if (!LoadDDSTexture(&level, full_path, mip_level))
{
ERROR_LOG_FMT(VIDEO, "Custom mipmap '{}' failed to load", mip_level_filename);
return false;
}
}
else if (extension_lower == ".png")
{
if (!LoadPNGTexture(&level, full_path))
{
ERROR_LOG_FMT(VIDEO, "Custom mipmap '{}' failed to load", mip_level_filename);
return false;
}
}
else
{
ERROR_LOG_FMT(VIDEO, "Custom mipmap '{}' has unsupported extension", mip_level_filename);
return false;
}
data->m_levels.push_back(std::move(level));
}
return true;
}
} // namespace
bool LoadTextureDataFromFile(const CustomAssetLibrary::AssetID& asset_id,
const std::filesystem::path& asset_path,
TextureAndSamplerData::Type type, CustomTextureData* data)
{
auto ext = PathToString(asset_path.extension());
Common::ToLower(&ext);
if (ext == ".dds")
{
if (!LoadDDSTexture(data, PathToString(asset_path)))
{
ERROR_LOG_FMT(VIDEO, "Asset '{}' error - could not load dds texture!", asset_id);
return false;
}
if (data->m_slices.empty()) [[unlikely]]
data->m_slices.emplace_back();
if (!LoadMips(asset_path, data->m_slices.data()))
return false;
return true;
}
if (ext == ".png")
{
// PNG could support more complicated texture types in the future
// but for now just error
if (type != TextureAndSamplerData::Type::Type_Texture2D)
{
ERROR_LOG_FMT(VIDEO, "Asset '{}' error - PNG is not supported for texture type '{}'!",
asset_id, type);
return {};
}
// If we have no slices, create one
if (data->m_slices.empty())
data->m_slices.emplace_back();
auto& slice = data->m_slices[0];
// If we have no levels, create one to pass into LoadPNGTexture
if (slice.m_levels.empty())
slice.m_levels.emplace_back();
if (!LoadPNGTexture(slice.m_levels.data(), PathToString(asset_path)))
{
ERROR_LOG_FMT(VIDEO, "Asset '{}' error - could not load png texture!", asset_id);
return false;
}
if (!LoadMips(asset_path, &slice))
return false;
return true;
}
ERROR_LOG_FMT(VIDEO, "Asset '{}' error - extension '{}' unknown!", asset_id, ext);
return false;
}
bool ValidateTextureData(const CustomAssetLibrary::AssetID& asset_id, const CustomTextureData& data,
u32 native_width, u32 native_height)
{
if (data.m_slices.empty())
{
ERROR_LOG_FMT(VIDEO,
"Texture data can't be validated for asset '{}' because no data was available.",
asset_id);
return false;
}
if (data.m_slices.size() > 1)
{
ERROR_LOG_FMT(
VIDEO,
"Texture data can't be validated for asset '{}' because it has more slices than expected.",
asset_id);
return false;
}
const auto& slice = data.m_slices[0];
if (slice.m_levels.empty())
{
ERROR_LOG_FMT(
VIDEO,
"Texture data can't be validated for asset '{}' because first slice has no data available.",
asset_id);
return false;
}
// Verify that the aspect ratio of the texture hasn't changed, as this could have
// side-effects.
const CustomTextureData::ArraySlice::Level& first_mip = slice.m_levels[0];
if (first_mip.width * native_height != first_mip.height * native_width)
{
// Note: this feels like this should return an error but
// for legacy reasons this is only a notice that something *could*
// go wrong
WARN_LOG_FMT(VIDEO,
"Invalid texture data size {}x{} for asset '{}'. The aspect differs "
"from the native size {}x{}.",
first_mip.width, first_mip.height, asset_id, native_width, native_height);
}
// Same deal if the custom texture isn't a multiple of the native size.
if (native_width != 0 && native_height != 0 &&
(first_mip.width % native_width || first_mip.height % native_height))
{
// Note: this feels like this should return an error but
// for legacy reasons this is only a notice that something *could*
// go wrong
WARN_LOG_FMT(VIDEO,
"Invalid texture data size {}x{} for asset '{}'. Please use an integer "
"upscaling factor based on the native size {}x{}.",
first_mip.width, first_mip.height, asset_id, native_width, native_height);
}
return true;
}
bool PurgeInvalidMipsFromTextureData(const CustomAssetLibrary::AssetID& asset_id,
CustomTextureData* data)
{
for (std::size_t slice_index = 0; slice_index < data->m_slices.size(); slice_index++)
{
auto& slice = data->m_slices[slice_index];
const auto& first_mip = slice.m_levels[0];
// Verify that each mip level is the correct size (divide by 2 each time).
u32 current_mip_width = first_mip.width;
u32 current_mip_height = first_mip.height;
for (u32 mip_level = 1; mip_level < static_cast<u32>(slice.m_levels.size()); mip_level++)
{
if (current_mip_width != 1 || current_mip_height != 1)
{
current_mip_width = std::max(current_mip_width / 2, 1u);
current_mip_height = std::max(current_mip_height / 2, 1u);
const VideoCommon::CustomTextureData::ArraySlice::Level& level = slice.m_levels[mip_level];
if (current_mip_width == level.width && current_mip_height == level.height)
continue;
ERROR_LOG_FMT(VIDEO,
"Invalid custom game texture size {}x{} for texture asset {}. Slice {} with "
"mipmap level {} "
"must be {}x{}.",
level.width, level.height, asset_id, slice_index, mip_level,
current_mip_width, current_mip_height);
}
else
{
// It is invalid to have more than a single 1x1 mipmap.
ERROR_LOG_FMT(VIDEO,
"Custom game texture {} has too many 1x1 mipmaps for slice {}. Skipping "
"extra levels.",
asset_id, slice_index);
}
// Drop this mip level and any others after it.
while (slice.m_levels.size() > mip_level)
slice.m_levels.pop_back();
}
// All levels have to have the same format.
if (std::ranges::any_of(slice.m_levels,
[&first_mip](const auto& l) { return l.format != first_mip.format; }))
{
ERROR_LOG_FMT(
VIDEO, "Custom game texture {} has inconsistent formats across mip levels for slice {}.",
asset_id, slice_index);
return false;
}
}
return true;
}
} // namespace VideoCommon

View File

@ -0,0 +1,22 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <filesystem>
#include "VideoCommon/Assets/CustomAssetLibrary.h"
#include "VideoCommon/Assets/TextureAsset.h"
namespace VideoCommon
{
bool LoadTextureDataFromFile(const CustomAssetLibrary::AssetID& asset_id,
const std::filesystem::path& asset_path,
TextureAndSamplerData::Type type, CustomTextureData* data);
bool ValidateTextureData(const CustomAssetLibrary::AssetID& asset_id, const CustomTextureData& data,
u32 native_width, u32 native_height);
bool PurgeInvalidMipsFromTextureData(const CustomAssetLibrary::AssetID& asset_id,
CustomTextureData* data);
} // namespace VideoCommon

View File

@ -0,0 +1,13 @@
// Copyright 2024 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <filesystem>
#include <map>
#include <string>
namespace VideoCommon::Assets
{
using AssetMap = std::map<std::string, std::filesystem::path>;
}

View File

@ -0,0 +1,14 @@
// Copyright 2024 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Common/FilesystemWatcher.h"
#include "VideoCommon/Assets/CustomAssetLibrary.h"
namespace VideoCommon
{
class WatchableFilesystemAssetLibrary : public CustomAssetLibrary, public Common::FilesystemWatcher
{
};
} // namespace VideoCommon

View File

@ -10,10 +10,11 @@ add_library(videocommon
AbstractTexture.h
Assets/CustomAsset.cpp
Assets/CustomAsset.h
Assets/CustomAssetLibrary.cpp
Assets/CustomAssetLibrary.h
Assets/CustomAssetLoader.cpp
Assets/CustomAssetLoader.h
Assets/CustomResourceManager.cpp
Assets/CustomResourceManager.h
Assets/CustomTextureData.cpp
Assets/CustomTextureData.h
Assets/DirectFilesystemAssetLibrary.cpp
@ -26,6 +27,10 @@ add_library(videocommon
Assets/ShaderAsset.h
Assets/TextureAsset.cpp
Assets/TextureAsset.h
Assets/TextureAssetUtils.cpp
Assets/TextureAssetUtils.h
Assets/Types.h
Assets/WatchableFilesystemAssetLibrary.h
AsyncRequests.cpp
AsyncRequests.h
AsyncShaderCompiler.cpp

View File

@ -7,12 +7,13 @@
#include <picojson.h>
#include "VideoCommon/Assets/DirectFilesystemAssetLibrary.h"
#include "VideoCommon/Assets/CustomAssetLibrary.h"
#include "VideoCommon/Assets/Types.h"
struct GraphicsModAssetConfig
{
VideoCommon::CustomAssetLibrary::AssetID m_asset_id;
VideoCommon::DirectFilesystemAssetLibrary::AssetMap m_map;
VideoCommon::Assets::AssetMap m_map;
void SerializeToConfig(picojson::object& json_obj) const;
bool DeserializeFromConfig(const picojson::object& obj);

View File

@ -16,7 +16,6 @@
#include "Core/System.h"
#include "VideoCommon/AbstractGfx.h"
#include "VideoCommon/Assets/CustomAssetLoader.h"
#include "VideoCommon/Assets/DirectFilesystemAssetLibrary.h"
#include "VideoCommon/ShaderGenCommon.h"
#include "VideoCommon/TextureCacheBase.h"
@ -98,28 +97,6 @@ CustomPipelineAction::CustomPipelineAction(
m_pipeline_passes.resize(m_passes_config.size());
}
void CustomPipelineAction::OnDrawStarted(GraphicsModActionData::DrawStarted* draw_started)
void CustomPipelineAction::OnDrawStarted(GraphicsModActionData::DrawStarted*)
{
if (!draw_started) [[unlikely]]
return;
if (!draw_started->custom_pixel_shader) [[unlikely]]
return;
if (m_pipeline_passes.empty()) [[unlikely]]
return;
auto& loader = Core::System::GetInstance().GetCustomAssetLoader();
// For now assume a single pass
const auto& pass_config = m_passes_config[0];
auto& pass = m_pipeline_passes[0];
pass.UpdatePixelData(loader, m_library, draw_started->texture_units,
pass_config.m_pixel_material_asset);
CustomPixelShader custom_pixel_shader;
custom_pixel_shader.custom_shader = pass.m_last_generated_shader_code.GetBuffer();
custom_pixel_shader.material_uniform_block = pass.m_last_generated_material_code.GetBuffer();
*draw_started->custom_pixel_shader = custom_pixel_shader;
*draw_started->material_uniform_buffer = pass.m_material_data;
}

View File

@ -12,7 +12,6 @@
#include "Common/VariantUtil.h"
#include "VideoCommon/AbstractGfx.h"
#include "VideoCommon/Assets/CustomAssetLoader.h"
namespace
{
@ -172,238 +171,8 @@ std::vector<std::string> GlobalConflicts(std::string_view source)
} // namespace
void CustomPipeline::UpdatePixelData(
VideoCommon::CustomAssetLoader& loader,
std::shared_ptr<VideoCommon::CustomAssetLibrary> library, std::span<const u32> texture_units,
const VideoCommon::CustomAssetLibrary::AssetID& material_to_load)
void CustomPipeline::UpdatePixelData(std::shared_ptr<VideoCommon::CustomAssetLibrary>,
std::span<const u32>,
const VideoCommon::CustomAssetLibrary::AssetID&)
{
if (!m_pixel_material.m_asset || material_to_load != m_pixel_material.m_asset->GetAssetId())
{
m_pixel_material.m_asset = loader.LoadMaterial(material_to_load, library);
}
const auto material_data = m_pixel_material.m_asset->GetData();
if (!material_data)
{
return;
}
std::size_t max_material_data_size = 0;
if (m_pixel_material.m_asset->GetLastLoadedTime() > m_pixel_material.m_cached_write_time)
{
m_last_generated_material_code = ShaderCode{};
m_pixel_material.m_cached_write_time = m_pixel_material.m_asset->GetLastLoadedTime();
std::size_t texture_count = 0;
for (const auto& property : material_data->properties)
{
max_material_data_size += VideoCommon::MaterialProperty::GetMemorySize(property);
VideoCommon::MaterialProperty::WriteAsShaderCode(m_last_generated_material_code, property);
if (std::holds_alternative<VideoCommon::CustomAssetLibrary::AssetID>(property.m_value))
{
texture_count++;
}
}
m_material_data.resize(max_material_data_size);
m_game_textures.resize(texture_count);
}
if (!m_pixel_shader.m_asset ||
m_pixel_shader.m_asset->GetLastLoadedTime() > m_pixel_shader.m_cached_write_time ||
material_data->shader_asset != m_pixel_shader.m_asset->GetAssetId())
{
m_pixel_shader.m_asset = loader.LoadPixelShader(material_data->shader_asset, library);
m_pixel_shader.m_cached_write_time = m_pixel_shader.m_asset->GetLastLoadedTime();
m_last_generated_shader_code = ShaderCode{};
}
const auto shader_data = m_pixel_shader.m_asset->GetData();
if (!shader_data)
{
return;
}
if (shader_data->m_properties.size() != material_data->properties.size())
{
return;
}
u8* material_buffer = m_material_data.data();
u32 sampler_index = 8;
for (std::size_t index = 0; index < material_data->properties.size(); index++)
{
auto& property = material_data->properties[index];
const auto shader_it = shader_data->m_properties.find(property.m_code_name);
if (shader_it == shader_data->m_properties.end())
{
ERROR_LOG_FMT(VIDEO,
"Custom pipeline, has material asset '{}' that uses a "
"code name of '{}' but that can't be found on shader asset '{}'!",
m_pixel_material.m_asset->GetAssetId(), property.m_code_name,
m_pixel_shader.m_asset->GetAssetId());
return;
}
if (auto* texture_asset_id =
std::get_if<VideoCommon::CustomAssetLibrary::AssetID>(&property.m_value))
{
if (*texture_asset_id != "")
{
auto asset = loader.LoadGameTexture(*texture_asset_id, library);
if (!asset)
{
return;
}
auto& texture_asset = m_game_textures[index];
if (!texture_asset ||
texture_asset->m_cached_asset.m_asset->GetLastLoadedTime() >
texture_asset->m_cached_asset.m_cached_write_time ||
*texture_asset_id != texture_asset->m_cached_asset.m_asset->GetAssetId())
{
if (!texture_asset)
{
texture_asset = CachedTextureAsset{};
}
const auto loaded_time = asset->GetLastLoadedTime();
texture_asset->m_cached_asset = VideoCommon::CachedAsset<VideoCommon::GameTextureAsset>{
std::move(asset), loaded_time};
texture_asset->m_texture.reset();
if (std::holds_alternative<VideoCommon::ShaderProperty::Sampler2D>(
shader_it->second.m_default))
{
texture_asset->m_sampler_code =
fmt::format("SAMPLER_BINDING({}) uniform sampler2D samp_{};\n", sampler_index,
property.m_code_name);
texture_asset->m_define_code = fmt::format("#define HAS_{} 1\n", property.m_code_name);
}
else if (std::holds_alternative<VideoCommon::ShaderProperty::Sampler2DArray>(
shader_it->second.m_default))
{
texture_asset->m_sampler_code =
fmt::format("SAMPLER_BINDING({}) uniform sampler2DArray samp_{};\n", sampler_index,
property.m_code_name);
texture_asset->m_define_code = fmt::format("#define HAS_{} 1\n", property.m_code_name);
}
else if (std::holds_alternative<VideoCommon::ShaderProperty::SamplerCube>(
shader_it->second.m_default))
{
texture_asset->m_sampler_code =
fmt::format("SAMPLER_BINDING({}) uniform samplerCube samp_{};\n", sampler_index,
property.m_code_name);
texture_asset->m_define_code = fmt::format("#define HAS_{} 1\n", property.m_code_name);
}
}
const auto texture_data = texture_asset->m_cached_asset.m_asset->GetData();
if (!texture_data)
{
return;
}
if (texture_asset->m_texture)
{
g_gfx->SetTexture(sampler_index, texture_asset->m_texture.get());
g_gfx->SetSamplerState(sampler_index, texture_data->m_sampler);
}
else
{
AbstractTextureType texture_type = AbstractTextureType::Texture_2DArray;
if (std::holds_alternative<VideoCommon::ShaderProperty::SamplerCube>(
shader_it->second.m_default))
{
texture_type = AbstractTextureType::Texture_CubeMap;
}
else if (std::holds_alternative<VideoCommon::ShaderProperty::Sampler2D>(
shader_it->second.m_default))
{
texture_type = AbstractTextureType::Texture_2D;
}
if (texture_data->m_texture.m_slices.empty() ||
texture_data->m_texture.m_slices[0].m_levels.empty())
{
return;
}
auto& first_slice = texture_data->m_texture.m_slices[0];
const TextureConfig texture_config(
first_slice.m_levels[0].width, first_slice.m_levels[0].height,
static_cast<u32>(first_slice.m_levels.size()),
static_cast<u32>(texture_data->m_texture.m_slices.size()), 1,
first_slice.m_levels[0].format, 0, texture_type);
texture_asset->m_texture = g_gfx->CreateTexture(
texture_config, fmt::format("Custom shader texture '{}'", property.m_code_name));
if (texture_asset->m_texture)
{
for (std::size_t slice_index = 0; slice_index < texture_data->m_texture.m_slices.size();
slice_index++)
{
auto& slice = texture_data->m_texture.m_slices[slice_index];
for (u32 level_index = 0; level_index < static_cast<u32>(slice.m_levels.size());
++level_index)
{
auto& level = slice.m_levels[level_index];
texture_asset->m_texture->Load(level_index, level.width, level.height,
level.row_length, level.data.data(),
level.data.size(), static_cast<u32>(slice_index));
}
}
}
}
sampler_index++;
}
}
else
{
VideoCommon::MaterialProperty::WriteToMemory(material_buffer, property);
}
}
if (m_last_generated_shader_code.GetBuffer().empty())
{
// Calculate shader details
std::string color_shader_data =
ReplaceAll(shader_data->m_shader_source, "custom_main", CUSTOM_PIXELSHADER_COLOR_FUNC);
const auto global_conflicts = GlobalConflicts(color_shader_data);
color_shader_data = ReplaceAll(color_shader_data, "\r\n", "\n");
color_shader_data = ReplaceAll(color_shader_data, "{", "{{");
color_shader_data = ReplaceAll(color_shader_data, "}", "}}");
// First replace global conflicts with dummy strings
// This avoids the problem where a shorter word
// is in a longer word, ex two functions: 'execute' and 'execute_fast'
for (std::size_t i = 0; i < global_conflicts.size(); i++)
{
const std::string& identifier = global_conflicts[i];
color_shader_data =
ReplaceAll(color_shader_data, identifier, fmt::format("_{0}_DOLPHIN_TEMP_{0}_", i));
}
// Now replace the temporaries with the actual value
for (std::size_t i = 0; i < global_conflicts.size(); i++)
{
const std::string& identifier = global_conflicts[i];
color_shader_data = ReplaceAll(color_shader_data, fmt::format("_{0}_DOLPHIN_TEMP_{0}_", i),
fmt::format("{}_{{0}}", identifier));
}
for (const auto& game_texture : m_game_textures)
{
if (!game_texture)
continue;
m_last_generated_shader_code.Write("{}", game_texture->m_sampler_code);
m_last_generated_shader_code.Write("{}", game_texture->m_define_code);
}
for (std::size_t i = 0; i < texture_units.size(); i++)
{
const auto& texture_unit = texture_units[i];
m_last_generated_shader_code.Write(
"#define TEX_COORD{} data.texcoord[data.texmap_to_texcoord_index[{}]].xy\n", i,
texture_unit);
}
m_last_generated_shader_code.Write("{}", color_shader_data);
}
}

View File

@ -17,15 +17,9 @@
#include "VideoCommon/Assets/TextureAsset.h"
#include "VideoCommon/ShaderGenCommon.h"
namespace VideoCommon
{
class CustomAssetLoader;
}
struct CustomPipeline
{
void UpdatePixelData(VideoCommon::CustomAssetLoader& loader,
std::shared_ptr<VideoCommon::CustomAssetLibrary> library,
void UpdatePixelData(std::shared_ptr<VideoCommon::CustomAssetLibrary> library,
std::span<const u32> texture_units,
const VideoCommon::CustomAssetLibrary::AssetID& material_to_load);
@ -34,7 +28,7 @@ struct CustomPipeline
struct CachedTextureAsset
{
VideoCommon::CachedAsset<VideoCommon::GameTextureAsset> m_cached_asset;
VideoCommon::CachedAsset<VideoCommon::TextureAsset> m_cached_asset;
std::unique_ptr<AbstractTexture> m_texture;
std::string m_sampler_code;
std::string m_define_code;

View File

@ -47,7 +47,7 @@ struct TextureCreate
std::string_view texture_name;
u32 texture_width;
u32 texture_height;
std::vector<VideoCommon::CachedAsset<VideoCommon::GameTextureAsset>>* custom_textures;
std::vector<VideoCommon::CachedAsset<VideoCommon::TextureAsset>>* custom_textures;
// Dependencies needed to reload the texture and trigger this create again
std::vector<VideoCommon::CachedAsset<VideoCommon::CustomAsset>>* additional_dependencies;

View File

@ -25,7 +25,6 @@
#include "Core/ConfigManager.h"
#include "Core/System.h"
#include "VideoCommon/Assets/CustomAsset.h"
#include "VideoCommon/Assets/CustomAssetLoader.h"
#include "VideoCommon/Assets/DirectFilesystemAssetLibrary.h"
#include "VideoCommon/OnScreenDisplay.h"
#include "VideoCommon/VideoConfig.h"
@ -95,10 +94,11 @@ void HiresTexture::Update()
GetTextureDirectoriesWithGameId(File::GetUserPath(D_HIRESTEXTURES_IDX), game_id);
const std::vector<std::string> extensions{".png", ".dds"};
auto& system = Core::System::GetInstance();
for (const auto& texture_directory : texture_directories)
{
// Watch this directory for any texture reloads
s_file_library->Watch(texture_directory);
const auto texture_paths =
Common::DoFileSearch({texture_directory}, extensions, /*recursive*/ true);
@ -130,10 +130,10 @@ void HiresTexture::Update()
if (g_ActiveConfig.bCacheHiresTextures)
{
auto hires_texture = std::make_shared<HiresTexture>(
has_arbitrary_mipmaps,
system.GetCustomAssetLoader().LoadGameTexture(filename, s_file_library));
s_hires_texture_cache.try_emplace(filename, std::move(hires_texture));
auto hires_texture =
std::make_shared<HiresTexture>(has_arbitrary_mipmaps, std::move(filename));
static_cast<void>(hires_texture->LoadTexture());
s_hires_texture_cache.try_emplace(hires_texture->GetId(), hires_texture);
}
}
}
@ -167,7 +167,7 @@ void HiresTexture::Clear()
std::shared_ptr<HiresTexture> HiresTexture::Search(const TextureInfo& texture_info)
{
const auto [base_filename, has_arb_mipmaps] = GetNameArbPair(texture_info);
auto [base_filename, has_arb_mipmaps] = GetNameArbPair(texture_info);
if (base_filename == "")
return nullptr;
@ -177,24 +177,27 @@ std::shared_ptr<HiresTexture> HiresTexture::Search(const TextureInfo& texture_in
}
else
{
auto& system = Core::System::GetInstance();
auto hires_texture = std::make_shared<HiresTexture>(
has_arb_mipmaps,
system.GetCustomAssetLoader().LoadGameTexture(base_filename, s_file_library));
auto hires_texture = std::make_shared<HiresTexture>(has_arb_mipmaps, std::move(base_filename));
if (g_ActiveConfig.bCacheHiresTextures)
{
s_hires_texture_cache.try_emplace(base_filename, hires_texture);
s_hires_texture_cache.try_emplace(hires_texture->GetId(), hires_texture);
}
return hires_texture;
}
}
HiresTexture::HiresTexture(bool has_arbitrary_mipmaps,
std::shared_ptr<VideoCommon::GameTextureAsset> asset)
: m_has_arbitrary_mipmaps(has_arbitrary_mipmaps), m_game_texture(std::move(asset))
HiresTexture::HiresTexture(bool has_arbitrary_mipmaps, std::string id)
: m_has_arbitrary_mipmaps(has_arbitrary_mipmaps), m_id(std::move(id))
{
}
VideoCommon::CustomResourceManager::TextureTimePair HiresTexture::LoadTexture() const
{
auto& system = Core::System::GetInstance();
auto& custom_resource_manager = system.GetCustomResourceManager();
return custom_resource_manager.GetTextureDataFromAsset(m_id, s_file_library);
}
std::set<std::string> GetTextureDirectoriesWithGameId(const std::string& root_directory,
const std::string& game_id)
{

View File

@ -9,8 +9,8 @@
#include <vector>
#include "Common/CommonTypes.h"
#include "VideoCommon/Assets/CustomResourceManager.h"
#include "VideoCommon/Assets/CustomTextureData.h"
#include "VideoCommon/Assets/TextureAsset.h"
#include "VideoCommon/TextureConfig.h"
#include "VideoCommon/TextureInfo.h"
@ -27,12 +27,13 @@ public:
static void Shutdown();
static std::shared_ptr<HiresTexture> Search(const TextureInfo& texture_info);
HiresTexture(bool has_arbitrary_mipmaps, std::shared_ptr<VideoCommon::GameTextureAsset> asset);
HiresTexture(bool has_arbitrary_mipmaps, std::string id);
bool HasArbitraryMipmaps() const { return m_has_arbitrary_mipmaps; }
const std::shared_ptr<VideoCommon::GameTextureAsset>& GetAsset() const { return m_game_texture; }
VideoCommon::CustomResourceManager::TextureTimePair LoadTexture() const;
const std::string& GetId() const { return m_id; }
private:
bool m_has_arbitrary_mipmaps = false;
std::shared_ptr<VideoCommon::GameTextureAsset> m_game_texture;
std::string m_id;
};

View File

@ -1007,6 +1007,12 @@ ShaderCode GeneratePixelShaderCode(APIType api_type, const ShaderHostConfig& hos
out.Write("\tfrag_input.tex{0} = tex{0};\n", i);
}
// Initialize other texture coordinates that are unused
for (u32 i = uid_data->genMode_numtexgens; i < 8; i++)
{
out.Write("\tfrag_input.tex{0} = vec3(0, 0, 0);\n", i);
}
if (!custom_contents.shader.empty())
GenerateCustomLighting(&out, uid_data->lighting);
@ -2061,11 +2067,7 @@ static void WriteFragmentDefinitions(APIType api_type, const ShaderHostConfig& h
out.Write("\tint layer;\n");
out.Write("\tvec3 normal;\n");
out.Write("\tvec3 position;\n");
for (u32 i = 0; i < uid_data->genMode_numtexgens; i++)
{
out.Write("\tvec3 tex{};\n", i);
}
for (u32 i = uid_data->genMode_numtexgens; i < 8; i++)
for (u32 i = 0; i < 8; i++)
{
out.Write("\tvec3 tex{};\n", i);
}

View File

@ -37,13 +37,14 @@
#include "VideoCommon/AbstractFramebuffer.h"
#include "VideoCommon/AbstractGfx.h"
#include "VideoCommon/AbstractStagingTexture.h"
#include "VideoCommon/Assets/CustomResourceManager.h"
#include "VideoCommon/Assets/CustomTextureData.h"
#include "VideoCommon/Assets/TextureAssetUtils.h"
#include "VideoCommon/BPMemory.h"
#include "VideoCommon/FramebufferManager.h"
#include "VideoCommon/GraphicsModSystem/Runtime/FBInfo.h"
#include "VideoCommon/GraphicsModSystem/Runtime/GraphicsModActionData.h"
#include "VideoCommon/GraphicsModSystem/Runtime/GraphicsModManager.h"
#include "VideoCommon/HiresTextures.h"
#include "VideoCommon/OpcodeDecoding.h"
#include "VideoCommon/PixelShaderManager.h"
#include "VideoCommon/Present.h"
@ -262,25 +263,12 @@ void TextureCacheBase::SetBackupConfig(const VideoConfig& config)
bool TextureCacheBase::DidLinkedAssetsChange(const TCacheEntry& entry)
{
for (const auto& cached_asset : entry.linked_game_texture_assets)
{
if (cached_asset.m_asset)
{
if (cached_asset.m_asset->GetLastLoadedTime() > cached_asset.m_cached_write_time)
return true;
}
}
if (!entry.hires_texture)
return false;
for (const auto& cached_asset : entry.linked_asset_dependencies)
{
if (cached_asset.m_asset)
{
if (cached_asset.m_asset->GetLastLoadedTime() > cached_asset.m_cached_write_time)
return true;
}
}
const auto [texture_data, load_time] = entry.hires_texture->LoadTexture();
return false;
return load_time > entry.last_load_time;
}
RcTcacheEntry TextureCacheBase::ApplyPaletteToEntry(RcTcacheEntry& entry, const u8* palette,
@ -1566,80 +1554,50 @@ RcTcacheEntry TextureCacheBase::GetTexture(const int textureCacheSafetyColorSamp
InvalidateTexture(oldest_entry);
}
std::vector<VideoCommon::CachedAsset<VideoCommon::GameTextureAsset>> cached_game_assets;
std::vector<std::shared_ptr<VideoCommon::TextureData>> data_for_assets;
std::shared_ptr<HiresTexture> hires_texture;
bool has_arbitrary_mipmaps = false;
bool skip_texture_dump = false;
std::shared_ptr<HiresTexture> hires_texture;
std::shared_ptr<VideoCommon::CustomTextureData> custom_texture_data = nullptr;
VideoCommon::CustomAsset::TimeType load_time = {};
if (g_ActiveConfig.bHiresTextures)
{
hires_texture = HiresTexture::Search(texture_info);
if (hires_texture)
{
auto asset = hires_texture->GetAsset();
const auto loaded_time = asset->GetLastLoadedTime();
cached_game_assets.push_back(
VideoCommon::CachedAsset<VideoCommon::GameTextureAsset>{std::move(asset), loaded_time});
has_arbitrary_mipmaps = hires_texture->HasArbitraryMipmaps();
std::tie(custom_texture_data, load_time) = hires_texture->LoadTexture();
if (custom_texture_data && !VideoCommon::ValidateTextureData(
hires_texture->GetId(), *custom_texture_data,
texture_info.GetRawWidth(), texture_info.GetRawHeight()))
{
custom_texture_data = nullptr;
load_time = {};
}
skip_texture_dump = true;
}
}
std::vector<VideoCommon::CachedAsset<VideoCommon::CustomAsset>> additional_dependencies;
std::string texture_name = "";
if (g_ActiveConfig.bGraphicMods)
{
u32 height = texture_info.GetRawHeight();
u32 width = texture_info.GetRawWidth();
if (hires_texture)
{
auto asset = hires_texture->GetAsset();
if (asset)
{
auto data = asset->GetData();
if (data)
{
if (!data->m_texture.m_slices.empty())
{
if (!data->m_texture.m_slices[0].m_levels.empty())
{
height = data->m_texture.m_slices[0].m_levels[0].height;
width = data->m_texture.m_slices[0].m_levels[0].width;
}
}
}
}
}
texture_name = texture_info.CalculateTextureName().GetFullName();
GraphicsModActionData::TextureCreate texture_create{
texture_name, width, height, &cached_game_assets, &additional_dependencies};
GraphicsModActionData::TextureCreate texture_create{texture_name, width, height, nullptr,
nullptr};
for (const auto& action : g_graphics_mod_manager->GetTextureCreateActions(texture_name))
{
action->OnTextureCreate(&texture_create);
}
}
data_for_assets.reserve(cached_game_assets.size());
for (auto& cached_asset : cached_game_assets)
{
auto data = cached_asset.m_asset->GetData();
if (data)
{
if (cached_asset.m_asset->Validate(texture_info.GetRawWidth(), texture_info.GetRawHeight()))
{
data_for_assets.push_back(data);
}
}
}
auto entry =
CreateTextureEntry(TextureCreationInfo{base_hash, full_hash, bytes_per_block, palette_size},
texture_info, textureCacheSafetyColorSampleSize,
std::move(data_for_assets), has_arbitrary_mipmaps, skip_texture_dump);
entry->linked_game_texture_assets = std::move(cached_game_assets);
entry->linked_asset_dependencies = std::move(additional_dependencies);
texture_info, textureCacheSafetyColorSampleSize, custom_texture_data.get(),
has_arbitrary_mipmaps, skip_texture_dump);
entry->hires_texture = std::move(hires_texture);
entry->last_load_time = load_time;
entry->texture_info_name = std::move(texture_name);
return entry;
}
@ -1649,8 +1607,7 @@ RcTcacheEntry TextureCacheBase::GetTexture(const int textureCacheSafetyColorSamp
// expected because each texture is loaded into a texture array
RcTcacheEntry TextureCacheBase::CreateTextureEntry(
const TextureCreationInfo& creation_info, const TextureInfo& texture_info,
const int safety_color_sample_size,
std::vector<std::shared_ptr<VideoCommon::TextureData>> assets_data,
const int safety_color_sample_size, VideoCommon::CustomTextureData* custom_texture_data,
const bool custom_arbitrary_mipmaps, bool skip_texture_dump)
{
#ifdef __APPLE__
@ -1660,33 +1617,22 @@ RcTcacheEntry TextureCacheBase::CreateTextureEntry(
#endif
RcTcacheEntry entry;
if (!assets_data.empty())
if (custom_texture_data)
{
const auto calculate_max_levels = [&]() {
const auto max_element = std::ranges::max_element(
assets_data, {}, [](const auto& v) { return v->m_texture.m_slices[0].m_levels.size(); });
return (*max_element)->m_texture.m_slices[0].m_levels.size();
};
const u32 texLevels = no_mips ? 1 : (u32)calculate_max_levels();
const auto& first_level = assets_data[0]->m_texture.m_slices[0].m_levels[0];
const TextureConfig config(first_level.width, first_level.height, texLevels,
static_cast<u32>(assets_data.size()), 1, first_level.format, 0,
AbstractTextureType::Texture_2DArray);
const u32 texLevels = no_mips ? 1 : (u32)custom_texture_data->m_slices[0].m_levels.size();
const auto& first_level = custom_texture_data->m_slices[0].m_levels[0];
const TextureConfig config(first_level.width, first_level.height, texLevels, 1, 1,
first_level.format, 0, AbstractTextureType::Texture_2DArray);
entry = AllocateCacheEntry(config);
if (!entry) [[unlikely]]
return entry;
for (u32 data_index = 0; data_index < static_cast<u32>(assets_data.size()); data_index++)
const auto& slice = custom_texture_data->m_slices[0];
for (u32 level_index = 0;
level_index < std::min(texLevels, static_cast<u32>(slice.m_levels.size())); ++level_index)
{
const auto& asset = assets_data[data_index];
const auto& slice = asset->m_texture.m_slices[0];
for (u32 level_index = 0;
level_index < std::min(texLevels, static_cast<u32>(slice.m_levels.size()));
++level_index)
{
const auto& level = slice.m_levels[level_index];
entry->texture->Load(level_index, level.width, level.height, level.row_length,
level.data.data(), level.data.size(), data_index);
}
const auto& level = slice.m_levels[level_index];
entry->texture->Load(level_index, level.width, level.height, level.row_length,
level.data.data(), level.data.size());
}
entry->has_arbitrary_mips = custom_arbitrary_mipmaps;

View File

@ -24,6 +24,7 @@
#include "VideoCommon/AbstractTexture.h"
#include "VideoCommon/Assets/CustomAsset.h"
#include "VideoCommon/BPMemory.h"
#include "VideoCommon/HiresTextures.h"
#include "VideoCommon/TextureConfig.h"
#include "VideoCommon/TextureDecoder.h"
#include "VideoCommon/TextureInfo.h"
@ -167,8 +168,8 @@ struct TCacheEntry
std::string texture_info_name = "";
std::vector<VideoCommon::CachedAsset<VideoCommon::GameTextureAsset>> linked_game_texture_assets;
std::vector<VideoCommon::CachedAsset<VideoCommon::CustomAsset>> linked_asset_dependencies;
VideoCommon::CustomAsset::TimeType last_load_time;
std::shared_ptr<HiresTexture> hires_texture;
explicit TCacheEntry(std::unique_ptr<AbstractTexture> tex,
std::unique_ptr<AbstractFramebuffer> fb);
@ -351,11 +352,10 @@ private:
void SetBackupConfig(const VideoConfig& config);
RcTcacheEntry
CreateTextureEntry(const TextureCreationInfo& creation_info, const TextureInfo& texture_info,
int safety_color_sample_size,
std::vector<std::shared_ptr<VideoCommon::TextureData>> assets_data,
bool custom_arbitrary_mipmaps, bool skip_texture_dump);
RcTcacheEntry CreateTextureEntry(const TextureCreationInfo& creation_info,
const TextureInfo& texture_info, int safety_color_sample_size,
VideoCommon::CustomTextureData* custom_texture_data,
bool custom_arbitrary_mipmaps, bool skip_texture_dump);
RcTcacheEntry GetXFBFromCache(u32 address, u32 width, u32 height, u32 stride);

View File

@ -41,6 +41,7 @@
#endif
#include "VideoCommon/AbstractGfx.h"
#include "VideoCommon/Assets/CustomResourceManager.h"
#include "VideoCommon/AsyncRequests.h"
#include "VideoCommon/BPStructs.h"
#include "VideoCommon/BoundingBox.h"
@ -341,12 +342,16 @@ bool VideoBackendBase::InitializeShared(std::unique_ptr<AbstractGfx> gfx,
}
g_shader_cache->InitializeShaderCache();
system.GetCustomResourceManager().Initialize();
return true;
}
void VideoBackendBase::ShutdownShared()
{
auto& system = Core::System::GetInstance();
system.GetCustomResourceManager().Shutdown();
g_frame_dumper.reset();
g_presenter.reset();
@ -369,7 +374,6 @@ void VideoBackendBase::ShutdownShared()
m_initialized = false;
auto& system = Core::System::GetInstance();
VertexLoaderManager::Clear();
system.GetFifo().Shutdown();
}

View File

@ -17,6 +17,7 @@
<AdditionalIncludeDirectories>$(ExternalsDir)rangeset\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>$(ExternalsDir)Vulkan-Headers\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>$(ExternalsDir)VulkanMemoryAllocator\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>$(ExternalsDir)watcher\watcher\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>$(ExternalsDir)WIL\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<!--WIL doesn't have it's own vcxproj/exports, and no externals reference WIL, so this is fine to define only for Dolphin-->
<PreprocessorDefinitions>WIL_SUPPRESS_EXCEPTIONS;%(PreprocessorDefinitions)</PreprocessorDefinitions>