mirror of
				https://git.h3cjp.net/H3cJP/citra.git
				synced 2025-10-31 06:55:03 +00:00 
			
		
		
		
	Merge pull request #314 from jroweboy/tegra-progress-3b
GPU: Bind uploaded textures when drawing (Rebased)
This commit is contained in:
		
						commit
						227bc78cbe
					
				|  | @ -231,6 +231,8 @@ Texture::TICEntry Maxwell3D::GetTICEntry(u32 tic_index) const { | ||||||
| 
 | 
 | ||||||
|     // TODO(Subv): Different data types for separate components are not supported
 |     // TODO(Subv): Different data types for separate components are not supported
 | ||||||
|     ASSERT(r_type == g_type && r_type == b_type && r_type == a_type); |     ASSERT(r_type == g_type && r_type == b_type && r_type == a_type); | ||||||
|  |     // TODO(Subv): Only UNORM formats are supported for now.
 | ||||||
|  |     ASSERT(r_type == Texture::ComponentType::UNORM); | ||||||
| 
 | 
 | ||||||
|     return tic_entry; |     return tic_entry; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -66,6 +66,12 @@ RasterizerOpenGL::RasterizerOpenGL() { | ||||||
|     has_ARB_separate_shader_objects = false; |     has_ARB_separate_shader_objects = false; | ||||||
|     has_ARB_vertex_attrib_binding = false; |     has_ARB_vertex_attrib_binding = false; | ||||||
| 
 | 
 | ||||||
|  |     // Create sampler objects
 | ||||||
|  |     for (size_t i = 0; i < texture_samplers.size(); ++i) { | ||||||
|  |         texture_samplers[i].Create(); | ||||||
|  |         state.texture_units[i].sampler = texture_samplers[i].sampler.handle; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     GLint ext_num; |     GLint ext_num; | ||||||
|     glGetIntegerv(GL_NUM_EXTENSIONS, &ext_num); |     glGetIntegerv(GL_NUM_EXTENSIONS, &ext_num); | ||||||
|     for (GLint i = 0; i < ext_num; i++) { |     for (GLint i = 0; i < ext_num; i++) { | ||||||
|  | @ -270,7 +276,9 @@ void RasterizerOpenGL::DrawArrays() { | ||||||
| 
 | 
 | ||||||
|     // TODO(bunnei): Sync framebuffer_scale uniform here
 |     // TODO(bunnei): Sync framebuffer_scale uniform here
 | ||||||
|     // TODO(bunnei): Sync scissorbox uniform(s) here
 |     // TODO(bunnei): Sync scissorbox uniform(s) here
 | ||||||
|     // TODO(bunnei): Sync and bind the texture surfaces
 | 
 | ||||||
|  |     // Sync and bind the texture surfaces
 | ||||||
|  |     BindTextures(); | ||||||
| 
 | 
 | ||||||
|     // Sync and bind the shader
 |     // Sync and bind the shader
 | ||||||
|     if (shader_dirty) { |     if (shader_dirty) { | ||||||
|  | @ -374,6 +382,39 @@ void RasterizerOpenGL::DrawArrays() { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | void RasterizerOpenGL::BindTextures() { | ||||||
|  |     using Regs = Tegra::Engines::Maxwell3D::Regs; | ||||||
|  |     auto maxwell3d = Core::System::GetInstance().GPU().Get3DEngine(); | ||||||
|  | 
 | ||||||
|  |     // Each Maxwell shader stage can have an arbitrary number of textures, but we're limited to a
 | ||||||
|  |     // certain number in OpenGL. We try to only use the minimum amount of host textures by not
 | ||||||
|  |     // keeping a 1:1 relation between guest texture ids and host texture ids, ie, guest texture id 8
 | ||||||
|  |     // can be host texture id 0 if it's the only texture used in the guest shader program.
 | ||||||
|  |     u32 host_texture_index = 0; | ||||||
|  |     for (u32 stage = 0; stage < Regs::MaxShaderStage; ++stage) { | ||||||
|  |         ASSERT(host_texture_index < texture_samplers.size()); | ||||||
|  |         const auto textures = maxwell3d.GetStageTextures(static_cast<Regs::ShaderStage>(stage)); | ||||||
|  |         for (unsigned texture_index = 0; texture_index < textures.size(); ++texture_index) { | ||||||
|  |             const auto& texture = textures[texture_index]; | ||||||
|  | 
 | ||||||
|  |             if (texture.enabled) { | ||||||
|  |                 texture_samplers[host_texture_index].SyncWithConfig(texture.tsc); | ||||||
|  |                 Surface surface = res_cache.GetTextureSurface(texture); | ||||||
|  |                 if (surface != nullptr) { | ||||||
|  |                     state.texture_units[host_texture_index].texture_2d = surface->texture.handle; | ||||||
|  |                 } else { | ||||||
|  |                     // Can occur when texture addr is null or its memory is unmapped/invalid
 | ||||||
|  |                     state.texture_units[texture_index].texture_2d = 0; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 ++host_texture_index; | ||||||
|  |             } else { | ||||||
|  |                 state.texture_units[texture_index].texture_2d = 0; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| void RasterizerOpenGL::NotifyMaxwellRegisterChanged(u32 id) {} | void RasterizerOpenGL::NotifyMaxwellRegisterChanged(u32 id) {} | ||||||
| 
 | 
 | ||||||
| void RasterizerOpenGL::FlushAll() { | void RasterizerOpenGL::FlushAll() { | ||||||
|  | @ -452,6 +493,44 @@ bool RasterizerOpenGL::AccelerateDisplay(const Tegra::FramebufferConfig& framebu | ||||||
|     return true; |     return true; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | void RasterizerOpenGL::SamplerInfo::Create() { | ||||||
|  |     sampler.Create(); | ||||||
|  |     mag_filter = min_filter = Tegra::Texture::TextureFilter::Linear; | ||||||
|  |     wrap_u = wrap_v = Tegra::Texture::WrapMode::Wrap; | ||||||
|  |     border_color_r = border_color_g = border_color_b = border_color_a = 0; | ||||||
|  | 
 | ||||||
|  |     // default is GL_LINEAR_MIPMAP_LINEAR
 | ||||||
|  |     glSamplerParameteri(sampler.handle, GL_TEXTURE_MIN_FILTER, GL_LINEAR); | ||||||
|  |     // Other attributes have correct defaults
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void RasterizerOpenGL::SamplerInfo::SyncWithConfig(const Tegra::Texture::TSCEntry& config) { | ||||||
|  |     GLuint s = sampler.handle; | ||||||
|  | 
 | ||||||
|  |     if (mag_filter != config.mag_filter) { | ||||||
|  |         mag_filter = config.mag_filter; | ||||||
|  |         glSamplerParameteri(s, GL_TEXTURE_MAG_FILTER, MaxwellToGL::TextureFilterMode(mag_filter)); | ||||||
|  |     } | ||||||
|  |     if (min_filter != config.min_filter) { | ||||||
|  |         min_filter = config.min_filter; | ||||||
|  |         glSamplerParameteri(s, GL_TEXTURE_MIN_FILTER, MaxwellToGL::TextureFilterMode(min_filter)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (wrap_u != config.wrap_u) { | ||||||
|  |         wrap_u = config.wrap_u; | ||||||
|  |         glSamplerParameteri(s, GL_TEXTURE_WRAP_S, MaxwellToGL::WrapMode(wrap_u)); | ||||||
|  |     } | ||||||
|  |     if (wrap_v != config.wrap_v) { | ||||||
|  |         wrap_v = config.wrap_v; | ||||||
|  |         glSamplerParameteri(s, GL_TEXTURE_WRAP_T, MaxwellToGL::WrapMode(wrap_v)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (wrap_u == Tegra::Texture::WrapMode::Border || wrap_v == Tegra::Texture::WrapMode::Border) { | ||||||
|  |         // TODO(Subv): Implement border color
 | ||||||
|  |         ASSERT(false); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| void RasterizerOpenGL::SetShader() { | void RasterizerOpenGL::SetShader() { | ||||||
|     // TODO(bunnei): The below sets up a static test shader for passing untransformed vertices to
 |     // TODO(bunnei): The below sets up a static test shader for passing untransformed vertices to
 | ||||||
|     // OpenGL for rendering. This should be removed/replaced when we start emulating Maxwell
 |     // OpenGL for rendering. This should be removed/replaced when we start emulating Maxwell
 | ||||||
|  | @ -479,10 +558,10 @@ void main() { | ||||||
| in vec2 frag_tex_coord; | in vec2 frag_tex_coord; | ||||||
| out vec4 color; | out vec4 color; | ||||||
| 
 | 
 | ||||||
| uniform sampler2D color_texture; | uniform sampler2D tex[32]; | ||||||
| 
 | 
 | ||||||
| void main() { | void main() { | ||||||
|     color = vec4(1.0, 0.0, 1.0, 0.0); |     color = texture(tex[0], frag_tex_coord); | ||||||
| } | } | ||||||
| )"; | )"; | ||||||
| 
 | 
 | ||||||
|  | @ -503,6 +582,15 @@ void main() { | ||||||
|     state.draw.shader_program = test_shader.shader.handle; |     state.draw.shader_program = test_shader.shader.handle; | ||||||
|     state.Apply(); |     state.Apply(); | ||||||
| 
 | 
 | ||||||
|  |     for (u32 texture = 0; texture < texture_samplers.size(); ++texture) { | ||||||
|  |         // Set the texture samplers to correspond to different texture units
 | ||||||
|  |         std::string uniform_name = "tex[" + std::to_string(texture) + "]"; | ||||||
|  |         GLint uniform_tex = glGetUniformLocation(test_shader.shader.handle, uniform_name.c_str()); | ||||||
|  |         if (uniform_tex != -1) { | ||||||
|  |             glUniform1i(uniform_tex, TextureUnits::MaxwellTexture(texture).id); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     if (has_ARB_separate_shader_objects) { |     if (has_ARB_separate_shader_objects) { | ||||||
|         state.draw.shader_program = 0; |         state.draw.shader_program = 0; | ||||||
|         state.Apply(); |         state.Apply(); | ||||||
|  |  | ||||||
|  | @ -85,12 +85,34 @@ public: | ||||||
|                   "FSUniformData structure must be less than 16kb as per the OpenGL spec"); |                   "FSUniformData structure must be less than 16kb as per the OpenGL spec"); | ||||||
| 
 | 
 | ||||||
| private: | private: | ||||||
|     struct SamplerInfo {}; |     class SamplerInfo { | ||||||
|  |     public: | ||||||
|  |         OGLSampler sampler; | ||||||
|  | 
 | ||||||
|  |         /// Creates the sampler object, initializing its state so that it's in sync with the
 | ||||||
|  |         /// SamplerInfo struct.
 | ||||||
|  |         void Create(); | ||||||
|  |         /// Syncs the sampler object with the config, updating any necessary state.
 | ||||||
|  |         void SyncWithConfig(const Tegra::Texture::TSCEntry& config); | ||||||
|  | 
 | ||||||
|  |     private: | ||||||
|  |         Tegra::Texture::TextureFilter mag_filter; | ||||||
|  |         Tegra::Texture::TextureFilter min_filter; | ||||||
|  |         Tegra::Texture::WrapMode wrap_u; | ||||||
|  |         Tegra::Texture::WrapMode wrap_v; | ||||||
|  |         u32 border_color_r; | ||||||
|  |         u32 border_color_g; | ||||||
|  |         u32 border_color_b; | ||||||
|  |         u32 border_color_a; | ||||||
|  |     }; | ||||||
| 
 | 
 | ||||||
|     /// Binds the framebuffer color and depth surface
 |     /// Binds the framebuffer color and depth surface
 | ||||||
|     void BindFramebufferSurfaces(const Surface& color_surface, const Surface& depth_surface, |     void BindFramebufferSurfaces(const Surface& color_surface, const Surface& depth_surface, | ||||||
|                                  bool has_stencil); |                                  bool has_stencil); | ||||||
| 
 | 
 | ||||||
|  |     /// Binds the required textures to OpenGL before drawing a batch.
 | ||||||
|  |     void BindTextures(); | ||||||
|  | 
 | ||||||
|     /// Syncs the viewport to match the guest state
 |     /// Syncs the viewport to match the guest state
 | ||||||
|     void SyncViewport(const MathUtil::Rectangle<u32>& surfaces_rect, u16 res_scale); |     void SyncViewport(const MathUtil::Rectangle<u32>& surfaces_rect, u16 res_scale); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -30,6 +30,7 @@ | ||||||
| #include "video_core/engines/maxwell_3d.h" | #include "video_core/engines/maxwell_3d.h" | ||||||
| #include "video_core/renderer_opengl/gl_rasterizer_cache.h" | #include "video_core/renderer_opengl/gl_rasterizer_cache.h" | ||||||
| #include "video_core/renderer_opengl/gl_state.h" | #include "video_core/renderer_opengl/gl_state.h" | ||||||
|  | #include "video_core/textures/decoders.h" | ||||||
| #include "video_core/utils.h" | #include "video_core/utils.h" | ||||||
| #include "video_core/video_core.h" | #include "video_core/video_core.h" | ||||||
| 
 | 
 | ||||||
|  | @ -40,36 +41,36 @@ struct FormatTuple { | ||||||
|     GLint internal_format; |     GLint internal_format; | ||||||
|     GLenum format; |     GLenum format; | ||||||
|     GLenum type; |     GLenum type; | ||||||
|  |     bool compressed; | ||||||
|  |     // How many pixels in the original texture are equivalent to one pixel in the compressed
 | ||||||
|  |     // texture.
 | ||||||
|  |     u32 compression_factor; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| static constexpr std::array<FormatTuple, 5> fb_format_tuples = {{ | static constexpr std::array<FormatTuple, 1> fb_format_tuples = {{ | ||||||
|     {GL_RGBA8, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8},     // RGBA8
 |     {GL_RGBA8, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, false, 1}, // RGBA8
 | ||||||
|     {GL_RGB8, GL_BGR, GL_UNSIGNED_BYTE},              // RGB8
 |  | ||||||
|     {GL_RGB5_A1, GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1}, // RGB5A1
 |  | ||||||
|     {GL_RGB565, GL_RGB, GL_UNSIGNED_SHORT_5_6_5},     // RGB565
 |  | ||||||
|     {GL_RGBA4, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4},   // RGBA4
 |  | ||||||
| }}; | }}; | ||||||
| 
 | 
 | ||||||
| static constexpr std::array<FormatTuple, 4> depth_format_tuples = {{ | static constexpr std::array<FormatTuple, 2> tex_format_tuples = {{ | ||||||
|     {GL_DEPTH_COMPONENT16, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT}, // D16
 |     {GL_RGBA8, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, false, 1},                       // RGBA8
 | ||||||
|     {}, |     {GL_COMPRESSED_RGB_S3TC_DXT1_EXT, GL_RGB, GL_UNSIGNED_INT_8_8_8_8, true, 16}, // DXT1
 | ||||||
|     {GL_DEPTH_COMPONENT24, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT},   // D24
 |  | ||||||
|     {GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8}, // D24S8
 |  | ||||||
| }}; | }}; | ||||||
| 
 | 
 | ||||||
| static constexpr FormatTuple tex_tuple = {GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE}; |  | ||||||
| 
 |  | ||||||
| static const FormatTuple& GetFormatTuple(PixelFormat pixel_format) { | static const FormatTuple& GetFormatTuple(PixelFormat pixel_format) { | ||||||
|     const SurfaceType type = SurfaceParams::GetFormatType(pixel_format); |     const SurfaceType type = SurfaceParams::GetFormatType(pixel_format); | ||||||
|     if (type == SurfaceType::Color) { |     if (type == SurfaceType::Color) { | ||||||
|         ASSERT(static_cast<size_t>(pixel_format) < fb_format_tuples.size()); |         ASSERT(static_cast<size_t>(pixel_format) < fb_format_tuples.size()); | ||||||
|         return fb_format_tuples[static_cast<unsigned int>(pixel_format)]; |         return fb_format_tuples[static_cast<unsigned int>(pixel_format)]; | ||||||
|     } else if (type == SurfaceType::Depth || type == SurfaceType::DepthStencil) { |     } else if (type == SurfaceType::Depth || type == SurfaceType::DepthStencil) { | ||||||
|         size_t tuple_idx = static_cast<size_t>(pixel_format) - 14; |         // TODO(Subv): Implement depth formats
 | ||||||
|         ASSERT(tuple_idx < depth_format_tuples.size()); |         ASSERT_MSG(false, "Unimplemented"); | ||||||
|         return depth_format_tuples[tuple_idx]; |     } else if (type == SurfaceType::Texture) { | ||||||
|  |         ASSERT(static_cast<size_t>(pixel_format) < tex_format_tuples.size()); | ||||||
|  |         return tex_format_tuples[static_cast<unsigned int>(pixel_format)]; | ||||||
|     } |     } | ||||||
|     return tex_tuple; | 
 | ||||||
|  |     UNREACHABLE(); | ||||||
|  |     return {}; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| template <typename Map, typename Interval> | template <typename Map, typename Interval> | ||||||
|  | @ -92,26 +93,16 @@ static void MortonCopyTile(u32 stride, u8* tile_buffer, u8* gl_buffer) { | ||||||
|             u8* tile_ptr = tile_buffer + VideoCore::MortonInterleave(x, y) * bytes_per_pixel; |             u8* tile_ptr = tile_buffer + VideoCore::MortonInterleave(x, y) * bytes_per_pixel; | ||||||
|             u8* gl_ptr = gl_buffer + ((7 - y) * stride + x) * gl_bytes_per_pixel; |             u8* gl_ptr = gl_buffer + ((7 - y) * stride + x) * gl_bytes_per_pixel; | ||||||
|             if (morton_to_gl) { |             if (morton_to_gl) { | ||||||
|                 if (format == PixelFormat::D24S8) { |                 std::memcpy(gl_ptr, tile_ptr, bytes_per_pixel); | ||||||
|                     gl_ptr[0] = tile_ptr[3]; |  | ||||||
|                     std::memcpy(gl_ptr + 1, tile_ptr, 3); |  | ||||||
|                 } else { |  | ||||||
|                     std::memcpy(gl_ptr, tile_ptr, bytes_per_pixel); |  | ||||||
|                 } |  | ||||||
|             } else { |             } else { | ||||||
|                 if (format == PixelFormat::D24S8) { |                 std::memcpy(tile_ptr, gl_ptr, bytes_per_pixel); | ||||||
|                     std::memcpy(tile_ptr, gl_ptr + 1, 3); |  | ||||||
|                     tile_ptr[3] = gl_ptr[0]; |  | ||||||
|                 } else { |  | ||||||
|                     std::memcpy(tile_ptr, gl_ptr, bytes_per_pixel); |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| template <bool morton_to_gl, PixelFormat format> | template <bool morton_to_gl, PixelFormat format> | ||||||
| static void MortonCopy(u32 stride, u32 height, u8* gl_buffer, VAddr base, VAddr start, VAddr end) { | void MortonCopy(u32 stride, u32 height, u8* gl_buffer, VAddr base, VAddr start, VAddr end) { | ||||||
|     constexpr u32 bytes_per_pixel = SurfaceParams::GetFormatBpp(format) / 8; |     constexpr u32 bytes_per_pixel = SurfaceParams::GetFormatBpp(format) / 8; | ||||||
|     constexpr u32 gl_bytes_per_pixel = CachedSurface::GetGLBytesPerPixel(format); |     constexpr u32 gl_bytes_per_pixel = CachedSurface::GetGLBytesPerPixel(format); | ||||||
| 
 | 
 | ||||||
|  | @ -122,46 +113,28 @@ static void MortonCopy(u32 stride, u32 height, u8* gl_buffer, VAddr base, VAddr | ||||||
|                                    Memory::GetPointer(base), gl_buffer, morton_to_gl); |                                    Memory::GetPointer(base), gl_buffer, morton_to_gl); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static constexpr std::array<void (*)(u32, u32, u8*, VAddr, VAddr, VAddr), 18> morton_to_gl_fns = { | template <> | ||||||
|  | void MortonCopy<true, PixelFormat::DXT1>(u32 stride, u32 height, u8* gl_buffer, VAddr base, | ||||||
|  |                                          VAddr start, VAddr end) { | ||||||
|  |     constexpr u32 bytes_per_pixel = SurfaceParams::GetFormatBpp(PixelFormat::DXT1) / 8; | ||||||
|  |     constexpr u32 gl_bytes_per_pixel = CachedSurface::GetGLBytesPerPixel(PixelFormat::DXT1); | ||||||
|  | 
 | ||||||
|  |     // TODO(bunnei): Assumes the default rendering GOB size of 16 (128 lines). We should check the
 | ||||||
|  |     // configuration for this and perform more generic un/swizzle
 | ||||||
|  |     LOG_WARNING(Render_OpenGL, "need to use correct swizzle/GOB parameters!"); | ||||||
|  |     auto data = | ||||||
|  |         Tegra::Texture::UnswizzleTexture(base, Tegra::Texture::TextureFormat::DXT1, stride, height); | ||||||
|  |     std::memcpy(gl_buffer, data.data(), data.size()); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static constexpr std::array<void (*)(u32, u32, u8*, VAddr, VAddr, VAddr), 2> morton_to_gl_fns = { | ||||||
|     MortonCopy<true, PixelFormat::RGBA8>, |     MortonCopy<true, PixelFormat::RGBA8>, | ||||||
|     nullptr, |     MortonCopy<true, PixelFormat::DXT1>, | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| static constexpr std::array<void (*)(u32, u32, u8*, VAddr, VAddr, VAddr), 18> gl_to_morton_fns = { | static constexpr std::array<void (*)(u32, u32, u8*, VAddr, VAddr, VAddr), 2> gl_to_morton_fns = { | ||||||
|     MortonCopy<false, PixelFormat::RGBA8>, |     MortonCopy<false, PixelFormat::RGBA8>, | ||||||
|     nullptr, |     MortonCopy<false, PixelFormat::DXT1>, | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
|     nullptr, |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| // Allocate an uninitialized texture of appropriate size and format for the surface
 | // Allocate an uninitialized texture of appropriate size and format for the surface
 | ||||||
|  | @ -175,8 +148,11 @@ static void AllocateSurfaceTexture(GLuint texture, const FormatTuple& format_tup | ||||||
|     cur_state.Apply(); |     cur_state.Apply(); | ||||||
|     glActiveTexture(GL_TEXTURE0); |     glActiveTexture(GL_TEXTURE0); | ||||||
| 
 | 
 | ||||||
|     glTexImage2D(GL_TEXTURE_2D, 0, format_tuple.internal_format, width, height, 0, |     if (!format_tuple.compressed) { | ||||||
|                  format_tuple.format, format_tuple.type, nullptr); |         // Only pre-create the texture for non-compressed textures.
 | ||||||
|  |         glTexImage2D(GL_TEXTURE_2D, 0, format_tuple.internal_format, width, height, 0, | ||||||
|  |                      format_tuple.format, format_tuple.type, nullptr); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); |     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); | ||||||
|     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); |     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); | ||||||
|  | @ -606,9 +582,18 @@ void CachedSurface::UploadGLTexture(const MathUtil::Rectangle<u32>& rect, GLuint | ||||||
|     glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast<GLint>(stride)); |     glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast<GLint>(stride)); | ||||||
| 
 | 
 | ||||||
|     glActiveTexture(GL_TEXTURE0); |     glActiveTexture(GL_TEXTURE0); | ||||||
|     glTexSubImage2D(GL_TEXTURE_2D, 0, x0, y0, static_cast<GLsizei>(rect.GetWidth()), |     if (tuple.compressed) { | ||||||
|                     static_cast<GLsizei>(rect.GetHeight()), tuple.format, tuple.type, |         glCompressedTexImage2D(GL_TEXTURE_2D, 0, tuple.internal_format, | ||||||
|                     &gl_buffer[buffer_offset]); |                                static_cast<GLsizei>(rect.GetWidth()), | ||||||
|  |                                static_cast<GLsizei>(rect.GetHeight()), 0, | ||||||
|  |                                rect.GetWidth() * rect.GetHeight() * | ||||||
|  |                                    GetGLBytesPerPixel(pixel_format) / tuple.compression_factor, | ||||||
|  |                                &gl_buffer[buffer_offset]); | ||||||
|  |     } else { | ||||||
|  |         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); |     glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); | ||||||
| 
 | 
 | ||||||
|  | @ -954,15 +939,6 @@ Surface RasterizerCacheOpenGL::GetSurface(const SurfaceParams& params, ScaleMatc | ||||||
|             if (expandable != nullptr && expandable->res_scale > target_res_scale) { |             if (expandable != nullptr && expandable->res_scale > target_res_scale) { | ||||||
|                 target_res_scale = expandable->res_scale; |                 target_res_scale = expandable->res_scale; | ||||||
|             } |             } | ||||||
|             // Keep res_scale when reinterpreting d24s8 -> rgba8
 |  | ||||||
|             if (params.pixel_format == PixelFormat::RGBA8) { |  | ||||||
|                 find_params.pixel_format = PixelFormat::D24S8; |  | ||||||
|                 expandable = FindMatch<MatchFlags::Expand | MatchFlags::Invalid>( |  | ||||||
|                     surface_cache, find_params, match_res_scale); |  | ||||||
|                 if (expandable != nullptr && expandable->res_scale > target_res_scale) { |  | ||||||
|                     target_res_scale = expandable->res_scale; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|         SurfaceParams new_params = params; |         SurfaceParams new_params = params; | ||||||
|         new_params.res_scale = target_res_scale; |         new_params.res_scale = target_res_scale; | ||||||
|  | @ -1056,9 +1032,34 @@ SurfaceRect_Tuple RasterizerCacheOpenGL::GetSurfaceSubRect(const SurfaceParams& | ||||||
|     return std::make_tuple(surface, surface->GetScaledSubRect(params)); |     return std::make_tuple(surface, surface->GetScaledSubRect(params)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| Surface RasterizerCacheOpenGL::GetTextureSurface(const void* config) { | Surface RasterizerCacheOpenGL::GetTextureSurface(const Tegra::Texture::FullTextureInfo& config) { | ||||||
|     UNREACHABLE(); |     auto& gpu = Core::System::GetInstance().GPU(); | ||||||
|     return {}; | 
 | ||||||
|  |     SurfaceParams params; | ||||||
|  |     params.addr = gpu.memory_manager->PhysicalToVirtualAddress(config.tic.Address()); | ||||||
|  |     params.width = config.tic.Width(); | ||||||
|  |     params.height = config.tic.Height(); | ||||||
|  |     params.is_tiled = config.tic.IsTiled(); | ||||||
|  |     params.pixel_format = SurfaceParams::PixelFormatFromTextureFormat(config.tic.format); | ||||||
|  |     params.UpdateParams(); | ||||||
|  | 
 | ||||||
|  |     if (config.tic.Width() % 8 != 0 || config.tic.Height() % 8 != 0) { | ||||||
|  |         Surface src_surface; | ||||||
|  |         MathUtil::Rectangle<u32> rect; | ||||||
|  |         std::tie(src_surface, rect) = GetSurfaceSubRect(params, ScaleMatch::Ignore, true); | ||||||
|  | 
 | ||||||
|  |         params.res_scale = src_surface->res_scale; | ||||||
|  |         Surface tmp_surface = CreateSurface(params); | ||||||
|  |         BlitTextures(src_surface->texture.handle, rect, tmp_surface->texture.handle, | ||||||
|  |                      tmp_surface->GetScaledRect(), | ||||||
|  |                      SurfaceParams::GetFormatType(params.pixel_format), read_framebuffer.handle, | ||||||
|  |                      draw_framebuffer.handle); | ||||||
|  | 
 | ||||||
|  |         remove_surfaces.emplace(tmp_surface); | ||||||
|  |         return tmp_surface; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return GetSurface(params, ScaleMatch::Ignore, true); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| SurfaceSurfaceRect_Tuple RasterizerCacheOpenGL::GetFramebufferSurfaces( | SurfaceSurfaceRect_Tuple RasterizerCacheOpenGL::GetFramebufferSurfaces( | ||||||
|  | @ -1240,27 +1241,6 @@ void RasterizerCacheOpenGL::ValidateSurface(const Surface& surface, VAddr addr, | ||||||
|             continue; |             continue; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // D24S8 to RGBA8
 |  | ||||||
|         if (surface->pixel_format == PixelFormat::RGBA8) { |  | ||||||
|             params.pixel_format = PixelFormat::D24S8; |  | ||||||
|             Surface reinterpret_surface = |  | ||||||
|                 FindMatch<MatchFlags::Copy>(surface_cache, params, ScaleMatch::Ignore, interval); |  | ||||||
|             if (reinterpret_surface != nullptr) { |  | ||||||
|                 ASSERT(reinterpret_surface->pixel_format == PixelFormat::D24S8); |  | ||||||
| 
 |  | ||||||
|                 SurfaceInterval convert_interval = params.GetCopyableInterval(reinterpret_surface); |  | ||||||
|                 SurfaceParams convert_params = surface->FromInterval(convert_interval); |  | ||||||
|                 auto src_rect = reinterpret_surface->GetScaledSubRect(convert_params); |  | ||||||
|                 auto dest_rect = surface->GetScaledSubRect(convert_params); |  | ||||||
| 
 |  | ||||||
|                 ConvertD24S8toABGR(reinterpret_surface->texture.handle, src_rect, |  | ||||||
|                                    surface->texture.handle, dest_rect); |  | ||||||
| 
 |  | ||||||
|                 surface->invalid_regions.erase(convert_interval); |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Load data from Switch memory
 |         // Load data from Switch memory
 | ||||||
|         FlushRegion(params.addr, params.size); |         FlushRegion(params.addr, params.size); | ||||||
|         surface->LoadGLBuffer(params.addr, params.end); |         surface->LoadGLBuffer(params.addr, params.end); | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ | ||||||
| #include "common/math_util.h" | #include "common/math_util.h" | ||||||
| #include "video_core/gpu.h" | #include "video_core/gpu.h" | ||||||
| #include "video_core/renderer_opengl/gl_resource_manager.h" | #include "video_core/renderer_opengl/gl_resource_manager.h" | ||||||
|  | #include "video_core/textures/texture.h" | ||||||
| 
 | 
 | ||||||
| struct CachedSurface; | struct CachedSurface; | ||||||
| using Surface = std::shared_ptr<CachedSurface>; | using Surface = std::shared_ptr<CachedSurface>; | ||||||
|  | @ -51,30 +52,8 @@ enum class ScaleMatch { | ||||||
| 
 | 
 | ||||||
| struct SurfaceParams { | struct SurfaceParams { | ||||||
|     enum class PixelFormat { |     enum class PixelFormat { | ||||||
|         // First 5 formats are shared between textures and color buffers
 |  | ||||||
|         RGBA8 = 0, |         RGBA8 = 0, | ||||||
|         RGB8 = 1, |         DXT1 = 1, | ||||||
|         RGB5A1 = 2, |  | ||||||
|         RGB565 = 3, |  | ||||||
|         RGBA4 = 4, |  | ||||||
| 
 |  | ||||||
|         // Texture-only formats
 |  | ||||||
|         IA8 = 5, |  | ||||||
|         RG8 = 6, |  | ||||||
|         I8 = 7, |  | ||||||
|         A8 = 8, |  | ||||||
|         IA4 = 9, |  | ||||||
|         I4 = 10, |  | ||||||
|         A4 = 11, |  | ||||||
|         ETC1 = 12, |  | ||||||
|         ETC1A4 = 13, |  | ||||||
| 
 |  | ||||||
|         // Depth buffer-only formats
 |  | ||||||
|         D16 = 14, |  | ||||||
|         // gap
 |  | ||||||
|         D24 = 16, |  | ||||||
|         D24S8 = 17, |  | ||||||
| 
 |  | ||||||
|         Invalid = 255, |         Invalid = 255, | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  | @ -88,28 +67,15 @@ struct SurfaceParams { | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     static constexpr unsigned int GetFormatBpp(PixelFormat format) { |     static constexpr unsigned int GetFormatBpp(PixelFormat format) { | ||||||
|         constexpr std::array<unsigned int, 18> bpp_table = { |         if (format == PixelFormat::Invalid) | ||||||
|  |             return 0; | ||||||
|  | 
 | ||||||
|  |         constexpr std::array<unsigned int, 2> bpp_table = { | ||||||
|             32, // RGBA8
 |             32, // RGBA8
 | ||||||
|             24, // RGB8
 |             64, // DXT1
 | ||||||
|             16, // RGB5A1
 |  | ||||||
|             16, // RGB565
 |  | ||||||
|             16, // RGBA4
 |  | ||||||
|             16, // IA8
 |  | ||||||
|             16, // RG8
 |  | ||||||
|             8,  // I8
 |  | ||||||
|             8,  // A8
 |  | ||||||
|             8,  // IA4
 |  | ||||||
|             4,  // I4
 |  | ||||||
|             4,  // A4
 |  | ||||||
|             4,  // ETC1
 |  | ||||||
|             8,  // ETC1A4
 |  | ||||||
|             16, // D16
 |  | ||||||
|             0, |  | ||||||
|             24, // D24
 |  | ||||||
|             32, // D24S8
 |  | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         assert(static_cast<size_t>(format) < bpp_table.size()); |         ASSERT(static_cast<size_t>(format) < bpp_table.size()); | ||||||
|         return bpp_table[static_cast<size_t>(format)]; |         return bpp_table[static_cast<size_t>(format)]; | ||||||
|     } |     } | ||||||
|     unsigned int GetFormatBpp() const { |     unsigned int GetFormatBpp() const { | ||||||
|  | @ -134,6 +100,18 @@ struct SurfaceParams { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     static PixelFormat PixelFormatFromTextureFormat(Tegra::Texture::TextureFormat format) { | ||||||
|  |         // TODO(Subv): Properly implement this
 | ||||||
|  |         switch (format) { | ||||||
|  |         case Tegra::Texture::TextureFormat::A8R8G8B8: | ||||||
|  |             return PixelFormat::RGBA8; | ||||||
|  |         case Tegra::Texture::TextureFormat::DXT1: | ||||||
|  |             return PixelFormat::DXT1; | ||||||
|  |         default: | ||||||
|  |             UNREACHABLE(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     static bool CheckFormatsBlittable(PixelFormat pixel_format_a, PixelFormat pixel_format_b) { |     static bool CheckFormatsBlittable(PixelFormat pixel_format_a, PixelFormat pixel_format_b) { | ||||||
|         SurfaceType a_type = GetFormatType(pixel_format_a); |         SurfaceType a_type = GetFormatType(pixel_format_a); | ||||||
|         SurfaceType b_type = GetFormatType(pixel_format_b); |         SurfaceType b_type = GetFormatType(pixel_format_b); | ||||||
|  | @ -154,22 +132,17 @@ struct SurfaceParams { | ||||||
|         return false; |         return false; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     static constexpr SurfaceType GetFormatType(PixelFormat pixel_format) { |     static SurfaceType GetFormatType(PixelFormat pixel_format) { | ||||||
|         if ((unsigned int)pixel_format < 5) { |         if ((unsigned int)pixel_format <= static_cast<unsigned int>(PixelFormat::RGBA8)) { | ||||||
|             return SurfaceType::Color; |             return SurfaceType::Color; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if ((unsigned int)pixel_format < 14) { |         if ((unsigned int)pixel_format <= static_cast<unsigned int>(PixelFormat::DXT1)) { | ||||||
|             return SurfaceType::Texture; |             return SurfaceType::Texture; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (pixel_format == PixelFormat::D16 || pixel_format == PixelFormat::D24) { |         // TODO(Subv): Implement the other formats
 | ||||||
|             return SurfaceType::Depth; |         ASSERT(false); | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (pixel_format == PixelFormat::D24S8) { |  | ||||||
|             return SurfaceType::DepthStencil; |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         return SurfaceType::Invalid; |         return SurfaceType::Invalid; | ||||||
|     } |     } | ||||||
|  | @ -265,12 +238,10 @@ struct CachedSurface : SurfaceParams { | ||||||
|     OGLTexture texture; |     OGLTexture texture; | ||||||
| 
 | 
 | ||||||
|     static constexpr unsigned int GetGLBytesPerPixel(PixelFormat format) { |     static constexpr unsigned int GetGLBytesPerPixel(PixelFormat format) { | ||||||
|         // OpenGL needs 4 bpp alignment for D24 since using GL_UNSIGNED_INT as type
 |         if (format == PixelFormat::Invalid) | ||||||
|         return format == PixelFormat::Invalid |             return 0; | ||||||
|                    ? 0 | 
 | ||||||
|                    : (format == PixelFormat::D24 || GetFormatType(format) == SurfaceType::Texture) |         return SurfaceParams::GetFormatBpp(format) / 8; | ||||||
|                          ? 4 |  | ||||||
|                          : SurfaceParams::GetFormatBpp(format) / 8; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     std::unique_ptr<u8[]> gl_buffer; |     std::unique_ptr<u8[]> gl_buffer; | ||||||
|  | @ -313,7 +284,7 @@ public: | ||||||
|                                         bool load_if_create); |                                         bool load_if_create); | ||||||
| 
 | 
 | ||||||
|     /// Get a surface based on the texture configuration
 |     /// Get a surface based on the texture configuration
 | ||||||
|     Surface GetTextureSurface(const void* config); |     Surface GetTextureSurface(const Tegra::Texture::FullTextureInfo& config); | ||||||
| 
 | 
 | ||||||
|     /// Get the color and depth surfaces based on the framebuffer configuration
 |     /// Get the color and depth surfaces based on the framebuffer configuration
 | ||||||
|     SurfaceSurfaceRect_Tuple GetFramebufferSurfaces(bool using_color_fb, bool using_depth_fb, |     SurfaceSurfaceRect_Tuple GetFramebufferSurfaces(bool using_color_fb, bool using_depth_fb, | ||||||
|  |  | ||||||
|  | @ -194,7 +194,7 @@ void OpenGLState::Apply() const { | ||||||
|     // Textures
 |     // Textures
 | ||||||
|     for (unsigned i = 0; i < ARRAY_SIZE(texture_units); ++i) { |     for (unsigned i = 0; i < ARRAY_SIZE(texture_units); ++i) { | ||||||
|         if (texture_units[i].texture_2d != cur_state.texture_units[i].texture_2d) { |         if (texture_units[i].texture_2d != cur_state.texture_units[i].texture_2d) { | ||||||
|             glActiveTexture(TextureUnits::PicaTexture(i).Enum()); |             glActiveTexture(TextureUnits::MaxwellTexture(i).Enum()); | ||||||
|             glBindTexture(GL_TEXTURE_2D, texture_units[i].texture_2d); |             glBindTexture(GL_TEXTURE_2D, texture_units[i].texture_2d); | ||||||
|         } |         } | ||||||
|         if (texture_units[i].sampler != cur_state.texture_units[i].sampler) { |         if (texture_units[i].sampler != cur_state.texture_units[i].sampler) { | ||||||
|  |  | ||||||
|  | @ -16,7 +16,7 @@ struct TextureUnit { | ||||||
|     } |     } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| constexpr TextureUnit PicaTexture(int unit) { | constexpr TextureUnit MaxwellTexture(int unit) { | ||||||
|     return TextureUnit{unit}; |     return TextureUnit{unit}; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -47,4 +47,27 @@ inline GLenum PrimitiveTopology(Maxwell::PrimitiveTopology topology) { | ||||||
|     return {}; |     return {}; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | inline GLenum TextureFilterMode(Tegra::Texture::TextureFilter filter_mode) { | ||||||
|  |     switch (filter_mode) { | ||||||
|  |     case Tegra::Texture::TextureFilter::Linear: | ||||||
|  |         return GL_LINEAR; | ||||||
|  |     case Tegra::Texture::TextureFilter::Nearest: | ||||||
|  |         return GL_NEAREST; | ||||||
|  |     } | ||||||
|  |     LOG_CRITICAL(Render_OpenGL, "Unimplemented texture filter mode=%u", | ||||||
|  |                  static_cast<u32>(filter_mode)); | ||||||
|  |     UNREACHABLE(); | ||||||
|  |     return {}; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | inline GLenum WrapMode(Tegra::Texture::WrapMode wrap_mode) { | ||||||
|  |     switch (wrap_mode) { | ||||||
|  |     case Tegra::Texture::WrapMode::ClampToEdge: | ||||||
|  |         return GL_CLAMP_TO_EDGE; | ||||||
|  |     } | ||||||
|  |     LOG_CRITICAL(Render_OpenGL, "Unimplemented texture wrap mode=%u", static_cast<u32>(wrap_mode)); | ||||||
|  |     UNREACHABLE(); | ||||||
|  |     return {}; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| } // namespace MaxwellToGL
 | } // namespace MaxwellToGL
 | ||||||
|  |  | ||||||
|  | @ -37,6 +37,16 @@ enum class TICHeaderVersion : u32 { | ||||||
|     BlockLinearColorKey = 4, |     BlockLinearColorKey = 4, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | enum class ComponentType : u32 { | ||||||
|  |     SNORM = 1, | ||||||
|  |     UNORM = 2, | ||||||
|  |     SINT = 3, | ||||||
|  |     UINT = 4, | ||||||
|  |     SNORM_FORCE_FP16 = 5, | ||||||
|  |     UNORM_FORCE_FP16 = 6, | ||||||
|  |     FLOAT = 7 | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| union TextureHandle { | union TextureHandle { | ||||||
|     u32 raw; |     u32 raw; | ||||||
|     BitField<0, 20, u32> tic_id; |     BitField<0, 20, u32> tic_id; | ||||||
|  | @ -48,10 +58,10 @@ struct TICEntry { | ||||||
|     union { |     union { | ||||||
|         u32 raw; |         u32 raw; | ||||||
|         BitField<0, 7, TextureFormat> format; |         BitField<0, 7, TextureFormat> format; | ||||||
|         BitField<7, 3, u32> r_type; |         BitField<7, 3, ComponentType> r_type; | ||||||
|         BitField<10, 3, u32> g_type; |         BitField<10, 3, ComponentType> g_type; | ||||||
|         BitField<13, 3, u32> b_type; |         BitField<13, 3, ComponentType> b_type; | ||||||
|         BitField<16, 3, u32> a_type; |         BitField<16, 3, ComponentType> a_type; | ||||||
|     }; |     }; | ||||||
|     u32 address_low; |     u32 address_low; | ||||||
|     union { |     union { | ||||||
|  | @ -77,6 +87,11 @@ struct TICEntry { | ||||||
|     u32 Height() const { |     u32 Height() const { | ||||||
|         return height_minus_1 + 1; |         return height_minus_1 + 1; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     bool IsTiled() const { | ||||||
|  |         return header_version == TICHeaderVersion::BlockLinear || | ||||||
|  |                header_version == TICHeaderVersion::BlockLinearColorKey; | ||||||
|  |     } | ||||||
| }; | }; | ||||||
| static_assert(sizeof(TICEntry) == 0x20, "TICEntry has wrong size"); | static_assert(sizeof(TICEntry) == 0x20, "TICEntry has wrong size"); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue