diff --git a/src/common/gl/texture.cpp b/src/common/gl/texture.cpp
index 461fee141..2d821e66a 100644
--- a/src/common/gl/texture.cpp
+++ b/src/common/gl/texture.cpp
@@ -96,6 +96,15 @@ bool Texture::Create(u32 width, u32 height, u32 layers, u32 levels, u32 samples,
         glTexStorage3D(target, levels, internal_format, width, height, layers);
       else
         glTexStorage2D(target, levels, internal_format, width, height);
+
+      if (data)
+      {
+        // TODO: Fix data for mipmaps here.
+        if (layers > 1)
+          glTexSubImage3D(target, 0, 0, 0, 0, width, height, layers, format, type, data);
+        else
+          glTexSubImage2D(target, 0, 0, 0, width, height, format, type, data);
+      }
     }
     else
     {
@@ -200,7 +209,7 @@ void Texture::ReplaceSubImage(u32 layer, u32 level, u32 x, u32 y, u32 width, u32
     glTexSubImage2D(target, level, x, y, width, height, format, type, data);
 }
 
-void Texture::SetLinearFilter(bool enabled)
+void Texture::SetLinearFilter(bool enabled) const
 {
   Assert(!IsMultisampled());
 
@@ -255,18 +264,18 @@ void Texture::Destroy()
   m_samples = 0;
 }
 
-void Texture::Bind()
+void Texture::Bind() const
 {
   glBindTexture(GetGLTarget(), m_id);
 }
 
-void Texture::BindFramebuffer(GLenum target /*= GL_DRAW_FRAMEBUFFER*/)
+void Texture::BindFramebuffer(GLenum target /*= GL_DRAW_FRAMEBUFFER*/) const
 {
   DebugAssert(m_fbo_id != 0);
   glBindFramebuffer(target, m_fbo_id);
 }
 
-void Texture::Unbind()
+void Texture::Unbind() const
 {
   glBindTexture(GetGLTarget(), 0);
 }
diff --git a/src/common/gl/texture.h b/src/common/gl/texture.h
index b9d2d1a1f..f5fc25b06 100644
--- a/src/common/gl/texture.h
+++ b/src/common/gl/texture.h
@@ -23,7 +23,7 @@ public:
   void Destroy();
 
   bool UseTextureStorage() const;
-  void SetLinearFilter(bool enabled);
+  void SetLinearFilter(bool enabled) const;
 
   ALWAYS_INLINE bool IsValid() const { return m_id != 0; }
   ALWAYS_INLINE bool IsTextureArray() const { return m_layers > 1; }
@@ -42,9 +42,9 @@ public:
                                (IsTextureArray() ? GL_TEXTURE_2D_ARRAY : GL_TEXTURE_2D));
   }
 
-  void Bind();
-  void BindFramebuffer(GLenum target = GL_DRAW_FRAMEBUFFER);
-  void Unbind();
+  void Bind() const;
+  void BindFramebuffer(GLenum target = GL_DRAW_FRAMEBUFFER) const;
+  void Unbind() const;
 
   Texture& operator=(const Texture& copy) = delete;
   Texture& operator=(Texture&& moved);
diff --git a/src/core/gpu_hw_opengl.cpp b/src/core/gpu_hw_opengl.cpp
index f85b5875b..0f2a6f5c3 100644
--- a/src/core/gpu_hw_opengl.cpp
+++ b/src/core/gpu_hw_opengl.cpp
@@ -867,16 +867,14 @@ void GPU_HW_OpenGL::UpdateDisplay()
     {
       UpdateVRAMReadTexture();
 
-      g_host_display->SetDisplayTexture(reinterpret_cast<void*>(static_cast<uintptr_t>(m_vram_read_texture.GetGLId())),
-                                        HostDisplayPixelFormat::RGBA8, m_vram_read_texture.GetWidth(),
-                                        static_cast<s32>(m_vram_read_texture.GetHeight()), 0,
-                                        m_vram_read_texture.GetHeight(), m_vram_read_texture.GetWidth(),
-                                        -static_cast<s32>(m_vram_read_texture.GetHeight()));
+      g_host_display->SetDisplayTexture(
+        &m_vram_read_texture, HostDisplayPixelFormat::RGBA8, m_vram_read_texture.GetWidth(),
+        static_cast<s32>(m_vram_read_texture.GetHeight()), 0, m_vram_read_texture.GetHeight(),
+        m_vram_read_texture.GetWidth(), -static_cast<s32>(m_vram_read_texture.GetHeight()));
     }
     else
     {
-      g_host_display->SetDisplayTexture(reinterpret_cast<void*>(static_cast<uintptr_t>(m_vram_texture.GetGLId())),
-                                        HostDisplayPixelFormat::RGBA8, m_vram_texture.GetWidth(),
+      g_host_display->SetDisplayTexture(&m_vram_texture, HostDisplayPixelFormat::RGBA8, m_vram_texture.GetWidth(),
                                         static_cast<s32>(m_vram_texture.GetHeight()), 0, m_vram_texture.GetHeight(),
                                         m_vram_texture.GetWidth(), -static_cast<s32>(m_vram_texture.GetHeight()));
     }
@@ -916,8 +914,7 @@ void GPU_HW_OpenGL::UpdateDisplay()
       }
       else
       {
-        g_host_display->SetDisplayTexture(reinterpret_cast<void*>(static_cast<uintptr_t>(m_vram_texture.GetGLId())),
-                                          HostDisplayPixelFormat::RGBA8, m_vram_texture.GetWidth(),
+        g_host_display->SetDisplayTexture(&m_vram_texture, HostDisplayPixelFormat::RGBA8, m_vram_texture.GetWidth(),
                                           m_vram_texture.GetHeight(), scaled_vram_offset_x,
                                           m_vram_texture.GetHeight() - scaled_vram_offset_y, scaled_display_width,
                                           -static_cast<s32>(scaled_display_height));
@@ -963,9 +960,9 @@ void GPU_HW_OpenGL::UpdateDisplay()
       }
       else
       {
-        g_host_display->SetDisplayTexture(reinterpret_cast<void*>(static_cast<uintptr_t>(m_display_texture.GetGLId())),
-                                          HostDisplayPixelFormat::RGBA8, m_display_texture.GetWidth(),
-                                          m_display_texture.GetHeight(), 0, scaled_display_height, scaled_display_width,
+        g_host_display->SetDisplayTexture(&m_display_texture, HostDisplayPixelFormat::RGBA8,
+                                          m_display_texture.GetWidth(), m_display_texture.GetHeight(), 0,
+                                          scaled_display_height, scaled_display_width,
                                           -static_cast<s32>(scaled_display_height));
       }
 
@@ -1356,9 +1353,8 @@ void GPU_HW_OpenGL::DownsampleFramebufferBoxFilter(GL::Texture& source, u32 left
 
   RestoreGraphicsAPIState();
 
-  g_host_display->SetDisplayTexture(reinterpret_cast<void*>(static_cast<uintptr_t>(m_downsample_texture.GetGLId())),
-                                    HostDisplayPixelFormat::RGBA8, m_downsample_texture.GetWidth(),
-                                    m_downsample_texture.GetHeight(), ds_left,
+  g_host_display->SetDisplayTexture(&m_downsample_texture, HostDisplayPixelFormat::RGBA8,
+                                    m_downsample_texture.GetWidth(), m_downsample_texture.GetHeight(), ds_left,
                                     m_downsample_texture.GetHeight() - ds_top, ds_width, -static_cast<s32>(ds_height));
 }
 
diff --git a/src/frontend-common/imgui_impl_opengl3.cpp b/src/frontend-common/imgui_impl_opengl3.cpp
index 4210fe28e..952be29ad 100644
--- a/src/frontend-common/imgui_impl_opengl3.cpp
+++ b/src/frontend-common/imgui_impl_opengl3.cpp
@@ -105,13 +105,14 @@
 
 // GL includes
 #include "common/gl/loader.h"
+#include "common/gl/texture.h"
 
 // OpenGL Data
 struct ImGui_ImplOpenGL3_Data
 {
     GLuint          GlVersion;               // Extracted at runtime using GL_MAJOR_VERSION, GL_MINOR_VERSION queries (e.g. 320 for GL 3.2)
     char            GlslVersionString[32];   // Specified by user or detected based on compile time GL settings.
-    GLuint          FontTexture;
+    GL::Texture     FontTexture;
     GLuint          ShaderHandle;
     GLint           AttribLocationTex;       // Uniforms location
     GLint           AttribLocationProjMtx;
@@ -292,7 +293,9 @@ void    ImGui_ImplOpenGL3_RenderDrawData(ImDrawData* draw_data)
                 glScissor((int)clip_min.x, (int)((float)fb_height - clip_max.y), (int)(clip_max.x - clip_min.x), (int)(clip_max.y - clip_min.y));
 
                 // Bind texture, Draw
-                glBindTexture(GL_TEXTURE_2D, (GLuint)(intptr_t)pcmd->GetTexID());
+                const GL::Texture* tex = static_cast<const GL::Texture*>(pcmd->GetTexID());
+                if (tex)
+                  tex->Bind();
                 glDrawElementsBaseVertex(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, (void*)(intptr_t)(pcmd->IdxOffset * sizeof(ImDrawIdx)), (GLint)pcmd->VtxOffset);
             }
         }
@@ -313,19 +316,11 @@ bool ImGui_ImplOpenGL3_CreateFontsTexture()
 
     // Upload texture to graphics system
     // (Bilinear sampling is required by default. Set 'io.Fonts->Flags |= ImFontAtlasFlags_NoBakedLines' or 'style.AntiAliasedLinesUseTex = false' to allow point/nearest sampling)
-    if (bd->FontTexture == 0)
-      glGenTextures(1, &bd->FontTexture);
-
-    glBindTexture(GL_TEXTURE_2D, bd->FontTexture);
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
-    glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
-    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
+    bd->FontTexture.Create(width, height, 1, 1, 1, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
+    bd->FontTexture.SetLinearFilter(true);
 
     // Store our identifier
-    io.Fonts->SetTexID((ImTextureID)(intptr_t)bd->FontTexture);
+    io.Fonts->SetTexID(&bd->FontTexture);
     return true;
 }
 
@@ -333,12 +328,8 @@ void ImGui_ImplOpenGL3_DestroyFontsTexture()
 {
     ImGuiIO& io = ImGui::GetIO();
     ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData();
-    if (bd->FontTexture)
-    {
-        glDeleteTextures(1, &bd->FontTexture);
-        io.Fonts->SetTexID(0);
-        bd->FontTexture = 0;
-    }
+    if (bd->FontTexture.IsValid())
+      bd->FontTexture.Destroy();
 }
 
 // If you get an error please report on github. You may try different GL context version or GLSL version. See GL<>GLSL version table at the top of this file.
diff --git a/src/frontend-common/opengl_host_display.cpp b/src/frontend-common/opengl_host_display.cpp
index 96ce53af5..30f29b0ce 100644
--- a/src/frontend-common/opengl_host_display.cpp
+++ b/src/frontend-common/opengl_host_display.cpp
@@ -19,18 +19,22 @@ enum : u32
 class OpenGLHostDisplayTexture final : public HostDisplayTexture
 {
 public:
-  OpenGLHostDisplayTexture(GL::Texture texture, HostDisplayPixelFormat format);
-  ~OpenGLHostDisplayTexture() override;
+  OpenGLHostDisplayTexture(GL::Texture texture, HostDisplayPixelFormat format)
+    : m_texture(std::move(texture)), m_format(format)
+  {
+  }
 
-  void* GetHandle() const override;
-  u32 GetWidth() const override;
-  u32 GetHeight() const override;
-  u32 GetLayers() const override;
-  u32 GetLevels() const override;
-  u32 GetSamples() const override;
-  HostDisplayPixelFormat GetFormat() const override;
+  ~OpenGLHostDisplayTexture() = default;
 
-  GLuint GetGLID() const;
+  void* GetHandle() const override { return const_cast<GL::Texture*>(&m_texture); }
+
+  u32 GetWidth() const override { return m_texture.GetWidth(); }
+  u32 GetHeight() const override { return m_texture.GetHeight(); }
+  u32 GetLayers() const override { return m_texture.GetLayers(); }
+  u32 GetLevels() const override { return m_texture.GetLevels(); }
+  u32 GetSamples() const override { return m_texture.GetSamples(); }
+  HostDisplayPixelFormat GetFormat() const override { return m_format; }
+  GLuint GetGLID() const { return m_texture.GetGLId(); }
 
   bool BeginUpdate(u32 width, u32 height, void** out_buffer, u32* out_pitch) override;
   void EndUpdate(u32 x, u32 y, u32 width, u32 height) override;
@@ -136,11 +140,11 @@ bool OpenGLHostDisplay::DownloadTexture(const void* texture_handle, HostDisplayP
     glPixelStorei(GL_PACK_ROW_LENGTH, out_data_stride / GetDisplayPixelFormatSize(texture_format));
   }
 
-  const GLuint texture = static_cast<GLuint>(reinterpret_cast<uintptr_t>(texture_handle));
+  const GL::Texture* texture = static_cast<const GL::Texture*>(texture_handle);
   const auto [gl_internal_format, gl_format, gl_type] = GetPixelFormatMapping(m_gl_context->IsGLES(), texture_format);
 
-  GL::Texture::GetTextureSubImage(texture, 0, x, y, 0, width, height, 1, gl_format, gl_type, height * out_data_stride,
-                                  out_data);
+  GL::Texture::GetTextureSubImage(texture->GetGLId(), 0, x, y, 0, width, height, 1, gl_format, gl_type,
+                                  height * out_data_stride, out_data);
 
   glPixelStorei(GL_PACK_ALIGNMENT, old_alignment);
   if (!m_use_gles2_draw_path)
@@ -689,13 +693,15 @@ void OpenGLHostDisplay::RenderDisplay(s32 left, s32 bottom, s32 width, s32 heigh
                                       u32 texture_width, s32 texture_height, s32 texture_view_x, s32 texture_view_y,
                                       s32 texture_view_width, s32 texture_view_height, bool linear_filter)
 {
+  const GL::Texture* texture = static_cast<const GL::Texture*>(texture_handle);
+
   glViewport(left, bottom, width, height);
   glDisable(GL_BLEND);
   glDisable(GL_CULL_FACE);
   glDisable(GL_DEPTH_TEST);
   glDepthMask(GL_FALSE);
-  glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(reinterpret_cast<uintptr_t>(texture_handle)));
   m_display_program.Bind();
+  texture->Bind();
 
   const bool linear = IsUsingLinearFiltering();
 
@@ -716,9 +722,7 @@ void OpenGLHostDisplay::RenderDisplay(s32 left, s32 bottom, s32 width, s32 heigh
   }
   else
   {
-    // TODO: This sucks.
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, linear ? GL_LINEAR : GL_NEAREST);
-    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, linear ? GL_LINEAR : GL_NEAREST);
+    texture->SetLinearFilter(linear_filter);
 
     DrawFullscreenQuadES2(m_display_texture_view_x, m_display_texture_view_y, m_display_texture_view_width,
                           m_display_texture_view_height, m_display_texture_width, m_display_texture_height);
@@ -884,7 +888,7 @@ void OpenGLHostDisplay::ApplyPostProcessingChain(GLuint final_target, s32 final_
                 texture_width, texture_height, texture_view_x, texture_view_y, texture_view_width, texture_view_height,
                 IsUsingLinearFiltering());
 
-  texture_handle = reinterpret_cast<void*>(static_cast<uintptr_t>(m_post_processing_input_texture.GetGLId()));
+  texture_handle = &m_post_processing_input_texture;
   texture_width = m_post_processing_input_texture.GetWidth();
   texture_height = m_post_processing_input_texture.GetHeight();
   texture_view_x = final_left;
@@ -909,8 +913,8 @@ void OpenGLHostDisplay::ApplyPostProcessingChain(GLuint final_target, s32 final_
     }
 
     pps.program.Bind();
-    glBindSampler(0, m_display_linear_sampler);
-    glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(reinterpret_cast<uintptr_t>(texture_handle)));
+
+    static_cast<const GL::Texture*>(texture_handle)->Bind();
     glBindSampler(0, m_display_nearest_sampler);
 
     const auto map_result = m_post_processing_ubo->Map(m_uniform_buffer_alignment, pps.uniforms_size);
@@ -924,7 +928,7 @@ void OpenGLHostDisplay::ApplyPostProcessingChain(GLuint final_target, s32 final_
     glDrawArrays(GL_TRIANGLES, 0, 3);
 
     if (i != final_stage)
-      texture_handle = reinterpret_cast<void*>(static_cast<uintptr_t>(pps.output_texture.GetGLId()));
+      texture_handle = &pps.output_texture;
   }
 
   glBindSampler(0, 0);
@@ -1059,53 +1063,6 @@ GL::StreamBuffer* OpenGLHostDisplay::GetTextureStreamBuffer()
   return m_texture_stream_buffer.get();
 }
 
-OpenGLHostDisplayTexture::OpenGLHostDisplayTexture(GL::Texture texture, HostDisplayPixelFormat format)
-  : m_texture(std::move(texture)), m_format(format)
-{
-}
-
-OpenGLHostDisplayTexture::~OpenGLHostDisplayTexture() = default;
-
-void* OpenGLHostDisplayTexture::GetHandle() const
-{
-  return reinterpret_cast<void*>(static_cast<uintptr_t>(m_texture.GetGLId()));
-}
-
-u32 OpenGLHostDisplayTexture::GetWidth() const
-{
-  return m_texture.GetWidth();
-}
-
-u32 OpenGLHostDisplayTexture::GetHeight() const
-{
-  return m_texture.GetHeight();
-}
-
-u32 OpenGLHostDisplayTexture::GetLayers() const
-{
-  return 1;
-}
-
-u32 OpenGLHostDisplayTexture::GetLevels() const
-{
-  return 1;
-}
-
-u32 OpenGLHostDisplayTexture::GetSamples() const
-{
-  return m_texture.GetSamples();
-}
-
-HostDisplayPixelFormat OpenGLHostDisplayTexture::GetFormat() const
-{
-  return m_format;
-}
-
-GLuint OpenGLHostDisplayTexture::GetGLID() const
-{
-  return m_texture.GetGLId();
-}
-
 bool OpenGLHostDisplayTexture::BeginUpdate(u32 width, u32 height, void** out_buffer, u32* out_pitch)
 {
   const u32 pixel_size = HostDisplay::GetDisplayPixelFormatSize(m_format);