diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props
index 9737ceeac8..8d59805dee 100644
--- a/Source/Core/DolphinLib.props
+++ b/Source/Core/DolphinLib.props
@@ -756,7 +756,10 @@
+
+
+
@@ -1412,7 +1415,10 @@
+
+
+
diff --git a/Source/Core/VideoCommon/CMakeLists.txt b/Source/Core/VideoCommon/CMakeLists.txt
index d3b68ba95c..69a0968e4d 100644
--- a/Source/Core/VideoCommon/CMakeLists.txt
+++ b/Source/Core/VideoCommon/CMakeLists.txt
@@ -152,8 +152,14 @@ add_library(videocommon
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
diff --git a/Source/Core/VideoCommon/Resources/CustomResourceManager.cpp b/Source/Core/VideoCommon/Resources/CustomResourceManager.cpp
index 35eeb0393a..8148ccd899 100644
--- a/Source/Core/VideoCommon/Resources/CustomResourceManager.cpp
+++ b/Source/Core/VideoCommon/Resources/CustomResourceManager.cpp
@@ -3,6 +3,11 @@
#include "VideoCommon/Resources/CustomResourceManager.h"
+#include "Common/Logging/Log.h"
+
+#include "VideoCommon/AbstractGfx.h"
+#include "VideoCommon/PipelineUtils.h"
+#include "VideoCommon/Resources/InvalidTextures.h"
#include "VideoCommon/VideoEvents.h"
namespace VideoCommon
@@ -10,22 +15,46 @@ 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 =
- AfterFrameEvent::Register([this](Core::System&) { XFBTriggered(); }, "CustomResourceManager");
+ 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)
@@ -38,24 +67,99 @@ 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)
{
- const auto [it, added] = m_texture_data_resources.try_emplace(asset_id, nullptr);
- if (added)
+ auto& resource = m_texture_data_resources[asset_id];
+ if (resource == nullptr)
{
- it->second = std::make_unique(CreateResourceContext(asset_id, library));
+ resource =
+ std::make_unique(CreateResourceContext(asset_id, std::move(library)));
}
- ProcessResource(it->second.get());
- return it->second.get();
+ ProcessResource(resource.get());
+ 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);
+ }
+ ProcessResource(resource.get());
+ 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);
+ }
+ ProcessResource(resource.get());
+ 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)));
+ }
+ ProcessResource(resource.get());
+ return resource.get();
}
Resource::ResourceContext CustomResourceManager::CreateResourceContext(
const CustomAssetLibrary::AssetID& asset_id,
- const std::shared_ptr& library)
+ std::shared_ptr library)
{
- return Resource::ResourceContext{asset_id, library, &m_asset_cache, this};
+ 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()};
}
void CustomResourceManager::ProcessResource(Resource* resource)
@@ -71,18 +175,15 @@ void CustomResourceManager::ProcessResource(Resource* resource)
return;
}
- // Early out if we're already at our end state
- if (resource->GetState() == Resource::State::DataAvailable)
- return;
-
ProcessResourceState(resource);
}
void CustomResourceManager::ProcessResourceState(Resource* resource)
{
- Resource::State next_state = resource->GetState();
+ const auto current_state = resource->GetState();
+ Resource::State next_state = current_state;
Resource::TaskComplete task_complete = Resource::TaskComplete::No;
- switch (resource->GetState())
+ switch (current_state)
{
case Resource::State::ReloadData:
resource->ResetData();
@@ -102,6 +203,14 @@ void CustomResourceManager::ProcessResourceState(Resource* resource)
case Resource::State::ProcessingData:
task_complete = resource->ProcessData();
next_state = Resource::State::DataAvailable;
+ break;
+ case Resource::State::DataAvailable:
+ // Early out, we're already at our end state
+ return;
+ default:
+ ERROR_LOG_FMT(VIDEO, "Unknown resource state '{}' for resource '{}'",
+ static_cast(current_state), resource->m_resource_context.primary_asset_id);
+ return;
};
if (task_complete == Resource::TaskComplete::Yes)
diff --git a/Source/Core/VideoCommon/Resources/CustomResourceManager.h b/Source/Core/VideoCommon/Resources/CustomResourceManager.h
index 512bd77712..538bec13f1 100644
--- a/Source/Core/VideoCommon/Resources/CustomResourceManager.h
+++ b/Source/Core/VideoCommon/Resources/CustomResourceManager.h
@@ -7,9 +7,16 @@
#include
#include "Common/HookableEvent.h"
+#include "Common/WorkQueueThread.h"
+#include "VideoCommon/AbstractTexture.h"
#include "VideoCommon/Assets/CustomAssetCache.h"
+#include "VideoCommon/AsyncShaderCompiler.h"
+#include "VideoCommon/Resources/MaterialResource.h"
+#include "VideoCommon/Resources/ShaderResource.h"
+#include "VideoCommon/Resources/TextureAndSamplerResource.h"
#include "VideoCommon/Resources/TextureDataResource.h"
+#include "VideoCommon/Resources/TexturePool.h"
namespace VideoCommon
{
@@ -25,22 +32,53 @@ public:
void MarkAssetDirty(const CustomAssetLibrary::AssetID& asset_id);
void XFBTriggered();
+ void SetHostConfig(const ShaderHostConfig& host_config);
TextureDataResource*
GetTextureDataFromAsset(const CustomAssetLibrary::AssetID& asset_id,
std::shared_ptr library);
+ MaterialResource* GetMaterialFromAsset(const CustomAssetLibrary::AssetID& asset_id,
+ const GXPipelineUid& pipeline_uid,
+ std::shared_ptr library);
+
+ ShaderResource* GetShaderFromAsset(const CustomAssetLibrary::AssetID& asset_id,
+ std::size_t shader_key, const GXPipelineUid& pipeline_uid,
+ const std::string& preprocessor_settings,
+ std::shared_ptr library);
+ TextureAndSamplerResource*
+ GetTextureAndSamplerFromAsset(const CustomAssetLibrary::AssetID& asset_id,
+ std::shared_ptr library);
private:
Resource::ResourceContext
CreateResourceContext(const CustomAssetLibrary::AssetID& asset_id,
- const std::shared_ptr& library);
+ std::shared_ptr library);
void ProcessResource(Resource* resource);
void ProcessResourceState(Resource* resource);
CustomAssetCache m_asset_cache;
+ TexturePool m_texture_pool;
+ Common::AsyncWorkThreadSP m_worker_thread;
+ std::unique_ptr m_async_shader_compiler;
+
+ using PipelineIdToMaterial = std::map>;
+ std::map m_material_resources;
+
+ using ShaderKeyToShader = std::map>;
+ std::map m_shader_resources;
std::map>
m_texture_data_resources;
+ std::map>
+ m_texture_sampler_resources;
+
+ ShaderHostConfig m_host_config;
+
Common::EventHook m_xfb_event;
+
+ std::unique_ptr m_invalid_transparent_texture;
+ std::unique_ptr m_invalid_color_texture;
+ std::unique_ptr m_invalid_cubemap_texture;
+ std::unique_ptr m_invalid_array_texture;
};
} // namespace VideoCommon
diff --git a/Source/Core/VideoCommon/Resources/MaterialResource.cpp b/Source/Core/VideoCommon/Resources/MaterialResource.cpp
new file mode 100644
index 0000000000..657647ffb9
--- /dev/null
+++ b/Source/Core/VideoCommon/Resources/MaterialResource.cpp
@@ -0,0 +1,385 @@
+// Copyright 2025 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "VideoCommon/Resources/MaterialResource.h"
+
+#include
+
+#include "Common/VariantUtil.h"
+
+#include "VideoCommon/AbstractGfx.h"
+#include "VideoCommon/Assets/CustomAssetCache.h"
+#include "VideoCommon/AsyncShaderCompiler.h"
+#include "VideoCommon/FramebufferManager.h"
+#include "VideoCommon/PipelineUtils.h"
+#include "VideoCommon/Resources/CustomResourceManager.h"
+#include "VideoCommon/VideoConfig.h"
+
+namespace
+{
+// TODO: absorb this with TextureCacheBase
+bool IsAnisotropicEnhancementSafe(const SamplerState::TM0& tm0)
+{
+ return !(tm0.min_filter == FilterMode::Near && tm0.mag_filter == FilterMode::Near);
+}
+
+// TODO: absorb this with TextureCacheBase
+SamplerState CalculateSamplerAnisotropy(const SamplerState& initial_sampler)
+{
+ SamplerState state = initial_sampler;
+ if (g_ActiveConfig.iMaxAnisotropy != AnisotropicFilteringMode::Default &&
+ IsAnisotropicEnhancementSafe(state.tm0))
+ {
+ state.tm0.anisotropic_filtering = Common::ToUnderlying(g_ActiveConfig.iMaxAnisotropy);
+ }
+
+ if (state.tm0.anisotropic_filtering != 0)
+ {
+ // https://www.opengl.org/registry/specs/EXT/texture_filter_anisotropic.txt
+ // For predictable results on all hardware/drivers, only use one of:
+ // GL_LINEAR + GL_LINEAR (No Mipmaps [Bilinear])
+ // GL_LINEAR + GL_LINEAR_MIPMAP_LINEAR (w/ Mipmaps [Trilinear])
+ // Letting the game set other combinations will have varying arbitrary results;
+ // possibly being interpreted as equal to bilinear/trilinear, implicitly
+ // disabling anisotropy, or changing the anisotropic algorithm employed.
+ state.tm0.min_filter = FilterMode::Linear;
+ state.tm0.mag_filter = FilterMode::Linear;
+ state.tm0.mipmap_filter = FilterMode::Linear;
+ }
+ return state;
+}
+} // namespace
+
+namespace VideoCommon
+{
+MaterialResource::MaterialResource(Resource::ResourceContext resource_context,
+ const GXPipelineUid& pipeline_uid)
+ : Resource(std::move(resource_context)), m_uid(pipeline_uid)
+{
+ m_material_asset = m_resource_context.asset_cache->CreateAsset(
+ m_resource_context.primary_asset_id, m_resource_context.asset_library, this);
+ m_uid_vertex_format_copy =
+ g_gfx->CreateNativeVertexFormat(m_uid.vertex_format->GetVertexDeclaration());
+ m_uid.vertex_format = m_uid_vertex_format_copy.get();
+}
+
+void MaterialResource::ResetData()
+{
+ if (m_current_data)
+ {
+ m_current_data->m_shader_resource->RemoveReference(this);
+ for (const auto& texture_like_resource : m_current_data->m_texture_like_resources)
+ {
+ if (texture_like_resource)
+ texture_like_resource->RemoveReference(this);
+ }
+ if (m_current_data->m_next_material)
+ m_current_data->m_next_material->RemoveReference(this);
+ }
+ m_load_data = std::make_shared();
+ m_processing_load_data = false;
+}
+
+Resource::TaskComplete MaterialResource::CollectPrimaryData()
+{
+ const auto material_data = m_material_asset->GetData();
+ if (!material_data) [[unlikely]]
+ {
+ return Resource::TaskComplete::No;
+ }
+ m_load_data->m_material_data = material_data;
+
+ // A shader asset is required to function
+ if (m_load_data->m_material_data->shader_asset == "")
+ {
+ return Resource::TaskComplete::Error;
+ }
+
+ CreateTextureData(m_load_data.get());
+ SetShaderKey(m_load_data.get(), &m_uid);
+
+ return Resource::TaskComplete::Yes;
+}
+
+Resource::TaskComplete MaterialResource::CollectDependencyData()
+{
+ bool loaded = true;
+ {
+ auto* const shader_resource = m_resource_context.resource_manager->GetShaderFromAsset(
+ m_load_data->m_material_data->shader_asset, m_load_data->m_shader_key, m_uid,
+ m_load_data->m_preprocessor_settings, m_resource_context.asset_library);
+ shader_resource->AddReference(this);
+ m_load_data->m_shader_resource = shader_resource;
+ const auto data_processed = shader_resource->IsDataProcessed();
+ if (data_processed == TaskComplete::Error)
+ return TaskComplete::Error;
+
+ loaded &= data_processed == TaskComplete::Yes;
+ }
+
+ for (std::size_t i = 0; i < m_load_data->m_material_data->textures.size(); i++)
+ {
+ const auto& texture_and_sampler = m_load_data->m_material_data->textures[i];
+ if (texture_and_sampler.asset == "")
+ continue;
+
+ const auto texture = m_resource_context.resource_manager->GetTextureAndSamplerFromAsset(
+ texture_and_sampler.asset, m_resource_context.asset_library);
+ m_load_data->m_texture_like_resources[i] = texture;
+ m_load_data->m_texture_like_data[i] = texture->GetData();
+ texture->AddReference(this);
+
+ const auto data_processed = texture->IsDataProcessed();
+ if (data_processed == TaskComplete::Error)
+ return TaskComplete::Error;
+
+ loaded &= data_processed == TaskComplete::Yes;
+ }
+
+ if (m_load_data->m_material_data->next_material_asset != "")
+ {
+ m_load_data->m_next_material = m_resource_context.resource_manager->GetMaterialFromAsset(
+ m_load_data->m_material_data->next_material_asset, m_uid, m_resource_context.asset_library);
+ m_load_data->m_next_material->AddReference(this);
+ const auto data_processed = m_load_data->m_next_material->IsDataProcessed();
+ if (data_processed == TaskComplete::Error)
+ return TaskComplete::Error;
+
+ loaded &= data_processed == TaskComplete::Yes;
+ }
+
+ return loaded ? TaskComplete::Yes : TaskComplete::No;
+}
+
+Resource::TaskComplete MaterialResource::ProcessData()
+{
+ auto shader_data = m_load_data->m_shader_resource->GetData();
+
+ if (!shader_data) [[unlikely]]
+ return Resource::TaskComplete::Error;
+
+ for (std::size_t i = 0; i < m_load_data->m_texture_like_data.size(); i++)
+ {
+ auto& texture_like_reference = m_load_data->m_texture_like_references[i];
+ const auto& texture_and_sampler = m_load_data->m_material_data->textures[i];
+
+ // If the texture doesn't exist, use one of the placeholders
+ if (texture_and_sampler.asset == "")
+ {
+ const auto texture_type = shader_data->GetTextureType(i);
+ if (texture_type == AbstractTextureType::Texture_2D)
+ texture_like_reference.texture = m_resource_context.invalid_color_texture;
+ else if (texture_type == AbstractTextureType::Texture_2DArray)
+ texture_like_reference.texture = m_resource_context.invalid_array_texture;
+ else if (texture_type == AbstractTextureType::Texture_CubeMap)
+ texture_like_reference.texture = m_resource_context.invalid_cubemap_texture;
+
+ if (texture_like_reference.texture == nullptr)
+ {
+ PanicAlertFmt("Invalid texture (texture_type={}) is not found during material "
+ "resource processing (asset_id={})",
+ texture_type, m_resource_context.primary_asset_id);
+ }
+
+ continue;
+ }
+
+ auto& texture_like_data = m_load_data->m_texture_like_data[i];
+
+ std::visit(overloaded{[&](const std::shared_ptr& data) {
+ texture_like_reference.texture = data->GetTexture();
+ texture_like_reference.sampler = CalculateSamplerAnisotropy(data->GetSampler());
+ ;
+ }},
+ texture_like_data);
+ }
+
+ class WorkItem final : public VideoCommon::AsyncShaderCompiler::WorkItem
+ {
+ public:
+ WorkItem(std::shared_ptr material_resource_data,
+ std::shared_ptr shader_resource_data,
+ VideoCommon::GXPipelineUid* uid, FramebufferState frame_buffer_state)
+ : m_material_resource_data(std::move(material_resource_data)),
+ m_shader_resource_data(std::move(shader_resource_data)), m_uid(uid),
+ m_frame_buffer_state(frame_buffer_state)
+ {
+ }
+
+ bool Compile() override
+ {
+ // Sanity check
+ if (!m_shader_resource_data->IsCompiled())
+ {
+ m_material_resource_data->m_processing_finished = true;
+ return false;
+ }
+
+ AbstractPipelineConfig config;
+ config.vertex_shader = m_shader_resource_data->GetVertexShader();
+ config.pixel_shader = m_shader_resource_data->GetPixelShader();
+ config.geometry_shader = m_shader_resource_data->GetGeometryShader();
+
+ const auto actual_uid = ApplyDriverBugs(*m_uid);
+
+ if (m_material_resource_data->m_material_data->blending_state)
+ config.blending_state = *m_material_resource_data->m_material_data->blending_state;
+ else
+ config.blending_state = actual_uid.blending_state;
+
+ if (m_material_resource_data->m_material_data->depth_state)
+ config.depth_state = *m_material_resource_data->m_material_data->depth_state;
+ else
+ config.depth_state = actual_uid.depth_state;
+
+ config.framebuffer_state = std::move(m_frame_buffer_state);
+ config.framebuffer_state.additional_color_attachment_count = 0;
+
+ config.rasterization_state = actual_uid.rasterization_state;
+ if (m_material_resource_data->m_material_data->cull_mode)
+ {
+ config.rasterization_state.cull_mode =
+ *m_material_resource_data->m_material_data->cull_mode;
+ }
+
+ config.vertex_format = actual_uid.vertex_format;
+ config.usage = AbstractPipelineUsage::GX;
+
+ m_material_resource_data->m_pipeline = g_gfx->CreatePipeline(config);
+
+ if (m_material_resource_data->m_pipeline)
+ {
+ WriteUniforms(m_material_resource_data.get());
+ }
+ m_material_resource_data->m_processing_finished = true;
+ return true;
+ }
+ void Retrieve() override {}
+
+ private:
+ std::shared_ptr m_material_resource_data;
+ std::shared_ptr m_shader_resource_data;
+ VideoCommon::GXPipelineUid* m_uid;
+ FramebufferState m_frame_buffer_state;
+ };
+
+ if (!m_processing_load_data)
+ {
+ auto wi = m_resource_context.shader_compiler->CreateWorkItem(
+ m_load_data, std::move(shader_data), &m_uid,
+ g_framebuffer_manager->GetEFBFramebufferState());
+
+ // We don't need priority, that is already handled by the resource system
+ m_resource_context.shader_compiler->QueueWorkItem(std::move(wi), 0);
+ m_processing_load_data = true;
+ }
+
+ if (!m_load_data->m_processing_finished)
+ return TaskComplete::No;
+
+ if (!m_load_data->m_pipeline)
+ {
+ return TaskComplete::Error;
+ }
+
+ std::swap(m_current_data, m_load_data);
+ return TaskComplete::Yes;
+}
+
+void MaterialResource::MarkAsActive()
+{
+ if (!m_current_data) [[unlikely]]
+ return;
+
+ m_resource_context.asset_cache->MarkAssetActive(m_material_asset);
+ for (const auto& texture_like_resource : m_current_data->m_texture_like_resources)
+ {
+ if (texture_like_resource)
+ texture_like_resource->MarkAsActive();
+ }
+ if (m_current_data->m_shader_resource)
+ m_current_data->m_shader_resource->MarkAsActive();
+ if (m_current_data->m_next_material)
+ m_current_data->m_next_material->MarkAsActive();
+}
+
+void MaterialResource::MarkAsPending()
+{
+ m_resource_context.asset_cache->MarkAssetPending(m_material_asset);
+}
+
+void MaterialResource::CreateTextureData(Data* data)
+{
+ ShaderCode preprocessor_settings;
+
+ const auto& material_data = *data->m_material_data;
+ data->m_texture_like_data.clear();
+ data->m_texture_like_resources.clear();
+ data->m_texture_like_references.clear();
+ const u32 custom_sampler_index_offset = 8;
+ for (u32 i = 0; i < static_cast(material_data.textures.size()); i++)
+ {
+ const auto& texture_and_sampler = material_data.textures[i];
+ data->m_texture_like_references.push_back(TextureLikeReference{});
+
+ TextureAndSamplerResource* value = nullptr;
+ data->m_texture_like_resources.push_back(value);
+ data->m_texture_like_data.push_back(std::shared_ptr{});
+
+ auto& texture_like_reference = data->m_texture_like_references[i];
+
+ if (texture_and_sampler.asset == "")
+ {
+ preprocessor_settings.Write("#define HAS_SAMPLER_{} 0\n", i);
+
+ // For an invalid asset, force the sampler to use the default sampler
+ texture_like_reference.sampler_origin =
+ VideoCommon::TextureSamplerValue::SamplerOrigin::Asset;
+ texture_like_reference.texture_hash = "";
+ }
+ else
+ {
+ preprocessor_settings.Write("#define HAS_SAMPLER_{} 1\n", i);
+
+ texture_like_reference.sampler_origin = texture_and_sampler.sampler_origin;
+ texture_like_reference.texture_hash = texture_and_sampler.texture_hash;
+ }
+
+ texture_like_reference.sampler_index = i + custom_sampler_index_offset;
+ texture_like_reference.texture = nullptr;
+ }
+
+ data->m_preprocessor_settings = preprocessor_settings.GetBuffer();
+}
+
+void MaterialResource::SetShaderKey(Data* data, GXPipelineUid* uid)
+{
+ XXH3_state_t shader_key_hash;
+ XXH3_INITSTATE(&shader_key_hash);
+ XXH3_64bits_reset_withSeed(&shader_key_hash, static_cast(1));
+
+ UpdateHashWithPipeline(*uid, &shader_key_hash);
+ XXH3_64bits_update(&shader_key_hash, data->m_preprocessor_settings.c_str(),
+ data->m_preprocessor_settings.size());
+
+ data->m_shader_key = XXH3_64bits_digest(&shader_key_hash);
+}
+
+void MaterialResource::WriteUniforms(Data* data)
+{
+ // Calculate the size in memory of the buffer
+ std::size_t max_uniformdata_size = 0;
+ for (const auto& property : data->m_material_data->properties)
+ {
+ max_uniformdata_size += VideoCommon::MaterialProperty::GetMemorySize(property);
+ }
+ data->m_uniform_data = Common::UniqueBuffer(max_uniformdata_size);
+
+ // Now write the memory
+ u8* uniform_data = data->m_uniform_data.data();
+ for (const auto& property : data->m_material_data->properties)
+ {
+ VideoCommon::MaterialProperty::WriteToMemory(uniform_data, property);
+ }
+}
+} // namespace VideoCommon
diff --git a/Source/Core/VideoCommon/Resources/MaterialResource.h b/Source/Core/VideoCommon/Resources/MaterialResource.h
new file mode 100644
index 0000000000..4842f000ea
--- /dev/null
+++ b/Source/Core/VideoCommon/Resources/MaterialResource.h
@@ -0,0 +1,99 @@
+// Copyright 2025 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+#include "Common/Buffer.h"
+#include "Common/SmallVector.h"
+
+#include "VideoCommon/AbstractPipeline.h"
+#include "VideoCommon/Assets/MaterialAsset.h"
+#include "VideoCommon/Assets/TextureSamplerValue.h"
+#include "VideoCommon/Constants.h"
+#include "VideoCommon/GXPipelineTypes.h"
+#include "VideoCommon/RenderState.h"
+#include "VideoCommon/Resources/Resource.h"
+#include "VideoCommon/Resources/ShaderResource.h"
+#include "VideoCommon/Resources/TextureAndSamplerResource.h"
+
+namespace VideoCommon
+{
+class MaterialResource final : public Resource
+{
+public:
+ MaterialResource(Resource::ResourceContext resource_context, const GXPipelineUid& pipeline_uid);
+
+ struct TextureLikeReference
+ {
+ SamplerState sampler;
+ u32 sampler_index;
+ TextureSamplerValue::SamplerOrigin sampler_origin;
+ std::string_view texture_hash;
+ AbstractTexture* texture;
+ };
+
+ class Data
+ {
+ public:
+ AbstractPipeline* GetPipeline() const { return m_pipeline.get(); }
+ std::span GetUniforms() const { return m_uniform_data; }
+ std::span GetTextures() const { return m_texture_like_references; }
+ MaterialResource* GetNextMaterial() const { return m_next_material; }
+
+ private:
+ friend class MaterialResource;
+ std::unique_ptr m_pipeline = nullptr;
+ Common::UniqueBuffer m_uniform_data;
+ std::shared_ptr m_material_data = nullptr;
+ ShaderResource* m_shader_resource = nullptr;
+
+ using TextureLikeResource = Resource*;
+ Common::SmallVector
+ m_texture_like_resources;
+
+ // Variant for future expansion...
+ using TextureLikeData = std::variant>;
+ Common::SmallVector
+ m_texture_like_data;
+
+ Common::SmallVector
+ m_texture_like_references;
+
+ MaterialResource* m_next_material = nullptr;
+ std::size_t m_shader_key;
+ std::string m_preprocessor_settings;
+ std::atomic_bool m_processing_finished;
+ };
+
+ const std::shared_ptr& GetData() const { return m_current_data; }
+ void MarkAsActive() override;
+ void MarkAsPending() override;
+
+private:
+ void ResetData() override;
+ Resource::TaskComplete CollectPrimaryData() override;
+ Resource::TaskComplete CollectDependencyData() override;
+ Resource::TaskComplete ProcessData() override;
+
+ static void CreateTextureData(Data* data);
+ static void SetShaderKey(Data* data, GXPipelineUid* uid);
+ static void WriteUniforms(Data* data);
+
+ std::shared_ptr m_current_data;
+
+ std::shared_ptr m_load_data;
+ bool m_processing_load_data = false;
+
+ // Note: asset cache owns the asset, we access as a reference
+ MaterialAsset* m_material_asset = nullptr;
+
+ GXPipelineUid m_uid;
+ std::unique_ptr m_uid_vertex_format_copy;
+};
+} // namespace VideoCommon
diff --git a/Source/Core/VideoCommon/Resources/Resource.h b/Source/Core/VideoCommon/Resources/Resource.h
index f9656fe2fa..4bcec910c7 100644
--- a/Source/Core/VideoCommon/Resources/Resource.h
+++ b/Source/Core/VideoCommon/Resources/Resource.h
@@ -3,6 +3,8 @@
#pragma once
+#include "Common/WorkQueueThread.h"
+
#include "VideoCommon/Assets/AssetListener.h"
#include "VideoCommon/Assets/CustomAssetLibrary.h"
@@ -12,8 +14,10 @@
class AbstractTexture;
namespace VideoCommon
{
+class AsyncShaderCompiler;
class CustomAssetCache;
class CustomResourceManager;
+class TexturePool;
// A resource is an abstract object that maintains
// relationships between assets (ex: a material that references a texture),
@@ -28,6 +32,13 @@ public:
std::shared_ptr asset_library;
CustomAssetCache* asset_cache;
CustomResourceManager* resource_manager;
+ TexturePool* texture_pool;
+ Common::AsyncWorkThreadSP* worker_queue;
+ AsyncShaderCompiler* shader_compiler;
+ AbstractTexture* invalid_array_texture;
+ AbstractTexture* invalid_color_texture;
+ AbstractTexture* invalid_cubemap_texture;
+ AbstractTexture* invalid_transparent_texture;
};
explicit Resource(ResourceContext resource_context);
diff --git a/Source/Core/VideoCommon/Resources/ShaderResource.cpp b/Source/Core/VideoCommon/Resources/ShaderResource.cpp
new file mode 100644
index 0000000000..e1aecd355c
--- /dev/null
+++ b/Source/Core/VideoCommon/Resources/ShaderResource.cpp
@@ -0,0 +1,311 @@
+// Copyright 2025 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "VideoCommon/Resources/ShaderResource.h"
+
+#include
+
+#include
+
+#include "VideoCommon/AbstractGfx.h"
+#include "VideoCommon/Assets/CustomAssetCache.h"
+#include "VideoCommon/AsyncShaderCompiler.h"
+#include "VideoCommon/GeometryShaderGen.h"
+#include "VideoCommon/PipelineUtils.h"
+#include "VideoCommon/PixelShaderGen.h"
+#include "VideoCommon/VertexShaderGen.h"
+#include "VideoCommon/VideoConfig.h"
+
+namespace VideoCommon
+{
+namespace
+{
+std::unique_ptr
+CompileGeometryShader(const GeometryShaderUid& uid, APIType api_type, ShaderHostConfig host_config)
+{
+ const ShaderCode source_code =
+ GenerateGeometryShaderCode(api_type, host_config, uid.GetUidData());
+ return g_gfx->CreateShaderFromSource(ShaderStage::Geometry, source_code.GetBuffer(), nullptr,
+ fmt::format("Geometry shader: {}", *uid.GetUidData()));
+}
+
+std::unique_ptr CompilePixelShader(const PixelShaderUid& uid,
+ std::string_view preprocessor_settings,
+ APIType api_type,
+ const ShaderHostConfig& host_config,
+ RasterSurfaceShaderData* shader_data)
+{
+ ShaderCode shader_code;
+
+ // Write any preprocessor values that were passed in
+ shader_code.Write("{}", preprocessor_settings);
+
+ // TODO: in the future we could dynamically determine the amount of samplers
+ // available, for now just hardcode to start at 8 (the first non game
+ // sampler index available)
+ const std::size_t custom_sampler_index_offset = 8;
+ for (std::size_t i = 0; i < shader_data->samplers.size(); i++)
+ {
+ const auto& sampler = shader_data->samplers[i];
+ std::string_view sampler_type;
+ switch (sampler.type)
+ {
+ case AbstractTextureType::Texture_2D:
+ sampler_type = "sampler2D";
+ break;
+ case AbstractTextureType::Texture_2DArray:
+ sampler_type = "sampler2DArray";
+ break;
+ case AbstractTextureType::Texture_CubeMap:
+ sampler_type = "samplerCube";
+ break;
+ };
+ shader_code.Write("SAMPLER_BINDING({}) uniform {} samp_{};\n", custom_sampler_index_offset + i,
+ sampler_type, sampler.name);
+
+ // Sampler usage is passed in from the material
+ // Write a new preprocessor value with the sampler name
+ // for easier code in the shader
+ shader_code.Write("#if HAS_SAMPLER_{} == 1\n", i);
+ shader_code.Write("#define HAS_{} 1\n", sampler.name);
+ shader_code.Write("#endif\n");
+
+ shader_code.Write("\n");
+ }
+ shader_code.Write("\n");
+
+ // Now write the custom shader
+ shader_code.Write("{}", ReplaceAll(shader_data->pixel_source, "\r\n", "\n"));
+
+ // Write out the uniform data
+ ShaderCode uniform_code;
+ for (const auto& property : shader_data->uniform_properties)
+ {
+ VideoCommon::ShaderProperty::WriteAsShaderCode(uniform_code, property);
+ }
+ if (!shader_data->uniform_properties.empty())
+ uniform_code.Write("\n\n");
+
+ // Compile the shader
+ CustomPixelContents contents{.shader = shader_code.GetBuffer(),
+ .uniforms = uniform_code.GetBuffer()};
+ const ShaderCode source_code =
+ GeneratePixelShaderCode(api_type, host_config, uid.GetUidData(), contents);
+ ShaderIncluder* shader_includer =
+ shader_data->shader_includer ? &*shader_data->shader_includer : nullptr;
+ return g_gfx->CreateShaderFromSource(ShaderStage::Pixel, source_code.GetBuffer(), shader_includer,
+ "Custom Pixel Shader");
+}
+
+std::unique_ptr CompileVertexShader(const VertexShaderUid& uid,
+ std::string_view preprocessor_settings,
+ APIType api_type,
+ const ShaderHostConfig& host_config,
+ const RasterSurfaceShaderData& shader_data)
+{
+ ShaderCode shader_code;
+
+ // Write any preprocessor values that were passed in
+ shader_code.Write("{}", preprocessor_settings);
+
+ // TODO: in the future we could dynamically determine the amount of samplers
+ // available, for now just hardcode to start at 8 (the first non game
+ // sampler index available)
+ const std::size_t custom_sampler_index_offset = 8;
+ for (std::size_t i = 0; i < shader_data.samplers.size(); i++)
+ {
+ const auto& sampler = shader_data.samplers[i];
+ std::string_view sampler_type = "";
+ switch (sampler.type)
+ {
+ case AbstractTextureType::Texture_2D:
+ sampler_type = "sampler2D";
+ break;
+ case AbstractTextureType::Texture_2DArray:
+ sampler_type = "sampler2DArray";
+ break;
+ case AbstractTextureType::Texture_CubeMap:
+ sampler_type = "samplerCube";
+ break;
+ };
+ shader_code.Write("SAMPLER_BINDING({}) uniform {} samp_{};\n", custom_sampler_index_offset + i,
+ sampler_type, sampler.name);
+
+ // Sampler usage is passed in from the material
+ // Write a new preprocessor value with the sampler name
+ // for easier code in the shader
+ shader_code.Write("#if HAS_SAMPLER_{} == 1\n", i);
+ shader_code.Write("#define HAS_{} 1\n", sampler.name);
+ shader_code.Write("#endif\n");
+
+ shader_code.Write("\n");
+ }
+ shader_code.Write("\n");
+
+ // Now write the custom shader
+ shader_code.Write("{}", ReplaceAll(shader_data.vertex_source, "\r\n", "\n"));
+
+ // Write out the uniform data
+ ShaderCode uniform_code;
+ for (const auto& property : shader_data.uniform_properties)
+ {
+ VideoCommon::ShaderProperty::WriteAsShaderCode(uniform_code, property);
+ }
+ if (!shader_data.uniform_properties.empty())
+ uniform_code.Write("\n\n");
+
+ // Compile the shader
+ CustomVertexContents contents{.shader = shader_code.GetBuffer(),
+ .uniforms = uniform_code.GetBuffer()};
+ const ShaderCode source_code =
+ GenerateVertexShaderCode(api_type, host_config, uid.GetUidData(), contents);
+ return g_gfx->CreateShaderFromSource(ShaderStage::Vertex, source_code.GetBuffer(), nullptr,
+ "Custom Vertex Shader");
+}
+} // namespace
+ShaderResource::ShaderResource(Resource::ResourceContext resource_context,
+ const GXPipelineUid& pipeline_uid,
+ const std::string& preprocessor_setting,
+ const ShaderHostConfig& shader_host_config)
+ : Resource(std::move(resource_context)), m_uid(pipeline_uid),
+ m_preprocessor_settings(preprocessor_setting),
+ m_shader_host_config{.bits = shader_host_config.bits}
+{
+ m_shader_asset = m_resource_context.asset_cache->CreateAsset(
+ m_resource_context.primary_asset_id, m_resource_context.asset_library, this);
+}
+
+void ShaderResource::SetHostConfig(const ShaderHostConfig& host_config)
+{
+ m_shader_host_config.bits = host_config.bits;
+}
+
+void ShaderResource::MarkAsPending()
+{
+ m_resource_context.asset_cache->MarkAssetPending(m_shader_asset);
+}
+
+void ShaderResource::MarkAsActive()
+{
+ m_resource_context.asset_cache->MarkAssetActive(m_shader_asset);
+}
+
+AbstractShader* ShaderResource::Data::GetVertexShader() const
+{
+ if (!m_vertex_shader)
+ return nullptr;
+ return m_vertex_shader.get();
+}
+
+AbstractShader* ShaderResource::Data::GetPixelShader() const
+{
+ if (!m_pixel_shader)
+ return nullptr;
+ return m_pixel_shader.get();
+}
+
+AbstractShader* ShaderResource::Data::GetGeometryShader() const
+{
+ if (!m_geometry_shader)
+ return nullptr;
+ return m_geometry_shader.get();
+}
+
+bool ShaderResource::Data::IsCompiled() const
+{
+ return m_vertex_shader && m_pixel_shader && (!m_needs_geometry_shader || m_geometry_shader);
+}
+
+AbstractTextureType ShaderResource::Data::GetTextureType(std::size_t index)
+{
+ // If the data doesn't exist, just pick one...
+ if (!m_shader_data || index >= m_shader_data->samplers.size()) [[unlikely]]
+ return AbstractTextureType::Texture_2D;
+
+ return m_shader_data->samplers[index].type;
+}
+
+void ShaderResource::ResetData()
+{
+ m_load_data = std::make_shared();
+ m_processing_load_data = false;
+}
+
+Resource::TaskComplete ShaderResource::CollectPrimaryData()
+{
+ const auto shader_data = m_shader_asset->GetData();
+ if (!shader_data) [[unlikely]]
+ {
+ return Resource::TaskComplete::No;
+ }
+ m_load_data->m_shader_data = shader_data;
+
+ return Resource::TaskComplete::Yes;
+}
+
+Resource::TaskComplete ShaderResource::ProcessData()
+{
+ class WorkItem final : public VideoCommon::AsyncShaderCompiler::WorkItem
+ {
+ public:
+ WorkItem(std::shared_ptr resource_data, VideoCommon::GXPipelineUid* uid,
+ u32 shader_bits, std::string_view preprocessor_settings)
+ : m_resource_data(std::move(resource_data)), m_uid(uid), m_shader_bits(shader_bits),
+ m_preprocessor_settings(preprocessor_settings)
+ {
+ }
+
+ bool Compile() override
+ {
+ const ShaderHostConfig shader_host_config{.bits = m_shader_bits};
+ auto actual_uid = ApplyDriverBugs(*m_uid);
+
+ ClearUnusedPixelShaderUidBits(g_backend_info.api_type, shader_host_config,
+ &actual_uid.ps_uid);
+ m_resource_data->m_needs_geometry_shader = shader_host_config.backend_geometry_shaders &&
+ !actual_uid.gs_uid.GetUidData()->IsPassthrough();
+
+ if (m_resource_data->m_needs_geometry_shader)
+ {
+ m_resource_data->m_geometry_shader =
+ CompileGeometryShader(actual_uid.gs_uid, g_backend_info.api_type, shader_host_config);
+ }
+ m_resource_data->m_pixel_shader =
+ CompilePixelShader(actual_uid.ps_uid, m_preprocessor_settings, g_backend_info.api_type,
+ shader_host_config, m_resource_data->m_shader_data.get());
+ m_resource_data->m_vertex_shader =
+ CompileVertexShader(actual_uid.vs_uid, m_preprocessor_settings, g_backend_info.api_type,
+ shader_host_config, *m_resource_data->m_shader_data);
+ m_resource_data->m_processing_finished = true;
+ return true;
+ }
+ void Retrieve() override {}
+
+ private:
+ std::shared_ptr m_resource_data;
+ VideoCommon::GXPipelineUid* m_uid;
+ u32 m_shader_bits;
+ std::string_view m_preprocessor_settings;
+ };
+
+ if (!m_processing_load_data)
+ {
+ std::string_view preprocessor_settings = m_preprocessor_settings;
+ auto wi = m_resource_context.shader_compiler->CreateWorkItem(
+ m_load_data, &m_uid, m_shader_host_config.bits, preprocessor_settings);
+
+ // We don't need priority, that is already handled by the resource system
+ m_resource_context.shader_compiler->QueueWorkItem(std::move(wi), 0);
+ m_processing_load_data = true;
+ }
+
+ if (!m_load_data->m_processing_finished)
+ return Resource::TaskComplete::No;
+
+ if (!m_load_data->IsCompiled())
+ return Resource::TaskComplete::Error;
+
+ std::swap(m_current_data, m_load_data);
+ return Resource::TaskComplete::Yes;
+}
+} // namespace VideoCommon
diff --git a/Source/Core/VideoCommon/Resources/ShaderResource.h b/Source/Core/VideoCommon/Resources/ShaderResource.h
new file mode 100644
index 0000000000..0f91cd3714
--- /dev/null
+++ b/Source/Core/VideoCommon/Resources/ShaderResource.h
@@ -0,0 +1,67 @@
+// Copyright 2025 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include
+
+#include "VideoCommon/Resources/Resource.h"
+
+#include "VideoCommon/Assets/ShaderAsset.h"
+#include "VideoCommon/GXPipelineTypes.h"
+#include "VideoCommon/ShaderGenCommon.h"
+
+namespace VideoCommon
+{
+class ShaderResource final : public Resource
+{
+public:
+ ShaderResource(Resource::ResourceContext resource_context, const GXPipelineUid& pipeline_uid,
+ const std::string& preprocessor_settings,
+ const ShaderHostConfig& shader_host_config);
+
+ class Data
+ {
+ public:
+ AbstractShader* GetVertexShader() const;
+ AbstractShader* GetPixelShader() const;
+ AbstractShader* GetGeometryShader() const;
+
+ bool IsCompiled() const;
+ AbstractTextureType GetTextureType(std::size_t index);
+
+ private:
+ friend class ShaderResource;
+ std::unique_ptr m_vertex_shader;
+ std::unique_ptr m_pixel_shader;
+ std::unique_ptr m_geometry_shader;
+ std::shared_ptr m_shader_data;
+ bool m_needs_geometry_shader = false;
+ std::atomic_bool m_processing_finished;
+ };
+
+ // Changes the shader host config. Shaders should be reloaded afterwards.
+ void SetHostConfig(const ShaderHostConfig& host_config);
+ const std::shared_ptr& GetData() const { return m_current_data; }
+
+ void MarkAsActive() override;
+ void MarkAsPending() override;
+
+private:
+ void ResetData() override;
+ Resource::TaskComplete CollectPrimaryData() override;
+ TaskComplete ProcessData() override;
+
+ // Note: asset cache owns the asset, we access as a reference
+ RasterSurfaceShaderAsset* m_shader_asset = nullptr;
+
+ std::shared_ptr m_current_data;
+ std::shared_ptr m_load_data;
+
+ bool m_processing_load_data = false;
+
+ ShaderHostConfig m_shader_host_config;
+ GXPipelineUid m_uid;
+ std::string m_preprocessor_settings;
+};
+} // namespace VideoCommon
diff --git a/Source/Core/VideoCommon/Resources/TextureAndSamplerResource.cpp b/Source/Core/VideoCommon/Resources/TextureAndSamplerResource.cpp
new file mode 100644
index 0000000000..735a64a357
--- /dev/null
+++ b/Source/Core/VideoCommon/Resources/TextureAndSamplerResource.cpp
@@ -0,0 +1,103 @@
+// Copyright 2025 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "VideoCommon/Resources/TextureAndSamplerResource.h"
+
+#include "VideoCommon/Assets/CustomAssetCache.h"
+#include "VideoCommon/Resources/TexturePool.h"
+
+namespace VideoCommon
+{
+TextureAndSamplerResource::TextureAndSamplerResource(Resource::ResourceContext resource_context)
+ : Resource(std::move(resource_context))
+{
+ m_texture_and_sampler_asset = m_resource_context.asset_cache->CreateAsset(
+ m_resource_context.primary_asset_id, m_resource_context.asset_library, this);
+}
+
+void TextureAndSamplerResource::MarkAsActive()
+{
+ m_resource_context.asset_cache->MarkAssetActive(m_texture_and_sampler_asset);
+}
+
+void TextureAndSamplerResource::MarkAsPending()
+{
+ m_resource_context.asset_cache->MarkAssetPending(m_texture_and_sampler_asset);
+}
+
+const std::shared_ptr& TextureAndSamplerResource::GetData() const
+{
+ return m_current_data;
+}
+
+void TextureAndSamplerResource::ResetData()
+{
+ m_load_data = std::make_shared();
+}
+
+Resource::TaskComplete TextureAndSamplerResource::CollectPrimaryData()
+{
+ m_load_data->m_texture_and_sampler_data = m_texture_and_sampler_asset->GetData();
+ if (!m_load_data->m_texture_and_sampler_data)
+ return Resource::TaskComplete::No;
+
+ auto& texture_data = m_load_data->m_texture_and_sampler_data->texture_data;
+ if (texture_data.m_slices.empty())
+ return Resource::TaskComplete::Error;
+
+ if (texture_data.m_slices[0].m_levels.empty())
+ return Resource::TaskComplete::Error;
+
+ const auto& first_level = texture_data.m_slices[0].m_levels[0];
+
+ auto& config = m_load_data->m_config;
+ config.format = first_level.format;
+ config.flags = 0;
+ config.layers = 1;
+ config.levels = 1;
+ config.type = m_load_data->m_texture_and_sampler_data->type;
+ config.samples = 1;
+
+ config.width = first_level.width;
+ config.height = first_level.height;
+
+ return Resource::TaskComplete::Yes;
+}
+
+Resource::TaskComplete TextureAndSamplerResource::ProcessData()
+{
+ auto texture = m_resource_context.texture_pool->AllocateTexture(m_load_data->m_config);
+ if (!texture) [[unlikely]]
+ return Resource::TaskComplete::Error;
+
+ m_load_data->m_texture = std::move(texture);
+
+ auto& texture_data = m_load_data->m_texture_and_sampler_data->texture_data;
+ for (std::size_t slice_index = 0; slice_index < texture_data.m_slices.size(); slice_index++)
+ {
+ auto& slice = texture_data.m_slices[slice_index];
+ for (u32 level_index = 0; level_index < static_cast(slice.m_levels.size()); ++level_index)
+ {
+ auto& level = slice.m_levels[level_index];
+ m_load_data->m_texture->Load(level_index, level.width, level.height, level.row_length,
+ level.data.data(), level.data.size(),
+ static_cast(slice_index));
+ }
+ }
+ std::swap(m_current_data, m_load_data);
+
+ // Release old data back to the pool
+ if (m_load_data)
+ m_resource_context.texture_pool->ReleaseTexture(std::move(m_load_data->m_texture));
+
+ return Resource::TaskComplete::Yes;
+}
+
+void TextureAndSamplerResource::OnUnloadRequested()
+{
+ if (!m_current_data)
+ return;
+ m_resource_context.texture_pool->ReleaseTexture(std::move(m_current_data->m_texture));
+ m_current_data = nullptr;
+}
+} // namespace VideoCommon
diff --git a/Source/Core/VideoCommon/Resources/TextureAndSamplerResource.h b/Source/Core/VideoCommon/Resources/TextureAndSamplerResource.h
new file mode 100644
index 0000000000..3641b8eb91
--- /dev/null
+++ b/Source/Core/VideoCommon/Resources/TextureAndSamplerResource.h
@@ -0,0 +1,49 @@
+// Copyright 2025 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "VideoCommon/Resources/Resource.h"
+
+#include "VideoCommon/AbstractTexture.h"
+#include "VideoCommon/Assets/TextureAsset.h"
+
+namespace VideoCommon
+{
+class TextureAndSamplerResource final : public Resource
+{
+public:
+ explicit TextureAndSamplerResource(Resource::ResourceContext resource_context);
+ void MarkAsActive() override;
+ void MarkAsPending() override;
+
+ class Data
+ {
+ public:
+ AbstractTexture* GetTexture() const { return m_texture.get(); }
+ const SamplerState& GetSampler() const { return m_texture_and_sampler_data->sampler; }
+
+ private:
+ friend class TextureAndSamplerResource;
+
+ std::shared_ptr m_texture_and_sampler_data;
+ std::unique_ptr m_texture;
+ TextureConfig m_config;
+ };
+
+ const std::shared_ptr& GetData() const;
+
+private:
+ void ResetData() override;
+ TaskComplete CollectPrimaryData() override;
+ TaskComplete ProcessData() override;
+
+ void OnUnloadRequested() override;
+
+ // Note: asset cache owns the asset, we access as a reference
+ TextureAndSamplerAsset* m_texture_and_sampler_asset = nullptr;
+
+ std::shared_ptr m_current_data;
+ std::shared_ptr m_load_data;
+};
+} // namespace VideoCommon