diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8578f94..5b04ceb 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -132,6 +132,7 @@ gen_sample(nehe05 samples/nehe05/main.c)
 gen_sample(nehe06 samples/nehe06/main.c)
 gen_sample(nehe06_vq samples/nehe06_vq/main.c)
 gen_sample(nehe06_4444twid samples/nehe06_4444twid/main.c)
+gen_sample(nehe08 samples/nehe08/main.c samples/nehe08/pvr-texture.c)
 gen_sample(ortho2d samples/ortho2d/main.c)
 gen_sample(paletted samples/paletted/main.c)
 gen_sample(paletted_pcx samples/paletted_pcx/main.c)
diff --git a/samples/nehe08/main.c b/samples/nehe08/main.c
new file mode 100644
index 0000000..fb7ff6f
--- /dev/null
+++ b/samples/nehe08/main.c
@@ -0,0 +1,239 @@
+/*
+   KallistiOS 2.0.0
+
+   nehe08.c
+   (c)2021 Luke Benstead
+   (c)2014 Josh Pearson
+   (c)2001 Benoit Miller
+   (c)2000 Jeff Molofee
+*/
+
+#ifdef __DREAMCAST__
+#include <kos.h>
+#endif
+
+#include <GL/gl.h>
+#include <GL/glu.h>
+#include <GL/glkos.h>
+
+/* Simple OpenGL example to demonstrate blending and lighting.
+
+   Essentially the same thing as NeHe's lesson08 code.
+   To learn more, go to http://nehe.gamedev.net/.
+
+   DPAD controls the cube rotation, button A & B control the depth
+   of the cube, button X toggles filtering, and button Y toggles alpha
+   blending.
+*/
+
+static GLfloat xrot;        /* X Rotation */
+static GLfloat yrot;        /* Y Rotation */
+static GLfloat xspeed;      /* X Rotation Speed */
+static GLfloat yspeed;      /* Y Rotation Speed */
+static GLfloat z = -5.0f;   /* Depth Into The Screen */
+
+static GLuint filter;       /* Which Filter To Use */
+static GLuint texture[2];   /* Storage For Two Textures */
+
+/* Load a PVR texture - located in pvr-texture.c */
+extern GLuint glTextureLoadPVR(char *fname, unsigned char isMipMapped, unsigned char glMipMap);
+
+void draw_gl(void) {
+    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+    glLoadIdentity();
+    glTranslatef(0.0f, 0.0f, z);
+
+    glRotatef(xrot, 1.0f, 0.0f, 0.0f);
+    glRotatef(yrot, 0.0f, 1.0f, 0.0f);
+
+    glBindTexture(GL_TEXTURE_2D, texture[filter]);
+
+    glBegin(GL_QUADS);
+    /* Front Face */
+    glNormal3f(0.0f, 0.0f, 1.0f);
+    glTexCoord2f(0.0f, 0.0f);
+    glVertex3f(-1.0f, -1.0f,  1.0f);
+    glTexCoord2f(1.0f, 0.0f);
+    glVertex3f(1.0f, -1.0f,  1.0f);
+    glTexCoord2f(1.0f, 1.0f);
+    glVertex3f(1.0f,  1.0f,  1.0f);
+    glTexCoord2f(0.0f, 1.0f);
+    glVertex3f(-1.0f,  1.0f,  1.0f);
+    /* Back Face */
+    glNormal3f(0.0f, 0.0f, -1.0f);
+    glTexCoord2f(1.0f, 0.0f);
+    glVertex3f(-1.0f, -1.0f, -1.0f);
+    glTexCoord2f(1.0f, 1.0f);
+    glVertex3f(-1.0f,  1.0f, -1.0f);
+    glTexCoord2f(0.0f, 1.0f);
+    glVertex3f(1.0f,  1.0f, -1.0f);
+    glTexCoord2f(0.0f, 0.0f);
+    glVertex3f(1.0f, -1.0f, -1.0f);
+    /* Top Face */
+    glNormal3f(0.0f, 1.0f, 0.0f);
+    glTexCoord2f(0.0f, 1.0f);
+    glVertex3f(-1.0f,  1.0f, -1.0f);
+    glTexCoord2f(0.0f, 0.0f);
+    glVertex3f(-1.0f,  1.0f,  1.0f);
+    glTexCoord2f(1.0f, 0.0f);
+    glVertex3f(1.0f,  1.0f,  1.0f);
+    glTexCoord2f(1.0f, 1.0f);
+    glVertex3f(1.0f,  1.0f, -1.0f);
+    /* Bottom Face */
+    glNormal3f(0.0f, -1.0f, 0.0f);
+    glTexCoord2f(1.0f, 1.0f);
+    glVertex3f(-1.0f, -1.0f, -1.0f);
+    glTexCoord2f(0.0f, 1.0f);
+    glVertex3f(1.0f, -1.0f, -1.0f);
+    glTexCoord2f(0.0f, 0.0f);
+    glVertex3f(1.0f, -1.0f,  1.0f);
+    glTexCoord2f(1.0f, 0.0f);
+    glVertex3f(-1.0f, -1.0f,  1.0f);
+    /* Right face */
+    glNormal3f(1.0f, 0.0f, 0.0f);
+    glTexCoord2f(1.0f, 0.0f);
+    glVertex3f(1.0f, -1.0f, -1.0f);
+    glTexCoord2f(1.0f, 1.0f);
+    glVertex3f(1.0f,  1.0f, -1.0f);
+    glTexCoord2f(0.0f, 1.0f);
+    glVertex3f(1.0f,  1.0f,  1.0f);
+    glTexCoord2f(0.0f, 0.0f);
+    glVertex3f(1.0f, -1.0f,  1.0f);
+    /* Left Face */
+    glNormal3f(-1.0f, 0.0f, 0.0f);
+    glTexCoord2f(0.0f, 0.0f);
+    glVertex3f(-1.0f, -1.0f, -1.0f);
+    glTexCoord2f(1.0f, 0.0f);
+    glVertex3f(-1.0f, -1.0f,  1.0f);
+    glTexCoord2f(1.0f, 1.0f);
+    glVertex3f(-1.0f,  1.0f,  1.0f);
+    glTexCoord2f(0.0f, 1.0f);
+    glVertex3f(-1.0f,  1.0f, -1.0f);
+    glEnd();
+
+    xrot += xspeed;
+    yrot += yspeed;
+}
+
+#ifdef __DREAMCAST__
+extern uint8 romdisk[];
+KOS_INIT_ROMDISK(romdisk);
+#endif
+
+int main(int argc, char **argv) {
+#ifdef __DREAMCAST__
+    maple_device_t *cont;
+    cont_state_t *state;
+#endif
+
+    GLboolean xp = GL_FALSE;
+    GLboolean yp = GL_FALSE;
+    GLboolean blend = GL_FALSE;
+
+    printf("nehe08 beginning\n");
+
+    /* Get basic stuff initialized */
+    glKosInit();
+
+    glMatrixMode(GL_PROJECTION);
+    glLoadIdentity();
+    gluPerspective(45.0f, 640.0f / 480.0f, 0.1f, 100.0f);
+    glMatrixMode(GL_MODELVIEW);
+    glLoadIdentity();
+
+    glEnable(GL_TEXTURE_2D);
+    glShadeModel(GL_SMOOTH);
+    glClearColor(0.0f, 0.0f, 0.0f, 0.5f);
+    glClearDepth(1.0f);
+    glEnable(GL_DEPTH_TEST);
+    glDepthFunc(GL_LEQUAL);
+
+    glColor4f(1.0f, 1.0f, 1.0f, 0.5);
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
+
+    /* Enable Lighting and GL_LIGHT0 */
+    glEnable(GL_LIGHTING);
+    glEnable(GL_LIGHT0);
+
+    /* Set up the textures */
+    texture[0] = glTextureLoadPVR("/rd/glass.pvr", 0, 0);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+
+    texture[1] = glTextureLoadPVR("/rd/glass.pvr", 0, 0);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+
+    while(1) {
+#ifdef __DREAMCAST__
+        cont = maple_enum_type(0, MAPLE_FUNC_CONTROLLER);
+
+        /* Check key status */
+        state = (cont_state_t *)maple_dev_status(cont);
+
+        if(!state) {
+            printf("Error reading controller\n");
+            break;
+        }
+
+        if(state->buttons & CONT_START)
+            break;
+
+        if(state->buttons & CONT_A)
+            z -= 0.02f;
+
+        if(state->buttons & CONT_B)
+            z += 0.02f;
+
+        if((state->buttons & CONT_X) && !xp) {
+            xp = GL_TRUE;
+            filter += 1;
+
+            if(filter > 1)
+                filter = 0;
+        }
+
+        if(!(state->buttons & CONT_X))
+            xp = GL_FALSE;
+
+        if((state->buttons & CONT_Y) && !yp) {
+            yp = GL_TRUE;
+            blend = !blend;
+        }
+
+        if(!(state->buttons & CONT_Y))
+            yp = GL_FALSE;
+
+        if(state->buttons & CONT_DPAD_UP)
+            xspeed -= 0.01f;
+
+        if(state->buttons & CONT_DPAD_DOWN)
+            xspeed += 0.01f;
+
+        if(state->buttons & CONT_DPAD_LEFT)
+            yspeed -= 0.01f;
+
+        if(state->buttons & CONT_DPAD_RIGHT)
+            yspeed += 0.01f;
+#endif
+
+        /* Switch to the blended polygon list if needed */
+        if(blend) {
+            glEnable(GL_BLEND);
+            glDepthMask(0);
+        }
+        else {
+            glDisable(GL_BLEND);
+            glDepthMask(1);
+        }
+
+        /* Draw the GL "scene" */
+        draw_gl();
+
+        /* Finish the frame */
+        glKosSwapBuffers();
+    }
+
+    return 0;
+}
+
diff --git a/samples/nehe08/pvr-texture.c b/samples/nehe08/pvr-texture.c
new file mode 100644
index 0000000..c9d5f92
--- /dev/null
+++ b/samples/nehe08/pvr-texture.c
@@ -0,0 +1,176 @@
+/*
+   KallistiOS 2.0.0
+
+   pvr-texture.c
+   (c)2014 Josh PH3NOM Pearson
+
+   Load A PVR Texture to the PVR using Open GL
+*/
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdint.h>
+
+#include "GL/gl.h"
+#include "GL/glu.h"
+#include "GL/glkos.h"
+#include "GL/glext.h"
+
+#define PVR_HDR_SIZE 0x20
+#define MAX(x, y) ((x > y) ? x : y)
+
+static GLuint PVR_TextureHeight(unsigned char *HDR);
+static GLuint PVR_TextureWidth(unsigned char *HDR);
+static GLuint PVR_TextureFormat(unsigned char *HDR);
+
+static GLuint _glGetMipmapLevelCount(GLuint width, GLuint height) {
+    return 1 + floor(log2(MAX(width, height)));
+}
+
+static GLuint _glGetMipmapDataSize(GLuint width, GLuint height) {
+    GLuint size = 0;
+
+    GLuint i = 0;
+
+    for(; i < _glGetMipmapLevelCount(width, height); ++i) {
+        size += (width * height * 2);
+
+        if(width > 1) {
+            width /= 2;
+        }
+
+        if(height > 1) {
+            height /= 2;
+        }
+    }
+
+    return size;
+}
+
+/* Load a PVR texture file into memory, and then bind the texture to Open GL.
+   fname is the name of the PVR texture file to be opened and read.
+   isMipMapped should be passed as 1 if the texture contains MipMap levels, 0 otherwise.
+   glMipMap should be passed as 1 if Open GL should calculate the Mipmap levels, 0 otherwise */
+GLuint glTextureLoadPVR(char *fname, unsigned char isMipMapped, unsigned char glMipMap) {
+    FILE *tex = NULL;
+    uint16_t *TEX0 = NULL;
+    uint8_t HDR[PVR_HDR_SIZE];
+    GLuint texID, texSize, texW, texH, texFormat;
+
+    /* Open the PVR texture file, and get its file size */
+    tex = fopen(fname, "rb");
+
+    if(tex == NULL) {
+        printf("FILE READ ERROR: %s\n", fname);
+
+        while(1);
+    }
+
+    fseek(tex, 0, SEEK_END);
+    texSize = ftell(tex) - PVR_HDR_SIZE;
+    fseek(tex, 0, SEEK_SET);
+
+    /* Read in the PVR texture file header */
+    fread(HDR, 1, PVR_HDR_SIZE, tex);
+
+    /* Extract some information from the PVR texture file header */
+    texW = PVR_TextureWidth(HDR);
+    texH = PVR_TextureHeight(HDR);
+    texFormat = PVR_TextureFormat(HDR);
+
+    /* Allocate Some Memory for the texture. If we are using Open GL to build the MipMap,
+       we need to allocate enough space to hold the MipMap texture levels. */
+    if(!isMipMapped && glMipMap)
+        TEX0 = malloc(_glGetMipmapDataSize(texW, texH));
+    else
+        TEX0 = malloc(texSize);
+
+    fread(TEX0, 1, texSize, tex); /* Read in the PVR texture data */
+
+    /* Generate and bind a texture as normal for Open GL */
+    glGenTextures(1, &texID);
+    glBindTexture(GL_TEXTURE_2D, texID);
+
+    if(texFormat != GL_UNSIGNED_SHORT_5_6_5)
+        glCompressedTexImage2DARB(GL_TEXTURE_2D,
+                           0,
+                           texFormat,
+                           texW,
+                           texH,
+                           0,
+                           texSize,
+                           TEX0);
+    else {
+        fprintf(stderr, "%x\n", texFormat);
+        glTexImage2D(GL_TEXTURE_2D,
+                     0,
+                     GL_RGB,
+                     texW, texH,
+                     0,
+                     GL_RGB,
+                     texFormat,
+                     TEX0);
+        if(!isMipMapped && glMipMap)
+            glGenerateMipmapEXT(GL_TEXTURE_2D);
+    }
+
+    free(TEX0);
+
+    return texID;
+}
+
+static GLuint PVR_TextureFormat(unsigned char *HDR) {
+    GLuint color = (GLuint)HDR[PVR_HDR_SIZE - 8];
+    GLuint format = (GLuint)HDR[PVR_HDR_SIZE - 7];
+
+    GLboolean twiddled = format == 0x01;
+    GLboolean compressed = (format == 0x10 || format == 0x03);
+
+    if(compressed) {
+        if(twiddled) {
+            switch(color) {
+                case 0x0: {
+                    return GL_COMPRESSED_ARGB_1555_VQ_TWID_KOS;
+                } break;
+                case 0x01: {
+                    return GL_COMPRESSED_RGB_565_VQ_TWID_KOS;
+                } break;
+                case 0x02: {
+                    return GL_COMPRESSED_ARGB_4444_VQ_TWID_KOS;
+                }
+                break;
+                default:
+                    fprintf(stderr, "Invalid texture format");
+                    return 0;
+            }
+        } else {
+            switch(color) {
+                case 0: {
+                    return GL_COMPRESSED_ARGB_1555_VQ_KOS;
+                } break;
+                case 1: {
+                    return GL_COMPRESSED_RGB_565_VQ_KOS;
+                } break;
+                case 2: {
+                    return GL_COMPRESSED_ARGB_4444_VQ_KOS;
+                }
+                break;
+                default:
+                    fprintf(stderr, "Invalid texture format");
+                    return 0;
+            }
+        }
+    } else {
+        if(color == 1) {
+            return GL_UNSIGNED_SHORT_5_6_5;
+        }
+        return 0;
+    }
+}
+
+static GLuint PVR_TextureWidth(unsigned char *HDR) {
+    return (GLuint)HDR[PVR_HDR_SIZE - 4] | HDR[PVR_HDR_SIZE - 3] << 8;
+}
+
+static GLuint PVR_TextureHeight(unsigned char *HDR) {
+    return (GLuint)HDR[PVR_HDR_SIZE - 2] | HDR[PVR_HDR_SIZE - 1] << 8;
+}
diff --git a/samples/nehe08/romdisk/glass.pvr b/samples/nehe08/romdisk/glass.pvr
new file mode 100644
index 0000000..3f81ec0
Binary files /dev/null and b/samples/nehe08/romdisk/glass.pvr differ