diff --git a/Makeconf b/Makeconf
index 5d8a27e..4e62aab 100644
--- a/Makeconf
+++ b/Makeconf
@@ -17,7 +17,9 @@ modules_libemu.a := memory/device.o \
                     cpu/cpu.o \
                     cpu/decoder.o \
                     cartridge/mbc/mbc1.o \
-                    cartridge/cartridge.o
+                    cartridge/cartridge.o \
+                    lcd/lcd.o \
+                    lcd/palette.o \
 
 modules_vgbc := main.o libemu.a
 verb_vgbc := link
diff --git a/lcd/lcd.cpp b/lcd/lcd.cpp
new file mode 100644
index 0000000..ea331e4
--- /dev/null
+++ b/lcd/lcd.cpp
@@ -0,0 +1,117 @@
+#include <lcd/lcd.h>
+#include <cstring>
+
+LCD::LCD(Cpu& cpu)
+  : screenbuffer(), regLY(0), regLYC(0),
+    intHBlank(false), intVBlank(false), intOAM(false), intLYC(false),
+    cpu(cpu), bgp(false), obp{Palette(true),Palette(true)},
+    vram_dirty(true)
+{
+  screenbuffer.create(160,144);
+  screenbuffer.clear();
+
+  std::memset(vram_raw, 0, 0x2000);
+
+  for(int i = 0; i < 256; i++)
+    tilemap[i].create(8,8);
+}
+
+
+void LCD::write8(u16 addr, u8 data) {
+  // VRAM access
+  if (Range(0x8000,0x9FFF).contains(addr))
+    {
+      vram_raw[addr-0x8000] = data;
+      vram_dirty = true;
+      return;
+    }
+
+  switch(addr)
+    {
+    case 0xFF40: // LCDC
+      // TODO
+      break;
+    case 0xFF41: // STAT
+      intHBlank = data & IntSourceHBlank;
+      intVBlank = data & IntSourceVBlank;
+      intOAM = data & IntSourceOAM;
+      intLYC = data & IntSourceLYC;
+      break;
+    case 0xFF42: // SCY
+      // TODO
+      break;
+    case 0xFF43: // SCX
+      // TODO
+      break;
+    case 0xFF44: // LY
+      // Ignore
+      break;
+    case 0xFF45: // LYC
+      regLYC = data;
+      break;
+    case 0xFF46: // DMA (OAM DMA source address)
+      // TODO
+      break;
+    case 0xFF47: // BGP
+      bgp.setRegValue(data);
+      vram_dirty = true;
+      break;
+    case 0xFF48: // OBP0
+      obp[0].setRegValue(data);
+      vram_dirty = true;
+      break;
+    case 0xFF49: // OBP1
+      obp[1].setRegValue(data);
+      vram_dirty = true;
+      break;
+    case 0xFF4A: // WY
+    case 0xFF4B: // WX
+      // TODO
+      break;
+    }
+}
+
+u8 LCD::read8(u16 addr) {
+  // VRAM access
+  if (Range(0x8000,0x9FFF).contains(addr))
+    return vram_raw[addr-0x8000];
+
+  switch(addr)
+    {
+    case 0xFF40: // LCDC
+      // TODO
+      return 0x00;
+    case 0xFF41: // STAT
+      return (currentMode |
+              (regLY == regLYC ? LycEqual : 0) |
+              (intHBlank ? IntSourceHBlank : 0) |
+              (intVBlank ? IntSourceVBlank : 0) |
+              (intOAM ? IntSourceOAM : 0) |
+              (intLYC ? IntSourceLYC : 0));
+    case 0xFF42: // SCY
+      // TODO
+      return 0x00;
+    case 0xFF43: // SCX
+      // TODO
+      return 0x00;
+    case 0xFF44: // LY
+      return regLY;
+    case 0xFF45: // LYC
+      return regLYC;
+    case 0xFF46: // DMA (OAM DMA source address)
+      // TODO
+      return 0x00;
+    case 0xFF47: // BGP
+      return bgp.getRegValue();
+    case 0xFF48: // OBP0
+      return obp[0].getRegValue();
+    case 0xFF49: // OBP1
+      return obp[1].getRegValue();
+    case 0xFF4A: // WY
+    case 0xFF4B: // WY
+      // TODO
+      return 0x00;
+    default:
+      return 0xFF;
+    }
+}
diff --git a/lcd/lcd.h b/lcd/lcd.h
new file mode 100644
index 0000000..0ff6b41
--- /dev/null
+++ b/lcd/lcd.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#include <SFML/Graphics.hpp>
+#include <memory/device.h>
+#include <misc/types.h>
+#include <lcd/palette.h>
+#include <cpu/cpu.h>
+
+class LCD;
+
+enum LCDMode : u8 {
+  ModeHBlank       = 0b000000000,
+  ModeVBlank       = 0b000000001,
+  ModeOAMSearch    = 0b000000010,
+  ModeTransferring = 0b000000011,
+};
+
+enum RegStat : u8 {
+  LycEqual         = 0b000000100,
+  IntSourceHBlank  = 0b000001000,
+  IntSourceVBlank  = 0b000010000,
+  IntSourceOAM     = 0b000100000,
+  IntSourceLYC     = 0b001000000,
+};
+
+class LCD : public Mem_device {
+private:
+  // Graphics
+  sf::RenderTexture screenbuffer;
+  sf::Image tilemap[256];
+
+  // Emulated device
+  u8 regLY;
+  u8 regLYC;
+  bool intHBlank;
+  bool intVBlank;
+  bool intOAM;
+  bool intLYC;
+  LCDMode currentMode;
+  Palette bgp;
+  Palette obp[2];
+
+  u8 vram_raw[0x2000];
+
+  // misc
+  Cpu& cpu;
+  bool enabled;
+  bool vram_dirty;
+
+public:
+  LCD(Cpu& cpu);
+
+  virtual void write8(u16 addr, u8 data);
+  virtual u8 read8(u16 addr);
+};
diff --git a/lcd/palette.cpp b/lcd/palette.cpp
new file mode 100644
index 0000000..2918299
--- /dev/null
+++ b/lcd/palette.cpp
@@ -0,0 +1,36 @@
+#include <lcd/palette.h>
+
+const sf::Color Palette::colors[] = {
+  sf::Color(255,255,255),
+  sf::Color(170,170,170),
+  sf::Color(85,85,85),
+  sf::Color(0,0,0),
+};
+
+const sf::Color Palette::transparent(0,0,0,255);
+
+Palette::Palette(bool transparent0)
+  : transparent0(transparent0)
+{
+  setRegValue(0);
+}
+
+void Palette::setRegValue(u8 regval)
+{
+  for(int i = 0; i < 4; i++, regval >>= 2)
+    idx_to_color[i] = regval & 0x3;
+}
+
+u8 Palette::getRegValue()
+{
+  u8 regval = 0;
+  for(int i = 0; i < 4; i++, regval <<= 2)
+    regval |= idx_to_color[i] & 0x3;
+  return regval;
+}
+
+const sf::Color& Palette::getColorByIdx(u8 idx)
+{
+  if(idx == 0 && transparent0) return transparent;
+  return colors[idx_to_color[idx]];
+}
diff --git a/lcd/palette.h b/lcd/palette.h
new file mode 100644
index 0000000..8888e2d
--- /dev/null
+++ b/lcd/palette.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include <SFML/Graphics.hpp>
+#include <misc/types.h>
+
+class Palette {
+private:
+  static const sf::Color colors[4];
+  static const sf::Color transparent;
+
+  u8 idx_to_color[4];
+  bool transparent0;
+public:
+  Palette(bool transparent0);
+
+  void setRegValue(u8 regval);
+  u8 getRegValue();
+
+  const sf::Color& getColorByIdx(u8 idx);
+
+};