diff --git a/hw/xbox/nv2a/meson.build b/hw/xbox/nv2a/meson.build
index b02ab01d39..871ba02457 100644
--- a/hw/xbox/nv2a/meson.build
+++ b/hw/xbox/nv2a/meson.build
@@ -9,6 +9,7 @@ specific_ss.add(files(
 	'pmc.c',
 	'pramdac.c',
 	'prmcio.c',
+	'prmdio.c',
 	'prmvio.c',
 	'psh.c',
 	'ptimer.c',
diff --git a/hw/xbox/nv2a/nv2a.h b/hw/xbox/nv2a/nv2a.h
index ad633a39ae..9e3b0206f6 100644
--- a/hw/xbox/nv2a/nv2a.h
+++ b/hw/xbox/nv2a/nv2a.h
@@ -26,5 +26,6 @@ void nv2a_gl_context_init(void);
 int nv2a_get_framebuffer_surface(void);
 void nv2a_set_surface_scale_factor(unsigned int scale);
 unsigned int nv2a_get_surface_scale_factor(void);
+const uint8_t *nv2a_get_dac_palette(void);
 
 #endif
diff --git a/hw/xbox/nv2a/nv2a_int.h b/hw/xbox/nv2a/nv2a_int.h
index c8eb214d95..1308bc30e9 100644
--- a/hw/xbox/nv2a/nv2a_int.h
+++ b/hw/xbox/nv2a/nv2a_int.h
@@ -250,6 +250,7 @@ typedef struct PGRAPHState {
         GLint pvideo_enable_loc;
         GLint pvideo_tex_loc;
         GLint pvideo_pos_loc;
+        GLint palette_loc[256];
     } disp_rndr;
 
     /* subchannels state we're not sure the location of... */
@@ -454,6 +455,11 @@ typedef struct NV2AState {
         uint32_t fp_hvalid_end;
     } pramdac;
 
+    struct {
+        uint16_t write_mode_address;
+        uint8_t palette[256*3];
+    } puserdac;
+
 } NV2AState;
 
 typedef struct NV2ABlockInfo {
diff --git a/hw/xbox/nv2a/nv2a_regs.h b/hw/xbox/nv2a/nv2a_regs.h
index de5d30cb05..318b65479f 100644
--- a/hw/xbox/nv2a/nv2a_regs.h
+++ b/hw/xbox/nv2a/nv2a_regs.h
@@ -706,6 +706,10 @@
 #define NV_PRAMDAC_FP_HCRTC                              0x00000828
 #define NV_PRAMDAC_FP_HVALID_END                         0x00000838
 
+#define NV_USER_DAC_WRITE_MODE_ADDRESS                   0x000003C8
+#define NV_USER_DAC_PALETTE_DATA                         0x000003C9
+
+
 #define NV_USER_DMA_PUT                                  0x40
 #define NV_USER_DMA_GET                                  0x44
 #define NV_USER_REF                                      0x48
diff --git a/hw/xbox/nv2a/pgraph.c b/hw/xbox/nv2a/pgraph.c
index ecbcd90a4d..e76976849b 100644
--- a/hw/xbox/nv2a/pgraph.c
+++ b/hw/xbox/nv2a/pgraph.c
@@ -4438,7 +4438,6 @@ static void pgraph_init_display_renderer(NV2AState *d)
         "    float y = -1.0 + float((gl_VertexID & 2) << 1);\n"
         "    gl_Position = vec4(x, y, 0, 1);\n"
         "}\n";
-    /* FIXME: gamma correction */
     /* FIXME: improve interlace handling, pvideo */
 
     const char *fs =
@@ -4681,6 +4680,11 @@ void pgraph_gl_sync(NV2AState *d)
     qemu_event_set(&d->pgraph.gl_sync_complete);
 }
 
+const uint8_t *nv2a_get_dac_palette(void)
+{
+    return g_nv2a->puserdac.palette;
+}
+
 int nv2a_get_framebuffer_surface(void)
 {
     NV2AState *d = g_nv2a;
diff --git a/hw/xbox/nv2a/prmdio.c b/hw/xbox/nv2a/prmdio.c
new file mode 100644
index 0000000000..ff04adf661
--- /dev/null
+++ b/hw/xbox/nv2a/prmdio.c
@@ -0,0 +1,56 @@
+/*
+ * QEMU Geforce NV2A implementation
+ *
+ * Copyright (c) 2021 Matt Borgerson
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "nv2a_int.h"
+
+uint64_t prmdio_read(void *opaque, hwaddr addr, unsigned int size)
+{
+    NV2AState *d = (NV2AState *)opaque;
+
+    uint64_t r = 0;
+    switch (addr) {
+    case NV_USER_DAC_WRITE_MODE_ADDRESS:
+        r = d->puserdac.write_mode_address / 3;
+        break;
+    default:
+        break;
+    }
+
+    nv2a_reg_log_read(NV_PRMDIO, addr, r);
+    return r;
+}
+
+void prmdio_write(void *opaque, hwaddr addr, uint64_t val, unsigned int size)
+{
+    NV2AState *d = (NV2AState *)opaque;
+
+    nv2a_reg_log_write(NV_PRMDIO, addr, val);
+
+    switch (addr) {
+    case NV_USER_DAC_WRITE_MODE_ADDRESS:
+        d->puserdac.write_mode_address = (val & 0xff) * 3;
+        break;
+    case NV_USER_DAC_PALETTE_DATA:
+        /* FIXME: Confirm wrap-around */
+        d->puserdac.palette[d->puserdac.write_mode_address++ % (256*3)] = val;
+        break;
+    default:
+        break;
+    }
+}
diff --git a/hw/xbox/nv2a/stubs.c b/hw/xbox/nv2a/stubs.c
index ec3d8e8d85..3638a06108 100644
--- a/hw/xbox/nv2a/stubs.c
+++ b/hw/xbox/nv2a/stubs.c
@@ -42,7 +42,6 @@ DEFINE_STUB(pcounter, NV_PCOUNTER)
 DEFINE_STUB(pvpe, NV_PVPE)
 DEFINE_STUB(ptv, NV_PTV)
 DEFINE_STUB(prmfb, NV_PRMFB)
-DEFINE_STUB(prmdio, NV_PRMDIO)
 DEFINE_STUB(pstraps, NV_PSTRAPS)
 // DEFINE_STUB(pramin, NV_PRAMIN)
 
diff --git a/ui/xemu-shaders.c b/ui/xemu-shaders.c
index 9f43d06e4a..04279e412d 100644
--- a/ui/xemu-shaders.c
+++ b/ui/xemu-shaders.c
@@ -83,15 +83,31 @@ struct decal_shader *create_decal_shader(enum SHADER_TYPE type)
     const char *image_frag_src =
         "#version 150 core\n"
         "uniform sampler2D tex;\n"
-        "uniform vec4 in_ColorPrimary;\n"
-        "uniform vec4 in_ColorSecondary;\n"
-        "uniform vec4 in_ColorFill;\n"
         "in  vec2 Texcoord;\n"
         "out vec4 out_Color;\n"
         "void main() {\n"
-        "    vec4 t = texture(tex, Texcoord);\n"
-        "    out_Color.rgba = t;\n"
+        "    out_Color.rgba = texture(tex, Texcoord);\n"
         "}\n";
+
+    const char *image_gamma_frag_src =
+        "#version 400 core\n"
+        "uniform sampler2D tex;\n"
+        "uniform uint palette[256];\n"
+        "float gamma_ch(int ch, float col)\n"
+        "{\n"
+        "    return float(bitfieldExtract(palette[uint(col * 255.0)], ch*8, 8)) / 255.0;\n"
+        "}\n"
+        "\n"
+        "vec4 gamma(vec4 col)\n"
+        "{\n"
+        "    return vec4(gamma_ch(0, col.r), gamma_ch(1, col.g), gamma_ch(2, col.b), col.a);\n"
+        "}\n"
+        "in  vec2 Texcoord;\n"
+        "out vec4 out_Color;\n"
+        "void main() {\n"
+        "    out_Color.rgba = gamma(texture(tex, Texcoord));\n"
+        "}\n";
+
     // Simple 2-color decal shader
     // - in_ColorFill is first pass
     // - Red channel of the texture is used as primary color, mixed with 1-Red for
@@ -118,6 +134,8 @@ struct decal_shader *create_decal_shader(enum SHADER_TYPE type)
         frag_src = mask_frag_src;
     } else if (type == SHADER_TYPE_BLIT) {
         frag_src = image_frag_src;
+    } else if (type == SHADER_TYPE_BLIT_GAMMA) {
+        frag_src = image_gamma_frag_src;
     } else if (type == SHADER_TYPE_LOGO) {
         frag_src = xemu_logo_frag_src;
     } else {
@@ -147,6 +165,11 @@ struct decal_shader *create_decal_shader(enum SHADER_TYPE type)
     s->ColorFill_loc      = glGetUniformLocation(s->prog, "in_ColorFill");
     s->time_loc           = glGetUniformLocation(s->prog, "iTime");
     s->scale_loc          = glGetUniformLocation(s->prog, "scale");
+    for (int i = 0; i < 256; i++) {
+        char name[64];
+        snprintf(name, sizeof(name), "palette[%d]", i);
+        s->palette_loc[i] = glGetUniformLocation(s->prog, name);
+    }
 
     // Create a vertex array object
     glGenVertexArrays(1, &s->vao);
diff --git a/ui/xemu-shaders.h b/ui/xemu-shaders.h
index 37cf340a01..34bf1b8ad3 100644
--- a/ui/xemu-shaders.h
+++ b/ui/xemu-shaders.h
@@ -27,6 +27,7 @@
 
 enum SHADER_TYPE {
     SHADER_TYPE_BLIT,
+    SHADER_TYPE_BLIT_GAMMA,
     SHADER_TYPE_MASK,
     SHADER_TYPE_LOGO,
 };
@@ -52,8 +53,9 @@ struct decal_shader
     GLint ColorPrimary_loc;
     GLint ColorSecondary_loc;
     GLint ColorFill_loc;
-	GLint time_loc;
-	GLint scale_loc;
+    GLint time_loc;
+    GLint scale_loc;
+    GLint palette_loc[256];
 };
 
 struct fbo {
diff --git a/ui/xemu.c b/ui/xemu.c
index 8eb7e6d6f3..2558106c3d 100644
--- a/ui/xemu.c
+++ b/ui/xemu.c
@@ -905,7 +905,7 @@ static void sdl2_display_early_init(DisplayOptions *o)
     SDL_GL_MakeCurrent(m_window, m_context);
     SDL_GL_SetSwapInterval(0);
     xemu_hud_init(m_window, m_context);
-    blit = create_decal_shader(SHADER_TYPE_BLIT);
+    blit = create_decal_shader(SHADER_TYPE_BLIT_GAMMA);
 }
 
 static void sdl2_display_init(DisplayState *ds, DisplayOptions *o)
@@ -1194,6 +1194,14 @@ void sdl2_gl_refresh(DisplayChangeListener *dcl)
     glUniform4f(s->ScaleOffset_loc, scale[0], scale[1], 0, 0);
     glUniform4f(s->TexScaleOffset_loc, 1.0, 1.0, 0, 0);
     glUniform1i(s->tex_loc, 0);
+
+    const uint8_t *palette = nv2a_get_dac_palette();
+    for (int i = 0; i < 256; i++) {
+        uint32_t e = (palette[i * 3 + 2] << 16) | (palette[i * 3 + 1] << 8) |
+                     palette[i * 3];
+        glUniform1ui(s->palette_loc[i], e);
+    }
+
     glClearColor(0, 0, 0, 0);
     glClear(GL_COLOR_BUFFER_BIT);
     glDrawElements(GL_TRIANGLE_FAN, 4, GL_UNSIGNED_INT, NULL);