rasterizer_cache: Introduce TextureRuntime and separate CachedSurface
* This commit aims to both continue the rasterizer cache cleanup by separating CachedSurface into a dedicated header and to start weeding out the raw OpenGL code from the cache. * The latter is achieved by abstracting most texture operations in a new class called TextureRuntime. This has many benefits such as making it easier to port the functionality to other graphics APIs and the removal of the need to pass (read/draw) framebuffer handles everywhere. The filterer and reinterpreter get their own sets of FBOs due to this, something that might be a performance win since it reduces the state switching overhead on the runtime FBOs.
This commit is contained in:
parent
199671301d
commit
17ad594a62
23 changed files with 1176 additions and 1012 deletions
|
@ -23,6 +23,8 @@ add_library(video_core STATIC
|
||||||
regs_texturing.h
|
regs_texturing.h
|
||||||
renderer_base.cpp
|
renderer_base.cpp
|
||||||
renderer_base.h
|
renderer_base.h
|
||||||
|
rasterizer_cache/cached_surface.cpp
|
||||||
|
rasterizer_cache/cached_surface.h
|
||||||
rasterizer_cache/morton_swizzle.h
|
rasterizer_cache/morton_swizzle.h
|
||||||
rasterizer_cache/pixel_format.h
|
rasterizer_cache/pixel_format.h
|
||||||
rasterizer_cache/rasterizer_cache.cpp
|
rasterizer_cache/rasterizer_cache.cpp
|
||||||
|
@ -32,6 +34,8 @@ add_library(video_core STATIC
|
||||||
rasterizer_cache/rasterizer_cache_utils.h
|
rasterizer_cache/rasterizer_cache_utils.h
|
||||||
rasterizer_cache/surface_params.cpp
|
rasterizer_cache/surface_params.cpp
|
||||||
rasterizer_cache/surface_params.h
|
rasterizer_cache/surface_params.h
|
||||||
|
rasterizer_cache/texture_runtime.cpp
|
||||||
|
rasterizer_cache/texture_runtime.h
|
||||||
renderer_opengl/frame_dumper_opengl.cpp
|
renderer_opengl/frame_dumper_opengl.cpp
|
||||||
renderer_opengl/frame_dumper_opengl.h
|
renderer_opengl/frame_dumper_opengl.h
|
||||||
renderer_opengl/gl_rasterizer.cpp
|
renderer_opengl/gl_rasterizer.cpp
|
||||||
|
|
479
src/video_core/rasterizer_cache/cached_surface.cpp
Normal file
479
src/video_core/rasterizer_cache/cached_surface.cpp
Normal file
|
@ -0,0 +1,479 @@
|
||||||
|
// Copyright 2022 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include "common/microprofile.h"
|
||||||
|
#include "common/texture.h"
|
||||||
|
#include "common/scope_exit.h"
|
||||||
|
#include "core/core.h"
|
||||||
|
#include "video_core/rasterizer_cache/cached_surface.h"
|
||||||
|
#include "video_core/rasterizer_cache/morton_swizzle.h"
|
||||||
|
#include "video_core/rasterizer_cache/rasterizer_cache.h"
|
||||||
|
#include "video_core/renderer_opengl/texture_filters/texture_filterer.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_state.h"
|
||||||
|
#include "video_core/renderer_opengl/texture_downloader_es.h"
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
|
||||||
|
static Aspect ToAspect(SurfaceType type) {
|
||||||
|
switch (type) {
|
||||||
|
case SurfaceType::Color:
|
||||||
|
case SurfaceType::Texture:
|
||||||
|
case SurfaceType::Fill:
|
||||||
|
return Aspect::Color;
|
||||||
|
case SurfaceType::Depth:
|
||||||
|
return Aspect::Depth;
|
||||||
|
case SurfaceType::DepthStencil:
|
||||||
|
return Aspect::DepthStencil;
|
||||||
|
default:
|
||||||
|
LOG_CRITICAL(Render_OpenGL, "Unknown SurfaceType {}", type);
|
||||||
|
UNREACHABLE();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Aspect::Color;
|
||||||
|
}
|
||||||
|
|
||||||
|
CachedSurface::~CachedSurface() {
|
||||||
|
if (texture.handle) {
|
||||||
|
auto tag = is_custom ? HostTextureTag{GetFormatTuple(PixelFormat::RGBA8),
|
||||||
|
custom_tex_info.width, custom_tex_info.height}
|
||||||
|
: HostTextureTag{GetFormatTuple(pixel_format), GetScaledWidth(),
|
||||||
|
GetScaledHeight()};
|
||||||
|
|
||||||
|
owner.host_texture_recycler.emplace(tag, std::move(texture));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MICROPROFILE_DEFINE(OpenGL_SurfaceLoad, "OpenGL", "Surface Load", MP_RGB(128, 192, 64));
|
||||||
|
void CachedSurface::LoadGLBuffer(PAddr load_start, PAddr load_end) {
|
||||||
|
ASSERT(type != SurfaceType::Fill);
|
||||||
|
const bool need_swap =
|
||||||
|
GLES && (pixel_format == PixelFormat::RGBA8 || pixel_format == PixelFormat::RGB8);
|
||||||
|
|
||||||
|
const u8* const texture_src_data = VideoCore::g_memory->GetPhysicalPointer(addr);
|
||||||
|
if (texture_src_data == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (gl_buffer.empty()) {
|
||||||
|
gl_buffer.resize(width * height * GetBytesPerPixel(pixel_format));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Should probably be done in ::Memory:: and check for other regions too
|
||||||
|
if (load_start < Memory::VRAM_VADDR_END && load_end > Memory::VRAM_VADDR_END)
|
||||||
|
load_end = Memory::VRAM_VADDR_END;
|
||||||
|
|
||||||
|
if (load_start < Memory::VRAM_VADDR && load_end > Memory::VRAM_VADDR)
|
||||||
|
load_start = Memory::VRAM_VADDR;
|
||||||
|
|
||||||
|
MICROPROFILE_SCOPE(OpenGL_SurfaceLoad);
|
||||||
|
|
||||||
|
ASSERT(load_start >= addr && load_end <= end);
|
||||||
|
const u32 start_offset = load_start - addr;
|
||||||
|
|
||||||
|
if (!is_tiled) {
|
||||||
|
ASSERT(type == SurfaceType::Color);
|
||||||
|
if (need_swap) {
|
||||||
|
// TODO(liushuyu): check if the byteswap here is 100% correct
|
||||||
|
// cannot fully test this
|
||||||
|
if (pixel_format == PixelFormat::RGBA8) {
|
||||||
|
for (std::size_t i = start_offset; i < load_end - addr; i += 4) {
|
||||||
|
gl_buffer[i] = texture_src_data[i + 3];
|
||||||
|
gl_buffer[i + 1] = texture_src_data[i + 2];
|
||||||
|
gl_buffer[i + 2] = texture_src_data[i + 1];
|
||||||
|
gl_buffer[i + 3] = texture_src_data[i];
|
||||||
|
}
|
||||||
|
} else if (pixel_format == PixelFormat::RGB8) {
|
||||||
|
for (std::size_t i = start_offset; i < load_end - addr; i += 3) {
|
||||||
|
gl_buffer[i] = texture_src_data[i + 2];
|
||||||
|
gl_buffer[i + 1] = texture_src_data[i + 1];
|
||||||
|
gl_buffer[i + 2] = texture_src_data[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::memcpy(&gl_buffer[start_offset], texture_src_data + start_offset,
|
||||||
|
load_end - load_start);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (type == SurfaceType::Texture) {
|
||||||
|
Pica::Texture::TextureInfo tex_info{};
|
||||||
|
tex_info.width = width;
|
||||||
|
tex_info.height = height;
|
||||||
|
tex_info.format = static_cast<Pica::TexturingRegs::TextureFormat>(pixel_format);
|
||||||
|
tex_info.SetDefaultStride();
|
||||||
|
tex_info.physical_address = addr;
|
||||||
|
|
||||||
|
const SurfaceInterval load_interval(load_start, load_end);
|
||||||
|
const auto rect = GetSubRect(FromInterval(load_interval));
|
||||||
|
ASSERT(FromInterval(load_interval).GetInterval() == load_interval);
|
||||||
|
|
||||||
|
for (unsigned y = rect.bottom; y < rect.top; ++y) {
|
||||||
|
for (unsigned x = rect.left; x < rect.right; ++x) {
|
||||||
|
auto vec4 =
|
||||||
|
Pica::Texture::LookupTexture(texture_src_data, x, height - 1 - y, tex_info);
|
||||||
|
const std::size_t offset = (x + (width * y)) * 4;
|
||||||
|
std::memcpy(&gl_buffer[offset], vec4.AsArray(), 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
morton_to_gl_fns[static_cast<std::size_t>(pixel_format)](stride, height, &gl_buffer[0],
|
||||||
|
addr, load_start, load_end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MICROPROFILE_DEFINE(OpenGL_SurfaceFlush, "OpenGL", "Surface Flush", MP_RGB(128, 192, 64));
|
||||||
|
void CachedSurface::FlushGLBuffer(PAddr flush_start, PAddr flush_end) {
|
||||||
|
u8* const dst_buffer = VideoCore::g_memory->GetPhysicalPointer(addr);
|
||||||
|
if (dst_buffer == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ASSERT(gl_buffer.size() == width * height * GetBytesPerPixel(pixel_format));
|
||||||
|
|
||||||
|
// TODO: Should probably be done in ::Memory:: and check for other regions too
|
||||||
|
// same as loadglbuffer()
|
||||||
|
if (flush_start < Memory::VRAM_VADDR_END && flush_end > Memory::VRAM_VADDR_END)
|
||||||
|
flush_end = Memory::VRAM_VADDR_END;
|
||||||
|
|
||||||
|
if (flush_start < Memory::VRAM_VADDR && flush_end > Memory::VRAM_VADDR)
|
||||||
|
flush_start = Memory::VRAM_VADDR;
|
||||||
|
|
||||||
|
MICROPROFILE_SCOPE(OpenGL_SurfaceFlush);
|
||||||
|
|
||||||
|
ASSERT(flush_start >= addr && flush_end <= end);
|
||||||
|
const u32 start_offset = flush_start - addr;
|
||||||
|
const u32 end_offset = flush_end - addr;
|
||||||
|
|
||||||
|
if (type == SurfaceType::Fill) {
|
||||||
|
const u32 coarse_start_offset = start_offset - (start_offset % fill_size);
|
||||||
|
const u32 backup_bytes = start_offset % fill_size;
|
||||||
|
std::array<u8, 4> backup_data;
|
||||||
|
if (backup_bytes)
|
||||||
|
std::memcpy(&backup_data[0], &dst_buffer[coarse_start_offset], backup_bytes);
|
||||||
|
|
||||||
|
for (u32 offset = coarse_start_offset; offset < end_offset; offset += fill_size) {
|
||||||
|
std::memcpy(&dst_buffer[offset], &fill_data[0],
|
||||||
|
std::min(fill_size, end_offset - offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backup_bytes)
|
||||||
|
std::memcpy(&dst_buffer[coarse_start_offset], &backup_data[0], backup_bytes);
|
||||||
|
} else if (!is_tiled) {
|
||||||
|
ASSERT(type == SurfaceType::Color);
|
||||||
|
if (pixel_format == PixelFormat::RGBA8 && GLES) {
|
||||||
|
for (std::size_t i = start_offset; i < flush_end - addr; i += 4) {
|
||||||
|
dst_buffer[i] = gl_buffer[i + 3];
|
||||||
|
dst_buffer[i + 1] = gl_buffer[i + 2];
|
||||||
|
dst_buffer[i + 2] = gl_buffer[i + 1];
|
||||||
|
dst_buffer[i + 3] = gl_buffer[i];
|
||||||
|
}
|
||||||
|
} else if (pixel_format == PixelFormat::RGB8 && GLES) {
|
||||||
|
for (std::size_t i = start_offset; i < flush_end - addr; i += 3) {
|
||||||
|
dst_buffer[i] = gl_buffer[i + 2];
|
||||||
|
dst_buffer[i + 1] = gl_buffer[i + 1];
|
||||||
|
dst_buffer[i + 2] = gl_buffer[i];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::memcpy(dst_buffer + start_offset, &gl_buffer[start_offset],
|
||||||
|
flush_end - flush_start);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gl_to_morton_fns[static_cast<std::size_t>(pixel_format)](stride, height, &gl_buffer[0],
|
||||||
|
addr, flush_start, flush_end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CachedSurface::LoadCustomTexture(u64 tex_hash) {
|
||||||
|
auto& custom_tex_cache = Core::System::GetInstance().CustomTexCache();
|
||||||
|
const auto& image_interface = Core::System::GetInstance().GetImageInterface();
|
||||||
|
|
||||||
|
if (custom_tex_cache.IsTextureCached(tex_hash)) {
|
||||||
|
custom_tex_info = custom_tex_cache.LookupTexture(tex_hash);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!custom_tex_cache.CustomTextureExists(tex_hash)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& path_info = custom_tex_cache.LookupTexturePathInfo(tex_hash);
|
||||||
|
if (!image_interface->DecodePNG(custom_tex_info.tex, custom_tex_info.width,
|
||||||
|
custom_tex_info.height, path_info.path)) {
|
||||||
|
LOG_ERROR(Render_OpenGL, "Failed to load custom texture {}", path_info.path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::bitset<32> width_bits(custom_tex_info.width);
|
||||||
|
const std::bitset<32> height_bits(custom_tex_info.height);
|
||||||
|
if (width_bits.count() != 1 || height_bits.count() != 1) {
|
||||||
|
LOG_ERROR(Render_OpenGL, "Texture {} size is not a power of 2", path_info.path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG(Render_OpenGL, "Loaded custom texture from {}", path_info.path);
|
||||||
|
Common::FlipRGBA8Texture(custom_tex_info.tex, custom_tex_info.width, custom_tex_info.height);
|
||||||
|
custom_tex_cache.CacheTexture(tex_hash, custom_tex_info.tex, custom_tex_info.width,
|
||||||
|
custom_tex_info.height);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CachedSurface::DumpTexture(GLuint target_tex, u64 tex_hash) {
|
||||||
|
// Make sure the texture size is a power of 2
|
||||||
|
// If not, the surface is actually a framebuffer
|
||||||
|
std::bitset<32> width_bits(width);
|
||||||
|
std::bitset<32> height_bits(height);
|
||||||
|
if (width_bits.count() != 1 || height_bits.count() != 1) {
|
||||||
|
LOG_WARNING(Render_OpenGL, "Not dumping {:016X} because size isn't a power of 2 ({}x{})",
|
||||||
|
tex_hash, width, height);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump texture to RGBA8 and encode as PNG
|
||||||
|
const auto& image_interface = Core::System::GetInstance().GetImageInterface();
|
||||||
|
auto& custom_tex_cache = Core::System::GetInstance().CustomTexCache();
|
||||||
|
std::string dump_path =
|
||||||
|
fmt::format("{}textures/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir),
|
||||||
|
Core::System::GetInstance().Kernel().GetCurrentProcess()->codeset->program_id);
|
||||||
|
if (!FileUtil::CreateFullPath(dump_path)) {
|
||||||
|
LOG_ERROR(Render, "Unable to create {}", dump_path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dump_path += fmt::format("tex1_{}x{}_{:016X}_{}.png", width, height, tex_hash, pixel_format);
|
||||||
|
if (!custom_tex_cache.IsTextureDumped(tex_hash) && !FileUtil::Exists(dump_path)) {
|
||||||
|
custom_tex_cache.SetTextureDumped(tex_hash);
|
||||||
|
|
||||||
|
LOG_INFO(Render_OpenGL, "Dumping texture to {}", dump_path);
|
||||||
|
std::vector<u8> decoded_texture;
|
||||||
|
decoded_texture.resize(width * height * 4);
|
||||||
|
OpenGLState state = OpenGLState::GetCurState();
|
||||||
|
GLuint old_texture = state.texture_units[0].texture_2d;
|
||||||
|
state.Apply();
|
||||||
|
/*
|
||||||
|
GetTexImageOES is used even if not using OpenGL ES to work around a small issue that
|
||||||
|
happens if using custom textures with texture dumping at the same.
|
||||||
|
Let's say there's 2 textures that are both 32x32 and one of them gets replaced with a
|
||||||
|
higher quality 256x256 texture. If the 256x256 texture is displayed first and the
|
||||||
|
32x32 texture gets uploaded to the same underlying OpenGL texture, the 32x32 texture
|
||||||
|
will appear in the corner of the 256x256 texture. If texture dumping is enabled and
|
||||||
|
the 32x32 is undumped, Citra will attempt to dump it. Since the underlying OpenGL
|
||||||
|
texture is still 256x256, Citra crashes because it thinks the texture is only 32x32.
|
||||||
|
GetTexImageOES conveniently only dumps the specified region, and works on both
|
||||||
|
desktop and ES.
|
||||||
|
*/
|
||||||
|
// if the backend isn't OpenGL ES, this won't be initialized yet
|
||||||
|
if (!owner.texture_downloader_es) {
|
||||||
|
owner.texture_downloader_es = std::make_unique<TextureDownloaderES>(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
owner.texture_downloader_es->GetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||||
|
height, width, &decoded_texture[0]);
|
||||||
|
state.texture_units[0].texture_2d = old_texture;
|
||||||
|
state.Apply();
|
||||||
|
Common::FlipRGBA8Texture(decoded_texture, width, height);
|
||||||
|
if (!image_interface->EncodePNG(dump_path, decoded_texture, width, height))
|
||||||
|
LOG_ERROR(Render_OpenGL, "Failed to save decoded texture");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MICROPROFILE_DEFINE(OpenGL_TextureUL, "OpenGL", "Texture Upload", MP_RGB(128, 192, 64));
|
||||||
|
void CachedSurface::UploadGLTexture(Common::Rectangle<u32> rect) {
|
||||||
|
if (type == SurfaceType::Fill) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MICROPROFILE_SCOPE(OpenGL_TextureUL);
|
||||||
|
ASSERT(gl_buffer.size() == width * height * GetBytesPerPixel(pixel_format));
|
||||||
|
|
||||||
|
u64 tex_hash = 0;
|
||||||
|
|
||||||
|
if (Settings::values.dump_textures || Settings::values.custom_textures) {
|
||||||
|
tex_hash = Common::ComputeHash64(gl_buffer.data(), gl_buffer.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Settings::values.custom_textures) {
|
||||||
|
is_custom = LoadCustomTexture(tex_hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load data from memory to the surface
|
||||||
|
GLint x0 = static_cast<GLint>(rect.left);
|
||||||
|
GLint y0 = static_cast<GLint>(rect.bottom);
|
||||||
|
std::size_t buffer_offset = (y0 * stride + x0) * GetBytesPerPixel(pixel_format);
|
||||||
|
|
||||||
|
const FormatTuple& tuple = GetFormatTuple(pixel_format);
|
||||||
|
GLuint target_tex = texture.handle;
|
||||||
|
|
||||||
|
// If not 1x scale, create 1x texture that we will blit from to replace texture subrect in
|
||||||
|
// surface
|
||||||
|
OGLTexture unscaled_tex;
|
||||||
|
if (res_scale != 1) {
|
||||||
|
x0 = 0;
|
||||||
|
y0 = 0;
|
||||||
|
|
||||||
|
if (is_custom) {
|
||||||
|
const auto& tuple = GetFormatTuple(PixelFormat::RGBA8);
|
||||||
|
unscaled_tex = owner.AllocateSurfaceTexture(tuple, custom_tex_info.width,
|
||||||
|
custom_tex_info.height);
|
||||||
|
} else {
|
||||||
|
unscaled_tex = owner.AllocateSurfaceTexture(tuple, rect.GetWidth(), rect.GetHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
target_tex = unscaled_tex.handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenGLState cur_state = OpenGLState::GetCurState();
|
||||||
|
|
||||||
|
GLuint old_tex = cur_state.texture_units[0].texture_2d;
|
||||||
|
cur_state.texture_units[0].texture_2d = target_tex;
|
||||||
|
cur_state.Apply();
|
||||||
|
|
||||||
|
// Ensure no bad interactions with GL_UNPACK_ALIGNMENT
|
||||||
|
ASSERT(stride * GetBytesPerPixel(pixel_format) % 4 == 0);
|
||||||
|
if (is_custom) {
|
||||||
|
if (res_scale == 1) {
|
||||||
|
texture = owner.AllocateSurfaceTexture(GetFormatTuple(PixelFormat::RGBA8),
|
||||||
|
custom_tex_info.width, custom_tex_info.height);
|
||||||
|
cur_state.texture_units[0].texture_2d = texture.handle;
|
||||||
|
cur_state.Apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always going to be using rgba8
|
||||||
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast<GLint>(custom_tex_info.width));
|
||||||
|
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, x0, y0, custom_tex_info.width, custom_tex_info.height,
|
||||||
|
GL_RGBA, GL_UNSIGNED_BYTE, custom_tex_info.tex.data());
|
||||||
|
} else {
|
||||||
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast<GLint>(stride));
|
||||||
|
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, x0, y0, static_cast<GLsizei>(rect.GetWidth()),
|
||||||
|
static_cast<GLsizei>(rect.GetHeight()), tuple.format, tuple.type,
|
||||||
|
&gl_buffer[buffer_offset]);
|
||||||
|
}
|
||||||
|
|
||||||
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||||
|
if (Settings::values.dump_textures && !is_custom) {
|
||||||
|
DumpTexture(target_tex, tex_hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
cur_state.texture_units[0].texture_2d = old_tex;
|
||||||
|
cur_state.Apply();
|
||||||
|
|
||||||
|
if (res_scale != 1) {
|
||||||
|
auto scaled_rect = rect;
|
||||||
|
scaled_rect.left *= res_scale;
|
||||||
|
scaled_rect.top *= res_scale;
|
||||||
|
scaled_rect.right *= res_scale;
|
||||||
|
scaled_rect.bottom *= res_scale;
|
||||||
|
|
||||||
|
const u32 width = is_custom ? custom_tex_info.width : rect.GetWidth();
|
||||||
|
const u32 height = is_custom ? custom_tex_info.height : rect.GetHeight();
|
||||||
|
const Common::Rectangle<u32> from_rect{0, height, width, 0};
|
||||||
|
|
||||||
|
if (!owner.texture_filterer->Filter(unscaled_tex, from_rect, texture, scaled_rect, type)) {
|
||||||
|
const Aspect aspect = ToAspect(type);
|
||||||
|
runtime.BlitTextures(unscaled_tex, {aspect, from_rect},
|
||||||
|
texture, {aspect, scaled_rect});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InvalidateAllWatcher();
|
||||||
|
}
|
||||||
|
|
||||||
|
MICROPROFILE_DEFINE(OpenGL_TextureDL, "OpenGL", "Texture Download", MP_RGB(128, 192, 64));
|
||||||
|
void CachedSurface::DownloadGLTexture(const Common::Rectangle<u32>& rect) {
|
||||||
|
if (type == SurfaceType::Fill) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MICROPROFILE_SCOPE(OpenGL_TextureDL);
|
||||||
|
|
||||||
|
if (gl_buffer.empty()) {
|
||||||
|
gl_buffer.resize(width * height * GetBytesPerPixel(pixel_format));
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenGLState state = OpenGLState::GetCurState();
|
||||||
|
OpenGLState prev_state = state;
|
||||||
|
SCOPE_EXIT({ prev_state.Apply(); });
|
||||||
|
|
||||||
|
const FormatTuple& tuple = GetFormatTuple(pixel_format);
|
||||||
|
|
||||||
|
// Ensure no bad interactions with GL_PACK_ALIGNMENT
|
||||||
|
ASSERT(stride * GetBytesPerPixel(pixel_format) % 4 == 0);
|
||||||
|
glPixelStorei(GL_PACK_ROW_LENGTH, static_cast<GLint>(stride));
|
||||||
|
const std::size_t buffer_offset = (rect.bottom * stride + rect.left) * GetBytesPerPixel(pixel_format);
|
||||||
|
|
||||||
|
// If not 1x scale, blit scaled texture to a new 1x texture and use that to flush
|
||||||
|
const Aspect aspect = ToAspect(type);
|
||||||
|
if (res_scale != 1) {
|
||||||
|
auto scaled_rect = rect;
|
||||||
|
scaled_rect.left *= res_scale;
|
||||||
|
scaled_rect.top *= res_scale;
|
||||||
|
scaled_rect.right *= res_scale;
|
||||||
|
scaled_rect.bottom *= res_scale;
|
||||||
|
|
||||||
|
const Common::Rectangle<u32> unscaled_tex_rect{0, rect.GetHeight(), rect.GetWidth(), 0};
|
||||||
|
auto unscaled_tex = owner.AllocateSurfaceTexture(tuple, rect.GetWidth(),
|
||||||
|
rect.GetHeight());
|
||||||
|
// Blit scaled texture to the unscaled one
|
||||||
|
runtime.BlitTextures(texture, {aspect, scaled_rect},
|
||||||
|
unscaled_tex, {aspect, unscaled_tex_rect});
|
||||||
|
|
||||||
|
state.texture_units[0].texture_2d = unscaled_tex.handle;
|
||||||
|
state.Apply();
|
||||||
|
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
if (GLES) {
|
||||||
|
owner.texture_downloader_es->GetTexImage(GL_TEXTURE_2D, 0, tuple.format, tuple.type,
|
||||||
|
rect.GetHeight(), rect.GetWidth(),
|
||||||
|
&gl_buffer[buffer_offset]);
|
||||||
|
} else {
|
||||||
|
glGetTexImage(GL_TEXTURE_2D, 0, tuple.format, tuple.type, &gl_buffer[buffer_offset]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
runtime.ReadTexture(texture, {aspect, rect}, tuple, gl_buffer.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CachedSurface::CanFill(const SurfaceParams& dest_surface, SurfaceInterval fill_interval) const {
|
||||||
|
if (type == SurfaceType::Fill && IsRegionValid(fill_interval) &&
|
||||||
|
boost::icl::first(fill_interval) >= addr &&
|
||||||
|
boost::icl::last_next(fill_interval) <= end && // dest_surface is within our fill range
|
||||||
|
dest_surface.FromInterval(fill_interval).GetInterval() ==
|
||||||
|
fill_interval) { // make sure interval is a rectangle in dest surface
|
||||||
|
if (fill_size * 8 != dest_surface.GetFormatBpp()) {
|
||||||
|
// Check if bits repeat for our fill_size
|
||||||
|
const u32 dest_bytes_per_pixel = std::max(dest_surface.GetFormatBpp() / 8, 1u);
|
||||||
|
std::vector<u8> fill_test(fill_size * dest_bytes_per_pixel);
|
||||||
|
|
||||||
|
for (u32 i = 0; i < dest_bytes_per_pixel; ++i)
|
||||||
|
std::memcpy(&fill_test[i * fill_size], &fill_data[0], fill_size);
|
||||||
|
|
||||||
|
for (u32 i = 0; i < fill_size; ++i)
|
||||||
|
if (std::memcmp(&fill_test[dest_bytes_per_pixel * i], &fill_test[0],
|
||||||
|
dest_bytes_per_pixel) != 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (dest_surface.GetFormatBpp() == 4 && (fill_test[0] & 0xF) != (fill_test[0] >> 4))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CachedSurface::CanCopy(const SurfaceParams& dest_surface, SurfaceInterval copy_interval) const {
|
||||||
|
SurfaceParams subrect_params = dest_surface.FromInterval(copy_interval);
|
||||||
|
ASSERT(subrect_params.GetInterval() == copy_interval);
|
||||||
|
if (CanSubRect(subrect_params))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (CanFill(dest_surface, copy_interval))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace OpenGL
|
136
src/video_core/rasterizer_cache/cached_surface.h
Normal file
136
src/video_core/rasterizer_cache/cached_surface.h
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
// Copyright 2022 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "common/assert.h"
|
||||||
|
#include "core/custom_tex_cache.h"
|
||||||
|
#include "video_core/rasterizer_cache/surface_params.h"
|
||||||
|
#include "video_core/rasterizer_cache/texture_runtime.h"
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A watcher that notifies whether a cached surface has been changed. This is useful for caching
|
||||||
|
* surface collection objects, including texture cube and mipmap.
|
||||||
|
*/
|
||||||
|
class SurfaceWatcher {
|
||||||
|
friend class CachedSurface;
|
||||||
|
public:
|
||||||
|
explicit SurfaceWatcher(std::weak_ptr<CachedSurface>&& surface) :
|
||||||
|
surface(std::move(surface)) {}
|
||||||
|
|
||||||
|
/// Checks whether the surface has been changed.
|
||||||
|
bool IsValid() const {
|
||||||
|
return !surface.expired() && valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks that the content of the referencing surface has been updated to the watcher user.
|
||||||
|
void Validate() {
|
||||||
|
ASSERT(!surface.expired());
|
||||||
|
valid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the referencing surface. Returns null if the surface has been destroyed
|
||||||
|
Surface Get() const {
|
||||||
|
return surface.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::weak_ptr<CachedSurface> surface;
|
||||||
|
bool valid = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class RasterizerCacheOpenGL;
|
||||||
|
|
||||||
|
class CachedSurface : public SurfaceParams, public std::enable_shared_from_this<CachedSurface> {
|
||||||
|
public:
|
||||||
|
CachedSurface(RasterizerCacheOpenGL& owner, TextureRuntime& runtime) :
|
||||||
|
owner(owner), runtime(runtime) {}
|
||||||
|
~CachedSurface();
|
||||||
|
|
||||||
|
/// Read/Write data in 3DS memory to/from gl_buffer
|
||||||
|
void LoadGLBuffer(PAddr load_start, PAddr load_end);
|
||||||
|
void FlushGLBuffer(PAddr flush_start, PAddr flush_end);
|
||||||
|
|
||||||
|
/// Custom texture loading and dumping
|
||||||
|
bool LoadCustomTexture(u64 tex_hash);
|
||||||
|
void DumpTexture(GLuint target_tex, u64 tex_hash);
|
||||||
|
|
||||||
|
/// Upload/Download data in gl_buffer in/to this surface's texture
|
||||||
|
void UploadGLTexture(Common::Rectangle<u32> rect);
|
||||||
|
void DownloadGLTexture(const Common::Rectangle<u32>& rect);
|
||||||
|
|
||||||
|
bool CanFill(const SurfaceParams& dest_surface, SurfaceInterval fill_interval) const;
|
||||||
|
bool CanCopy(const SurfaceParams& dest_surface, SurfaceInterval copy_interval) const;
|
||||||
|
|
||||||
|
bool IsRegionValid(SurfaceInterval interval) const {
|
||||||
|
return (invalid_regions.find(interval) == invalid_regions.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsSurfaceFullyInvalid() const {
|
||||||
|
auto interval = GetInterval();
|
||||||
|
return *invalid_regions.equal_range(interval).first == interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<SurfaceWatcher> CreateWatcher() {
|
||||||
|
auto watcher = std::make_shared<SurfaceWatcher>(weak_from_this());
|
||||||
|
watchers.push_front(watcher);
|
||||||
|
return watcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InvalidateAllWatcher() {
|
||||||
|
for (const auto& watcher : watchers) {
|
||||||
|
if (auto locked = watcher.lock()) {
|
||||||
|
locked->valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnlinkAllWatcher() {
|
||||||
|
for (const auto& watcher : watchers) {
|
||||||
|
if (auto locked = watcher.lock()) {
|
||||||
|
locked->valid = false;
|
||||||
|
locked->surface.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watchers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
bool registered = false;
|
||||||
|
SurfaceRegions invalid_regions;
|
||||||
|
std::vector<u8> gl_buffer;
|
||||||
|
|
||||||
|
// Number of bytes to read from fill_data
|
||||||
|
u32 fill_size = 0;
|
||||||
|
std::array<u8, 4> fill_data;
|
||||||
|
OGLTexture texture;
|
||||||
|
|
||||||
|
// level_watchers[i] watches the (i+1)-th level mipmap source surface
|
||||||
|
std::array<std::shared_ptr<SurfaceWatcher>, 7> level_watchers;
|
||||||
|
u32 max_level = 0;
|
||||||
|
|
||||||
|
// Information about custom textures
|
||||||
|
bool is_custom = false;
|
||||||
|
Core::CustomTexInfo custom_tex_info;
|
||||||
|
|
||||||
|
private:
|
||||||
|
RasterizerCacheOpenGL& owner;
|
||||||
|
TextureRuntime& runtime;
|
||||||
|
std::list<std::weak_ptr<SurfaceWatcher>> watchers;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CachedTextureCube {
|
||||||
|
OGLTexture texture;
|
||||||
|
u16 res_scale = 1;
|
||||||
|
std::shared_ptr<SurfaceWatcher> px;
|
||||||
|
std::shared_ptr<SurfaceWatcher> nx;
|
||||||
|
std::shared_ptr<SurfaceWatcher> py;
|
||||||
|
std::shared_ptr<SurfaceWatcher> ny;
|
||||||
|
std::shared_ptr<SurfaceWatcher> pz;
|
||||||
|
std::shared_ptr<SurfaceWatcher> nz;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace OpenGL
|
|
@ -10,6 +10,8 @@
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
|
constexpr u32 PIXEL_FORMAT_COUNT = 18;
|
||||||
|
|
||||||
enum class PixelFormat : u8 {
|
enum class PixelFormat : u8 {
|
||||||
// First 5 formats are shared between textures and color buffers
|
// First 5 formats are shared between textures and color buffers
|
||||||
RGBA8 = 0,
|
RGBA8 = 0,
|
||||||
|
@ -43,7 +45,7 @@ enum class SurfaceType {
|
||||||
Invalid = 5
|
Invalid = 5
|
||||||
};
|
};
|
||||||
|
|
||||||
static constexpr std::string_view PixelFormatAsString(PixelFormat format) {
|
inline constexpr std::string_view PixelFormatAsString(PixelFormat format) {
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case PixelFormat::RGBA8:
|
case PixelFormat::RGBA8:
|
||||||
return "RGBA8";
|
return "RGBA8";
|
||||||
|
@ -84,23 +86,23 @@ static constexpr std::string_view PixelFormatAsString(PixelFormat format) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static constexpr PixelFormat PixelFormatFromTextureFormat(Pica::TexturingRegs::TextureFormat format) {
|
inline constexpr PixelFormat PixelFormatFromTextureFormat(Pica::TexturingRegs::TextureFormat format) {
|
||||||
const u32 format_index = static_cast<u32>(format);
|
const u32 format_index = static_cast<u32>(format);
|
||||||
return (format_index < 14) ? static_cast<PixelFormat>(format) : PixelFormat::Invalid;
|
return (format_index < 14) ? static_cast<PixelFormat>(format) : PixelFormat::Invalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
static constexpr PixelFormat PixelFormatFromColorFormat(Pica::FramebufferRegs::ColorFormat format) {
|
inline constexpr PixelFormat PixelFormatFromColorFormat(Pica::FramebufferRegs::ColorFormat format) {
|
||||||
const u32 format_index = static_cast<u32>(format);
|
const u32 format_index = static_cast<u32>(format);
|
||||||
return (format_index < 5) ? static_cast<PixelFormat>(format) : PixelFormat::Invalid;
|
return (format_index < 5) ? static_cast<PixelFormat>(format) : PixelFormat::Invalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
static PixelFormat PixelFormatFromDepthFormat(Pica::FramebufferRegs::DepthFormat format) {
|
inline PixelFormat PixelFormatFromDepthFormat(Pica::FramebufferRegs::DepthFormat format) {
|
||||||
const u32 format_index = static_cast<u32>(format);
|
const u32 format_index = static_cast<u32>(format);
|
||||||
return (format_index < 4) ? static_cast<PixelFormat>(format_index + 14)
|
return (format_index < 4) ? static_cast<PixelFormat>(format_index + 14)
|
||||||
: PixelFormat::Invalid;
|
: PixelFormat::Invalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
static constexpr PixelFormat PixelFormatFromGPUPixelFormat(GPU::Regs::PixelFormat format) {
|
inline constexpr PixelFormat PixelFormatFromGPUPixelFormat(GPU::Regs::PixelFormat format) {
|
||||||
switch (format) {
|
switch (format) {
|
||||||
// RGB565 and RGB5A1 are switched in PixelFormat compared to ColorFormat
|
// RGB565 and RGB5A1 are switched in PixelFormat compared to ColorFormat
|
||||||
case GPU::Regs::PixelFormat::RGB565:
|
case GPU::Regs::PixelFormat::RGB565:
|
||||||
|
@ -133,7 +135,7 @@ static constexpr SurfaceType GetFormatType(PixelFormat pixel_format) {
|
||||||
return SurfaceType::Invalid;
|
return SurfaceType::Invalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
static constexpr bool CheckFormatsBlittable(PixelFormat source_format, PixelFormat dest_format) {
|
inline constexpr bool CheckFormatsBlittable(PixelFormat source_format, PixelFormat dest_format) {
|
||||||
SurfaceType source_type = GetFormatType(source_format);
|
SurfaceType source_type = GetFormatType(source_format);
|
||||||
SurfaceType dest_type = GetFormatType(dest_format);
|
SurfaceType dest_type = GetFormatType(dest_format);
|
||||||
|
|
||||||
|
@ -182,7 +184,7 @@ static constexpr u32 GetFormatBpp(PixelFormat format) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static constexpr u32 GetBytesPerPixel(PixelFormat format) {
|
inline constexpr u32 GetBytesPerPixel(PixelFormat format) {
|
||||||
// OpenGL needs 4 bpp alignment for D24 since using GL_UNSIGNED_INT as type
|
// OpenGL needs 4 bpp alignment for D24 since using GL_UNSIGNED_INT as type
|
||||||
if (format == PixelFormat::D24 || GetFormatType(format) == SurfaceType::Texture) {
|
if (format == PixelFormat::D24 || GetFormatType(format) == SurfaceType::Texture) {
|
||||||
return 4;
|
return 4;
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -4,11 +4,9 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include "common/assert.h"
|
#include "video_core/rasterizer_cache/cached_surface.h"
|
||||||
#include "core/custom_tex_cache.h"
|
|
||||||
#include "video_core/rasterizer_cache/rasterizer_cache_utils.h"
|
#include "video_core/rasterizer_cache/rasterizer_cache_utils.h"
|
||||||
#include "video_core/rasterizer_cache/surface_params.h"
|
#include "video_core/rasterizer_cache/surface_params.h"
|
||||||
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
|
||||||
#include "video_core/texture/texture_decode.h"
|
#include "video_core/texture/texture_decode.h"
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
@ -19,129 +17,6 @@ enum class ScaleMatch {
|
||||||
Ignore // accept every scaled res
|
Ignore // accept every scaled res
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* A watcher that notifies whether a cached surface has been changed. This is useful for caching
|
|
||||||
* surface collection objects, including texture cube and mipmap.
|
|
||||||
*/
|
|
||||||
struct SurfaceWatcher {
|
|
||||||
public:
|
|
||||||
explicit SurfaceWatcher(std::weak_ptr<CachedSurface>&& surface) : surface(std::move(surface)) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the surface has been changed.
|
|
||||||
* @return false if the surface content has been changed since last Validate() call or has been
|
|
||||||
* destroyed; otherwise true
|
|
||||||
*/
|
|
||||||
bool IsValid() const {
|
|
||||||
return !surface.expired() && valid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Marks that the content of the referencing surface has been updated to the watcher user.
|
|
||||||
void Validate() {
|
|
||||||
ASSERT(!surface.expired());
|
|
||||||
valid = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the referencing surface. Returns null if the surface has been destroyed
|
|
||||||
Surface Get() const {
|
|
||||||
return surface.lock();
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
friend struct CachedSurface;
|
|
||||||
std::weak_ptr<CachedSurface> surface;
|
|
||||||
bool valid = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
class RasterizerCacheOpenGL;
|
|
||||||
|
|
||||||
struct CachedSurface : SurfaceParams, std::enable_shared_from_this<CachedSurface> {
|
|
||||||
CachedSurface(RasterizerCacheOpenGL& owner) : owner{owner} {}
|
|
||||||
~CachedSurface();
|
|
||||||
|
|
||||||
bool CanFill(const SurfaceParams& dest_surface, SurfaceInterval fill_interval) const;
|
|
||||||
bool CanCopy(const SurfaceParams& dest_surface, SurfaceInterval copy_interval) const;
|
|
||||||
|
|
||||||
bool IsRegionValid(SurfaceInterval interval) const {
|
|
||||||
return (invalid_regions.find(interval) == invalid_regions.end());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsSurfaceFullyInvalid() const {
|
|
||||||
auto interval = GetInterval();
|
|
||||||
return *invalid_regions.equal_range(interval).first == interval;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool registered = false;
|
|
||||||
SurfaceRegions invalid_regions;
|
|
||||||
|
|
||||||
u32 fill_size = 0; /// Number of bytes to read from fill_data
|
|
||||||
std::array<u8, 4> fill_data;
|
|
||||||
|
|
||||||
OGLTexture texture;
|
|
||||||
|
|
||||||
/// max mipmap level that has been attached to the texture
|
|
||||||
u32 max_level = 0;
|
|
||||||
/// level_watchers[i] watches the (i+1)-th level mipmap source surface
|
|
||||||
std::array<std::shared_ptr<SurfaceWatcher>, 7> level_watchers;
|
|
||||||
|
|
||||||
bool is_custom = false;
|
|
||||||
Core::CustomTexInfo custom_tex_info;
|
|
||||||
|
|
||||||
std::vector<u8> gl_buffer;
|
|
||||||
|
|
||||||
// Read/Write data in 3DS memory to/from gl_buffer
|
|
||||||
void LoadGLBuffer(PAddr load_start, PAddr load_end);
|
|
||||||
void FlushGLBuffer(PAddr flush_start, PAddr flush_end);
|
|
||||||
|
|
||||||
// Custom texture loading and dumping
|
|
||||||
bool LoadCustomTexture(u64 tex_hash);
|
|
||||||
void DumpTexture(GLuint target_tex, u64 tex_hash);
|
|
||||||
|
|
||||||
// Upload/Download data in gl_buffer in/to this surface's texture
|
|
||||||
void UploadGLTexture(Common::Rectangle<u32> rect, GLuint read_fb_handle, GLuint draw_fb_handle);
|
|
||||||
void DownloadGLTexture(const Common::Rectangle<u32>& rect, GLuint read_fb_handle,
|
|
||||||
GLuint draw_fb_handle);
|
|
||||||
|
|
||||||
std::shared_ptr<SurfaceWatcher> CreateWatcher() {
|
|
||||||
auto watcher = std::make_shared<SurfaceWatcher>(weak_from_this());
|
|
||||||
watchers.push_front(watcher);
|
|
||||||
return watcher;
|
|
||||||
}
|
|
||||||
|
|
||||||
void InvalidateAllWatcher() {
|
|
||||||
for (const auto& watcher : watchers) {
|
|
||||||
if (auto locked = watcher.lock()) {
|
|
||||||
locked->valid = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void UnlinkAllWatcher() {
|
|
||||||
for (const auto& watcher : watchers) {
|
|
||||||
if (auto locked = watcher.lock()) {
|
|
||||||
locked->valid = false;
|
|
||||||
locked->surface.reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
watchers.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
RasterizerCacheOpenGL& owner;
|
|
||||||
std::list<std::weak_ptr<SurfaceWatcher>> watchers;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct CachedTextureCube {
|
|
||||||
OGLTexture texture;
|
|
||||||
u16 res_scale = 1;
|
|
||||||
std::shared_ptr<SurfaceWatcher> px;
|
|
||||||
std::shared_ptr<SurfaceWatcher> nx;
|
|
||||||
std::shared_ptr<SurfaceWatcher> py;
|
|
||||||
std::shared_ptr<SurfaceWatcher> ny;
|
|
||||||
std::shared_ptr<SurfaceWatcher> pz;
|
|
||||||
std::shared_ptr<SurfaceWatcher> nz;
|
|
||||||
};
|
|
||||||
|
|
||||||
class TextureDownloaderES;
|
class TextureDownloaderES;
|
||||||
class TextureFilterer;
|
class TextureFilterer;
|
||||||
class FormatReinterpreterOpenGL;
|
class FormatReinterpreterOpenGL;
|
||||||
|
@ -233,14 +108,12 @@ private:
|
||||||
/// Increase/decrease the number of surface in pages touching the specified region
|
/// Increase/decrease the number of surface in pages touching the specified region
|
||||||
void UpdatePagesCachedCount(PAddr addr, u32 size, int delta);
|
void UpdatePagesCachedCount(PAddr addr, u32 size, int delta);
|
||||||
|
|
||||||
|
TextureRuntime runtime;
|
||||||
SurfaceCache surface_cache;
|
SurfaceCache surface_cache;
|
||||||
PageMap cached_pages;
|
PageMap cached_pages;
|
||||||
SurfaceMap dirty_regions;
|
SurfaceMap dirty_regions;
|
||||||
SurfaceSet remove_surfaces;
|
SurfaceSet remove_surfaces;
|
||||||
|
|
||||||
OGLFramebuffer read_framebuffer;
|
|
||||||
OGLFramebuffer draw_framebuffer;
|
|
||||||
|
|
||||||
u16 resolution_scale_factor;
|
u16 resolution_scale_factor;
|
||||||
|
|
||||||
std::unordered_map<TextureCubeConfig, CachedTextureCube> texture_cube_cache;
|
std::unordered_map<TextureCubeConfig, CachedTextureCube> texture_cube_cache;
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
struct CachedSurface;
|
class CachedSurface;
|
||||||
using Surface = std::shared_ptr<CachedSurface>;
|
using Surface = std::shared_ptr<CachedSurface>;
|
||||||
|
|
||||||
// Declare rasterizer interval types
|
// Declare rasterizer interval types
|
||||||
|
|
|
@ -4,9 +4,6 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <set>
|
|
||||||
#include <boost/icl/interval_map.hpp>
|
|
||||||
#include <boost/icl/interval_set.hpp>
|
|
||||||
#include "common/hash.h"
|
#include "common/hash.h"
|
||||||
#include "video_core/rasterizer_cache/pixel_format.h"
|
#include "video_core/rasterizer_cache/pixel_format.h"
|
||||||
|
|
||||||
|
|
196
src/video_core/rasterizer_cache/texture_runtime.cpp
Normal file
196
src/video_core/rasterizer_cache/texture_runtime.cpp
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
// Copyright 2022 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include "common/scope_exit.h"
|
||||||
|
#include "video_core/rasterizer_cache/rasterizer_cache_utils.h"
|
||||||
|
#include "video_core/rasterizer_cache/texture_runtime.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_state.h"
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
|
||||||
|
GLbitfield ToBufferMask(Aspect aspect) {
|
||||||
|
switch (aspect) {
|
||||||
|
case Aspect::Color:
|
||||||
|
return GL_COLOR_BUFFER_BIT;
|
||||||
|
case Aspect::Depth:
|
||||||
|
return GL_DEPTH_BUFFER_BIT;
|
||||||
|
case Aspect::DepthStencil:
|
||||||
|
return GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextureRuntime::TextureRuntime() {
|
||||||
|
read_fbo.Create();
|
||||||
|
draw_fbo.Create();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextureRuntime::ReadTexture(const OGLTexture& tex, Subresource subresource,
|
||||||
|
const FormatTuple& tuple, u8* pixels) {
|
||||||
|
|
||||||
|
OpenGLState prev_state = OpenGLState::GetCurState();
|
||||||
|
SCOPE_EXIT({ prev_state.Apply(); });
|
||||||
|
|
||||||
|
OpenGLState state;
|
||||||
|
state.ResetTexture(tex.handle);
|
||||||
|
state.draw.read_framebuffer = read_fbo.handle;
|
||||||
|
state.Apply();
|
||||||
|
|
||||||
|
const u32 level = subresource.level;
|
||||||
|
switch (subresource.aspect) {
|
||||||
|
case Aspect::Color:
|
||||||
|
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||||
|
tex.handle, level);
|
||||||
|
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D,
|
||||||
|
0, 0);
|
||||||
|
break;
|
||||||
|
case Aspect::Depth:
|
||||||
|
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||||
|
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D,
|
||||||
|
tex.handle, level);
|
||||||
|
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
||||||
|
break;
|
||||||
|
case Aspect::DepthStencil:
|
||||||
|
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||||
|
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D,
|
||||||
|
tex.handle, level);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& rect = subresource.region;
|
||||||
|
glReadPixels(rect.left, rect.bottom, rect.GetWidth(), rect.GetHeight(),
|
||||||
|
tuple.format, tuple.type, pixels);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TextureRuntime::ClearTexture(const OGLTexture& tex, Subresource subresource,
|
||||||
|
ClearValue value) {
|
||||||
|
OpenGLState prev_state = OpenGLState::GetCurState();
|
||||||
|
SCOPE_EXIT({ prev_state.Apply(); });
|
||||||
|
|
||||||
|
// Setup scissor rectangle according to the clear rectangle
|
||||||
|
const auto& clear_rect = subresource.region;
|
||||||
|
OpenGLState state;
|
||||||
|
state.scissor.enabled = true;
|
||||||
|
state.scissor.x = clear_rect.left;
|
||||||
|
state.scissor.y = clear_rect.bottom;
|
||||||
|
state.scissor.width = clear_rect.GetWidth();
|
||||||
|
state.scissor.height = clear_rect.GetHeight();
|
||||||
|
state.draw.draw_framebuffer = draw_fbo.handle;
|
||||||
|
state.Apply();
|
||||||
|
|
||||||
|
const u32 level = subresource.level;
|
||||||
|
switch (subresource.aspect) {
|
||||||
|
case Aspect::Color:
|
||||||
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||||
|
tex.handle, level);
|
||||||
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D,
|
||||||
|
0, 0);
|
||||||
|
|
||||||
|
state.color_mask.red_enabled = true;
|
||||||
|
state.color_mask.green_enabled = true;
|
||||||
|
state.color_mask.blue_enabled = true;
|
||||||
|
state.color_mask.alpha_enabled = true;
|
||||||
|
state.Apply();
|
||||||
|
|
||||||
|
glClearBufferfv(GL_COLOR, 0, value.color.AsArray());
|
||||||
|
break;
|
||||||
|
case Aspect::Depth:
|
||||||
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||||
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D,
|
||||||
|
tex.handle, level);
|
||||||
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
||||||
|
|
||||||
|
state.depth.write_mask = GL_TRUE;
|
||||||
|
state.Apply();
|
||||||
|
|
||||||
|
glClearBufferfv(GL_DEPTH, 0, &value.depth);
|
||||||
|
break;
|
||||||
|
case Aspect::DepthStencil:
|
||||||
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||||
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D,
|
||||||
|
tex.handle, level);
|
||||||
|
|
||||||
|
state.depth.write_mask = GL_TRUE;
|
||||||
|
state.stencil.write_mask = -1;
|
||||||
|
state.Apply();
|
||||||
|
|
||||||
|
glClearBufferfi(GL_DEPTH_STENCIL, 0, value.depth, value.stencil);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TextureRuntime::CopyTextures(const OGLTexture& src_tex, Subresource src_subresource,
|
||||||
|
const OGLTexture& dst_tex, Subresource dst_subresource) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TextureRuntime::BlitTextures(const OGLTexture& src_tex, Subresource src_subresource,
|
||||||
|
const OGLTexture& dst_tex, Subresource dst_subresource) {
|
||||||
|
OpenGLState prev_state = OpenGLState::GetCurState();
|
||||||
|
SCOPE_EXIT({ prev_state.Apply(); });
|
||||||
|
|
||||||
|
OpenGLState state;
|
||||||
|
state.draw.read_framebuffer = read_fbo.handle;
|
||||||
|
state.draw.draw_framebuffer = draw_fbo.handle;
|
||||||
|
state.Apply();
|
||||||
|
|
||||||
|
auto BindAttachment = [src_level = src_subresource.level,
|
||||||
|
dst_level = dst_subresource.level](GLenum target, u32 src_tex,
|
||||||
|
u32 dst_tex) -> void {
|
||||||
|
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, target, GL_TEXTURE_2D, src_tex, src_level);
|
||||||
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, target, GL_TEXTURE_2D, dst_tex, dst_level);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sanity check; Can't blit a color texture to a depth buffer
|
||||||
|
ASSERT(src_subresource.aspect == dst_subresource.aspect);
|
||||||
|
switch (src_subresource.aspect) {
|
||||||
|
case Aspect::Color:
|
||||||
|
// Bind only color
|
||||||
|
BindAttachment(GL_COLOR_ATTACHMENT0, src_tex.handle, dst_tex.handle);
|
||||||
|
BindAttachment(GL_DEPTH_STENCIL_ATTACHMENT, 0, 0);
|
||||||
|
break;
|
||||||
|
case Aspect::Depth:
|
||||||
|
// Bind only depth
|
||||||
|
BindAttachment(GL_COLOR_ATTACHMENT0, 0, 0);
|
||||||
|
BindAttachment(GL_DEPTH_ATTACHMENT, src_tex.handle, dst_tex.handle);
|
||||||
|
BindAttachment(GL_STENCIL_ATTACHMENT, 0, 0);
|
||||||
|
break;
|
||||||
|
case Aspect::DepthStencil:
|
||||||
|
// Bind to combined depth + stencil
|
||||||
|
BindAttachment(GL_COLOR_ATTACHMENT0, 0, 0);
|
||||||
|
BindAttachment(GL_DEPTH_STENCIL_ATTACHMENT, src_tex.handle, dst_tex.handle);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO (wwylele): use GL_NEAREST for shadow map texture
|
||||||
|
// Note: shadow map is treated as RGBA8 format in PICA, as well as in the rasterizer cache, but
|
||||||
|
// doing linear intepolation componentwise would cause incorrect value. However, for a
|
||||||
|
// well-programmed game this code path should be rarely executed for shadow map with
|
||||||
|
// inconsistent scale.
|
||||||
|
const GLenum filter = src_subresource.aspect == Aspect::Color ? GL_LINEAR : GL_NEAREST;
|
||||||
|
const auto& src_rect = src_subresource.region;
|
||||||
|
const auto& dst_rect = dst_subresource.region;
|
||||||
|
glBlitFramebuffer(src_rect.left, src_rect.bottom, src_rect.right, src_rect.top,
|
||||||
|
dst_rect.left, dst_rect.bottom, dst_rect.right, dst_rect.top,
|
||||||
|
ToBufferMask(src_subresource.aspect), filter);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextureRuntime::GenerateMipmaps(const OGLTexture& tex, u32 max_level) {
|
||||||
|
OpenGLState prev_state = OpenGLState::GetCurState();
|
||||||
|
SCOPE_EXIT({ prev_state.Apply(); });
|
||||||
|
|
||||||
|
OpenGLState state;
|
||||||
|
state.texture_units[0].texture_2d = tex.handle;
|
||||||
|
state.Apply();
|
||||||
|
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, max_level);
|
||||||
|
|
||||||
|
glGenerateMipmap(GL_TEXTURE_2D);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace OpenGL
|
73
src/video_core/rasterizer_cache/texture_runtime.h
Normal file
73
src/video_core/rasterizer_cache/texture_runtime.h
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
// Copyright 2022 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "common/math_util.h"
|
||||||
|
#include "common/vector_math.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
||||||
|
|
||||||
|
namespace OpenGL {
|
||||||
|
|
||||||
|
// Describes the type of data a texture holds
|
||||||
|
enum class Aspect {
|
||||||
|
Color = 0,
|
||||||
|
Depth = 1,
|
||||||
|
DepthStencil = 2
|
||||||
|
};
|
||||||
|
|
||||||
|
// A union for both color and depth/stencil clear values
|
||||||
|
union ClearValue {
|
||||||
|
Common::Vec4f color;
|
||||||
|
struct {
|
||||||
|
float depth;
|
||||||
|
u8 stencil;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Subresource {
|
||||||
|
Subresource(Aspect aspect, Common::Rectangle<u32> region, u32 level = 0, u32 layer = 0) :
|
||||||
|
aspect(aspect), region(region), level(level), layer(layer) {}
|
||||||
|
|
||||||
|
Aspect aspect;
|
||||||
|
Common::Rectangle<u32> region;
|
||||||
|
u32 level = 0;
|
||||||
|
u32 layer = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FormatTuple;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides texture manipulation functions to the rasterizer cache
|
||||||
|
* Separating this into a class makes it easier to abstract graphics API code
|
||||||
|
*/
|
||||||
|
class TextureRuntime {
|
||||||
|
public:
|
||||||
|
TextureRuntime();
|
||||||
|
~TextureRuntime() = default;
|
||||||
|
|
||||||
|
// Copies the GPU pixel data to the provided pixels buffer
|
||||||
|
void ReadTexture(const OGLTexture& tex, Subresource subresource,
|
||||||
|
const FormatTuple& tuple, u8* pixels);
|
||||||
|
|
||||||
|
// Fills the rectangle of the texture with the clear value provided
|
||||||
|
bool ClearTexture(const OGLTexture& texture, Subresource subresource, ClearValue value);
|
||||||
|
|
||||||
|
// Copies a rectangle of src_tex to another rectange of dst_rect
|
||||||
|
// NOTE: The width and height of the rectangles must be equal
|
||||||
|
bool CopyTextures(const OGLTexture& src_tex, Subresource src_subresource,
|
||||||
|
const OGLTexture& dst_tex, Subresource dst_subresource);
|
||||||
|
|
||||||
|
// Copies a rectangle of src_tex to another rectange of dst_rect performing
|
||||||
|
// scaling and format conversions
|
||||||
|
bool BlitTextures(const OGLTexture& src_tex, Subresource src_subresource,
|
||||||
|
const OGLTexture& dst_tex, Subresource dst_subresource);
|
||||||
|
|
||||||
|
// Generates mipmaps for all the available levels of the texture
|
||||||
|
void GenerateMipmaps(const OGLTexture& tex, u32 max_level);
|
||||||
|
|
||||||
|
private:
|
||||||
|
OGLFramebuffer read_fbo, draw_fbo;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace OpenGL
|
|
@ -1,14 +1,12 @@
|
||||||
// Copyright 2020 Citra Emulator Project
|
// Copyright 2022 Citra Emulator Project
|
||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
#include "common/scope_exit.h"
|
#include "common/scope_exit.h"
|
||||||
#include "video_core/renderer_opengl/gl_format_reinterpreter.h"
|
#include "video_core/renderer_opengl/gl_format_reinterpreter.h"
|
||||||
#include "video_core/rasterizer_cache/rasterizer_cache.h"
|
|
||||||
#include "video_core/renderer_opengl/gl_state.h"
|
#include "video_core/renderer_opengl/gl_state.h"
|
||||||
#include "video_core/renderer_opengl/gl_vars.h"
|
#include "video_core/renderer_opengl/gl_vars.h"
|
||||||
#include "video_core/renderer_opengl/texture_filters/texture_filterer.h"
|
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
|
@ -64,15 +62,18 @@ void main() {
|
||||||
vao.Create();
|
vao.Create();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Reinterpret(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint read_fb_handle,
|
PixelFormat GetSourceFormat() const override {
|
||||||
GLuint dst_tex, const Common::Rectangle<u32>& dst_rect,
|
return PixelFormat::RGBA4;
|
||||||
GLuint draw_fb_handle) override {
|
}
|
||||||
|
|
||||||
|
void Reinterpret(const OGLTexture& src_tex, Common::Rectangle<u32> src_rect,
|
||||||
|
const OGLTexture& dst_tex, Common::Rectangle<u32> dst_rect) override {
|
||||||
OpenGLState prev_state = OpenGLState::GetCurState();
|
OpenGLState prev_state = OpenGLState::GetCurState();
|
||||||
SCOPE_EXIT({ prev_state.Apply(); });
|
SCOPE_EXIT({ prev_state.Apply(); });
|
||||||
|
|
||||||
OpenGLState state;
|
OpenGLState state;
|
||||||
state.texture_units[0].texture_2d = src_tex;
|
state.texture_units[0].texture_2d = src_tex.handle;
|
||||||
state.draw.draw_framebuffer = draw_fb_handle;
|
state.draw.draw_framebuffer = draw_fbo.handle;
|
||||||
state.draw.shader_program = program.handle;
|
state.draw.shader_program = program.handle;
|
||||||
state.draw.vertex_array = vao.handle;
|
state.draw.vertex_array = vao.handle;
|
||||||
state.viewport = {static_cast<GLint>(dst_rect.left), static_cast<GLint>(dst_rect.bottom),
|
state.viewport = {static_cast<GLint>(dst_rect.left), static_cast<GLint>(dst_rect.bottom),
|
||||||
|
@ -80,10 +81,10 @@ void main() {
|
||||||
static_cast<GLsizei>(dst_rect.GetHeight())};
|
static_cast<GLsizei>(dst_rect.GetHeight())};
|
||||||
state.Apply();
|
state.Apply();
|
||||||
|
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex,
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||||
0);
|
dst_tex.handle, 0);
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0,
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D,
|
||||||
0);
|
0, 0);
|
||||||
|
|
||||||
glUniform2i(dst_size_loc, dst_rect.GetWidth(), dst_rect.GetHeight());
|
glUniform2i(dst_size_loc, dst_rect.GetWidth(), dst_rect.GetHeight());
|
||||||
glUniform2i(src_size_loc, src_rect.GetWidth(), src_rect.GetHeight());
|
glUniform2i(src_size_loc, src_rect.GetWidth(), src_rect.GetHeight());
|
||||||
|
@ -148,15 +149,18 @@ void main() {
|
||||||
|
|
||||||
~PixelBufferD24S8toABGR() {}
|
~PixelBufferD24S8toABGR() {}
|
||||||
|
|
||||||
void Reinterpret(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint read_fb_handle,
|
PixelFormat GetSourceFormat() const override {
|
||||||
GLuint dst_tex, const Common::Rectangle<u32>& dst_rect,
|
return PixelFormat::D24S8;
|
||||||
GLuint draw_fb_handle) override {
|
}
|
||||||
|
|
||||||
|
void Reinterpret(const OGLTexture& src_tex, Common::Rectangle<u32> src_rect,
|
||||||
|
const OGLTexture& dst_tex, Common::Rectangle<u32> dst_rect) override {
|
||||||
OpenGLState prev_state = OpenGLState::GetCurState();
|
OpenGLState prev_state = OpenGLState::GetCurState();
|
||||||
SCOPE_EXIT({ prev_state.Apply(); });
|
SCOPE_EXIT({ prev_state.Apply(); });
|
||||||
|
|
||||||
OpenGLState state;
|
OpenGLState state;
|
||||||
state.draw.read_framebuffer = read_fb_handle;
|
state.draw.read_framebuffer = read_fbo.handle;
|
||||||
state.draw.draw_framebuffer = draw_fb_handle;
|
state.draw.draw_framebuffer = draw_fbo.handle;
|
||||||
state.Apply();
|
state.Apply();
|
||||||
|
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, d24s8_abgr_buffer.handle);
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, d24s8_abgr_buffer.handle);
|
||||||
|
@ -170,7 +174,8 @@ void main() {
|
||||||
|
|
||||||
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||||
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D,
|
glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D,
|
||||||
src_tex, 0);
|
src_tex.handle, 0);
|
||||||
|
|
||||||
glReadPixels(static_cast<GLint>(src_rect.left), static_cast<GLint>(src_rect.bottom),
|
glReadPixels(static_cast<GLint>(src_rect.left), static_cast<GLint>(src_rect.bottom),
|
||||||
static_cast<GLsizei>(src_rect.GetWidth()),
|
static_cast<GLsizei>(src_rect.GetWidth()),
|
||||||
static_cast<GLsizei>(src_rect.GetHeight()), GL_DEPTH_STENCIL,
|
static_cast<GLsizei>(src_rect.GetHeight()), GL_DEPTH_STENCIL,
|
||||||
|
@ -200,12 +205,11 @@ void main() {
|
||||||
static_cast<GLfloat>(state.viewport.width),
|
static_cast<GLfloat>(state.viewport.width),
|
||||||
static_cast<GLfloat>(state.viewport.height));
|
static_cast<GLfloat>(state.viewport.height));
|
||||||
|
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex,
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||||
0);
|
dst_tex.handle, 0);
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0,
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D,
|
||||||
0);
|
0, 0);
|
||||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
|
||||||
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -292,19 +296,22 @@ void main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Reinterpret(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint read_fb_handle,
|
PixelFormat GetSourceFormat() const override {
|
||||||
GLuint dst_tex, const Common::Rectangle<u32>& dst_rect,
|
return PixelFormat::D24S8;
|
||||||
GLuint draw_fb_handle) override {
|
}
|
||||||
|
|
||||||
|
void Reinterpret(const OGLTexture& src_tex, Common::Rectangle<u32> src_rect,
|
||||||
|
const OGLTexture& dst_tex, Common::Rectangle<u32> dst_rect) override {
|
||||||
OpenGLState prev_state = OpenGLState::GetCurState();
|
OpenGLState prev_state = OpenGLState::GetCurState();
|
||||||
SCOPE_EXIT({ prev_state.Apply(); });
|
SCOPE_EXIT({ prev_state.Apply(); });
|
||||||
|
|
||||||
OpenGLState state;
|
OpenGLState state;
|
||||||
state.texture_units[0].texture_2d = src_tex;
|
state.texture_units[0].texture_2d = src_tex.handle;
|
||||||
|
|
||||||
if (use_texture_view) {
|
if (use_texture_view) {
|
||||||
temp_tex.Create();
|
temp_tex.Create();
|
||||||
glActiveTexture(GL_TEXTURE1);
|
glActiveTexture(GL_TEXTURE1);
|
||||||
glTextureView(temp_tex.handle, GL_TEXTURE_2D, src_tex, GL_DEPTH24_STENCIL8, 0, 1, 0, 1);
|
glTextureView(temp_tex.handle, GL_TEXTURE_2D, src_tex.handle, GL_DEPTH24_STENCIL8, 0, 1, 0, 1);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
} else if (src_rect.top > temp_rect.top || src_rect.right > temp_rect.right) {
|
} else if (src_rect.top > temp_rect.top || src_rect.right > temp_rect.right) {
|
||||||
|
@ -320,7 +327,7 @@ void main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
state.texture_units[1].texture_2d = temp_tex.handle;
|
state.texture_units[1].texture_2d = temp_tex.handle;
|
||||||
state.draw.draw_framebuffer = draw_fb_handle;
|
state.draw.draw_framebuffer = draw_fbo.handle;
|
||||||
state.draw.shader_program = program.handle;
|
state.draw.shader_program = program.handle;
|
||||||
state.draw.vertex_array = vao.handle;
|
state.draw.vertex_array = vao.handle;
|
||||||
state.viewport = {static_cast<GLint>(dst_rect.left), static_cast<GLint>(dst_rect.bottom),
|
state.viewport = {static_cast<GLint>(dst_rect.left), static_cast<GLint>(dst_rect.bottom),
|
||||||
|
@ -330,16 +337,16 @@ void main() {
|
||||||
|
|
||||||
glActiveTexture(GL_TEXTURE1);
|
glActiveTexture(GL_TEXTURE1);
|
||||||
if (!use_texture_view) {
|
if (!use_texture_view) {
|
||||||
glCopyImageSubData(src_tex, GL_TEXTURE_2D, 0, src_rect.left, src_rect.bottom, 0,
|
glCopyImageSubData(src_tex.handle, GL_TEXTURE_2D, 0, src_rect.left, src_rect.bottom, 0,
|
||||||
temp_tex.handle, GL_TEXTURE_2D, 0, src_rect.left, src_rect.bottom, 0,
|
temp_tex.handle, GL_TEXTURE_2D, 0, src_rect.left, src_rect.bottom, 0,
|
||||||
src_rect.GetWidth(), src_rect.GetHeight(), 1);
|
src_rect.GetWidth(), src_rect.GetHeight(), 1);
|
||||||
}
|
}
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX);
|
glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX);
|
||||||
|
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex,
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||||
0);
|
dst_tex.handle, 0);
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0,
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D,
|
||||||
0);
|
0, 0);
|
||||||
|
|
||||||
glUniform2i(dst_size_loc, dst_rect.GetWidth(), dst_rect.GetHeight());
|
glUniform2i(dst_size_loc, dst_rect.GetWidth(), dst_rect.GetHeight());
|
||||||
glUniform2i(src_size_loc, src_rect.GetWidth(), src_rect.GetHeight());
|
glUniform2i(src_size_loc, src_rect.GetWidth(), src_rect.GetHeight());
|
||||||
|
@ -363,32 +370,32 @@ private:
|
||||||
FormatReinterpreterOpenGL::FormatReinterpreterOpenGL() {
|
FormatReinterpreterOpenGL::FormatReinterpreterOpenGL() {
|
||||||
const std::string_view vendor{reinterpret_cast<const char*>(glGetString(GL_VENDOR))};
|
const std::string_view vendor{reinterpret_cast<const char*>(glGetString(GL_VENDOR))};
|
||||||
const std::string_view version{reinterpret_cast<const char*>(glGetString(GL_VERSION))};
|
const std::string_view version{reinterpret_cast<const char*>(glGetString(GL_VERSION))};
|
||||||
|
|
||||||
// Fallback to PBO path on obsolete intel drivers
|
// Fallback to PBO path on obsolete intel drivers
|
||||||
// intel`s GL_VERSION string - `3.3.0 - Build 25.20.100.6373`
|
// intel`s GL_VERSION string - `3.3.0 - Build 25.20.100.6373`
|
||||||
const bool intel_broken_drivers =
|
const bool intel_broken_drivers =
|
||||||
vendor.find("Intel") != vendor.npos && (std::atoi(version.substr(14, 2).data()) < 30);
|
vendor.find("Intel") != vendor.npos && (std::atoi(version.substr(14, 2).data()) < 30);
|
||||||
|
|
||||||
if ((!intel_broken_drivers && GLAD_GL_ARB_stencil_texturing && GLAD_GL_ARB_texture_storage &&
|
auto Register = [this](PixelFormat dest, std::unique_ptr<FormatReinterpreterBase>&& obj) {
|
||||||
GLAD_GL_ARB_copy_image) ||
|
const u32 dst_index = static_cast<u32>(dest);
|
||||||
GLES) {
|
return reinterpreters[dst_index].push_back(std::move(obj));
|
||||||
reinterpreters.emplace(PixelFormatPair{PixelFormat::RGBA8, PixelFormat::D24S8},
|
};
|
||||||
std::make_unique<ShaderD24S8toRGBA8>());
|
|
||||||
|
if ((!intel_broken_drivers && GLAD_GL_ARB_stencil_texturing &&
|
||||||
|
GLAD_GL_ARB_texture_storage && GLAD_GL_ARB_copy_image) || GLES) {
|
||||||
|
Register(PixelFormat::RGBA8, std::make_unique<ShaderD24S8toRGBA8>());
|
||||||
LOG_INFO(Render_OpenGL, "Using shader for D24S8 to RGBA8 reinterpretation");
|
LOG_INFO(Render_OpenGL, "Using shader for D24S8 to RGBA8 reinterpretation");
|
||||||
} else {
|
} else {
|
||||||
reinterpreters.emplace(PixelFormatPair{PixelFormat::RGBA8, PixelFormat::D24S8},
|
Register(PixelFormat::RGBA8, std::make_unique<PixelBufferD24S8toABGR>());
|
||||||
std::make_unique<PixelBufferD24S8toABGR>());
|
LOG_INFO(Render_OpenGL, "Using PBO for D24S8 to RGBA8 reinterpretation");
|
||||||
LOG_INFO(Render_OpenGL, "Using pbo for D24S8 to RGBA8 reinterpretation");
|
|
||||||
}
|
}
|
||||||
reinterpreters.emplace(PixelFormatPair{PixelFormat::RGB5A1, PixelFormat::RGBA4},
|
|
||||||
std::make_unique<RGBA4toRGB5A1>());
|
Register(PixelFormat::RGB5A1, std::make_unique<RGBA4toRGB5A1>());
|
||||||
}
|
}
|
||||||
|
|
||||||
FormatReinterpreterOpenGL::~FormatReinterpreterOpenGL() = default;
|
auto FormatReinterpreterOpenGL::GetPossibleReinterpretations(PixelFormat dst_format)
|
||||||
|
-> const ReinterpreterList& {
|
||||||
std::pair<FormatReinterpreterOpenGL::ReinterpreterMap::iterator,
|
return reinterpreters[static_cast<u32>(dst_format)];
|
||||||
FormatReinterpreterOpenGL::ReinterpreterMap::iterator>
|
|
||||||
FormatReinterpreterOpenGL::GetPossibleReinterpretations(PixelFormat dst_format) {
|
|
||||||
return reinterpreters.equal_range(dst_format);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace OpenGL
|
} // namespace OpenGL
|
||||||
|
|
|
@ -1,62 +1,46 @@
|
||||||
// Copyright 2020 Citra Emulator Project
|
// Copyright 2022 Citra Emulator Project
|
||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <map>
|
#include <unordered_map>
|
||||||
#include <type_traits>
|
|
||||||
#include <glad/glad.h>
|
|
||||||
#include "common/common_types.h"
|
|
||||||
#include "common/math_util.h"
|
#include "common/math_util.h"
|
||||||
#include "video_core/rasterizer_cache/pixel_format.h"
|
#include "video_core/rasterizer_cache/pixel_format.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
class RasterizerCacheOpenGL;
|
class RasterizerCacheOpenGL;
|
||||||
|
|
||||||
struct PixelFormatPair {
|
|
||||||
const PixelFormat dst_format, src_format;
|
|
||||||
|
|
||||||
struct less {
|
|
||||||
using is_transparent = void;
|
|
||||||
constexpr bool operator()(PixelFormatPair lhs, PixelFormatPair rhs) const {
|
|
||||||
return std::tie(lhs.dst_format, lhs.src_format) <
|
|
||||||
std::tie(rhs.dst_format, rhs.src_format);
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr bool operator()(PixelFormat lhs, PixelFormatPair rhs) const {
|
|
||||||
return lhs < rhs.dst_format;
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr bool operator()(PixelFormatPair lhs, PixelFormat rhs) const {
|
|
||||||
return lhs.dst_format < rhs;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
class FormatReinterpreterBase {
|
class FormatReinterpreterBase {
|
||||||
public:
|
public:
|
||||||
|
FormatReinterpreterBase() {
|
||||||
|
read_fbo.Create();
|
||||||
|
draw_fbo.Create();
|
||||||
|
}
|
||||||
|
|
||||||
virtual ~FormatReinterpreterBase() = default;
|
virtual ~FormatReinterpreterBase() = default;
|
||||||
|
|
||||||
virtual void Reinterpret(GLuint src_tex, const Common::Rectangle<u32>& src_rect,
|
virtual PixelFormat GetSourceFormat() const = 0;
|
||||||
GLuint read_fb_handle, GLuint dst_tex,
|
virtual void Reinterpret(const OGLTexture& src_tex, Common::Rectangle<u32> src_rect,
|
||||||
const Common::Rectangle<u32>& dst_rect, GLuint draw_fb_handle) = 0;
|
const OGLTexture& dst_tex, Common::Rectangle<u32> dst_rect) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
OGLFramebuffer read_fbo, draw_fbo;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
using ReinterpreterList = std::vector<std::unique_ptr<FormatReinterpreterBase>>;
|
||||||
|
|
||||||
class FormatReinterpreterOpenGL : NonCopyable {
|
class FormatReinterpreterOpenGL : NonCopyable {
|
||||||
using ReinterpreterMap =
|
|
||||||
std::map<PixelFormatPair, std::unique_ptr<FormatReinterpreterBase>, PixelFormatPair::less>;
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit FormatReinterpreterOpenGL();
|
FormatReinterpreterOpenGL();
|
||||||
~FormatReinterpreterOpenGL();
|
~FormatReinterpreterOpenGL() = default;
|
||||||
|
|
||||||
auto GetPossibleReinterpretations(PixelFormat dst_format) ->
|
const ReinterpreterList& GetPossibleReinterpretations(PixelFormat dst_format);
|
||||||
std::pair<ReinterpreterMap::iterator, ReinterpreterMap::iterator>;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ReinterpreterMap reinterpreters;
|
std::array<ReinterpreterList, PIXEL_FORMAT_COUNT> reinterpreters;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace OpenGL
|
} // namespace OpenGL
|
||||||
|
|
|
@ -30,7 +30,6 @@
|
||||||
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
#include "video_core/rasterizer_cache/rasterizer_cache.h"
|
|
||||||
#include "video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h"
|
#include "video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h"
|
||||||
|
|
||||||
#include "shaders/refine.frag"
|
#include "shaders/refine.frag"
|
||||||
|
@ -72,9 +71,8 @@ Anime4kUltrafast::Anime4kUltrafast(u16 scale_factor) : TextureFilterBase(scale_f
|
||||||
cur_state.Apply();
|
cur_state.Apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Anime4kUltrafast::Filter(GLuint src_tex, const Common::Rectangle<u32>& src_rect,
|
void Anime4kUltrafast::Filter(const OGLTexture& src_tex, Common::Rectangle<u32> src_rect,
|
||||||
GLuint dst_tex, const Common::Rectangle<u32>& dst_rect,
|
const OGLTexture& dst_tex, Common::Rectangle<u32> dst_rect) {
|
||||||
GLuint read_fb_handle, GLuint draw_fb_handle) {
|
|
||||||
const OpenGLState cur_state = OpenGLState::GetCurState();
|
const OpenGLState cur_state = OpenGLState::GetCurState();
|
||||||
|
|
||||||
// These will have handles from the previous texture that was filtered, reset them to avoid
|
// These will have handles from the previous texture that was filtered, reset them to avoid
|
||||||
|
@ -112,7 +110,7 @@ void Anime4kUltrafast::Filter(GLuint src_tex, const Common::Rectangle<u32>& src_
|
||||||
static_cast<GLint>(src_rect.bottom * internal_scale_factor),
|
static_cast<GLint>(src_rect.bottom * internal_scale_factor),
|
||||||
static_cast<GLsizei>(src_rect.GetWidth() * internal_scale_factor),
|
static_cast<GLsizei>(src_rect.GetWidth() * internal_scale_factor),
|
||||||
static_cast<GLsizei>(src_rect.GetHeight() * internal_scale_factor)};
|
static_cast<GLsizei>(src_rect.GetHeight() * internal_scale_factor)};
|
||||||
state.texture_units[0].texture_2d = src_tex;
|
state.texture_units[0].texture_2d = src_tex.handle;
|
||||||
state.texture_units[1].texture_2d = LUMAD.tex.handle;
|
state.texture_units[1].texture_2d = LUMAD.tex.handle;
|
||||||
state.texture_units[2].texture_2d = XY.tex.handle;
|
state.texture_units[2].texture_2d = XY.tex.handle;
|
||||||
state.draw.draw_framebuffer = XY.fbo.handle;
|
state.draw.draw_framebuffer = XY.fbo.handle;
|
||||||
|
@ -131,11 +129,12 @@ void Anime4kUltrafast::Filter(GLuint src_tex, const Common::Rectangle<u32>& src_
|
||||||
state.viewport = {static_cast<GLint>(dst_rect.left), static_cast<GLint>(dst_rect.bottom),
|
state.viewport = {static_cast<GLint>(dst_rect.left), static_cast<GLint>(dst_rect.bottom),
|
||||||
static_cast<GLsizei>(dst_rect.GetWidth()),
|
static_cast<GLsizei>(dst_rect.GetWidth()),
|
||||||
static_cast<GLsizei>(dst_rect.GetHeight())};
|
static_cast<GLsizei>(dst_rect.GetHeight())};
|
||||||
state.draw.draw_framebuffer = draw_fb_handle;
|
state.draw.draw_framebuffer = draw_fbo.handle;
|
||||||
state.draw.shader_program = refine_program.handle;
|
state.draw.shader_program = refine_program.handle;
|
||||||
state.Apply();
|
state.Apply();
|
||||||
|
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex, 0);
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||||
|
dst_tex.handle, 0);
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
||||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
|
||||||
|
|
|
@ -15,9 +15,8 @@ public:
|
||||||
static constexpr std::string_view NAME = "Anime4K Ultrafast";
|
static constexpr std::string_view NAME = "Anime4K Ultrafast";
|
||||||
|
|
||||||
explicit Anime4kUltrafast(u16 scale_factor);
|
explicit Anime4kUltrafast(u16 scale_factor);
|
||||||
void Filter(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint dst_tex,
|
void Filter(const OGLTexture& src_tex, Common::Rectangle<u32> src_rect,
|
||||||
const Common::Rectangle<u32>& dst_rect, GLuint read_fb_handle,
|
const OGLTexture& dst_tex, Common::Rectangle<u32> dst_rect) override;
|
||||||
GLuint draw_fb_handle) override;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr u8 internal_scale_factor = 2;
|
static constexpr u8 internal_scale_factor = 2;
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
#include "video_core/rasterizer_cache/rasterizer_cache.h"
|
|
||||||
#include "video_core/renderer_opengl/texture_filters/bicubic/bicubic.h"
|
#include "video_core/renderer_opengl/texture_filters/bicubic/bicubic.h"
|
||||||
|
|
||||||
#include "shaders/bicubic.frag"
|
#include "shaders/bicubic.frag"
|
||||||
|
@ -26,18 +25,18 @@ Bicubic::Bicubic(u16 scale_factor) : TextureFilterBase(scale_factor) {
|
||||||
glSamplerParameteri(src_sampler.handle, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
glSamplerParameteri(src_sampler.handle, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
} // namespace OpenGL
|
} // namespace OpenGL
|
||||||
|
|
||||||
void Bicubic::Filter(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint dst_tex,
|
void Bicubic::Filter(const OGLTexture& src_tex, Common::Rectangle<u32> src_rect,
|
||||||
const Common::Rectangle<u32>& dst_rect, GLuint read_fb_handle,
|
const OGLTexture& dst_tex, Common::Rectangle<u32> dst_rect) {
|
||||||
GLuint draw_fb_handle) {
|
|
||||||
const OpenGLState cur_state = OpenGLState::GetCurState();
|
const OpenGLState cur_state = OpenGLState::GetCurState();
|
||||||
state.texture_units[0].texture_2d = src_tex;
|
state.texture_units[0].texture_2d = src_tex.handle;
|
||||||
state.draw.draw_framebuffer = draw_fb_handle;
|
state.draw.draw_framebuffer = draw_fbo.handle;
|
||||||
state.viewport = {static_cast<GLint>(dst_rect.left), static_cast<GLint>(dst_rect.bottom),
|
state.viewport = {static_cast<GLint>(dst_rect.left), static_cast<GLint>(dst_rect.bottom),
|
||||||
static_cast<GLsizei>(dst_rect.GetWidth()),
|
static_cast<GLsizei>(dst_rect.GetWidth()),
|
||||||
static_cast<GLsizei>(dst_rect.GetHeight())};
|
static_cast<GLsizei>(dst_rect.GetHeight())};
|
||||||
state.Apply();
|
state.Apply();
|
||||||
|
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex, 0);
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||||
|
dst_tex.handle, 0);
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
||||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
|
||||||
|
|
|
@ -15,9 +15,8 @@ public:
|
||||||
static constexpr std::string_view NAME = "Bicubic";
|
static constexpr std::string_view NAME = "Bicubic";
|
||||||
|
|
||||||
explicit Bicubic(u16 scale_factor);
|
explicit Bicubic(u16 scale_factor);
|
||||||
void Filter(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint dst_tex,
|
void Filter(const OGLTexture& src_tex, Common::Rectangle<u32> src_rect,
|
||||||
const Common::Rectangle<u32>& dst_rect, GLuint read_fb_handle,
|
const OGLTexture& dst_tex, Common::Rectangle<u32> dst_rect) override;
|
||||||
GLuint draw_fb_handle) override;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
OpenGLState state{};
|
OpenGLState state{};
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
#include "video_core/rasterizer_cache/rasterizer_cache.h"
|
|
||||||
#include "video_core/renderer_opengl/texture_filters/scale_force/scale_force.h"
|
#include "video_core/renderer_opengl/texture_filters/scale_force/scale_force.h"
|
||||||
|
|
||||||
#include "shaders/scale_force.frag"
|
#include "shaders/scale_force.frag"
|
||||||
|
@ -26,18 +25,18 @@ ScaleForce::ScaleForce(u16 scale_factor) : TextureFilterBase(scale_factor) {
|
||||||
glSamplerParameteri(src_sampler.handle, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
glSamplerParameteri(src_sampler.handle, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScaleForce::Filter(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint dst_tex,
|
void ScaleForce::Filter(const OGLTexture& src_tex, Common::Rectangle<u32> src_rect,
|
||||||
const Common::Rectangle<u32>& dst_rect, GLuint read_fb_handle,
|
const OGLTexture& dst_tex, Common::Rectangle<u32> dst_rect) {
|
||||||
GLuint draw_fb_handle) {
|
|
||||||
const OpenGLState cur_state = OpenGLState::GetCurState();
|
const OpenGLState cur_state = OpenGLState::GetCurState();
|
||||||
state.texture_units[0].texture_2d = src_tex;
|
state.texture_units[0].texture_2d = src_tex.handle;
|
||||||
state.draw.draw_framebuffer = draw_fb_handle;
|
state.draw.draw_framebuffer = draw_fbo.handle;
|
||||||
state.viewport = {static_cast<GLint>(dst_rect.left), static_cast<GLint>(dst_rect.bottom),
|
state.viewport = {static_cast<GLint>(dst_rect.left), static_cast<GLint>(dst_rect.bottom),
|
||||||
static_cast<GLsizei>(dst_rect.GetWidth()),
|
static_cast<GLsizei>(dst_rect.GetWidth()),
|
||||||
static_cast<GLsizei>(dst_rect.GetHeight())};
|
static_cast<GLsizei>(dst_rect.GetHeight())};
|
||||||
state.Apply();
|
state.Apply();
|
||||||
|
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex, 0);
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||||
|
dst_tex.handle, 0);
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
||||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
|
||||||
|
|
|
@ -15,9 +15,8 @@ public:
|
||||||
static constexpr std::string_view NAME = "ScaleForce";
|
static constexpr std::string_view NAME = "ScaleForce";
|
||||||
|
|
||||||
explicit ScaleForce(u16 scale_factor);
|
explicit ScaleForce(u16 scale_factor);
|
||||||
void Filter(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint dst_tex,
|
void Filter(const OGLTexture& src_tex, Common::Rectangle<u32> src_rect,
|
||||||
const Common::Rectangle<u32>& dst_rect, GLuint read_fb_handle,
|
const OGLTexture& dst_tex, Common::Rectangle<u32> dst_rect) override;
|
||||||
GLuint draw_fb_handle) override;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
OpenGLState state{};
|
OpenGLState state{};
|
||||||
|
|
|
@ -3,23 +3,31 @@
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include <string_view>
|
||||||
#include <glad/glad.h>
|
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "common/math_util.h"
|
#include "common/math_util.h"
|
||||||
|
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
|
class TextureRuntime;
|
||||||
|
class OGLTexture;
|
||||||
|
|
||||||
class TextureFilterBase {
|
class TextureFilterBase {
|
||||||
friend class TextureFilterer;
|
friend class TextureFilterer;
|
||||||
virtual void Filter(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint dst_tex,
|
|
||||||
const Common::Rectangle<u32>& dst_rect, GLuint read_fb_handle,
|
|
||||||
GLuint draw_fb_handle) = 0;
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit TextureFilterBase(u16 scale_factor) : scale_factor{scale_factor} {};
|
explicit TextureFilterBase(u16 scale_factor) : scale_factor(scale_factor) {
|
||||||
|
draw_fbo.Create();
|
||||||
|
};
|
||||||
|
|
||||||
virtual ~TextureFilterBase() = default;
|
virtual ~TextureFilterBase() = default;
|
||||||
|
|
||||||
|
private:
|
||||||
|
virtual void Filter(const OGLTexture& src_tex, Common::Rectangle<u32> src_rect,
|
||||||
|
const OGLTexture& dst_tex, Common::Rectangle<u32> dst_rect) = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
OGLFramebuffer draw_fbo;
|
||||||
const u16 scale_factor{};
|
const u16 scale_factor{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -58,16 +58,16 @@ bool TextureFilterer::IsNull() const {
|
||||||
return !filter;
|
return !filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TextureFilterer::Filter(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint dst_tex,
|
bool TextureFilterer::Filter(const OGLTexture& src_tex, Common::Rectangle<u32> src_rect,
|
||||||
const Common::Rectangle<u32>& dst_rect,
|
const OGLTexture& dst_tex, Common::Rectangle<u32> dst_rect,
|
||||||
SurfaceType type, GLuint read_fb_handle,
|
SurfaceType type) {
|
||||||
GLuint draw_fb_handle) {
|
|
||||||
// depth / stencil texture filtering is not supported for now
|
// Depth/Stencil texture filtering is not supported for now
|
||||||
if (IsNull() || (type != SurfaceType::Color && type != SurfaceType::Texture)) {
|
if (IsNull() || (type != SurfaceType::Color && type != SurfaceType::Texture)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
filter->Filter(src_tex, src_rect, dst_tex, dst_rect, read_fb_handle, draw_fb_handle);
|
filter->Filter(src_tex, src_rect, dst_tex, dst_rect);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,6 +83,7 @@ std::vector<std::string_view> TextureFilterer::GetFilterNames() {
|
||||||
return lhs_is_none && !rhs_is_none;
|
return lhs_is_none && !rhs_is_none;
|
||||||
return lhs < rhs;
|
return lhs < rhs;
|
||||||
});
|
});
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,15 +16,19 @@ class TextureFilterer {
|
||||||
public:
|
public:
|
||||||
static constexpr std::string_view NONE = "none";
|
static constexpr std::string_view NONE = "none";
|
||||||
|
|
||||||
|
public:
|
||||||
explicit TextureFilterer(std::string_view filter_name, u16 scale_factor);
|
explicit TextureFilterer(std::string_view filter_name, u16 scale_factor);
|
||||||
// returns true if the filter actually changed
|
|
||||||
|
// Returns true if the filter actually changed
|
||||||
bool Reset(std::string_view new_filter_name, u16 new_scale_factor);
|
bool Reset(std::string_view new_filter_name, u16 new_scale_factor);
|
||||||
// returns true if there is no active filter
|
|
||||||
|
// Returns true if there is no active filter
|
||||||
bool IsNull() const;
|
bool IsNull() const;
|
||||||
// returns true if the texture was able to be filtered
|
|
||||||
bool Filter(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint dst_tex,
|
// Returns true if the texture was able to be filtered
|
||||||
const Common::Rectangle<u32>& dst_rect, SurfaceType type,
|
bool Filter(const OGLTexture& src_tex, Common::Rectangle<u32> src_rect,
|
||||||
GLuint read_fb_handle, GLuint draw_fb_handle);
|
const OGLTexture& dst_tex, Common::Rectangle<u32> dst_rect,
|
||||||
|
SurfaceType type);
|
||||||
|
|
||||||
static std::vector<std::string_view> GetFilterNames();
|
static std::vector<std::string_view> GetFilterNames();
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,6 @@
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
// THE SOFTWARE.
|
// THE SOFTWARE.
|
||||||
|
|
||||||
#include "video_core/rasterizer_cache/rasterizer_cache.h"
|
|
||||||
#include "video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h"
|
#include "video_core/renderer_opengl/texture_filters/xbrz/xbrz_freescale.h"
|
||||||
|
|
||||||
#include "shaders/xbrz_freescale.frag"
|
#include "shaders/xbrz_freescale.frag"
|
||||||
|
@ -48,7 +47,9 @@
|
||||||
|
|
||||||
namespace OpenGL {
|
namespace OpenGL {
|
||||||
|
|
||||||
XbrzFreescale::XbrzFreescale(u16 scale_factor) : TextureFilterBase(scale_factor) {
|
XbrzFreescale::XbrzFreescale(u16 scale_factor) :
|
||||||
|
TextureFilterBase(scale_factor) {
|
||||||
|
|
||||||
const OpenGLState cur_state = OpenGLState::GetCurState();
|
const OpenGLState cur_state = OpenGLState::GetCurState();
|
||||||
|
|
||||||
program.Create(xbrz_freescale_vert.data(), xbrz_freescale_frag.data());
|
program.Create(xbrz_freescale_vert.data(), xbrz_freescale_frag.data());
|
||||||
|
@ -62,7 +63,9 @@ XbrzFreescale::XbrzFreescale(u16 scale_factor) : TextureFilterBase(scale_factor)
|
||||||
glSamplerParameteri(src_sampler.handle, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
glSamplerParameteri(src_sampler.handle, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
glSamplerParameteri(src_sampler.handle, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
glSamplerParameteri(src_sampler.handle, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
glSamplerParameteri(src_sampler.handle, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
glSamplerParameteri(src_sampler.handle, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
glUniform1f(glGetUniformLocation(program.handle, "scale"), static_cast<GLfloat>(scale_factor));
|
|
||||||
|
const GLint scale_loc = glGetUniformLocation(program.handle, "scale");
|
||||||
|
glUniform1f(scale_loc, static_cast<GLfloat>(scale_factor));
|
||||||
|
|
||||||
cur_state.Apply();
|
cur_state.Apply();
|
||||||
state.draw.vertex_array = vao.handle;
|
state.draw.vertex_array = vao.handle;
|
||||||
|
@ -70,19 +73,19 @@ XbrzFreescale::XbrzFreescale(u16 scale_factor) : TextureFilterBase(scale_factor)
|
||||||
state.texture_units[0].sampler = src_sampler.handle;
|
state.texture_units[0].sampler = src_sampler.handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
void XbrzFreescale::Filter(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint dst_tex,
|
void XbrzFreescale::Filter(const OGLTexture& src_tex, Common::Rectangle<u32> src_rect,
|
||||||
const Common::Rectangle<u32>& dst_rect, GLuint read_fb_handle,
|
const OGLTexture& dst_tex, Common::Rectangle<u32> dst_rect) {
|
||||||
GLuint draw_fb_handle) {
|
|
||||||
const OpenGLState cur_state = OpenGLState::GetCurState();
|
const OpenGLState cur_state = OpenGLState::GetCurState();
|
||||||
|
|
||||||
state.texture_units[0].texture_2d = src_tex;
|
state.texture_units[0].texture_2d = src_tex.handle;
|
||||||
state.draw.draw_framebuffer = draw_fb_handle;
|
state.draw.draw_framebuffer = draw_fbo.handle;
|
||||||
state.viewport = {static_cast<GLint>(dst_rect.left), static_cast<GLint>(dst_rect.bottom),
|
state.viewport = {static_cast<GLint>(dst_rect.left), static_cast<GLint>(dst_rect.bottom),
|
||||||
static_cast<GLsizei>(dst_rect.GetWidth()),
|
static_cast<GLsizei>(dst_rect.GetWidth()),
|
||||||
static_cast<GLsizei>(dst_rect.GetHeight())};
|
static_cast<GLsizei>(dst_rect.GetHeight())};
|
||||||
state.Apply();
|
state.Apply();
|
||||||
|
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_tex, 0);
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||||
|
dst_tex.handle, 0);
|
||||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
|
||||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
#include "video_core/renderer_opengl/gl_resource_manager.h"
|
||||||
#include "video_core/renderer_opengl/gl_state.h"
|
#include "video_core/renderer_opengl/gl_state.h"
|
||||||
#include "video_core/renderer_opengl/texture_filters/texture_filter_base.h"
|
#include "video_core/renderer_opengl/texture_filters/texture_filter_base.h"
|
||||||
|
@ -15,9 +14,8 @@ public:
|
||||||
static constexpr std::string_view NAME = "xBRZ freescale";
|
static constexpr std::string_view NAME = "xBRZ freescale";
|
||||||
|
|
||||||
explicit XbrzFreescale(u16 scale_factor);
|
explicit XbrzFreescale(u16 scale_factor);
|
||||||
void Filter(GLuint src_tex, const Common::Rectangle<u32>& src_rect, GLuint dst_tex,
|
void Filter(const OGLTexture& src_tex, Common::Rectangle<u32> src_rect,
|
||||||
const Common::Rectangle<u32>& dst_rect, GLuint read_fb_handle,
|
const OGLTexture& dst_tex, Common::Rectangle<u32> dst_rect) override;
|
||||||
GLuint draw_fb_handle) override;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
OpenGLState state{};
|
OpenGLState state{};
|
||||||
|
|
Loading…
Reference in a new issue