diff --git a/Source/Core/Core/System.cpp b/Source/Core/Core/System.cpp
index 22681cac43..d22c058258 100644
--- a/Source/Core/Core/System.cpp
+++ b/Source/Core/Core/System.cpp
@@ -33,12 +33,12 @@
#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/CommandProcessor.h"
#include "VideoCommon/Fifo.h"
#include "VideoCommon/GeometryShaderManager.h"
#include "VideoCommon/PixelEngine.h"
#include "VideoCommon/PixelShaderManager.h"
+#include "VideoCommon/Resources/CustomResourceManager.h"
#include "VideoCommon/VertexShaderManager.h"
#include "VideoCommon/XFStateManager.h"
diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props
index 17572da524..128e875ae3 100644
--- a/Source/Core/DolphinLib.props
+++ b/Source/Core/DolphinLib.props
@@ -674,10 +674,11 @@
+
+
-
@@ -747,12 +748,21 @@
+
+
+
+
+
+
+
+
+
@@ -1342,8 +1352,8 @@
+
-
@@ -1398,12 +1408,21 @@
+
+
+
+
+
+
+
+
+
diff --git a/Source/Core/VideoCommon/Assets/AssetListener.h b/Source/Core/VideoCommon/Assets/AssetListener.h
new file mode 100644
index 0000000000..d543a2bb14
--- /dev/null
+++ b/Source/Core/VideoCommon/Assets/AssetListener.h
@@ -0,0 +1,23 @@
+// Copyright 2025 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+namespace VideoCommon
+{
+class AssetListener
+{
+public:
+ AssetListener() = default;
+ virtual ~AssetListener() = default;
+
+ AssetListener(const AssetListener&) = default;
+ AssetListener(AssetListener&&) = default;
+ AssetListener& operator=(const AssetListener&) = default;
+ AssetListener& operator=(AssetListener&&) = default;
+
+ virtual void NotifyAssetLoadSuccess() = 0;
+ virtual void NotifyAssetLoadFailed() = 0;
+ virtual void AssetUnloaded() = 0;
+};
+} // namespace VideoCommon
diff --git a/Source/Core/VideoCommon/Assets/CustomResourceManager.cpp b/Source/Core/VideoCommon/Assets/CustomAssetCache.cpp
similarity index 61%
rename from Source/Core/VideoCommon/Assets/CustomResourceManager.cpp
rename to Source/Core/VideoCommon/Assets/CustomAssetCache.cpp
index 6f8d3557bb..606f28d5fe 100644
--- a/Source/Core/VideoCommon/Assets/CustomResourceManager.cpp
+++ b/Source/Core/VideoCommon/Assets/CustomAssetCache.cpp
@@ -1,7 +1,7 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
-#include "VideoCommon/Assets/CustomResourceManager.h"
+#include "VideoCommon/Assets/CustomAssetCache.h"
#include "Common/Logging/Log.h"
#include "Common/MemoryUtil.h"
@@ -14,7 +14,7 @@
namespace VideoCommon
{
-void CustomResourceManager::Initialize()
+void CustomAssetCache::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);
@@ -28,19 +28,16 @@ void CustomResourceManager::Initialize()
ERROR_LOG_FMT(VIDEO, "Not enough system memory for custom resources.");
m_asset_loader.Initialize();
-
- m_xfb_event =
- GetVideoEvents().after_frame_event.Register([this](Core::System&) { XFBTriggered(); });
}
-void CustomResourceManager::Shutdown()
+void CustomAssetCache::Shutdown()
{
Reset();
m_asset_loader.Shutdown();
}
-void CustomResourceManager::Reset()
+void CustomAssetCache::Reset()
{
m_asset_loader.Reset(true);
@@ -48,66 +45,27 @@ void CustomResourceManager::Reset()
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)
+void CustomAssetCache::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 library)
+void CustomAssetCache::MarkAssetPending(CustomAsset* asset)
{
- 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 {};
+ m_pending_assets.MakeAssetHighestPriority(asset->GetHandle(), asset);
}
-void CustomResourceManager::LoadTextureDataAsset(
- const CustomAssetLibrary::AssetID& asset_id,
- std::shared_ptr library, InternalTextureDataResource* resource)
+void CustomAssetCache::MarkAssetActive(CustomAsset* asset)
{
- if (!resource->asset)
- {
- resource->asset =
- CreateAsset(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;
- }
+ m_active_assets.MakeAssetHighestPriority(asset->GetHandle(), asset);
}
-void CustomResourceManager::XFBTriggered()
+void CustomAssetCache::Update()
{
ProcessDirtyAssets();
ProcessLoadedAssets();
@@ -127,7 +85,7 @@ void CustomResourceManager::XFBTriggered()
m_asset_loader.ScheduleAssetsToLoad(m_pending_assets.Elements(), allowed_memory);
}
-void CustomResourceManager::ProcessDirtyAssets()
+void CustomAssetCache::ProcessDirtyAssets()
{
decltype(m_dirty_assets) dirty_assets;
@@ -154,7 +112,7 @@ void CustomResourceManager::ProcessDirtyAssets()
}
}
-void CustomResourceManager::ProcessLoadedAssets()
+void CustomAssetCache::ProcessLoadedAssets()
{
const auto load_results = m_asset_loader.TakeLoadResults();
@@ -189,10 +147,18 @@ void CustomResourceManager::ProcessLoadedAssets()
m_active_assets.InsertAsset(handle, asset_data.asset.get());
asset_data.load_status = AssetData::LoadStatus::LoadFinished;
}
+
+ for (const auto& listener : asset_data.listeners)
+ {
+ if (load_successful)
+ listener->NotifyAssetLoadSuccess();
+ else
+ listener->NotifyAssetLoadFailed();
+ }
}
}
-void CustomResourceManager::RemoveAssetsUntilBelowMemoryLimit()
+void CustomAssetCache::RemoveAssetsUntilBelowMemoryLimit()
{
const u64 threshold_ram = m_max_ram_available * 8 / 10;
@@ -209,11 +175,11 @@ void CustomResourceManager::RemoveAssetsUntilBelowMemoryLimit()
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)
+ for (const auto& listener : asset_data.listeners)
{
- m_texture_data_asset_cache.erase(asset->GetAssetId());
+ listener->AssetUnloaded();
}
+
// Remove the asset's copy
const std::size_t bytes_unloaded = asset_data.asset->Unload();
m_ram_used -= bytes_unloaded;
diff --git a/Source/Core/VideoCommon/Assets/CustomResourceManager.h b/Source/Core/VideoCommon/Assets/CustomAssetCache.h
similarity index 72%
rename from Source/Core/VideoCommon/Assets/CustomResourceManager.h
rename to Source/Core/VideoCommon/Assets/CustomAssetCache.h
index 6a2b5cbbe3..169c0faaf8 100644
--- a/Source/Core/VideoCommon/Assets/CustomResourceManager.h
+++ b/Source/Core/VideoCommon/Assets/CustomAssetCache.h
@@ -10,25 +10,73 @@
#include
#include "Common/CommonTypes.h"
-#include "Common/HookableEvent.h"
+#include "VideoCommon/Assets/AssetListener.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,
+// The asset cache manages custom assets (textures, shaders, meshes).
+// 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
+class CustomAssetCache
{
public:
+ // A generic interface to describe an asset
+ // and load state
+ struct AssetData
+ {
+ std::unique_ptr asset;
+
+ std::vector listeners;
+
+ CustomAsset::TimeType load_request_time = {};
+ bool has_load_error = false;
+
+ enum class LoadStatus
+ {
+ PendingReload,
+ LoadFinished,
+ Unloaded,
+ };
+ LoadStatus load_status = LoadStatus::PendingReload;
+ };
+
+ template
+ T* CreateAsset(const CustomAssetLibrary::AssetID& asset_id,
+ std::shared_ptr library, AssetListener* listener)
+ {
+ 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(std::move(library), asset_id, it->second);
+ 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.listeners.push_back(listener);
+ asset_data_from_handle.load_status = AssetData::LoadStatus::PendingReload;
+
+ return static_cast(asset_data_from_handle.asset.get());
+ }
+
+ AssetData* GetAssetData(const CustomAssetLibrary::AssetID& asset_id)
+ {
+ const auto it_handle = m_asset_id_to_handle.find(asset_id);
+ if (it_handle == m_asset_id_to_handle.end())
+ return nullptr;
+ return &m_asset_handle_to_data[it_handle->second];
+ }
+
void Initialize();
void Shutdown();
@@ -37,81 +85,21 @@ public:
// Request that an asset be reloaded
void MarkAssetDirty(const CustomAssetLibrary::AssetID& asset_id);
- void XFBTriggered();
+ // Notify the system that we are interested in this asset and
+ // are waiting for it to be loaded
+ void MarkAssetPending(CustomAsset* asset);
- using TextureTimePair = std::pair, CustomAsset::TimeType>;
+ // Notify the system we are interested in this asset and
+ // it has seen activity
+ void MarkAssetActive(CustomAsset* asset);
- // 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 library);
+ void Update();
private:
- // A generic interface to describe an assets' type
- // and load state
- struct AssetData
- {
- std::unique_ptr 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 texture_data;
- };
-
- void LoadTextureDataAsset(const CustomAssetLibrary::AssetID& asset_id,
- std::shared_ptr library,
- InternalTextureDataResource* resource);
-
void ProcessDirtyAssets();
void ProcessLoadedAssets();
void RemoveAssetsUntilBelowMemoryLimit();
- template
- T* CreateAsset(const CustomAssetLibrary::AssetID& asset_id, AssetData::AssetType asset_type,
- std::shared_ptr 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(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(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.
@@ -202,14 +190,10 @@ private:
// A calculated amount of memory to avoid exceeding.
u64 m_max_ram_available = 0;
- std::map m_texture_data_asset_cache;
-
std::mutex m_dirty_mutex;
std::set m_dirty_assets;
CustomAssetLoader m_asset_loader;
-
- Common::EventHook m_xfb_event;
};
} // namespace VideoCommon
diff --git a/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.cpp b/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.cpp
index 5e400f3de5..1945ca0aa7 100644
--- a/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.cpp
+++ b/Source/Core/VideoCommon/Assets/DirectFilesystemAssetLibrary.cpp
@@ -8,19 +8,20 @@
#include
+#include "Common/CommonPaths.h"
#include "Common/FileUtil.h"
#include "Common/IOFile.h"
#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"
+#include "VideoCommon/Resources/CustomResourceManager.h"
namespace VideoCommon
{
@@ -150,6 +151,12 @@ DirectFilesystemAssetLibrary::LoadRasterSurfaceShader(const AssetID& asset_id,
if (!RasterSurfaceShaderData::FromJson(asset_id, root_obj, data))
return {};
+ const std::string graphics_mod_builtin =
+ File::GetSysDirectory() + GRAPHICSMOD_DIR + "/Builtin" + "/Shaders";
+
+ data->shader_includer = std::make_unique(
+ PathToString(pixel_shader->second.parent_path()), graphics_mod_builtin);
+
return LoadInfo{approx_mem_size};
}
diff --git a/Source/Core/VideoCommon/Assets/ShaderAsset.h b/Source/Core/VideoCommon/Assets/ShaderAsset.h
index 81631fa0b3..0204f65bb3 100644
--- a/Source/Core/VideoCommon/Assets/ShaderAsset.h
+++ b/Source/Core/VideoCommon/Assets/ShaderAsset.h
@@ -4,6 +4,7 @@
#pragma once
#include
+#include
#include
#include
#include
@@ -13,6 +14,7 @@
#include
#include "VideoCommon/Assets/CustomAsset.h"
+#include "VideoCommon/ShaderCompileUtils.h"
#include "VideoCommon/TextureConfig.h"
class ShaderCode;
@@ -45,6 +47,13 @@ struct ShaderProperty
struct RasterSurfaceShaderData
{
+ RasterSurfaceShaderData() = default;
+ RasterSurfaceShaderData(const RasterSurfaceShaderData&) = delete;
+ RasterSurfaceShaderData(RasterSurfaceShaderData&&) = default;
+ ~RasterSurfaceShaderData() = default;
+ RasterSurfaceShaderData& operator=(const RasterSurfaceShaderData&) = delete;
+ RasterSurfaceShaderData& operator=(RasterSurfaceShaderData&&) = default;
+
static bool FromJson(const CustomAssetLibrary::AssetID& asset_id, const picojson::object& json,
RasterSurfaceShaderData* data);
static void ToJson(picojson::object& obj, const RasterSurfaceShaderData& data);
@@ -65,6 +74,8 @@ struct RasterSurfaceShaderData
bool operator==(const SamplerData&) const = default;
};
std::vector samplers;
+
+ std::unique_ptr shader_includer;
};
class RasterSurfaceShaderAsset final : public CustomLoadableAsset
diff --git a/Source/Core/VideoCommon/CMakeLists.txt b/Source/Core/VideoCommon/CMakeLists.txt
index 73b9221c4b..69a0968e4d 100644
--- a/Source/Core/VideoCommon/CMakeLists.txt
+++ b/Source/Core/VideoCommon/CMakeLists.txt
@@ -8,13 +8,14 @@ add_library(videocommon
AbstractStagingTexture.h
AbstractTexture.cpp
AbstractTexture.h
+ Assets/AssetListener.h
Assets/CustomAsset.cpp
Assets/CustomAsset.h
+ Assets/CustomAssetCache.cpp
+ Assets/CustomAssetCache.h
Assets/CustomAssetLibrary.h
Assets/CustomAssetLoader.cpp
Assets/CustomAssetLoader.h
- Assets/CustomResourceManager.cpp
- Assets/CustomResourceManager.h
Assets/CustomTextureData.cpp
Assets/CustomTextureData.h
Assets/DirectFilesystemAssetLibrary.cpp
@@ -133,6 +134,8 @@ add_library(videocommon
PerformanceMetrics.h
PerformanceTracker.cpp
PerformanceTracker.h
+ PipelineUtils.cpp
+ PipelineUtils.h
PixelEngine.cpp
PixelEngine.h
PixelShaderGen.cpp
@@ -145,6 +148,22 @@ add_library(videocommon
Present.h
RenderState.cpp
RenderState.h
+ Resources/CustomResourceManager.cpp
+ Resources/CustomResourceManager.h
+ Resources/InvalidTextures.cpp
+ Resources/InvalidTextures.h
+ Resources/MaterialResource.cpp
+ Resources/MaterialResource.h
+ Resources/Resource.cpp
+ Resources/Resource.h
+ Resources/ShaderResource.cpp
+ Resources/ShaderResource.h
+ Resources/TextureAndSamplerResource.cpp
+ Resources/TextureAndSamplerResource.h
+ Resources/TextureDataResource.cpp
+ Resources/TextureDataResource.h
+ Resources/TexturePool.cpp
+ Resources/TexturePool.h
ShaderCache.cpp
ShaderCache.h
ShaderCompileUtils.cpp
diff --git a/Source/Core/VideoCommon/HiresTextures.cpp b/Source/Core/VideoCommon/HiresTextures.cpp
index 8acc3aff03..12f59f5ff5 100644
--- a/Source/Core/VideoCommon/HiresTextures.cpp
+++ b/Source/Core/VideoCommon/HiresTextures.cpp
@@ -27,6 +27,7 @@
#include "VideoCommon/Assets/CustomAsset.h"
#include "VideoCommon/Assets/DirectFilesystemAssetLibrary.h"
#include "VideoCommon/OnScreenDisplay.h"
+#include "VideoCommon/Resources/CustomResourceManager.h"
#include "VideoCommon/VideoConfig.h"
constexpr std::string_view s_format_prefix{"tex1_"};
@@ -191,7 +192,7 @@ HiresTexture::HiresTexture(bool has_arbitrary_mipmaps, std::string id)
{
}
-VideoCommon::CustomResourceManager::TextureTimePair HiresTexture::LoadTexture() const
+VideoCommon::TextureDataResource* HiresTexture::LoadTexture() const
{
auto& system = Core::System::GetInstance();
auto& custom_resource_manager = system.GetCustomResourceManager();
diff --git a/Source/Core/VideoCommon/HiresTextures.h b/Source/Core/VideoCommon/HiresTextures.h
index e99a3fc966..f577aebd78 100644
--- a/Source/Core/VideoCommon/HiresTextures.h
+++ b/Source/Core/VideoCommon/HiresTextures.h
@@ -8,12 +8,13 @@
#include
#include
-#include "Common/CommonTypes.h"
-#include "VideoCommon/Assets/CustomResourceManager.h"
-#include "VideoCommon/Assets/CustomTextureData.h"
-#include "VideoCommon/TextureConfig.h"
#include "VideoCommon/TextureInfo.h"
+namespace VideoCommon
+{
+class TextureDataResource;
+}
+
enum class TextureFormat;
std::set GetTextureDirectoriesWithGameId(const std::string& root_directory,
@@ -30,7 +31,7 @@ public:
HiresTexture(bool has_arbitrary_mipmaps, std::string id);
bool HasArbitraryMipmaps() const { return m_has_arbitrary_mipmaps; }
- VideoCommon::CustomResourceManager::TextureTimePair LoadTexture() const;
+ VideoCommon::TextureDataResource* LoadTexture() const;
const std::string& GetId() const { return m_id; }
private:
diff --git a/Source/Core/VideoCommon/PipelineUtils.cpp b/Source/Core/VideoCommon/PipelineUtils.cpp
new file mode 100644
index 0000000000..a63f7f00e8
--- /dev/null
+++ b/Source/Core/VideoCommon/PipelineUtils.cpp
@@ -0,0 +1,172 @@
+// Copyright 2025 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "VideoCommon/PipelineUtils.h"
+
+#include "Common/Assert.h"
+#include "Common/Logging/Log.h"
+
+#include "VideoCommon/BPMemory.h"
+#include "VideoCommon/ConstantManager.h"
+#include "VideoCommon/DriverDetails.h"
+#include "VideoCommon/GeometryShaderGen.h"
+#include "VideoCommon/PixelShaderGen.h"
+#include "VideoCommon/RenderState.h"
+#include "VideoCommon/ShaderGenCommon.h"
+#include "VideoCommon/VertexShaderGen.h"
+#include "VideoCommon/VideoConfig.h"
+
+namespace VideoCommon
+{
+/// Edits the UID based on driver bugs and other special configurations
+GXPipelineUid ApplyDriverBugs(const GXPipelineUid& in)
+{
+ GXPipelineUid out;
+ // TODO: static_assert(std::is_trivially_copyable_v);
+ // GXPipelineUid is not trivially copyable because RasterizationState and BlendingState aren't
+ // either, but we can pretend it is for now. This will be improved after PR #10848 is finished.
+ memcpy(static_cast(&out), static_cast(&in), sizeof(out)); // copy padding
+ pixel_shader_uid_data* ps = out.ps_uid.GetUidData();
+ BlendingState& blend = out.blending_state;
+
+ if (ps->ztest == EmulatedZ::ForcedEarly && !out.depth_state.update_enable)
+ {
+ // No need to force early depth test if you're not writing z
+ ps->ztest = EmulatedZ::Early;
+ }
+
+ // If framebuffer fetch is available, we can emulate logic ops in the fragment shader
+ // and don't need the below blend approximation
+ if (blend.logic_op_enable && !g_backend_info.bSupportsLogicOp &&
+ !g_backend_info.bSupportsFramebufferFetch)
+ {
+ if (!blend.LogicOpApproximationIsExact())
+ WARN_LOG_FMT(VIDEO,
+ "Approximating logic op with blending, this will produce incorrect rendering.");
+ if (blend.LogicOpApproximationWantsShaderHelp())
+ {
+ ps->emulate_logic_op_with_blend = true;
+ ps->logic_op_mode = static_cast(blend.logic_mode.Value());
+ }
+ blend.ApproximateLogicOpWithBlending();
+ }
+
+ const bool benefits_from_ps_dual_source_off =
+ (!g_backend_info.bSupportsDualSourceBlend && g_backend_info.bSupportsFramebufferFetch) ||
+ DriverDetails::HasBug(DriverDetails::BUG_BROKEN_DUAL_SOURCE_BLENDING);
+ if (benefits_from_ps_dual_source_off && !blend.RequiresDualSrc())
+ {
+ // Only use dual-source blending when required on drivers that don't support it very well.
+ ps->no_dual_src = true;
+ blend.use_dual_src = false;
+ }
+
+ if (g_backend_info.bSupportsFramebufferFetch)
+ {
+ bool fbfetch_blend = false;
+ if ((DriverDetails::HasBug(DriverDetails::BUG_BROKEN_DISCARD_WITH_EARLY_Z) ||
+ !g_backend_info.bSupportsEarlyZ) &&
+ ps->ztest == EmulatedZ::ForcedEarly)
+ {
+ ps->ztest = EmulatedZ::EarlyWithFBFetch;
+ fbfetch_blend |= static_cast(out.blending_state.blend_enable);
+ ps->no_dual_src = true;
+ }
+ fbfetch_blend |= blend.logic_op_enable && !g_backend_info.bSupportsLogicOp;
+ fbfetch_blend |= blend.use_dual_src && !g_backend_info.bSupportsDualSourceBlend;
+ if (fbfetch_blend)
+ {
+ ps->no_dual_src = true;
+ if (blend.logic_op_enable)
+ {
+ ps->logic_op_enable = true;
+ ps->logic_op_mode = static_cast(blend.logic_mode.Value());
+ blend.logic_op_enable = false;
+ }
+ if (blend.blend_enable)
+ {
+ ps->blend_enable = true;
+ ps->blend_src_factor = blend.src_factor;
+ ps->blend_src_factor_alpha = blend.src_factor_alpha;
+ ps->blend_dst_factor = blend.dst_factor;
+ ps->blend_dst_factor_alpha = blend.dst_factor_alpha;
+ ps->blend_subtract = blend.subtract;
+ ps->blend_subtract_alpha = blend.subtract_alpha;
+ blend.blend_enable = false;
+ }
+ }
+ }
+
+ // force dual src off if we can't support it
+ if (!g_backend_info.bSupportsDualSourceBlend)
+ {
+ ps->no_dual_src = true;
+ blend.use_dual_src = false;
+ }
+
+ if (ps->ztest == EmulatedZ::ForcedEarly && !g_backend_info.bSupportsEarlyZ)
+ {
+ // These things should be false
+ ASSERT(!ps->zfreeze);
+ // ZCOMPLOC HACK:
+ // The only way to emulate alpha test + early-z is to force early-z in the shader.
+ // As this isn't available on all drivers and as we can't emulate this feature otherwise,
+ // we are only able to choose which one we want to respect more.
+ // Tests seem to have proven that writing depth even when the alpha test fails is more
+ // important that a reliable alpha test, so we just force the alpha test to always succeed.
+ // At least this seems to be less buggy.
+ ps->ztest = EmulatedZ::EarlyWithZComplocHack;
+ }
+
+ if (g_ActiveConfig.UseVSForLinePointExpand() &&
+ (out.rasterization_state.primitive == PrimitiveType::Points ||
+ out.rasterization_state.primitive == PrimitiveType::Lines))
+ {
+ // All primitives are expanded to triangles in the vertex shader
+ vertex_shader_uid_data* vs = out.vs_uid.GetUidData();
+ const PortableVertexDeclaration& decl = out.vertex_format->GetVertexDeclaration();
+ vs->position_has_3_elems = decl.position.components >= 3;
+ vs->texcoord_elem_count = 0;
+ for (int i = 0; i < 8; i++)
+ {
+ if (decl.texcoords[i].enable)
+ {
+ ASSERT(decl.texcoords[i].components <= 3);
+ vs->texcoord_elem_count |= decl.texcoords[i].components << (i * 2);
+ }
+ }
+ out.vertex_format = nullptr;
+ if (out.rasterization_state.primitive == PrimitiveType::Points)
+ vs->vs_expand = VSExpand::Point;
+ else
+ vs->vs_expand = VSExpand::Line;
+ PrimitiveType prim = g_backend_info.bSupportsPrimitiveRestart ? PrimitiveType::TriangleStrip :
+ PrimitiveType::Triangles;
+ out.rasterization_state.primitive = prim;
+ out.gs_uid.GetUidData()->primitive_type = static_cast(prim);
+ }
+
+ return out;
+}
+
+std::size_t PipelineToHash(const GXPipelineUid& in)
+{
+ XXH3_state_t pipeline_hash_state;
+ XXH3_INITSTATE(&pipeline_hash_state);
+ XXH3_64bits_reset_withSeed(&pipeline_hash_state, static_cast(1));
+ UpdateHashWithPipeline(in, &pipeline_hash_state);
+ return XXH3_64bits_digest(&pipeline_hash_state);
+}
+
+void UpdateHashWithPipeline(const GXPipelineUid& in, XXH3_state_t* hash_state)
+{
+ XXH3_64bits_update(hash_state, &in.vertex_format->GetVertexDeclaration(),
+ sizeof(PortableVertexDeclaration));
+ XXH3_64bits_update(hash_state, &in.blending_state, sizeof(BlendingState));
+ XXH3_64bits_update(hash_state, &in.depth_state, sizeof(DepthState));
+ XXH3_64bits_update(hash_state, &in.rasterization_state, sizeof(RasterizationState));
+ XXH3_64bits_update(hash_state, &in.gs_uid, sizeof(GeometryShaderUid));
+ XXH3_64bits_update(hash_state, &in.ps_uid, sizeof(PixelShaderUid));
+ XXH3_64bits_update(hash_state, &in.vs_uid, sizeof(VertexShaderUid));
+}
+} // namespace VideoCommon
diff --git a/Source/Core/VideoCommon/PipelineUtils.h b/Source/Core/VideoCommon/PipelineUtils.h
new file mode 100644
index 0000000000..4a9b7b1696
--- /dev/null
+++ b/Source/Core/VideoCommon/PipelineUtils.h
@@ -0,0 +1,22 @@
+// Copyright 2025 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include
+
+#include "VideoCommon/GXPipelineTypes.h"
+
+namespace VideoCommon
+{
+GXPipelineUid ApplyDriverBugs(const GXPipelineUid& in);
+
+// Returns a hash of the pipeline, hashing the
+// vertex declarations instead of the native vertex format
+// object
+std::size_t PipelineToHash(const GXPipelineUid& in);
+
+// Updates an existing hash with the hash of the pipeline
+void UpdateHashWithPipeline(const GXPipelineUid& in, XXH3_state_t* hash_state);
+
+} // namespace VideoCommon
diff --git a/Source/Core/VideoCommon/Resources/CustomResourceManager.cpp b/Source/Core/VideoCommon/Resources/CustomResourceManager.cpp
new file mode 100644
index 0000000000..56556285d8
--- /dev/null
+++ b/Source/Core/VideoCommon/Resources/CustomResourceManager.cpp
@@ -0,0 +1,162 @@
+// Copyright 2025 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "VideoCommon/Resources/CustomResourceManager.h"
+
+#include "VideoCommon/AbstractGfx.h"
+#include "VideoCommon/PipelineUtils.h"
+#include "VideoCommon/Resources/InvalidTextures.h"
+#include "VideoCommon/VideoEvents.h"
+
+namespace VideoCommon
+{
+void CustomResourceManager::Initialize()
+{
+ m_asset_cache.Initialize();
+ m_worker_thread.Reset("resource-worker");
+ m_host_config.bits = ShaderHostConfig::GetCurrent().bits;
+ m_async_shader_compiler = g_gfx->CreateAsyncShaderCompiler();
+
+ m_async_shader_compiler->StartWorkerThreads(1); // TODO expose to config
+
+ m_xfb_event =
+ GetVideoEvents().after_frame_event.Register([this](Core::System&) { XFBTriggered(); });
+
+ m_invalid_array_texture = CreateInvalidArrayTexture();
+ m_invalid_color_texture = CreateInvalidColorTexture();
+ m_invalid_cubemap_texture = CreateInvalidCubemapTexture();
+ m_invalid_transparent_texture = CreateInvalidTransparentTexture();
+}
+
+void CustomResourceManager::Shutdown()
+{
+ if (m_async_shader_compiler)
+ m_async_shader_compiler->StopWorkerThreads();
+
+ m_asset_cache.Shutdown();
+ m_worker_thread.Shutdown();
+ Reset();
+}
+
+void CustomResourceManager::Reset()
+{
+ m_material_resources.clear();
+ m_shader_resources.clear();
+ m_texture_data_resources.clear();
+ m_texture_sampler_resources.clear();
+
+ m_invalid_transparent_texture.reset();
+ m_invalid_color_texture.reset();
+ m_invalid_cubemap_texture.reset();
+ m_invalid_array_texture.reset();
+
+ m_asset_cache.Reset();
+ m_texture_pool.Reset();
+ m_worker_thread.Reset("resource-worker");
+}
+
+void CustomResourceManager::MarkAssetDirty(const CustomAssetLibrary::AssetID& asset_id)
+{
+ m_asset_cache.MarkAssetDirty(asset_id);
+}
+
+void CustomResourceManager::XFBTriggered()
+{
+ m_asset_cache.Update();
+}
+
+void CustomResourceManager::SetHostConfig(const ShaderHostConfig& host_config)
+{
+ for (auto& [id, shader_resources] : m_shader_resources)
+ {
+ for (auto& [key, shader_resource] : shader_resources)
+ {
+ shader_resource->SetHostConfig(host_config);
+
+ // Hack to get access to resource internals
+ Resource* resource = shader_resource.get();
+
+ // Tell shader and references to trigger a reload
+ // on next usage
+ resource->NotifyAssetChanged(false);
+ }
+ }
+
+ m_host_config.bits = host_config.bits;
+}
+
+TextureDataResource* CustomResourceManager::GetTextureDataFromAsset(
+ const CustomAssetLibrary::AssetID& asset_id,
+ std::shared_ptr library)
+{
+ auto& resource = m_texture_data_resources[asset_id];
+ if (resource == nullptr)
+ {
+ resource =
+ std::make_unique(CreateResourceContext(asset_id, std::move(library)));
+ }
+ resource->Process();
+ return resource.get();
+}
+
+MaterialResource* CustomResourceManager::GetMaterialFromAsset(
+ const CustomAssetLibrary::AssetID& asset_id, const GXPipelineUid& pipeline_uid,
+ std::shared_ptr library)
+{
+ auto& resource = m_material_resources[asset_id][PipelineToHash(pipeline_uid)];
+ if (resource == nullptr)
+ {
+ resource = std::make_unique(
+ CreateResourceContext(asset_id, std::move(library)), pipeline_uid);
+ }
+ resource->Process();
+ return resource.get();
+}
+
+ShaderResource*
+CustomResourceManager::GetShaderFromAsset(const CustomAssetLibrary::AssetID& asset_id,
+ std::size_t shader_key, const GXPipelineUid& pipeline_uid,
+ const std::string& preprocessor_settings,
+ std::shared_ptr library)
+{
+ auto& resource = m_shader_resources[asset_id][shader_key];
+ if (resource == nullptr)
+ {
+ resource = std::make_unique(CreateResourceContext(asset_id, std::move(library)),
+ pipeline_uid, preprocessor_settings, m_host_config);
+ }
+ resource->Process();
+ return resource.get();
+}
+
+TextureAndSamplerResource* CustomResourceManager::GetTextureAndSamplerFromAsset(
+ const CustomAssetLibrary::AssetID& asset_id,
+ std::shared_ptr library)
+{
+ auto& resource = m_texture_sampler_resources[asset_id];
+ if (resource == nullptr)
+ {
+ resource = std::make_unique(
+ CreateResourceContext(asset_id, std::move(library)));
+ }
+ resource->Process();
+ return resource.get();
+}
+
+Resource::ResourceContext CustomResourceManager::CreateResourceContext(
+ const CustomAssetLibrary::AssetID& asset_id,
+ std::shared_ptr library)
+{
+ return Resource::ResourceContext{asset_id,
+ std::move(library),
+ &m_asset_cache,
+ this,
+ &m_texture_pool,
+ &m_worker_thread,
+ m_async_shader_compiler.get(),
+ m_invalid_array_texture.get(),
+ m_invalid_color_texture.get(),
+ m_invalid_cubemap_texture.get(),
+ m_invalid_transparent_texture.get()};
+}
+} // namespace VideoCommon
diff --git a/Source/Core/VideoCommon/Resources/CustomResourceManager.h b/Source/Core/VideoCommon/Resources/CustomResourceManager.h
new file mode 100644
index 0000000000..04eaa46942
--- /dev/null
+++ b/Source/Core/VideoCommon/Resources/CustomResourceManager.h
@@ -0,0 +1,82 @@
+// Copyright 2025 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include