committed by
ReinUsesLisp
2 changed files with 0 additions and 1956 deletions
-
1381src/video_core/renderer_opengl/gl_rasterizer_cache.cpp
-
575src/video_core/renderer_opengl/gl_rasterizer_cache.h
1381
src/video_core/renderer_opengl/gl_rasterizer_cache.cpp
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -1,575 +0,0 @@ |
|||
// Copyright 2018 yuzu Emulator Project |
|||
// Licensed under GPLv2 or any later version |
|||
// Refer to the license.txt file included. |
|||
|
|||
#pragma once |
|||
|
|||
#include <array> |
|||
#include <memory> |
|||
#include <string> |
|||
#include <tuple> |
|||
#include <vector> |
|||
|
|||
#include "common/alignment.h" |
|||
#include "common/bit_util.h" |
|||
#include "common/common_types.h" |
|||
#include "common/hash.h" |
|||
#include "common/math_util.h" |
|||
#include "video_core/engines/fermi_2d.h" |
|||
#include "video_core/engines/maxwell_3d.h" |
|||
#include "video_core/rasterizer_cache.h" |
|||
#include "video_core/renderer_opengl/gl_resource_manager.h" |
|||
#include "video_core/renderer_opengl/gl_shader_gen.h" |
|||
#include "video_core/surface.h" |
|||
#include "video_core/textures/decoders.h" |
|||
#include "video_core/textures/texture.h" |
|||
|
|||
namespace OpenGL { |
|||
|
|||
class CachedSurface; |
|||
using Surface = std::shared_ptr<CachedSurface>; |
|||
using SurfaceSurfaceRect_Tuple = std::tuple<Surface, Surface, Common::Rectangle<u32>>; |
|||
|
|||
using SurfaceTarget = VideoCore::Surface::SurfaceTarget; |
|||
using SurfaceType = VideoCore::Surface::SurfaceType; |
|||
using PixelFormat = VideoCore::Surface::PixelFormat; |
|||
using ComponentType = VideoCore::Surface::ComponentType; |
|||
using Maxwell = Tegra::Engines::Maxwell3D::Regs; |
|||
|
|||
struct SurfaceParams { |
|||
enum class SurfaceClass { |
|||
Uploaded, |
|||
RenderTarget, |
|||
DepthBuffer, |
|||
Copy, |
|||
}; |
|||
|
|||
static std::string SurfaceTargetName(SurfaceTarget target) { |
|||
switch (target) { |
|||
case SurfaceTarget::Texture1D: |
|||
return "Texture1D"; |
|||
case SurfaceTarget::Texture2D: |
|||
return "Texture2D"; |
|||
case SurfaceTarget::Texture3D: |
|||
return "Texture3D"; |
|||
case SurfaceTarget::Texture1DArray: |
|||
return "Texture1DArray"; |
|||
case SurfaceTarget::Texture2DArray: |
|||
return "Texture2DArray"; |
|||
case SurfaceTarget::TextureCubemap: |
|||
return "TextureCubemap"; |
|||
case SurfaceTarget::TextureCubeArray: |
|||
return "TextureCubeArray"; |
|||
default: |
|||
LOG_CRITICAL(HW_GPU, "Unimplemented surface_target={}", static_cast<u32>(target)); |
|||
UNREACHABLE(); |
|||
return fmt::format("TextureUnknown({})", static_cast<u32>(target)); |
|||
} |
|||
} |
|||
|
|||
u32 GetFormatBpp() const { |
|||
return VideoCore::Surface::GetFormatBpp(pixel_format); |
|||
} |
|||
|
|||
/// Returns the rectangle corresponding to this surface |
|||
Common::Rectangle<u32> GetRect(u32 mip_level = 0) const; |
|||
|
|||
/// Returns the total size of this surface in bytes, adjusted for compression |
|||
std::size_t SizeInBytesRaw(bool ignore_tiled = false) const { |
|||
const u32 compression_factor{GetCompressionFactor(pixel_format)}; |
|||
const u32 bytes_per_pixel{GetBytesPerPixel(pixel_format)}; |
|||
const size_t uncompressed_size{ |
|||
Tegra::Texture::CalculateSize((ignore_tiled ? false : is_tiled), bytes_per_pixel, width, |
|||
height, depth, block_height, block_depth)}; |
|||
|
|||
// Divide by compression_factor^2, as height and width are factored by this |
|||
return uncompressed_size / (compression_factor * compression_factor); |
|||
} |
|||
|
|||
/// Returns the size of this surface as an OpenGL texture in bytes |
|||
std::size_t SizeInBytesGL() const { |
|||
return SizeInBytesRaw(true); |
|||
} |
|||
|
|||
/// Returns the size of this surface as a cube face in bytes |
|||
std::size_t SizeInBytesCubeFace() const { |
|||
return size_in_bytes / 6; |
|||
} |
|||
|
|||
/// Returns the size of this surface as an OpenGL cube face in bytes |
|||
std::size_t SizeInBytesCubeFaceGL() const { |
|||
return size_in_bytes_gl / 6; |
|||
} |
|||
|
|||
/// Returns the exact size of memory occupied by the texture in VRAM, including mipmaps. |
|||
std::size_t MemorySize() const { |
|||
std::size_t size = InnerMemorySize(false, is_layered); |
|||
if (is_layered) |
|||
return size * depth; |
|||
return size; |
|||
} |
|||
|
|||
/// Returns true if the parameters constitute a valid rasterizer surface. |
|||
bool IsValid() const { |
|||
return gpu_addr && host_ptr && height && width; |
|||
} |
|||
|
|||
/// Returns the exact size of the memory occupied by a layer in a texture in VRAM, including |
|||
/// mipmaps. |
|||
std::size_t LayerMemorySize() const { |
|||
return InnerMemorySize(false, true); |
|||
} |
|||
|
|||
/// Returns the size of a layer of this surface in OpenGL. |
|||
std::size_t LayerSizeGL(u32 mip_level) const { |
|||
return InnerMipmapMemorySize(mip_level, true, is_layered, false); |
|||
} |
|||
|
|||
std::size_t GetMipmapSizeGL(u32 mip_level, bool ignore_compressed = true) const { |
|||
std::size_t size = InnerMipmapMemorySize(mip_level, true, is_layered, ignore_compressed); |
|||
if (is_layered) |
|||
return size * depth; |
|||
return size; |
|||
} |
|||
|
|||
std::size_t GetMipmapLevelOffset(u32 mip_level) const { |
|||
std::size_t offset = 0; |
|||
for (u32 i = 0; i < mip_level; i++) |
|||
offset += InnerMipmapMemorySize(i, false, is_layered); |
|||
return offset; |
|||
} |
|||
|
|||
std::size_t GetMipmapLevelOffsetGL(u32 mip_level) const { |
|||
std::size_t offset = 0; |
|||
for (u32 i = 0; i < mip_level; i++) |
|||
offset += InnerMipmapMemorySize(i, true, is_layered); |
|||
return offset; |
|||
} |
|||
|
|||
std::size_t GetMipmapSingleSize(u32 mip_level) const { |
|||
return InnerMipmapMemorySize(mip_level, false, is_layered); |
|||
} |
|||
|
|||
u32 MipWidth(u32 mip_level) const { |
|||
return std::max(1U, width >> mip_level); |
|||
} |
|||
|
|||
u32 MipWidthGobAligned(u32 mip_level) const { |
|||
return Common::AlignUp(std::max(1U, width >> mip_level), 64U * 8U / GetFormatBpp()); |
|||
} |
|||
|
|||
u32 MipHeight(u32 mip_level) const { |
|||
return std::max(1U, height >> mip_level); |
|||
} |
|||
|
|||
u32 MipDepth(u32 mip_level) const { |
|||
return is_layered ? depth : std::max(1U, depth >> mip_level); |
|||
} |
|||
|
|||
// Auto block resizing algorithm from: |
|||
// https://cgit.freedesktop.org/mesa/mesa/tree/src/gallium/drivers/nouveau/nv50/nv50_miptree.c |
|||
u32 MipBlockHeight(u32 mip_level) const { |
|||
if (mip_level == 0) |
|||
return block_height; |
|||
u32 alt_height = MipHeight(mip_level); |
|||
u32 h = GetDefaultBlockHeight(pixel_format); |
|||
u32 blocks_in_y = (alt_height + h - 1) / h; |
|||
u32 bh = 16; |
|||
while (bh > 1 && blocks_in_y <= bh * 4) { |
|||
bh >>= 1; |
|||
} |
|||
return bh; |
|||
} |
|||
|
|||
u32 MipBlockDepth(u32 mip_level) const { |
|||
if (mip_level == 0) { |
|||
return block_depth; |
|||
} |
|||
|
|||
if (is_layered) { |
|||
return 1; |
|||
} |
|||
|
|||
const u32 mip_depth = MipDepth(mip_level); |
|||
u32 bd = 32; |
|||
while (bd > 1 && mip_depth * 2 <= bd) { |
|||
bd >>= 1; |
|||
} |
|||
|
|||
if (bd == 32) { |
|||
const u32 bh = MipBlockHeight(mip_level); |
|||
if (bh >= 4) { |
|||
return 16; |
|||
} |
|||
} |
|||
|
|||
return bd; |
|||
} |
|||
|
|||
u32 RowAlign(u32 mip_level) const { |
|||
const u32 m_width = MipWidth(mip_level); |
|||
const u32 bytes_per_pixel = GetBytesPerPixel(pixel_format); |
|||
const u32 l2 = Common::CountTrailingZeroes32(m_width * bytes_per_pixel); |
|||
return (1U << l2); |
|||
} |
|||
|
|||
/// Creates SurfaceParams from a texture configuration |
|||
static SurfaceParams CreateForTexture(const Tegra::Texture::FullTextureInfo& config, |
|||
const GLShader::SamplerEntry& entry); |
|||
|
|||
/// Creates SurfaceParams from a framebuffer configuration |
|||
static SurfaceParams CreateForFramebuffer(std::size_t index); |
|||
|
|||
/// Creates SurfaceParams for a depth buffer configuration |
|||
static SurfaceParams CreateForDepthBuffer( |
|||
u32 zeta_width, u32 zeta_height, GPUVAddr zeta_address, Tegra::DepthFormat format, |
|||
u32 block_width, u32 block_height, u32 block_depth, |
|||
Tegra::Engines::Maxwell3D::Regs::InvMemoryLayout type); |
|||
|
|||
/// Creates SurfaceParams for a Fermi2D surface copy |
|||
static SurfaceParams CreateForFermiCopySurface( |
|||
const Tegra::Engines::Fermi2D::Regs::Surface& config); |
|||
|
|||
/// Checks if surfaces are compatible for caching |
|||
bool IsCompatibleSurface(const SurfaceParams& other) const { |
|||
if (std::tie(pixel_format, type, width, height, target, depth, is_tiled) == |
|||
std::tie(other.pixel_format, other.type, other.width, other.height, other.target, |
|||
other.depth, other.is_tiled)) { |
|||
if (!is_tiled) |
|||
return true; |
|||
return std::tie(block_height, block_depth, tile_width_spacing) == |
|||
std::tie(other.block_height, other.block_depth, other.tile_width_spacing); |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
/// Initializes parameters for caching, should be called after everything has been initialized |
|||
void InitCacheParameters(GPUVAddr gpu_addr); |
|||
|
|||
std::string TargetName() const { |
|||
switch (target) { |
|||
case SurfaceTarget::Texture1D: |
|||
return "1D"; |
|||
case SurfaceTarget::TextureBuffer: |
|||
return "Buffer"; |
|||
case SurfaceTarget::Texture2D: |
|||
return "2D"; |
|||
case SurfaceTarget::Texture3D: |
|||
return "3D"; |
|||
case SurfaceTarget::Texture1DArray: |
|||
return "1DArray"; |
|||
case SurfaceTarget::Texture2DArray: |
|||
return "2DArray"; |
|||
case SurfaceTarget::TextureCubemap: |
|||
return "Cube"; |
|||
default: |
|||
LOG_CRITICAL(HW_GPU, "Unimplemented surface_target={}", static_cast<u32>(target)); |
|||
UNREACHABLE(); |
|||
return fmt::format("TUK({})", static_cast<u32>(target)); |
|||
} |
|||
} |
|||
|
|||
std::string ClassName() const { |
|||
switch (identity) { |
|||
case SurfaceClass::Uploaded: |
|||
return "UP"; |
|||
case SurfaceClass::RenderTarget: |
|||
return "RT"; |
|||
case SurfaceClass::DepthBuffer: |
|||
return "DB"; |
|||
case SurfaceClass::Copy: |
|||
return "CP"; |
|||
default: |
|||
LOG_CRITICAL(HW_GPU, "Unimplemented surface_class={}", static_cast<u32>(identity)); |
|||
UNREACHABLE(); |
|||
return fmt::format("CUK({})", static_cast<u32>(identity)); |
|||
} |
|||
} |
|||
|
|||
std::string IdentityString() const { |
|||
return ClassName() + '_' + TargetName() + '_' + (is_tiled ? 'T' : 'L'); |
|||
} |
|||
|
|||
bool is_tiled; |
|||
u32 block_width; |
|||
u32 block_height; |
|||
u32 block_depth; |
|||
u32 tile_width_spacing; |
|||
PixelFormat pixel_format; |
|||
ComponentType component_type; |
|||
SurfaceType type; |
|||
u32 width; |
|||
u32 height; |
|||
u32 depth; |
|||
u32 unaligned_height; |
|||
u32 pitch; |
|||
SurfaceTarget target; |
|||
SurfaceClass identity; |
|||
u32 max_mip_level; |
|||
bool is_layered; |
|||
bool is_array; |
|||
bool srgb_conversion; |
|||
// Parameters used for caching |
|||
u8* host_ptr; |
|||
GPUVAddr gpu_addr; |
|||
std::size_t size_in_bytes; |
|||
std::size_t size_in_bytes_gl; |
|||
|
|||
// Render target specific parameters, not used in caching |
|||
struct { |
|||
u32 index; |
|||
u32 array_mode; |
|||
u32 volume; |
|||
u32 layer_stride; |
|||
u32 base_layer; |
|||
} rt; |
|||
|
|||
private: |
|||
std::size_t InnerMipmapMemorySize(u32 mip_level, bool force_gl = false, bool layer_only = false, |
|||
bool uncompressed = false) const; |
|||
std::size_t InnerMemorySize(bool force_gl = false, bool layer_only = false, |
|||
bool uncompressed = false) const; |
|||
}; |
|||
|
|||
}; // namespace OpenGL |
|||
|
|||
/// Hashable variation of SurfaceParams, used for a key in the surface cache |
|||
struct SurfaceReserveKey : Common::HashableStruct<OpenGL::SurfaceParams> { |
|||
static SurfaceReserveKey Create(const OpenGL::SurfaceParams& params) { |
|||
SurfaceReserveKey res; |
|||
res.state = params; |
|||
res.state.identity = {}; // Ignore the origin of the texture |
|||
res.state.gpu_addr = {}; // Ignore GPU vaddr in caching |
|||
res.state.rt = {}; // Ignore rt config in caching |
|||
return res; |
|||
} |
|||
}; |
|||
namespace std { |
|||
template <> |
|||
struct hash<SurfaceReserveKey> { |
|||
std::size_t operator()(const SurfaceReserveKey& k) const { |
|||
return k.Hash(); |
|||
} |
|||
}; |
|||
} // namespace std |
|||
|
|||
namespace OpenGL { |
|||
|
|||
class RasterizerOpenGL; |
|||
|
|||
// This is used to store temporary big buffers, |
|||
// instead of creating/destroying all the time |
|||
struct RasterizerTemporaryMemory { |
|||
std::vector<std::vector<u8>> gl_buffer; |
|||
}; |
|||
|
|||
class CachedSurface final : public RasterizerCacheObject { |
|||
public: |
|||
explicit CachedSurface(const SurfaceParams& params); |
|||
|
|||
VAddr GetCpuAddr() const override { |
|||
return cpu_addr; |
|||
} |
|||
|
|||
std::size_t GetSizeInBytes() const override { |
|||
return cached_size_in_bytes; |
|||
} |
|||
|
|||
std::size_t GetMemorySize() const { |
|||
return memory_size; |
|||
} |
|||
|
|||
const OGLTexture& Texture() const { |
|||
return texture; |
|||
} |
|||
|
|||
const OGLTexture& Texture(bool as_array) { |
|||
if (params.is_array == as_array) { |
|||
return texture; |
|||
} else { |
|||
EnsureTextureDiscrepantView(); |
|||
return discrepant_view; |
|||
} |
|||
} |
|||
|
|||
GLenum Target() const { |
|||
return gl_target; |
|||
} |
|||
|
|||
const SurfaceParams& GetSurfaceParams() const { |
|||
return params; |
|||
} |
|||
|
|||
// Read/Write data in Switch memory to/from gl_buffer |
|||
void LoadGLBuffer(RasterizerTemporaryMemory& res_cache_tmp_mem); |
|||
void FlushGLBuffer(RasterizerTemporaryMemory& res_cache_tmp_mem); |
|||
|
|||
// Upload data in gl_buffer to this surface's texture |
|||
void UploadGLTexture(RasterizerTemporaryMemory& res_cache_tmp_mem, GLuint read_fb_handle, |
|||
GLuint draw_fb_handle); |
|||
|
|||
void UpdateSwizzle(Tegra::Texture::SwizzleSource swizzle_x, |
|||
Tegra::Texture::SwizzleSource swizzle_y, |
|||
Tegra::Texture::SwizzleSource swizzle_z, |
|||
Tegra::Texture::SwizzleSource swizzle_w); |
|||
|
|||
void MarkReinterpreted() { |
|||
reinterpreted = true; |
|||
} |
|||
|
|||
bool IsReinterpreted() const { |
|||
return reinterpreted; |
|||
} |
|||
|
|||
void MarkForReload(bool reload) { |
|||
must_reload = reload; |
|||
} |
|||
|
|||
bool MustReload() const { |
|||
return must_reload; |
|||
} |
|||
|
|||
bool IsUploaded() const { |
|||
return params.identity == SurfaceParams::SurfaceClass::Uploaded; |
|||
} |
|||
|
|||
private: |
|||
void UploadGLMipmapTexture(RasterizerTemporaryMemory& res_cache_tmp_mem, u32 mip_map, |
|||
GLuint read_fb_handle, GLuint draw_fb_handle); |
|||
|
|||
void EnsureTextureDiscrepantView(); |
|||
|
|||
OGLTexture texture; |
|||
OGLTexture discrepant_view; |
|||
OGLBuffer texture_buffer; |
|||
SurfaceParams params{}; |
|||
GLenum gl_target{}; |
|||
GLenum gl_internal_format{}; |
|||
std::size_t cached_size_in_bytes{}; |
|||
std::array<GLenum, 4> swizzle{GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA}; |
|||
std::size_t memory_size; |
|||
bool reinterpreted = false; |
|||
bool must_reload = false; |
|||
VAddr cpu_addr{}; |
|||
}; |
|||
|
|||
class RasterizerCacheOpenGL final : public RasterizerCache<Surface> { |
|||
public: |
|||
explicit RasterizerCacheOpenGL(RasterizerOpenGL& rasterizer); |
|||
|
|||
/// Get a surface based on the texture configuration |
|||
Surface GetTextureSurface(const Tegra::Texture::FullTextureInfo& config, |
|||
const GLShader::SamplerEntry& entry); |
|||
|
|||
/// Get the depth surface based on the framebuffer configuration |
|||
Surface GetDepthBufferSurface(bool preserve_contents); |
|||
|
|||
/// Get the color surface based on the framebuffer configuration and the specified render target |
|||
Surface GetColorBufferSurface(std::size_t index, bool preserve_contents); |
|||
|
|||
/// Tries to find a framebuffer using on the provided CPU address |
|||
Surface TryFindFramebufferSurface(const u8* host_ptr) const; |
|||
|
|||
/// Copies the contents of one surface to another |
|||
void FermiCopySurface(const Tegra::Engines::Fermi2D::Regs::Surface& src_config, |
|||
const Tegra::Engines::Fermi2D::Regs::Surface& dst_config, |
|||
const Common::Rectangle<u32>& src_rect, |
|||
const Common::Rectangle<u32>& dst_rect); |
|||
|
|||
void SignalPreDrawCall(); |
|||
void SignalPostDrawCall(); |
|||
|
|||
protected: |
|||
void FlushObjectInner(const Surface& object) override { |
|||
object->FlushGLBuffer(temporal_memory); |
|||
} |
|||
|
|||
private: |
|||
void LoadSurface(const Surface& surface); |
|||
Surface GetSurface(const SurfaceParams& params, bool preserve_contents = true); |
|||
|
|||
/// Gets an uncached surface, creating it if need be |
|||
Surface GetUncachedSurface(const SurfaceParams& params); |
|||
|
|||
/// Recreates a surface with new parameters |
|||
Surface RecreateSurface(const Surface& old_surface, const SurfaceParams& new_params); |
|||
|
|||
/// Reserves a unique surface that can be reused later |
|||
void ReserveSurface(const Surface& surface); |
|||
|
|||
/// Tries to get a reserved surface for the specified parameters |
|||
Surface TryGetReservedSurface(const SurfaceParams& params); |
|||
|
|||
// Partialy reinterpret a surface based on a triggering_surface that collides with it. |
|||
// returns true if the reinterpret was successful, false in case it was not. |
|||
bool PartialReinterpretSurface(Surface triggering_surface, Surface intersect); |
|||
|
|||
/// Performs a slow but accurate surface copy, flushing to RAM and reinterpreting the data |
|||
void AccurateCopySurface(const Surface& src_surface, const Surface& dst_surface); |
|||
void FastLayeredCopySurface(const Surface& src_surface, const Surface& dst_surface); |
|||
void FastCopySurface(const Surface& src_surface, const Surface& dst_surface); |
|||
void CopySurface(const Surface& src_surface, const Surface& dst_surface, |
|||
const GLuint copy_pbo_handle, const GLenum src_attachment = 0, |
|||
const GLenum dst_attachment = 0, const std::size_t cubemap_face = 0); |
|||
|
|||
/// The surface reserve is a "backup" cache, this is where we put unique surfaces that have |
|||
/// previously been used. This is to prevent surfaces from being constantly created and |
|||
/// destroyed when used with different surface parameters. |
|||
std::unordered_map<SurfaceReserveKey, Surface> surface_reserve; |
|||
|
|||
OGLFramebuffer read_framebuffer; |
|||
OGLFramebuffer draw_framebuffer; |
|||
|
|||
bool texception = false; |
|||
|
|||
/// Use a Pixel Buffer Object to download the previous texture and then upload it to the new one |
|||
/// using the new format. |
|||
OGLBuffer copy_pbo; |
|||
|
|||
std::array<Surface, Maxwell::NumRenderTargets> last_color_buffers; |
|||
std::array<Surface, Maxwell::NumRenderTargets> current_color_buffers; |
|||
Surface last_depth_buffer; |
|||
|
|||
RasterizerTemporaryMemory temporal_memory; |
|||
|
|||
using SurfaceIntervalCache = boost::icl::interval_map<CacheAddr, Surface>; |
|||
using SurfaceInterval = typename SurfaceIntervalCache::interval_type; |
|||
|
|||
static auto GetReinterpretInterval(const Surface& object) { |
|||
return SurfaceInterval::right_open(object->GetCacheAddr() + 1, |
|||
object->GetCacheAddr() + object->GetMemorySize() - 1); |
|||
} |
|||
|
|||
// Reinterpreted surfaces are very fragil as the game may keep rendering into them. |
|||
SurfaceIntervalCache reinterpreted_surfaces; |
|||
|
|||
void RegisterReinterpretSurface(Surface reinterpret_surface) { |
|||
auto interval = GetReinterpretInterval(reinterpret_surface); |
|||
reinterpreted_surfaces.insert({interval, reinterpret_surface}); |
|||
reinterpret_surface->MarkReinterpreted(); |
|||
} |
|||
|
|||
Surface CollideOnReinterpretedSurface(CacheAddr addr) const { |
|||
const SurfaceInterval interval{addr}; |
|||
for (auto& pair : |
|||
boost::make_iterator_range(reinterpreted_surfaces.equal_range(interval))) { |
|||
return pair.second; |
|||
} |
|||
return nullptr; |
|||
} |
|||
|
|||
void Register(const Surface& object) override { |
|||
RasterizerCache<Surface>::Register(object); |
|||
} |
|||
|
|||
/// Unregisters an object from the cache |
|||
void Unregister(const Surface& object) override { |
|||
if (object->IsReinterpreted()) { |
|||
auto interval = GetReinterpretInterval(object); |
|||
reinterpreted_surfaces.erase(interval); |
|||
} |
|||
RasterizerCache<Surface>::Unregister(object); |
|||
} |
|||
}; |
|||
|
|||
} // namespace OpenGL |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue