mirror of
				https://git.suyu.dev/suyu/suyu
				synced 2025-10-31 07:59:02 -05:00 
			
		
		
		
	renderer_opengl: Support rendering Switch framebuffer.
This commit is contained in:
		| @@ -15,7 +15,10 @@ public: | |||||||
|     /// Used to reference a framebuffer |     /// Used to reference a framebuffer | ||||||
|     enum kFramebuffer { kFramebuffer_VirtualXFB = 0, kFramebuffer_EFB, kFramebuffer_Texture }; |     enum kFramebuffer { kFramebuffer_VirtualXFB = 0, kFramebuffer_EFB, kFramebuffer_Texture }; | ||||||
|  |  | ||||||
|     /// Struct describing framebuffer metadata |     /** | ||||||
|  |      * Struct describing framebuffer metadata | ||||||
|  |      * TODO(bunnei): This struct belongs in the GPU code, but we don't have a good place for it yet. | ||||||
|  |      */ | ||||||
|     struct FramebufferInfo { |     struct FramebufferInfo { | ||||||
|         enum class PixelFormat : u32 { |         enum class PixelFormat : u32 { | ||||||
|             ABGR8 = 1, |             ABGR8 = 1, | ||||||
| @@ -44,7 +47,7 @@ public: | |||||||
|     virtual ~RendererBase() {} |     virtual ~RendererBase() {} | ||||||
|  |  | ||||||
|     /// Swap buffers (render frame) |     /// Swap buffers (render frame) | ||||||
|     virtual void SwapBuffers() = 0; |     virtual void SwapBuffers(const FramebufferInfo& framebuffer_info) = 0; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Set the emulator window to use for renderer |      * Set the emulator window to use for renderer | ||||||
|   | |||||||
| @@ -56,7 +56,9 @@ out vec4 color; | |||||||
| uniform sampler2D color_texture; | uniform sampler2D color_texture; | ||||||
|  |  | ||||||
| void main() { | void main() { | ||||||
|     color = texture(color_texture, frag_tex_coord); |     // Swap RGBA -> ABGR so we don't have to do this on the CPU. This needs to change if we have to | ||||||
|  |     // support more framebuffer pixel formats. | ||||||
|  |     color = texture(color_texture, frag_tex_coord).abgr; | ||||||
| } | } | ||||||
| )"; | )"; | ||||||
|  |  | ||||||
| @@ -98,44 +100,20 @@ RendererOpenGL::RendererOpenGL() = default; | |||||||
| RendererOpenGL::~RendererOpenGL() = default; | RendererOpenGL::~RendererOpenGL() = default; | ||||||
|  |  | ||||||
| /// Swap buffers (render frame) | /// Swap buffers (render frame) | ||||||
| void RendererOpenGL::SwapBuffers() { | void RendererOpenGL::SwapBuffers(const FramebufferInfo& framebuffer_info) { | ||||||
|     // Maintain the rasterizer's state as a priority |     // Maintain the rasterizer's state as a priority | ||||||
|     OpenGLState prev_state = OpenGLState::GetCurState(); |     OpenGLState prev_state = OpenGLState::GetCurState(); | ||||||
|     state.Apply(); |     state.Apply(); | ||||||
|  |  | ||||||
|     for (int i : {0, 1}) { |     if (screen_info.texture.width != (GLsizei)framebuffer_info.width || | ||||||
|         const auto& framebuffer = GPU::g_regs.framebuffer_config[i]; |         screen_info.texture.height != (GLsizei)framebuffer_info.height || | ||||||
|  |         screen_info.texture.pixel_format != framebuffer_info.pixel_format) { | ||||||
|         // Main LCD (0): 0x1ED02204, Sub LCD (1): 0x1ED02A04 |         // Reallocate texture if the framebuffer size has changed. | ||||||
|         u32 lcd_color_addr = |         // This is expected to not happen very often and hence should not be a | ||||||
|             (i == 0) ? LCD_REG_INDEX(color_fill_top) : LCD_REG_INDEX(color_fill_bottom); |         // performance problem. | ||||||
|         lcd_color_addr = HW::VADDR_LCD + 4 * lcd_color_addr; |         ConfigureFramebufferTexture(screen_info.texture, framebuffer_info); | ||||||
|         LCD::Regs::ColorFill color_fill = {0}; |  | ||||||
|         LCD::Read(color_fill.raw, lcd_color_addr); |  | ||||||
|  |  | ||||||
|         if (color_fill.is_enabled) { |  | ||||||
|             LoadColorToActiveGLTexture(color_fill.color_r, color_fill.color_g, color_fill.color_b, |  | ||||||
|                                        screen_infos[i].texture); |  | ||||||
|  |  | ||||||
|             // Resize the texture in case the framebuffer size has changed |  | ||||||
|             screen_infos[i].texture.width = 1; |  | ||||||
|             screen_infos[i].texture.height = 1; |  | ||||||
|         } else { |  | ||||||
|             if (screen_infos[i].texture.width != (GLsizei)framebuffer.width || |  | ||||||
|                 screen_infos[i].texture.height != (GLsizei)framebuffer.height || |  | ||||||
|                 screen_infos[i].texture.format != framebuffer.color_format) { |  | ||||||
|                 // Reallocate texture if the framebuffer size has changed. |  | ||||||
|                 // This is expected to not happen very often and hence should not be a |  | ||||||
|                 // performance problem. |  | ||||||
|                 ConfigureFramebufferTexture(screen_infos[i].texture, framebuffer); |  | ||||||
|             } |  | ||||||
|             LoadFBToScreenInfo(framebuffer, screen_infos[i]); |  | ||||||
|  |  | ||||||
|             // Resize the texture in case the framebuffer size has changed |  | ||||||
|             screen_infos[i].texture.width = framebuffer.width; |  | ||||||
|             screen_infos[i].texture.height = framebuffer.height; |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|  |     LoadFBToScreenInfo(framebuffer_info, screen_info); | ||||||
|  |  | ||||||
|     DrawScreens(); |     DrawScreens(); | ||||||
|  |  | ||||||
| @@ -270,56 +248,48 @@ static void MortonCopyPixels128(u32 width, u32 height, u32 bytes_per_pixel, u32 | |||||||
| /** | /** | ||||||
|  * Loads framebuffer from emulated memory into the active OpenGL texture. |  * Loads framebuffer from emulated memory into the active OpenGL texture. | ||||||
|  */ |  */ | ||||||
| void RendererOpenGL::LoadFBToScreenInfo(const GPU::Regs::FramebufferConfig& framebuffer, | void RendererOpenGL::LoadFBToScreenInfo(const FramebufferInfo& framebuffer_info, | ||||||
|                                         ScreenInfo& screen_info) { |                                         ScreenInfo& screen_info) { | ||||||
|  |     const u32 bpp{FramebufferInfo::BytesPerPixel(framebuffer_info.pixel_format)}; | ||||||
|  |     const u32 size_in_bytes{framebuffer_info.stride * framebuffer_info.height * bpp}; | ||||||
|  |  | ||||||
|     const PAddr framebuffer_addr = |     MortonCopyPixels128(framebuffer_info.width, framebuffer_info.height, bpp, 4, | ||||||
|         framebuffer.active_fb == 0 ? framebuffer.address_left1 : framebuffer.address_left2; |                         Memory::GetPointer(framebuffer_info.address), gl_framebuffer_data.data(), | ||||||
|  |                         true); | ||||||
|  |  | ||||||
|     LOG_TRACE(Render_OpenGL, "0x%08x bytes from 0x%08x(%dx%d), fmt %x", |     LOG_TRACE(Render_OpenGL, "0x%08x bytes from 0x%llx(%dx%d), fmt %x", size_in_bytes, | ||||||
|               framebuffer.stride * framebuffer.height, framebuffer_addr, (int)framebuffer.width, |               framebuffer_info.address, framebuffer_info.width, framebuffer_info.height, | ||||||
|               (int)framebuffer.height, (int)framebuffer.format); |               (int)framebuffer_info.format); | ||||||
|  |  | ||||||
|     int bpp = GPU::Regs::BytesPerPixel(framebuffer.color_format); |  | ||||||
|     size_t pixel_stride = framebuffer.stride / bpp; |  | ||||||
|  |  | ||||||
|     // OpenGL only supports specifying a stride in units of pixels, not bytes, unfortunately |  | ||||||
|     ASSERT(pixel_stride * bpp == framebuffer.stride); |  | ||||||
|  |  | ||||||
|     // Ensure no bad interactions with GL_UNPACK_ALIGNMENT, which by default |     // Ensure no bad interactions with GL_UNPACK_ALIGNMENT, which by default | ||||||
|     // only allows rows to have a memory alignement of 4. |     // only allows rows to have a memory alignement of 4. | ||||||
|     ASSERT(pixel_stride % 4 == 0); |     ASSERT(framebuffer_info.stride % 4 == 0); | ||||||
|  |  | ||||||
|     if (!Rasterizer()->AccelerateDisplay(framebuffer, framebuffer_addr, |     // Reset the screen info's display texture to its own permanent texture | ||||||
|                                          static_cast<u32>(pixel_stride), screen_info)) { |     screen_info.display_texture = screen_info.texture.resource.handle; | ||||||
|         // Reset the screen info's display texture to its own permanent texture |     screen_info.display_texcoords = MathUtil::Rectangle<float>(0.f, 0.f, 1.f, 1.f); | ||||||
|         screen_info.display_texture = screen_info.texture.resource.handle; |  | ||||||
|         screen_info.display_texcoords = MathUtil::Rectangle<float>(0.f, 0.f, 1.f, 1.f); |  | ||||||
|  |  | ||||||
|         Memory::RasterizerFlushRegion(framebuffer_addr, framebuffer.stride * framebuffer.height); |     Memory::RasterizerFlushRegion(framebuffer_info.address, size_in_bytes); | ||||||
|  |  | ||||||
|         const u8* framebuffer_data = Memory::GetPhysicalPointer(framebuffer_addr); |     state.texture_units[0].texture_2d = screen_info.texture.resource.handle; | ||||||
|  |     state.Apply(); | ||||||
|  |  | ||||||
|         state.texture_units[0].texture_2d = screen_info.texture.resource.handle; |     glActiveTexture(GL_TEXTURE0); | ||||||
|         state.Apply(); |     glPixelStorei(GL_UNPACK_ROW_LENGTH, (GLint)framebuffer_info.stride); | ||||||
|  |  | ||||||
|         glActiveTexture(GL_TEXTURE0); |     // Update existing texture | ||||||
|         glPixelStorei(GL_UNPACK_ROW_LENGTH, (GLint)pixel_stride); |     // TODO: Test what happens on hardware when you change the framebuffer dimensions so that | ||||||
|  |     //       they differ from the LCD resolution. | ||||||
|  |     // TODO: Applications could theoretically crash Citra here by specifying too large | ||||||
|  |     //       framebuffer sizes. We should make sure that this cannot happen. | ||||||
|  |     glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, framebuffer_info.width, framebuffer_info.height, | ||||||
|  |                     screen_info.texture.gl_format, screen_info.texture.gl_type, | ||||||
|  |                     gl_framebuffer_data.data()); | ||||||
|  |  | ||||||
|         // Update existing texture |     glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); | ||||||
|         // TODO: Test what happens on hardware when you change the framebuffer dimensions so that |  | ||||||
|         //       they differ from the LCD resolution. |  | ||||||
|         // TODO: Applications could theoretically crash Citra here by specifying too large |  | ||||||
|         //       framebuffer sizes. We should make sure that this cannot happen. |  | ||||||
|         glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, framebuffer.width, framebuffer.height, |  | ||||||
|                         screen_info.texture.gl_format, screen_info.texture.gl_type, |  | ||||||
|                         framebuffer_data); |  | ||||||
|  |  | ||||||
|         glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); |     state.texture_units[0].texture_2d = 0; | ||||||
|  |     state.Apply(); | ||||||
|         state.texture_units[0].texture_2d = 0; |  | ||||||
|         state.Apply(); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -377,74 +347,43 @@ void RendererOpenGL::InitOpenGLObjects() { | |||||||
|     glEnableVertexAttribArray(attrib_position); |     glEnableVertexAttribArray(attrib_position); | ||||||
|     glEnableVertexAttribArray(attrib_tex_coord); |     glEnableVertexAttribArray(attrib_tex_coord); | ||||||
|  |  | ||||||
|     // Allocate textures for each screen |     // Allocate textures for the screen | ||||||
|     for (auto& screen_info : screen_infos) { |     screen_info.texture.resource.Create(); | ||||||
|         screen_info.texture.resource.Create(); |  | ||||||
|  |  | ||||||
|         // Allocation of storage is deferred until the first frame, when we |     // Allocation of storage is deferred until the first frame, when we | ||||||
|         // know the framebuffer size. |     // know the framebuffer size. | ||||||
|  |  | ||||||
|         state.texture_units[0].texture_2d = screen_info.texture.resource.handle; |     state.texture_units[0].texture_2d = screen_info.texture.resource.handle; | ||||||
|         state.Apply(); |     state.Apply(); | ||||||
|  |  | ||||||
|         glActiveTexture(GL_TEXTURE0); |     glActiveTexture(GL_TEXTURE0); | ||||||
|         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); |     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); | ||||||
|         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); |     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); | ||||||
|         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); |     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); | ||||||
|         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); |     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); | ||||||
|         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); |     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); | ||||||
|  |  | ||||||
|         screen_info.display_texture = screen_info.texture.resource.handle; |     screen_info.display_texture = screen_info.texture.resource.handle; | ||||||
|     } |  | ||||||
|  |  | ||||||
|     state.texture_units[0].texture_2d = 0; |     state.texture_units[0].texture_2d = 0; | ||||||
|     state.Apply(); |     state.Apply(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void RendererOpenGL::ConfigureFramebufferTexture(TextureInfo& texture, | void RendererOpenGL::ConfigureFramebufferTexture(TextureInfo& texture, | ||||||
|                                                  const GPU::Regs::FramebufferConfig& framebuffer) { |                                                  const FramebufferInfo& framebuffer_info) { | ||||||
|     GPU::Regs::PixelFormat format = framebuffer.color_format; |  | ||||||
|  |     texture.width = framebuffer_info.width; | ||||||
|  |     texture.height = framebuffer_info.height; | ||||||
|  |  | ||||||
|     GLint internal_format; |     GLint internal_format; | ||||||
|  |     switch (framebuffer_info.pixel_format) { | ||||||
|     texture.format = format; |     case FramebufferInfo::PixelFormat::ABGR8: | ||||||
|     texture.width = framebuffer.width; |         // Use RGBA8 and swap in the fragment shader | ||||||
|     texture.height = framebuffer.height; |  | ||||||
|  |  | ||||||
|     switch (format) { |  | ||||||
|     case GPU::Regs::PixelFormat::RGBA8: |  | ||||||
|         internal_format = GL_RGBA; |         internal_format = GL_RGBA; | ||||||
|         texture.gl_format = GL_RGBA; |         texture.gl_format = GL_RGBA; | ||||||
|         texture.gl_type = GL_UNSIGNED_INT_8_8_8_8; |         texture.gl_type = GL_UNSIGNED_INT_8_8_8_8; | ||||||
|  |         gl_framebuffer_data.resize(texture.width * texture.height * 4); | ||||||
|         break; |         break; | ||||||
|  |  | ||||||
|     case GPU::Regs::PixelFormat::RGB8: |  | ||||||
|         // This pixel format uses BGR since GL_UNSIGNED_BYTE specifies byte-order, unlike every |  | ||||||
|         // specific OpenGL type used in this function using native-endian (that is, little-endian |  | ||||||
|         // mostly everywhere) for words or half-words. |  | ||||||
|         // TODO: check how those behave on big-endian processors. |  | ||||||
|         internal_format = GL_RGB; |  | ||||||
|         texture.gl_format = GL_BGR; |  | ||||||
|         texture.gl_type = GL_UNSIGNED_BYTE; |  | ||||||
|         break; |  | ||||||
|  |  | ||||||
|     case GPU::Regs::PixelFormat::RGB565: |  | ||||||
|         internal_format = GL_RGB; |  | ||||||
|         texture.gl_format = GL_RGB; |  | ||||||
|         texture.gl_type = GL_UNSIGNED_SHORT_5_6_5; |  | ||||||
|         break; |  | ||||||
|  |  | ||||||
|     case GPU::Regs::PixelFormat::RGB5A1: |  | ||||||
|         internal_format = GL_RGBA; |  | ||||||
|         texture.gl_format = GL_RGBA; |  | ||||||
|         texture.gl_type = GL_UNSIGNED_SHORT_5_5_5_1; |  | ||||||
|         break; |  | ||||||
|  |  | ||||||
|     case GPU::Regs::PixelFormat::RGBA4: |  | ||||||
|         internal_format = GL_RGBA; |  | ||||||
|         texture.gl_format = GL_RGBA; |  | ||||||
|         texture.gl_type = GL_UNSIGNED_SHORT_4_4_4_4; |  | ||||||
|         break; |  | ||||||
|  |  | ||||||
|     default: |     default: | ||||||
|         UNIMPLEMENTED(); |         UNIMPLEMENTED(); | ||||||
|     } |     } | ||||||
| @@ -465,10 +404,10 @@ void RendererOpenGL::DrawSingleScreen(const ScreenInfo& screen_info, float x, fl | |||||||
|     auto& texcoords = screen_info.display_texcoords; |     auto& texcoords = screen_info.display_texcoords; | ||||||
|  |  | ||||||
|     std::array<ScreenRectVertex, 4> vertices = {{ |     std::array<ScreenRectVertex, 4> vertices = {{ | ||||||
|         ScreenRectVertex(x, y, texcoords.top, texcoords.left), |         ScreenRectVertex(x, y, texcoords.top, texcoords.right), | ||||||
|         ScreenRectVertex(x + w, y, texcoords.bottom, texcoords.left), |         ScreenRectVertex(x + w, y, texcoords.bottom, texcoords.right), | ||||||
|         ScreenRectVertex(x, y + h, texcoords.top, texcoords.right), |         ScreenRectVertex(x, y + h, texcoords.top, texcoords.left), | ||||||
|         ScreenRectVertex(x + w, y + h, texcoords.bottom, texcoords.right), |         ScreenRectVertex(x + w, y + h, texcoords.bottom, texcoords.left), | ||||||
|     }}; |     }}; | ||||||
|  |  | ||||||
|     state.texture_units[0].texture_2d = screen_info.display_texture; |     state.texture_units[0].texture_2d = screen_info.display_texture; | ||||||
| @@ -500,8 +439,8 @@ void RendererOpenGL::DrawScreens() { | |||||||
|     glActiveTexture(GL_TEXTURE0); |     glActiveTexture(GL_TEXTURE0); | ||||||
|     glUniform1i(uniform_color_texture, 0); |     glUniform1i(uniform_color_texture, 0); | ||||||
|  |  | ||||||
|     DrawSingleScreen(screen_infos[0], (float)screen.left, (float)screen.top, |     DrawSingleScreen(screen_info, (float)screen.left, (float)screen.top, (float)screen.GetWidth(), | ||||||
|                      (float)screen.GetWidth(), (float)screen.GetHeight()); |                      (float)screen.GetHeight()); | ||||||
|  |  | ||||||
|     m_current_frame++; |     m_current_frame++; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ | |||||||
|  |  | ||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| #include <array> | #include <vector> | ||||||
| #include <glad/glad.h> | #include <glad/glad.h> | ||||||
| #include "common/common_types.h" | #include "common/common_types.h" | ||||||
| #include "common/math_util.h" | #include "common/math_util.h" | ||||||
| @@ -20,9 +20,9 @@ struct TextureInfo { | |||||||
|     OGLTexture resource; |     OGLTexture resource; | ||||||
|     GLsizei width; |     GLsizei width; | ||||||
|     GLsizei height; |     GLsizei height; | ||||||
|     GPU::Regs::PixelFormat format; |  | ||||||
|     GLenum gl_format; |     GLenum gl_format; | ||||||
|     GLenum gl_type; |     GLenum gl_type; | ||||||
|  |     RendererBase::FramebufferInfo::PixelFormat pixel_format; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| /// Structure used for storing information about the display target for each 3DS screen | /// Structure used for storing information about the display target for each 3DS screen | ||||||
| @@ -38,7 +38,7 @@ public: | |||||||
|     ~RendererOpenGL() override; |     ~RendererOpenGL() override; | ||||||
|  |  | ||||||
|     /// Swap buffers (render frame) |     /// Swap buffers (render frame) | ||||||
|     void SwapBuffers() override; |     void SwapBuffers(const FramebufferInfo& framebuffer_info) override; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Set the emulator window to use for renderer |      * Set the emulator window to use for renderer | ||||||
| @@ -55,13 +55,13 @@ public: | |||||||
| private: | private: | ||||||
|     void InitOpenGLObjects(); |     void InitOpenGLObjects(); | ||||||
|     void ConfigureFramebufferTexture(TextureInfo& texture, |     void ConfigureFramebufferTexture(TextureInfo& texture, | ||||||
|                                      const GPU::Regs::FramebufferConfig& framebuffer); |                                      const FramebufferInfo& framebuffer_info); | ||||||
|     void DrawScreens(); |     void DrawScreens(); | ||||||
|     void DrawSingleScreen(const ScreenInfo& screen_info, float x, float y, float w, float h); |     void DrawSingleScreen(const ScreenInfo& screen_info, float x, float y, float w, float h); | ||||||
|     void UpdateFramerate(); |     void UpdateFramerate(); | ||||||
|  |  | ||||||
|     // Loads framebuffer from emulated memory into the display information structure |     // Loads framebuffer from emulated memory into the display information structure | ||||||
|     void LoadFBToScreenInfo(const GPU::Regs::FramebufferConfig& framebuffer, |     void LoadFBToScreenInfo(const FramebufferInfo& framebuffer_info, | ||||||
|                             ScreenInfo& screen_info); |                             ScreenInfo& screen_info); | ||||||
|     // Fills active OpenGL texture with the given RGB color. |     // Fills active OpenGL texture with the given RGB color. | ||||||
|     void LoadColorToActiveGLTexture(u8 color_r, u8 color_g, u8 color_b, const TextureInfo& texture); |     void LoadColorToActiveGLTexture(u8 color_r, u8 color_g, u8 color_b, const TextureInfo& texture); | ||||||
| @@ -75,8 +75,11 @@ private: | |||||||
|     OGLBuffer vertex_buffer; |     OGLBuffer vertex_buffer; | ||||||
|     OGLShader shader; |     OGLShader shader; | ||||||
|  |  | ||||||
|     /// Display information for top and bottom screens respectively |     /// Display information for Switch screen | ||||||
|     std::array<ScreenInfo, 2> screen_infos; |     ScreenInfo screen_info; | ||||||
|  |  | ||||||
|  |     /// OpenGL framebuffer data | ||||||
|  |     std::vector<u8> gl_framebuffer_data; | ||||||
|  |  | ||||||
|     // Shader uniform location indices |     // Shader uniform location indices | ||||||
|     GLuint uniform_modelview_matrix; |     GLuint uniform_modelview_matrix; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 bunnei
					bunnei