diff --git a/asm/crash.s b/asm/crash.s
new file mode 100644
index 00000000..033bf952
--- /dev/null
+++ b/asm/crash.s
@@ -0,0 +1,153 @@
+// SM64 Crash Handler
+// See Readme below.
+
+#include "macros.inc"
+
+/* ---------------------------------------------------------------
+ * IMPORTANT README:
+ * ---------------------------------------------------------------
+ * Frame buffer emulation is required. To enable it in GlideN64,
+ * check "Emulate frame buffer" and "Render frame buffer to output"
+ * in the "Frame buffer" tab.
+ *
+ * Your emulator's CPU core style should be set to interpreter for best results.
+ *
+ * See the DEBUG_ASSERT macro on how to call the crash screen for
+ * detected exceptions.
+ *
+ */
+
+.set noat
+.set noreorder
+.set gp=64
+
+.set COP0_CAUSE, $13
+.set COP0_EPC, $14
+.set COP0_BADVADDR, $8
+
+glabel crashFont
+    .incbin "enhancements/crash_font.bin"
+    .align 4
+
+glabel exceptionRegContext
+    .fill 0x108
+
+glabel pAssertFile
+    .dword 0
+glabel nAssertLine
+    .dword 0
+glabel pAssertExpression
+    .dword 0
+glabel nAssertStopProgram
+    .dword 0
+
+glabel _n64_assert
+    lui  $at, %hi(pAssertFile)
+    sw   $a0, %lo(pAssertFile)($at)
+    lui  $at, %hi(nAssertLine)
+    sw   $a1, %lo(nAssertLine)($at)
+    lui  $at, %hi(pAssertExpression)
+    sw   $a2, %lo(pAssertExpression)($at)
+    lui  $at, %hi(nAssertStopProgram)
+    sw   $a3, %lo(nAssertStopProgram)($at)
+    beqz $a3, .end_2
+     nop
+    syscall // trigger crash screen
+.end_2:
+    jr $ra
+     nop
+
+glabel cop0_get_cause
+    jr   $ra
+     mfc0 $v0, COP0_CAUSE
+
+glabel cop0_get_epc
+    jr   $ra
+     mfc0 $v0, COP0_EPC
+
+glabel cop0_get_badvaddr
+    jr   $ra
+     mfc0 $v0, COP0_BADVADDR
+
+// If the error code field of cop0's cause register is non-zero,
+// draw crash details to the screen and hang
+
+// If there wasn't an error, continue to the original handler
+
+glabel __crash_handler_entry
+    mfc0  $k1, COP0_CAUSE
+    andi  $k1, $k1, (0x1F << 2)
+    beqzl $k1, .end2  // exit if ExCode is 0
+     lui   $k0, %hi(__osException)
+    la    $k0, exceptionRegContext
+    sd    $zero, 0x018 ($k0)
+    sd    $at, 0x020 ($k0)
+    sd    $v0, 0x028 ($k0)
+    sd    $v1, 0x030 ($k0)
+    sd    $a0, 0x038 ($k0)
+    sd    $a1, 0x040 ($k0)
+    sd    $a2, 0x048 ($k0)
+    sd    $a3, 0x050 ($k0)
+    sd    $t0, 0x058 ($k0)
+    sd    $t1, 0x060 ($k0)
+    sd    $t2, 0x068 ($k0)
+    sd    $t3, 0x070 ($k0)
+    sd    $t4, 0x078 ($k0)
+    sd    $t5, 0x080 ($k0)
+    sd    $t6, 0x088 ($k0)
+    sd    $t7, 0x090 ($k0)
+    sd    $s0, 0x098 ($k0)
+    sd    $s1, 0x0A0 ($k0)
+    sd    $s2, 0x0A8 ($k0)
+    sd    $s3, 0x0B0 ($k0)
+    sd    $s4, 0x0B8 ($k0)
+    sd    $s5, 0x0C0 ($k0)
+    sd    $s6, 0x0C8 ($k0)
+    sd    $s7, 0x0D0 ($k0)
+    sd    $t8, 0x0D8 ($k0)
+    sd    $t9, 0x0E0 ($k0)
+    sd    $gp, 0x0E8 ($k0)
+    sd    $sp, 0x0F0 ($k0)
+    sd    $fp, 0x0F8 ($k0)
+    sd    $ra, 0x100 ($k0)
+    // cop unusable exception fired twice on startup so we'll ignore it for now
+    li    $t0, (0x0B << 2)
+    beq   $k1, $t0, .end
+     nop
+    jal   show_crash_screen_and_hang
+     nop
+    .end:
+    ld    $at, 0x020 ($k0)
+    ld    $v0, 0x028 ($k0)
+    ld    $v1, 0x030 ($k0)
+    ld    $a0, 0x038 ($k0)
+    ld    $a1, 0x040 ($k0)
+    ld    $a2, 0x048 ($k0)
+    ld    $a3, 0x050 ($k0)
+    ld    $t0, 0x058 ($k0)
+    ld    $t1, 0x060 ($k0)
+    ld    $t2, 0x068 ($k0)
+    ld    $t3, 0x070 ($k0)
+    ld    $t4, 0x078 ($k0)
+    ld    $t5, 0x080 ($k0)
+    ld    $t6, 0x088 ($k0)
+    ld    $t7, 0x090 ($k0)
+    ld    $s0, 0x098 ($k0)
+    ld    $s1, 0x0A0 ($k0)
+    ld    $s2, 0x0A8 ($k0)
+    ld    $s3, 0x0B0 ($k0)
+    ld    $s4, 0x0B8 ($k0)
+    ld    $s5, 0x0C0 ($k0)
+    ld    $s6, 0x0C8 ($k0)
+    ld    $s7, 0x0D0 ($k0)
+    ld    $t8, 0x0D8 ($k0)
+    ld    $t9, 0x0E0 ($k0)
+    ld    $gp, 0x0E8 ($k0)
+    ld    $sp, 0x0F0 ($k0)
+    ld    $fp, 0x0F8 ($k0)
+    ld    $ra, 0x100 ($k0)
+    lui   $k0, %hi(__osException)
+    .end2:
+    addiu $k0, $k0, %lo(__osException)
+    jr    $k0 // run the original handler
+     nop
diff --git a/lib/asm/__osExceptionPreamble.s b/lib/asm/__osExceptionPreamble.s
index 4e841ea0..ab0c7d13 100644
--- a/lib/asm/__osExceptionPreamble.s
+++ b/lib/asm/__osExceptionPreamble.s
@@ -11,8 +11,8 @@
 #endif
 
 glabel __osExceptionPreamble
-    lui   $k0, %hi(__osException)
-    addiu $k0, %lo(__osException)
+    lui   $k0, %hi(__crash_handler_entry)
+    addiu $k0, %lo(__crash_handler_entry)
     jr    $k0
      nop
 
diff --git a/sm64.ld b/sm64.ld
index da9bc4dd..25ac57e8 100755
--- a/sm64.ld
+++ b/sm64.ld
@@ -117,6 +117,7 @@ SECTIONS
         BUILD_DIR/src/game/rendering_graph_node.o(.text);
         BUILD_DIR/src/game/profiler.o(.text);
         BUILD_DIR/asm/decompress.o(.text);
+        BUILD_DIR/asm/crash.o(.text);
         BUILD_DIR/src/game/camera.o(.text);
         BUILD_DIR/src/game/debug_course.o(.text);
         BUILD_DIR/src/game/object_list_processor.o(.text);
diff --git a/src/game/crash.c b/src/game/crash.c
new file mode 100644
index 00000000..716adfbd
--- /dev/null
+++ b/src/game/crash.c
@@ -0,0 +1,260 @@
+/* SM64 Crash Handler */
+
+#include <sm64.h>
+
+#include "crash.h"
+
+extern u32 exceptionRegContext[];
+
+extern char *pAssertFile;
+extern int nAssertLine;
+extern char *pAssertExpression;
+extern int nAssertStopProgram;
+
+u16 fbFillColor = 0xFFFF;
+u16 fbShadeColor = 0x0000;
+u16 *fbAddress = NULL;
+
+extern u8 crashFont[];
+
+const char *szErrCodes[] = {
+    "INTERRUPT",
+    "TLB MOD",
+    "UNMAPPED LOAD ADDR",
+    "UNMAPPED STORE ADDR",
+    "BAD LOAD ADDR",
+    "BAD STORE ADDR",
+    "BUS ERR ON INSTR FETCH",
+    "BUS ERR ON LOADSTORE",
+    "SYSCALL",
+    "BREAKPOINT",
+    "UNKNOWN INSTR",
+    "COP UNUSABLE",
+    "ARITHMETIC OVERFLOW",
+    "TRAP EXC",
+    "VIRTUAL COHERENCY INSTR",
+    "FLOAT EXC",
+};
+
+const char *szGPRegisters1[] = { "R0", "AT", "V0", "V1", "A0", "A1", "A2", "A3",
+                                 "T0", "T1", "T2", "T3", "T4", "T5", "T6", NULL };
+
+const char *szGPRegisters2[] = { "T7", "S0", "S1", "S2", "S3", "S4",
+                                 "S5", "S6", "S7", "T8", "T9", /*"K0", "K1",*/
+                                 "GP", "SP", "FP", "RA", NULL };
+
+int crash_strlen(char *str) {
+    int len = 0;
+    while (*str++) {
+        len++;
+    }
+    return len;
+}
+
+void show_crash_screen_and_hang(void) {
+    u32 cause;
+    u32 epc;
+    u8 errno;
+
+    fb_set_address((void *) (*(u32 *) 0xA4400004 | 0x80000000)); // replace me
+
+    cause = cop0_get_cause();
+    epc = cop0_get_epc();
+
+    errno = (cause >> 2) & 0x1F;
+
+    if (nAssertStopProgram == 0) {
+        fbFillColor = 0x6253;
+        fb_fill(10, 10, 300, 220);
+
+        fb_print_str(80, 20, "AN ERROR HAS OCCURRED!");
+        fb_print_int_hex(80, 30, errno, 8);
+        fb_print_str(95, 30, szErrCodes[errno]);
+
+        if (errno >= 2 && errno <= 5) {
+            /*
+            2 UNMAPPED LOAD ADDR
+            3 UNMAPPED STORE ADDR
+            4 BAD LOAD ADDR
+            5 BAD STORE ADDR
+            */
+            u32 badvaddr = cop0_get_badvaddr();
+
+            fb_print_str(145, 50, "VA");
+            fb_print_int_hex(160, 50, badvaddr, 32);
+        }
+    } else {
+        int afterFileX;
+        int exprBoxWidth;
+        fbFillColor = 0x5263;
+        fb_fill(10, 10, 300, 220);
+
+        fb_print_str(80, 20, "ASSERTION FAILED!");
+
+        afterFileX = fb_print_str(80, 30, pAssertFile);
+        fb_print_str(afterFileX, 30, ":");
+        fb_print_uint(afterFileX + 5, 30, nAssertLine);
+
+        exprBoxWidth = (crash_strlen(pAssertExpression) * 5) + 2;
+        fbFillColor = 0x0001;
+        fb_fill(80 - 1, 40 - 1, exprBoxWidth, 10);
+        fb_print_str(80, 40, pAssertExpression);
+    }
+
+    fb_print_str(80, 50, "PC");
+    fb_print_int_hex(95, 50, epc, 32);
+
+    fb_print_gpr_states(80, 70, szGPRegisters1, &exceptionRegContext[6 + 0]);
+    fb_print_gpr_states(145, 70, szGPRegisters2, &exceptionRegContext[6 + 15 * 2]);
+
+    fb_swap();
+    osWritebackDCacheAll();
+
+    while (1) // hang forever
+    {
+        UNUSED volatile int t = 0; // keep pj64 happy
+    }
+}
+
+u8 ascii_to_idx(char c) {
+    return c - 0x20;
+}
+
+void fb_set_address(void *address) {
+    fbAddress = (u16 *) address;
+}
+
+void fb_swap() {
+    // update VI frame buffer register
+    // todo other registers
+    *(u32 *) (0xA4400004) = (u32) fbAddress & 0x00FFFFFF;
+}
+
+void fb_fill(int baseX, int baseY, int width, int height) {
+    int y, x;
+
+    for (y = baseY; y < baseY + height; y++) {
+        for (x = baseX; x < baseX + width; x++) {
+            fbAddress[y * 320 + x] = fbFillColor;
+        }
+    }
+}
+
+void fb_draw_char(int x, int y, u8 idx) {
+    u16 *out = &fbAddress[y * 320 + x];
+    const u8 *in = &crashFont[idx * 3];
+    int nbyte;
+    int nrow;
+    int ncol;
+
+    for (nbyte = 0; nbyte < 3; nbyte++) {
+        u8 curbyte = in[nbyte];
+        for (nrow = 0; nrow < 2; nrow++) {
+            for (ncol = 0; ncol < 4; ncol++) {
+                u8 px = curbyte & (1 << (7 - (nrow * 4 + ncol)));
+                if (px != 0) {
+                    out[ncol] = fbFillColor;
+                }
+            }
+            out += 320;
+        }
+    }
+}
+
+void fb_draw_char_shaded(int x, int y, u8 idx) {
+    fbFillColor = 0x0001;
+    fb_draw_char(x - 1, y + 1, idx);
+
+    fbFillColor = 0xFFFF;
+    fb_draw_char(x, y, idx);
+}
+
+int fb_print_str(int x, int y, const char *str) {
+    while (1) {
+        int yoffs = 0;
+        u8 idx;
+        char c = *str++;
+
+        if (c == '\0') {
+            break;
+        }
+
+        if (c == ' ') {
+            x += 5;
+            continue;
+        }
+
+        switch (c) {
+            case 'j':
+            case 'g':
+            case 'p':
+            case 'q':
+            case 'y':
+            case 'Q':
+                yoffs = 1;
+                break;
+            case ',':
+                yoffs = 2;
+                break;
+        }
+
+        idx = ascii_to_idx(c);
+        fb_draw_char_shaded(x, y + yoffs, idx);
+        x += 5;
+    }
+
+    return x;
+}
+
+void fb_print_int_hex(int x, int y, u32 value, int nbits) {
+    nbits -= 4;
+
+    while (nbits >= 0) {
+        int nib = ((value >> nbits) & 0xF);
+        u8 idx;
+
+        if (nib > 9) {
+            idx = ('A' - 0x20) + (nib - 0xa);
+        } else {
+            idx = ('0' - 0x20) + nib;
+        }
+
+        fb_draw_char_shaded(x, y, idx);
+        x += 5;
+
+        nbits -= 4;
+    }
+}
+
+int fb_print_uint(int x, int y, u32 value) {
+    int nchars = 0;
+
+    int v = value;
+    int i;
+    while (v /= 10) {
+        nchars++;
+    }
+
+    x += nchars * 5;
+
+    for (i = nchars; i >= 0; i--) {
+        fb_draw_char_shaded(x, y, ('0' - 0x20) + (value % 10));
+        value /= 10;
+        x -= 5;
+    }
+
+    return (x + nchars * 5);
+}
+
+void fb_print_gpr_states(int x, int y, const char *regNames[], u32 *regContext) {
+    int i;
+    for (i = 0;; i++) {
+        if (regNames[i] == NULL) {
+            break;
+        }
+
+        fb_print_str(x, y, regNames[i]);
+        fb_print_int_hex(x + 15, y, regContext[i * 2 + 1], 32);
+        y += 10;
+    }
+}
diff --git a/src/game/crash.h b/src/game/crash.h
new file mode 100644
index 00000000..1386930d
--- /dev/null
+++ b/src/game/crash.h
@@ -0,0 +1,28 @@
+#ifndef _CRASH_H_
+#define _CRASH_H_
+
+#include <types.h>
+
+#define CRASH_SCREEN_INCLUDED 1
+
+extern u32 cop0_get_cause(void);
+extern u32 cop0_get_epc(void);
+extern u32 cop0_get_badvaddr(void);
+
+extern void _n64_assert(const char* pFile, int nLine, const char *pExpression, int nStopProgram);
+
+extern u8 __crash_handler_entry[];
+
+void show_crash_screen_and_hang(void);
+u8 ascii_to_idx(char c);
+void fb_set_address(void *address);
+void fb_swap(void);
+void fb_fill(int baseX, int baseY, int width, int height);
+void fb_draw_char(int x, int y, u8 idx);
+void fb_draw_char_shaded(int x, int y, u8 idx);
+int fb_print_str(int x, int y, const char *str);
+int fb_print_uint(int x, int y, u32 value);
+void fb_print_int_hex(int x, int y, u32 value, int nbits);
+void fb_print_gpr_states(int x, int y, const char* regStrs[], u32 *regContext);
+
+#endif /* _CRASH_H_ */
