Compare commits

..

1 Commits

Author SHA1 Message Date
Jordan Woyak
7dedcc10a4
Merge 1554e4ab6c into 903eafcf65 2025-06-07 07:48:51 +01:00
51 changed files with 1055 additions and 1459 deletions

3
.gitmodules vendored
View File

@ -84,9 +84,6 @@
[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

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

View File

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

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

View File

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

View File

@ -1,67 +0,0 @@
// 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

@ -1,47 +0,0 @@
// 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

@ -82,6 +82,7 @@
#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"
@ -527,6 +528,9 @@ 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

@ -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/CustomResourceManager.h"
#include "VideoCommon/Assets/CustomAssetLoader.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::CustomResourceManager m_custom_resource_manager;
VideoCommon::CustomAssetLoader m_custom_asset_loader;
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::CustomResourceManager& System::GetCustomResourceManager() const
VideoCommon::CustomAssetLoader& System::GetCustomAssetLoader() const
{
return m_impl->m_custom_resource_manager;
return m_impl->m_custom_asset_loader;
}
} // namespace Core

View File

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

View File

@ -59,7 +59,6 @@
<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" />
@ -670,16 +669,12 @@
<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" />
@ -819,7 +814,6 @@
<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" />
@ -1326,15 +1320,14 @@
<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

@ -80,36 +80,6 @@ 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,7 +11,6 @@
#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>
@ -39,25 +38,6 @@ 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

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

View File

@ -17,7 +17,6 @@ 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();
@ -25,7 +24,4 @@ signals:
#ifdef USE_RETRO_ACHIEVEMENTS
void OpenAchievementSettings();
#endif // USE_RETRO_ACHIEVEMENTS
private:
const std::string m_filepath;
};

View File

@ -59,18 +59,10 @@ 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;
UpdateControls();
if (state == Core::State::Running)
if (state == Core::State::Running && m_emu_state != Core::State::Paused)
OnEmulationStarted();
else if (state == Core::State::Uninitialized)
OnEmulationStopped();
@ -274,6 +266,8 @@ void FIFOPlayerWindow::StopRecording()
void FIFOPlayerWindow::OnEmulationStarted()
{
UpdateControls();
if (m_fifo_player.GetFile())
OnFIFOLoaded();
}
@ -284,6 +278,7 @@ 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

@ -566,15 +566,6 @@ 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

@ -12,6 +12,7 @@
#include <QLabel>
#include <QRadioButton>
#include <QSignalBlocker>
#include <QSlider>
#include <QVBoxLayout>
#include <cmath>
@ -24,8 +25,6 @@
#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"
@ -116,24 +115,12 @@ 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 ConfigFloatSlider(0.01f, 4.0f, Config::MAIN_OVERCLOCK, 0.01f);
m_cpu_clock_override_slider = new QSlider(Qt::Horizontal);
m_cpu_clock_override_slider->setRange(1, 400);
cpu_clock_override_slider_layout->addWidget(m_cpu_clock_override_slider);
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_slider_label = new QLabel();
cpu_clock_override_slider_layout->addWidget(m_cpu_clock_override_slider_label);
m_cpu_clock_override_checkbox->SetDescription(
tr("Adjusts the emulated CPU's clock rate.<br><br>"
@ -160,25 +147,12 @@ 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 ConfigFloatSlider(0.01f, 5.0f, Config::MAIN_VI_OVERCLOCK, 0.01f);
m_vi_rate_override_slider = new QSlider(Qt::Horizontal);
m_vi_rate_override_slider->setRange(1, 500);
vi_rate_override_slider_layout->addWidget(m_vi_rate_override_slider);
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_slider_label = new QLabel();
vi_rate_override_slider_layout->addWidget(m_vi_rate_override_slider_label);
m_vi_rate_override_checkbox->SetDescription(
tr("Adjusts the VBI frequency. Also adjusts the emulated CPU's "
@ -205,34 +179,27 @@ 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 ConfigSliderU32(24, 64, Config::MAIN_MEM1_SIZE, 0x100000);
m_mem1_override_slider = new QSlider(Qt::Horizontal);
m_mem1_override_slider->setRange(24, 64);
mem1_override_slider_layout->addWidget(m_mem1_override_slider);
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)));
});
m_mem1_override_slider_label = new QLabel();
mem1_override_slider_layout->addWidget(m_mem1_override_slider_label);
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 ConfigSliderU32(64, 128, Config::MAIN_MEM2_SIZE, 0x100000);
m_mem2_override_slider = new QSlider(Qt::Horizontal);
m_mem2_override_slider->setRange(64, 128);
mem2_override_slider_layout->addWidget(m_mem2_override_slider);
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_mem2_override_slider_label = new QLabel();
mem2_override_slider_layout->addWidget(m_mem2_override_slider_label);
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>"));
@ -272,12 +239,36 @@ 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()));
@ -316,7 +307,21 @@ void AdvancedPane::Update()
}
m_cpu_clock_override_slider->setEnabled(enable_cpu_clock_override_widgets);
m_cpu_label->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));
}());
QFont vi_bf = font();
vi_bf.setBold(Config::GetActiveLayerForConfig(Config::MAIN_VI_OVERCLOCK_ENABLE) !=
@ -325,16 +330,53 @@ 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_label->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_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_label->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_mem2_override_slider->setEnabled(enable_ram_override_widgets && is_uninitialized);
m_mem2_label->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_custom_rtc_checkbox->setEnabled(is_uninitialized);
SignalBlocking(m_custom_rtc_checkbox)->setChecked(Config::Get(Config::MAIN_CUSTOM_RTC_ENABLE));

View File

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

View File

@ -3,54 +3,46 @@
#include "VideoCommon/Assets/CustomAsset.h"
#include <utility>
namespace VideoCommon
{
CustomAsset::CustomAsset(std::shared_ptr<CustomAssetLibrary> library,
const CustomAssetLibrary::AssetID& asset_id, u64 asset_handle)
: m_owning_library(std::move(library)), m_asset_id(asset_id), m_handle(asset_handle)
const CustomAssetLibrary::AssetID& asset_id)
: m_owning_library(std::move(library)), m_asset_id(asset_id)
{
}
std::size_t CustomAsset::Load()
bool 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)
if (load_information.m_bytes_loaded > 0)
{
m_bytes_loaded = load_information.bytes_loaded;
m_last_loaded_time = load_time;
return m_bytes_loaded;
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 0;
return load_information.m_bytes_loaded != 0;
}
std::size_t CustomAsset::Unload()
CustomAssetLibrary::TimeType CustomAsset::GetLastWriteTime() const
{
return m_owning_library->GetLastAssetWriteTime(m_asset_id);
}
const CustomAssetLibrary::TimeType& CustomAsset::GetLastLoadedTime() const
{
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,47 +17,42 @@ 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, u64 session_id);
const CustomAssetLibrary::AssetID& asset_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 the number of bytes loaded
std::size_t Load();
// Loads the asset from the library returning a pass/fail result
bool Load();
// Unloads the asset data, resets the bytes loaded and
// returns the number of bytes unloaded
std::size_t Unload();
// 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;
// Returns the time that the data was last loaded
TimeType GetLastLoadedTime() const;
const CustomAssetLibrary::TimeType& GetLastLoadedTime() const;
// Returns an id that uniquely identifies this asset
const CustomAssetLibrary::AssetID& GetAssetId() 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;
// A rough estimate of how much space this asset
// will take in memroy
std::size_t GetByteSizeInMemory() 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;
std::atomic<TimeType> m_last_loaded_time = {};
CustomAssetLibrary::TimeType m_last_loaded_time = {};
};
// An abstract class that is expected to
@ -88,14 +83,6 @@ 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
@ -109,7 +96,7 @@ template <typename AssetType>
struct CachedAsset
{
std::shared_ptr<AssetType> m_asset;
CustomAsset::TimeType m_cached_write_time;
VideoCommon::CustomAssetLibrary::TimeType m_cached_write_time;
};
} // namespace VideoCommon

View File

@ -0,0 +1,85 @@
// 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,11 +10,10 @@
namespace VideoCommon
{
class CustomTextureData;
struct MaterialData;
struct MeshData;
struct PixelShaderData;
struct TextureAndSamplerData;
struct TextureData;
// This class provides functionality to load
// specific data (like textures). Where this data
@ -22,21 +21,28 @@ struct TextureAndSamplerData;
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 bytes_loaded = 0;
std::size_t m_bytes_loaded = 0;
TimeType m_load_time = {};
};
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, CustomTextureData* data) = 0;
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);
// Loads a pixel shader
virtual LoadInfo LoadPixelShader(const AssetID& asset_id, PixelShaderData* data) = 0;

View File

@ -1,157 +1,108 @@
// Copyright 2025 Dolphin Emulator Project
// Copyright 2023 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "VideoCommon/Assets/CustomAssetLoader.h"
#include <fmt/format.h>
#include "Common/Logging/Log.h"
#include "Common/Thread.h"
#include "UICommon/UICommon.h"
#include "Common/MemoryUtil.h"
#include "VideoCommon/Assets/CustomAssetLibrary.h"
namespace VideoCommon
{
void CustomAssetLoader::Initialize()
void CustomAssetLoader::Init()
{
ResizeWorkerThreads(2);
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;
}
}
}
});
}
void CustomAssetLoader::Shutdown()
{
Reset(false);
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;
}
bool CustomAssetLoader::StartWorkerThreads(u32 num_worker_threads)
std::shared_ptr<GameTextureAsset>
CustomAssetLoader::LoadGameTexture(const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr<CustomAssetLibrary> library)
{
for (u32 i = 0; i < num_worker_threads; i++)
{
m_worker_threads.emplace_back(&CustomAssetLoader::WorkerThreadRun, this, i);
}
return HasWorkerThreads();
return LoadOrCreateAsset<GameTextureAsset>(asset_id, m_game_textures, std::move(library));
}
bool CustomAssetLoader::ResizeWorkerThreads(u32 num_worker_threads)
std::shared_ptr<PixelShaderAsset>
CustomAssetLoader::LoadPixelShader(const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr<CustomAssetLibrary> library)
{
if (m_worker_threads.size() == num_worker_threads)
return true;
StopWorkerThreads();
return StartWorkerThreads(num_worker_threads);
return LoadOrCreateAsset<PixelShaderAsset>(asset_id, m_pixel_shaders, std::move(library));
}
bool CustomAssetLoader::HasWorkerThreads() const
std::shared_ptr<MaterialAsset>
CustomAssetLoader::LoadMaterial(const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr<CustomAssetLibrary> library)
{
return !m_worker_threads.empty();
return LoadOrCreateAsset<MaterialAsset>(asset_id, m_materials, std::move(library));
}
void CustomAssetLoader::StopWorkerThreads()
std::shared_ptr<MeshAsset> CustomAssetLoader::LoadMesh(const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr<CustomAssetLibrary> 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();
return LoadOrCreateAsset<MeshAsset>(asset_id, m_meshes, std::move(library));
}
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,24 +1,27 @@
// Copyright 2025 Dolphin Emulator Project
// Copyright 2023 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <condition_variable>
#include <list>
#include <chrono>
#include <map>
#include <memory>
#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 takes any number of assets
// and loads them across a configurable
// thread pool
// This class is responsible for loading data asynchronously when requested
// and watches that data asynchronously reloading it if it changes
class CustomAssetLoader
{
public:
@ -29,54 +32,77 @@ public:
CustomAssetLoader& operator=(const CustomAssetLoader&) = delete;
CustomAssetLoader& operator=(CustomAssetLoader&&) = delete;
void Initialize();
void Init();
void Shutdown();
using AssetHandle = std::pair<std::size_t, bool>;
struct LoadResults
// 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);
{
std::vector<AssetHandle> asset_handles;
s64 change_in_memory;
};
std::shared_ptr<PixelShaderAsset> LoadPixelShader(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<MaterialAsset> LoadMaterial(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);
std::shared_ptr<MeshAsset> LoadMesh(const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr<CustomAssetLibrary> library);
private:
bool StartWorkerThreads(u32 num_worker_threads);
bool ResizeWorkerThreads(u32 num_worker_threads);
bool HasWorkerThreads() const;
void StopWorkerThreads();
// 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;
}
void WorkerThreadRun(u32 thread_index);
static constexpr auto TIME_BETWEEN_ASSET_MONITOR_CHECKS = std::chrono::milliseconds{500};
Common::Flag m_exit_flag;
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;
std::vector<std::thread> m_worker_threads;
std::size_t m_total_bytes_loaded = 0;
std::size_t m_max_memory_available = 0;
std::atomic_bool m_memory_exceeded = false;
std::mutex m_assets_to_load_lock;
std::list<CustomAsset*> m_assets_to_load;
std::map<CustomAssetLibrary::AssetID, std::weak_ptr<CustomAsset>> m_assets_to_monitor;
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;
// 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;
};
} // namespace VideoCommon

View File

@ -1,229 +0,0 @@
// 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

@ -1,215 +0,0 @@
// 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,19 +13,30 @@
#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;
@ -39,6 +50,30 @@ 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)
@ -123,7 +158,7 @@ CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadPixelShader(const
if (!PixelShaderData::FromJson(asset_id, root_obj, data))
return {};
return LoadInfo{approx_mem_size};
return LoadInfo{approx_mem_size, GetLastAssetWriteTime(asset_id)};
}
CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadMaterial(const AssetID& asset_id,
@ -181,7 +216,7 @@ CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadMaterial(const As
return {};
}
return LoadInfo{metadata_size};
return LoadInfo{metadata_size, GetLastAssetWriteTime(asset_id)};
}
CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadMesh(const AssetID& asset_id,
@ -276,41 +311,11 @@ CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadMesh(const AssetI
if (!MeshData::FromJson(asset_id, root_obj, data))
return {};
return LoadInfo{approx_mem_size};
return LoadInfo{approx_mem_size, GetLastAssetWriteTime(asset_id)};
}
CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadTexture(const AssetID& asset_id,
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)
TextureData* data)
{
const auto asset_map = GetAssetMapForID(asset_id);
@ -363,7 +368,7 @@ CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadTexture(const Ass
}
const auto& root_obj = root.get<picojson::object>();
if (!TextureAndSamplerData::FromJson(asset_id, root_obj, data))
if (!TextureData::FromJson(asset_id, root_obj, data))
{
return {};
}
@ -371,62 +376,128 @@ CustomAssetLibrary::LoadInfo DirectFilesystemAssetLibrary::LoadTexture(const Ass
else
{
data->m_sampler = RenderState::GetLinearSamplerState();
data->m_type = TextureAndSamplerData::Type::Type_Texture2D;
data->m_type = TextureData::Type::Type_Texture2D;
}
if (!LoadTextureDataFromFile(asset_id, texture_path->second, data->m_type, &data->m_texture))
return {};
if (!PurgeInvalidMipsFromTextureData(asset_id, &data->m_texture))
return {};
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 {};
}
return LoadInfo{GetAssetSize(data->m_texture) + metadata_size};
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 {};
}
void DirectFilesystemAssetLibrary::SetAssetIDMapData(const AssetID& asset_id,
VideoCommon::Assets::AssetMap asset_path_map)
AssetMap asset_path_map)
{
VideoCommon::Assets::AssetMap previous_asset_map;
{
std::lock_guard lk(m_asset_map_lock);
previous_asset_map = m_asset_id_to_asset_map_path[asset_id];
}
{
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);
}
std::lock_guard lk(m_lock);
m_assetid_to_asset_map_path[asset_id] = std::move(asset_path_map);
}
void DirectFilesystemAssetLibrary::PathModified(std::string_view path)
bool DirectFilesystemAssetLibrary::LoadMips(const std::filesystem::path& asset_path,
CustomTextureData::ArraySlice* data)
{
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())
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++)
{
auto& system = Core::System::GetInstance();
auto& resource_manager = system.GetCustomResourceManager();
resource_manager.MarkAssetDirty(iter->second);
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;
}
VideoCommon::Assets::AssetMap
DirectFilesystemAssetLibrary::AssetMap
DirectFilesystemAssetLibrary::GetAssetMapForID(const AssetID& asset_id) const
{
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())
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())
{
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 WatchableFilesystemAssetLibrary
class DirectFilesystemAssetLibrary final : public CustomAssetLibrary
{
public:
LoadInfo LoadTexture(const AssetID& asset_id, TextureAndSamplerData* data) override;
LoadInfo LoadTexture(const AssetID& asset_id, CustomTextureData* data) override;
using AssetMap = std::map<std::string, std::filesystem::path>;
LoadInfo LoadTexture(const AssetID& asset_id, TextureData* 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, Assets::AssetMap asset_path_map);
void SetAssetIDMapData(const AssetID& asset_id, AssetMap asset_path_map);
private:
void PathModified(std::string_view path) override;
// 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);
// Gets the asset map given an asset id
Assets::AssetMap GetAssetMapForID(const AssetID& asset_id) const;
AssetMap GetAssetMapForID(const AssetID& asset_id) const;
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;
mutable std::mutex m_lock;
std::map<AssetID, std::map<std::string, std::filesystem::path>> m_assetid_to_asset_map_path;
};
} // 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.bytes_loaded == 0)
if (loaded_info.m_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.bytes_loaded == 0)
if (loaded_info.m_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.bytes_loaded == 0)
if (loaded_info.m_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 TextureAndSamplerData::FromJson(const CustomAssetLibrary::AssetID& asset_id,
const picojson::object& json, TextureAndSamplerData* data)
bool TextureData::FromJson(const CustomAssetLibrary::AssetID& asset_id,
const picojson::object& json, TextureData* data)
{
const auto type_iter = json.find("type");
if (type_iter == json.end())
@ -176,7 +176,7 @@ bool TextureAndSamplerData::FromJson(const CustomAssetLibrary::AssetID& asset_id
if (type == "texture2d")
{
data->m_type = TextureAndSamplerData::Type::Type_Texture2D;
data->m_type = TextureData::Type::Type_Texture2D;
if (!ParseSampler(asset_id, json, &data->m_sampler))
{
@ -185,7 +185,7 @@ bool TextureAndSamplerData::FromJson(const CustomAssetLibrary::AssetID& asset_id
}
else if (type == "texturecube")
{
data->m_type = TextureAndSamplerData::Type::Type_TextureCube;
data->m_type = TextureData::Type::Type_TextureCube;
}
else
{
@ -199,7 +199,7 @@ bool TextureAndSamplerData::FromJson(const CustomAssetLibrary::AssetID& asset_id
return true;
}
void TextureAndSamplerData::ToJson(picojson::object* obj, const TextureAndSamplerData& data)
void TextureData::ToJson(picojson::object* obj, const TextureData& data)
{
if (!obj) [[unlikely]]
return;
@ -207,13 +207,13 @@ void TextureAndSamplerData::ToJson(picojson::object* obj, const TextureAndSample
auto& json_obj = *obj;
switch (data.m_type)
{
case TextureAndSamplerData::Type::Type_Texture2D:
case TextureData::Type::Type_Texture2D:
json_obj.emplace("type", "texture2d");
break;
case TextureAndSamplerData::Type::Type_TextureCube:
case TextureData::Type::Type_TextureCube:
json_obj.emplace("type", "texturecube");
break;
case TextureAndSamplerData::Type::Type_Undefined:
case TextureData::Type::Type_Undefined:
break;
};
@ -254,11 +254,11 @@ void TextureAndSamplerData::ToJson(picojson::object* obj, const TextureAndSample
json_obj.emplace("filter_mode", filter_mode);
}
CustomAssetLibrary::LoadInfo TextureAsset::LoadImpl(const CustomAssetLibrary::AssetID& asset_id)
CustomAssetLibrary::LoadInfo GameTextureAsset::LoadImpl(const CustomAssetLibrary::AssetID& asset_id)
{
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)
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)
return {};
{
std::lock_guard lk(m_data_lock);
@ -267,4 +267,75 @@ CustomAssetLibrary::LoadInfo TextureAsset::LoadImpl(const CustomAssetLibrary::As
}
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 TextureAndSamplerData
struct TextureData
{
static bool FromJson(const CustomAssetLibrary::AssetID& asset_id, const picojson::object& json,
TextureAndSamplerData* data);
static void ToJson(picojson::object* obj, const TextureAndSamplerData& data);
TextureData* data);
static void ToJson(picojson::object* obj, const TextureData& data);
enum class Type
{
Type_Undefined,
@ -30,19 +30,23 @@ struct TextureAndSamplerData
SamplerState m_sampler;
};
class TextureAsset final : public CustomLoadableAsset<CustomTextureData>
class GameTextureAsset final : public CustomLoadableAsset<TextureData>
{
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::TextureAndSamplerData::Type>
: EnumFormatter<VideoCommon::TextureAndSamplerData::Type::Type_Max>
struct fmt::formatter<VideoCommon::TextureData::Type>
: EnumFormatter<VideoCommon::TextureData::Type::Type_Max>
{
constexpr formatter() : EnumFormatter({"Undefined", "Texture2D", "TextureCube"}) {}
};

View File

@ -1,244 +0,0 @@
// 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

@ -1,22 +0,0 @@
// 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

@ -1,13 +0,0 @@
// 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

@ -1,14 +0,0 @@
// 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,11 +10,10 @@ 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
@ -27,10 +26,6 @@ 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,13 +7,12 @@
#include <picojson.h>
#include "VideoCommon/Assets/CustomAssetLibrary.h"
#include "VideoCommon/Assets/Types.h"
#include "VideoCommon/Assets/DirectFilesystemAssetLibrary.h"
struct GraphicsModAssetConfig
{
VideoCommon::CustomAssetLibrary::AssetID m_asset_id;
VideoCommon::Assets::AssetMap m_map;
VideoCommon::DirectFilesystemAssetLibrary::AssetMap m_map;
void SerializeToConfig(picojson::object& json_obj) const;
bool DeserializeFromConfig(const picojson::object& obj);

View File

@ -16,6 +16,7 @@
#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"
@ -97,6 +98,28 @@ CustomPipelineAction::CustomPipelineAction(
m_pipeline_passes.resize(m_passes_config.size());
}
void CustomPipelineAction::OnDrawStarted(GraphicsModActionData::DrawStarted*)
void CustomPipelineAction::OnDrawStarted(GraphicsModActionData::DrawStarted* draw_started)
{
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,6 +12,7 @@
#include "Common/VariantUtil.h"
#include "VideoCommon/AbstractGfx.h"
#include "VideoCommon/Assets/CustomAssetLoader.h"
namespace
{
@ -171,8 +172,238 @@ std::vector<std::string> GlobalConflicts(std::string_view source)
} // namespace
void CustomPipeline::UpdatePixelData(std::shared_ptr<VideoCommon::CustomAssetLibrary>,
std::span<const u32>,
const VideoCommon::CustomAssetLibrary::AssetID&)
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)
{
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,9 +17,15 @@
#include "VideoCommon/Assets/TextureAsset.h"
#include "VideoCommon/ShaderGenCommon.h"
namespace VideoCommon
{
class CustomAssetLoader;
}
struct CustomPipeline
{
void UpdatePixelData(std::shared_ptr<VideoCommon::CustomAssetLibrary> library,
void UpdatePixelData(VideoCommon::CustomAssetLoader& loader,
std::shared_ptr<VideoCommon::CustomAssetLibrary> library,
std::span<const u32> texture_units,
const VideoCommon::CustomAssetLibrary::AssetID& material_to_load);
@ -28,7 +34,7 @@ struct CustomPipeline
struct CachedTextureAsset
{
VideoCommon::CachedAsset<VideoCommon::TextureAsset> m_cached_asset;
VideoCommon::CachedAsset<VideoCommon::GameTextureAsset> 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::TextureAsset>>* custom_textures;
std::vector<VideoCommon::CachedAsset<VideoCommon::GameTextureAsset>>* custom_textures;
// Dependencies needed to reload the texture and trigger this create again
std::vector<VideoCommon::CachedAsset<VideoCommon::CustomAsset>>* additional_dependencies;

View File

@ -25,6 +25,7 @@
#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"
@ -94,11 +95,10 @@ 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, std::move(filename));
static_cast<void>(hires_texture->LoadTexture());
s_hires_texture_cache.try_emplace(hires_texture->GetId(), hires_texture);
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));
}
}
}
@ -167,7 +167,7 @@ void HiresTexture::Clear()
std::shared_ptr<HiresTexture> HiresTexture::Search(const TextureInfo& texture_info)
{
auto [base_filename, has_arb_mipmaps] = GetNameArbPair(texture_info);
const auto [base_filename, has_arb_mipmaps] = GetNameArbPair(texture_info);
if (base_filename == "")
return nullptr;
@ -177,27 +177,24 @@ std::shared_ptr<HiresTexture> HiresTexture::Search(const TextureInfo& texture_in
}
else
{
auto hires_texture = std::make_shared<HiresTexture>(has_arb_mipmaps, std::move(base_filename));
auto& system = Core::System::GetInstance();
auto hires_texture = std::make_shared<HiresTexture>(
has_arb_mipmaps,
system.GetCustomAssetLoader().LoadGameTexture(base_filename, s_file_library));
if (g_ActiveConfig.bCacheHiresTextures)
{
s_hires_texture_cache.try_emplace(hires_texture->GetId(), hires_texture);
s_hires_texture_cache.try_emplace(base_filename, hires_texture);
}
return hires_texture;
}
}
HiresTexture::HiresTexture(bool has_arbitrary_mipmaps, std::string id)
: m_has_arbitrary_mipmaps(has_arbitrary_mipmaps), m_id(std::move(id))
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))
{
}
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,13 +27,12 @@ public:
static void Shutdown();
static std::shared_ptr<HiresTexture> Search(const TextureInfo& texture_info);
HiresTexture(bool has_arbitrary_mipmaps, std::string id);
HiresTexture(bool has_arbitrary_mipmaps, std::shared_ptr<VideoCommon::GameTextureAsset> asset);
bool HasArbitraryMipmaps() const { return m_has_arbitrary_mipmaps; }
VideoCommon::CustomResourceManager::TextureTimePair LoadTexture() const;
const std::string& GetId() const { return m_id; }
const std::shared_ptr<VideoCommon::GameTextureAsset>& GetAsset() const { return m_game_texture; }
private:
bool m_has_arbitrary_mipmaps = false;
std::string m_id;
std::shared_ptr<VideoCommon::GameTextureAsset> m_game_texture;
};

View File

@ -37,14 +37,13 @@
#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"
@ -263,12 +262,25 @@ void TextureCacheBase::SetBackupConfig(const VideoConfig& config)
bool TextureCacheBase::DidLinkedAssetsChange(const TCacheEntry& entry)
{
if (!entry.hires_texture)
return false;
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;
}
}
const auto [texture_data, load_time] = entry.hires_texture->LoadTexture();
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;
}
}
return load_time > entry.last_load_time;
return false;
}
RcTcacheEntry TextureCacheBase::ApplyPaletteToEntry(RcTcacheEntry& entry, const u8* palette,
@ -1554,50 +1566,80 @@ RcTcacheEntry TextureCacheBase::GetTexture(const int textureCacheSafetyColorSamp
InvalidateTexture(oldest_entry);
}
std::shared_ptr<HiresTexture> hires_texture;
std::vector<VideoCommon::CachedAsset<VideoCommon::GameTextureAsset>> cached_game_assets;
std::vector<std::shared_ptr<VideoCommon::TextureData>> data_for_assets;
bool has_arbitrary_mipmaps = false;
bool skip_texture_dump = false;
std::shared_ptr<VideoCommon::CustomTextureData> custom_texture_data = nullptr;
VideoCommon::CustomAsset::TimeType load_time = {};
std::shared_ptr<HiresTexture> hires_texture;
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, nullptr,
nullptr};
GraphicsModActionData::TextureCreate texture_create{
texture_name, width, height, &cached_game_assets, &additional_dependencies};
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, custom_texture_data.get(),
has_arbitrary_mipmaps, skip_texture_dump);
entry->hires_texture = std::move(hires_texture);
entry->last_load_time = load_time;
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);
entry->texture_info_name = std::move(texture_name);
return entry;
}
@ -1607,7 +1649,8 @@ 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, VideoCommon::CustomTextureData* custom_texture_data,
const int safety_color_sample_size,
std::vector<std::shared_ptr<VideoCommon::TextureData>> assets_data,
const bool custom_arbitrary_mipmaps, bool skip_texture_dump)
{
#ifdef __APPLE__
@ -1617,22 +1660,33 @@ RcTcacheEntry TextureCacheBase::CreateTextureEntry(
#endif
RcTcacheEntry entry;
if (custom_texture_data)
if (!assets_data.empty())
{
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);
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);
entry = AllocateCacheEntry(config);
if (!entry) [[unlikely]]
return entry;
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)
for (u32 data_index = 0; data_index < static_cast<u32>(assets_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());
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);
}
}
entry->has_arbitrary_mips = custom_arbitrary_mipmaps;

View File

@ -24,7 +24,6 @@
#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"
@ -168,8 +167,8 @@ struct TCacheEntry
std::string texture_info_name = "";
VideoCommon::CustomAsset::TimeType last_load_time;
std::shared_ptr<HiresTexture> hires_texture;
std::vector<VideoCommon::CachedAsset<VideoCommon::GameTextureAsset>> linked_game_texture_assets;
std::vector<VideoCommon::CachedAsset<VideoCommon::CustomAsset>> linked_asset_dependencies;
explicit TCacheEntry(std::unique_ptr<AbstractTexture> tex,
std::unique_ptr<AbstractFramebuffer> fb);
@ -352,10 +351,11 @@ private:
void SetBackupConfig(const VideoConfig& config);
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
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 GetXFBFromCache(u32 address, u32 width, u32 height, u32 stride);

View File

@ -41,7 +41,6 @@
#endif
#include "VideoCommon/AbstractGfx.h"
#include "VideoCommon/Assets/CustomResourceManager.h"
#include "VideoCommon/AsyncRequests.h"
#include "VideoCommon/BPStructs.h"
#include "VideoCommon/BoundingBox.h"
@ -342,16 +341,12 @@ 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();
@ -374,6 +369,7 @@ void VideoBackendBase::ShutdownShared()
m_initialized = false;
auto& system = Core::System::GetInstance();
VertexLoaderManager::Clear();
system.GetFifo().Shutdown();
}

View File

@ -17,7 +17,6 @@
<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>