From 3352625afe18293bc0d7d87b7be6e019962054d0 Mon Sep 17 00:00:00 2001
From: Gary Talent <gary@drinkingtea.net>
Date: Thu, 11 Mar 2021 01:22:43 -0600
Subject: [PATCH] [nostalgia/core] Start port of PC gfx to OpenGL

---
 src/nostalgia/core/context.hpp                |  20 ++-
 src/nostalgia/core/gfx.cpp                    |   2 +-
 src/nostalgia/core/gfx.hpp                    |   4 +-
 src/nostalgia/core/sdl/core.cpp               |   4 +
 src/nostalgia/core/sdl/core.hpp               |  23 +++
 src/nostalgia/core/sdl/gfx.cpp                | 136 +++++++----------
 src/nostalgia/core/studio/imgconv.cpp         |  10 +-
 src/nostalgia/core/studio/tilesheeteditor.cpp |  32 ++--
 src/nostalgia/core/userland/CMakeLists.txt    |  12 +-
 src/nostalgia/core/userland/gfx.hpp           |  23 +++
 src/nostalgia/core/userland/gfx_opengl.cpp    | 139 ++++++++++++++++++
 11 files changed, 289 insertions(+), 116 deletions(-)
 create mode 100644 src/nostalgia/core/sdl/core.hpp
 create mode 100644 src/nostalgia/core/userland/gfx.hpp
 create mode 100644 src/nostalgia/core/userland/gfx_opengl.cpp

diff --git a/src/nostalgia/core/context.hpp b/src/nostalgia/core/context.hpp
index a422c9f1..6ad84305 100644
--- a/src/nostalgia/core/context.hpp
+++ b/src/nostalgia/core/context.hpp
@@ -17,16 +17,26 @@ class Context {
 	public:
 		ox::FileSystem *rom = nullptr;
 	private:
-		void *m_implData = nullptr;
+		void *m_windowerData = nullptr;
+		void *m_rendererData = nullptr;
 
 	public:
-		constexpr void setImplData(void *implData) noexcept {
-			m_implData = implData;
+		constexpr void setWindowerData(void *windowerData) noexcept {
+			m_windowerData = windowerData;
 		}
 
 		template<typename T>
-		constexpr T *implData() noexcept {
-			return static_cast<T*>(m_implData);
+		constexpr T *windowerData() noexcept {
+			return static_cast<T*>(m_windowerData);
+		}
+
+		constexpr void setRendererData(void *rendererData) noexcept {
+			m_rendererData = rendererData;
+		}
+
+		template<typename T>
+		constexpr T *rendererData() noexcept {
+			return static_cast<T*>(m_rendererData);
 		}
 
 };
diff --git a/src/nostalgia/core/gfx.cpp b/src/nostalgia/core/gfx.cpp
index bf32cc53..529ff06d 100644
--- a/src/nostalgia/core/gfx.cpp
+++ b/src/nostalgia/core/gfx.cpp
@@ -146,7 +146,7 @@ Color32 toColor32(Color16 nc) noexcept {
 	Color32 g = static_cast<Color32>(((nc & 0b0000001111100000) >> 5) * 8);
 	Color32 b = static_cast<Color32>(((nc & 0b0111110000000000) >> 10) * 8);
 	Color32 a = 255;
-	return a | (b << 8) | (g << 16) | (r << 24);
+	return r | (g << 8) | (b << 16) | (a << 24);
 }
 
 
diff --git a/src/nostalgia/core/gfx.hpp b/src/nostalgia/core/gfx.hpp
index 9d2a1801..94fcb31c 100644
--- a/src/nostalgia/core/gfx.hpp
+++ b/src/nostalgia/core/gfx.hpp
@@ -46,7 +46,7 @@ struct NostalgiaGraphic {
 	int columns = 1;
 	ox::FileAddress defaultPalette;
 	NostalgiaPalette pal;
-	ox::Vector<uint8_t> tiles;
+	ox::Vector<uint8_t> pixels;
 };
 
 template<typename T>
@@ -64,7 +64,7 @@ constexpr ox::Error model(T *io, NostalgiaGraphic *ng) {
 	oxReturnError(io->field("columns", &ng->columns));
 	oxReturnError(io->field("defaultPalette", &ng->defaultPalette));
 	oxReturnError(io->field("pal", &ng->pal));
-	oxReturnError(io->field("tiles", &ng->tiles));
+	oxReturnError(io->field("pixels", &ng->pixels));
 	return OxError(0);
 }
 
diff --git a/src/nostalgia/core/sdl/core.cpp b/src/nostalgia/core/sdl/core.cpp
index 24be503e..918f9354 100644
--- a/src/nostalgia/core/sdl/core.cpp
+++ b/src/nostalgia/core/sdl/core.cpp
@@ -12,6 +12,8 @@
 #include <nostalgia/core/input.hpp>
 #include <nostalgia/core/core.hpp>
 
+#include "core.hpp"
+
 namespace nostalgia::core {
 
 static event_handler g_eventHandler = nullptr;
@@ -24,6 +26,7 @@ ox::Error init(Context *ctx) {
 }
 
 ox::Error run(Context *ctx) {
+	auto id = ctx->windowerData<SdlImplData>();
 	for (auto running = true; running;) {
 		SDL_Event event;
 		while (SDL_PollEvent(&event)) {
@@ -40,6 +43,7 @@ ox::Error run(Context *ctx) {
 			}
 		}
 		draw(ctx);
+		SDL_GL_SwapWindow(id->window);
 		SDL_Delay(1);
 	}
 	return OxError(0);
diff --git a/src/nostalgia/core/sdl/core.hpp b/src/nostalgia/core/sdl/core.hpp
new file mode 100644
index 00000000..bd28752a
--- /dev/null
+++ b/src/nostalgia/core/sdl/core.hpp
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2016 - 2021 gary@drinkingtea.net
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#pragma once
+
+#include <array>
+
+#include <SDL.h>
+
+namespace nostalgia::core {
+
+struct SdlImplData {
+	SDL_Window *window = nullptr;
+	SDL_GLContext renderer = nullptr;
+	std::array<SDL_Texture*, 4> bgTextures{};
+};
+
+}
diff --git a/src/nostalgia/core/sdl/gfx.cpp b/src/nostalgia/core/sdl/gfx.cpp
index c95fe2b6..8a10f7aa 100644
--- a/src/nostalgia/core/sdl/gfx.cpp
+++ b/src/nostalgia/core/sdl/gfx.cpp
@@ -10,26 +10,21 @@
 #ifdef NOST_FPS_PRINT
 #include <iostream>
 #endif
+#include <iostream>
 
 #include <SDL.h>
 
 #include <ox/claw/read.hpp>
 
 #include <nostalgia/core/gfx.hpp>
+#include <nostalgia/core/userland/gfx.hpp>
+
+#include "core.hpp"
 
 namespace nostalgia::core {
 
 using TileMap = std::array<std::array<int, 128>, 128>;
 
-struct SdlImplData {
-	SDL_Window *window = nullptr;
-	SDL_Renderer *renderer = nullptr;
-	std::array<SDL_Texture*, 4> bgTextures{};
-	std::array<TileMap, 4> bgTileMaps{};
-	int64_t prevFpsCheckTime = 0;
-	uint64_t draws = 0;
-};
-
 constexpr auto Scale = 5;
 
 static ox::Result<ox::Vector<char>> readFile(Context *ctx, const ox::FileAddress &file) {
@@ -51,22 +46,32 @@ ox::Result<T> readObj(Context *ctx, const ox::FileAddress &file) {
 
 ox::Error initGfx(Context *ctx) {
 	auto id = new SdlImplData;
-	ctx->setImplData(id);
-	id->window = SDL_CreateWindow("nostalgia", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 240 * Scale, 160 * Scale,
-	                              SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI);
-	id->renderer = SDL_CreateRenderer(id->window, -1, SDL_RENDERER_ACCELERATED);
-	return OxError(id->window == nullptr);
+	ctx->setWindowerData(id);
+	id->window = SDL_CreateWindow("nostalgia", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
+	                              240 * Scale, 160 * Scale,
+	                              SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI);
+	if (id->window == nullptr) {
+		return OxError(1, SDL_GetError());
+	}
+	id->renderer = SDL_GL_CreateContext(id->window);
+	if (id->renderer == nullptr) {
+		return OxError(1, SDL_GetError());
+	}
+	oxReturnError(renderer::init(ctx));
+	return OxError(0);
 }
 
 ox::Error shutdownGfx(Context *ctx) {
-	auto id = ctx->implData<SdlImplData>();
+	oxReturnError(renderer::shutdown(ctx));
+	auto id = ctx->windowerData<SdlImplData>();
 	for (auto tex : id->bgTextures) {
 		if (tex) {
 			SDL_DestroyTexture(tex);
 		}
 	}
-	SDL_DestroyRenderer(id->renderer);
+	SDL_GL_DeleteContext(id->renderer);
 	SDL_DestroyWindow(id->window);
+	ctx->setWindowerData(nullptr);
 	delete id;
 	return OxError(0);
 }
@@ -98,8 +103,8 @@ ox::Error loadBgTileSheet(Context *ctx,
                           int section,
                           ox::FileAddress tilesheetPath,
                           ox::FileAddress palettePath) {
-	auto id = ctx->implData<SdlImplData>();
-	auto [tilesheet, tserr] = readObj<NostalgiaGraphic>(ctx, tilesheetPath);
+	//auto id = ctx->windowerData<SdlImplData>();
+	const auto [tilesheet, tserr] = readObj<NostalgiaGraphic>(ctx, tilesheetPath);
 	oxReturnError(tserr);
 	NostalgiaPalette palette;
 	if (!palettePath) {
@@ -108,31 +113,36 @@ ox::Error loadBgTileSheet(Context *ctx,
 	oxReturnError(readObj<NostalgiaPalette>(ctx, palettePath).get(&palette));
 
 	const unsigned bytesPerTile = tilesheet.bpp == 8 ? 64 : 32;
-	const auto tiles = tilesheet.tiles.size() / bytesPerTile;
+	const auto tiles = tilesheet.pixels.size() / bytesPerTile;
 	const int width = 8;
 	const int height = 8 * tiles;
-	const auto format = SDL_PIXELFORMAT_INDEX8;
-	auto surface = SDL_CreateRGBSurfaceWithFormat(0, width, height, 16, format);
-	auto sdlPalette = createSDL_Palette(palette);
-	SDL_SetSurfacePalette(surface, sdlPalette);
-	if (bytesPerTile == 64) {
-		SDL_memcpy(surface->pixels, tilesheet.tiles.data(), bytesPerTile * tiles);
-	} else {
-		for (std::size_t i = 0; i < tilesheet.tiles.size(); ++i) {
-			static_cast<uint8_t*>(surface->pixels)[i * 2 + 0] = tilesheet.tiles[i] & 0xF;
-			static_cast<uint8_t*>(surface->pixels)[i * 2 + 1] = tilesheet.tiles[i] >> 4;
+	//const auto format = SDL_PIXELFORMAT_INDEX8;
+	//const auto sdlPalette = createSDL_Palette(palette);
+	std::vector<uint32_t> pixels;
+	if (bytesPerTile == 64) { // 8 BPP
+		pixels.resize(tilesheet.pixels.size());
+		for (std::size_t i = 0; i < tilesheet.pixels.size(); ++i) {
+			pixels[i] = toColor32(palette.colors[tilesheet.pixels[i]]);
+		}
+	} else { // 4 BPP
+		pixels.resize(tilesheet.pixels.size() * 2);
+		for (std::size_t i = 0; i < tilesheet.pixels.size(); ++i) {
+			pixels[i * 2 + 0] = toColor32(palette.colors[tilesheet.pixels[i] & 0xF]);
+			pixels[i * 2 + 1] = toColor32(palette.colors[tilesheet.pixels[i] >> 4]);
 		}
 	}
 
-	auto texture = SDL_CreateTextureFromSurface(id->renderer, surface);
-	SDL_FreeSurface(surface);
-	SDL_FreePalette(sdlPalette);
+	oxReturnError(renderer::loadTexture(ctx, section, pixels.data(), width, height));
 
-	auto sectionIdx = static_cast<unsigned>(section);
-	if (id->bgTextures[sectionIdx]) {
-		SDL_DestroyTexture(id->bgTextures[sectionIdx]);
-	}
-	id->bgTextures[sectionIdx] = texture;
+	//auto texture = SDL_CreateTextureFromSurface(nullptr, surface);
+	//SDL_FreeSurface(surface);
+	//SDL_FreePalette(sdlPalette);
+
+	//auto sectionIdx = static_cast<unsigned>(section);
+	//if (id->bgTextures[sectionIdx]) {
+	//	SDL_DestroyTexture(id->bgTextures[sectionIdx]);
+	//}
+	//id->bgTextures[sectionIdx] = texture;
 
 	return OxError(0);
 }
@@ -144,11 +154,10 @@ ox::Error loadSpriteTileSheet(Context*,
 	return OxError(0);
 }
 
-void drawBackground(Context *ctx, const TileMap &tm, SDL_Texture *tex) {
+void drawBackground(Context*, const TileMap &tm, SDL_Texture *tex) {
 	if (tex) {
 		constexpr auto DstSize = 8 * Scale;
-		auto id = ctx->implData<SdlImplData>();
-		//oxTrace("nostalgia::core::drawBackground") << "Drawing background";
+		oxTracef("nostalgia::core::sdl::drawBackground", "Drawing background");
 		SDL_Rect src = {}, dst = {};
 		src.x = 0;
 		src.w = 8;
@@ -160,7 +169,7 @@ void drawBackground(Context *ctx, const TileMap &tm, SDL_Texture *tex) {
 		for (auto &m : tm) {
 			for (auto t : m) {
 				src.y = t * 8;
-				SDL_RenderCopy(id->renderer, tex, &src, &dst);
+				SDL_RenderCopy(nullptr, tex, &src, &dst);
 				dst.x += DstSize;
 			}
 			dst.x = 0;
@@ -169,49 +178,4 @@ void drawBackground(Context *ctx, const TileMap &tm, SDL_Texture *tex) {
 	}
 }
 
-void draw(Context *ctx) {
-	auto id = ctx->implData<SdlImplData>();
-	++id->draws;
-	if (id->draws >= 5000) {
-		using namespace std::chrono;
-		const auto now = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
-		const auto duration = static_cast<double>(now - id->prevFpsCheckTime) / 1000.0;
-		const auto fps = static_cast<int>(static_cast<double>(id->draws) / duration);
-#ifdef NOST_FPS_PRINT
-		std::cout << "FPS: " << fps << '\n';
-#endif
-		oxTrace("nostalgia::core::gfx::fps") << "FPS:" << fps;
-		id->prevFpsCheckTime = now;
-		id->draws = 0;
-	}
-	SDL_RenderClear(id->renderer);
-	for (std::size_t i = 0; i < id->bgTileMaps.size(); i++) {
-		auto tex = id->bgTextures[i];
-		auto &tm = id->bgTileMaps[i];
-		drawBackground(ctx, tm, tex);
-	}
-	SDL_RenderPresent(id->renderer);
-}
-
-void puts(Context *ctx, int column, int row, const char *str) {
-	for (int i = 0; str[i]; i++) {
-		setTile(ctx, 0, column + i, row, static_cast<uint8_t>(charMap[static_cast<uint8_t>(str[i])]));
-	}
-}
-
-void setTile(Context *ctx, int layer, int column, int row, uint8_t tile) {
-	auto id = ctx->implData<SdlImplData>();
-	auto z = static_cast<unsigned>(layer);
-	auto y = static_cast<unsigned>(row);
-	auto x = static_cast<unsigned>(column);
-	id->bgTileMaps[z][y][x] = tile;
-}
-
-[[maybe_unused]]
-void hideSprite(Context*, unsigned) {
-}
-
-void setSprite(Context*, unsigned, unsigned, unsigned, unsigned, unsigned, unsigned, unsigned) {
-}
-
 }
diff --git a/src/nostalgia/core/studio/imgconv.cpp b/src/nostalgia/core/studio/imgconv.cpp
index b7680018..96f31915 100644
--- a/src/nostalgia/core/studio/imgconv.cpp
+++ b/src/nostalgia/core/studio/imgconv.cpp
@@ -64,9 +64,9 @@ namespace {
 	auto ng = std::make_unique<core::NostalgiaGraphic>();
 	ng->pal.colors.resize(static_cast<std::size_t>(countColors(src, Tiles)));
 	if (argBpp == 4) {
-		ng->tiles.resize(static_cast<std::size_t>(Pixels / 2));
+		ng->pixels.resize(static_cast<std::size_t>(Pixels / 2));
 	} else {
-		ng->tiles.resize(static_cast<std::size_t>(Pixels));
+		ng->pixels.resize(static_cast<std::size_t>(Pixels));
 	}
 	ng->bpp = argBpp;
 	ng->columns = src.width() / TileWidth;
@@ -87,12 +87,12 @@ namespace {
 				// set pixel color
 				if (argBpp == 4) {
 					if (destI % 2) { // is odd number pixel
-						ng->tiles[static_cast<std::size_t>(destI / 2)] |= colors[c] << 4;
+						ng->pixels[static_cast<std::size_t>(destI / 2)] |= colors[c] << 4;
 					} else {
-						ng->tiles[static_cast<std::size_t>(destI / 2)] |= colors[c];
+						ng->pixels[static_cast<std::size_t>(destI / 2)] |= colors[c];
 					}
 				} else {
-					ng->tiles[static_cast<std::size_t>(destI)] = static_cast<std::size_t>(colors[c]);
+					ng->pixels[static_cast<std::size_t>(destI)] = static_cast<std::size_t>(colors[c]);
 				}
 			}
 		}
diff --git a/src/nostalgia/core/studio/tilesheeteditor.cpp b/src/nostalgia/core/studio/tilesheeteditor.cpp
index 6edffbc5..c29bae5c 100644
--- a/src/nostalgia/core/studio/tilesheeteditor.cpp
+++ b/src/nostalgia/core/studio/tilesheeteditor.cpp
@@ -663,18 +663,18 @@ std::unique_ptr<NostalgiaGraphic> SheetData::toNostalgiaGraphic() const {
 	ng->rows = m_rows;
 	auto pixelCount = static_cast<std::size_t>(ng->rows * ng->columns * PixelsPerTile);
 	if (ng->bpp == 4) {
-		ng->tiles.resize(pixelCount / 2);
+		ng->pixels.resize(pixelCount / 2);
 		for (std::size_t i = 0; i < pixelCount && i < static_cast<std::size_t>(m_pixels.size()); ++i) {
 			if (i & 1) {
-				ng->tiles[i / 2] |= static_cast<uint8_t>(m_pixels[i]) << 4;
+				ng->pixels[i / 2] |= static_cast<uint8_t>(m_pixels[i]) << 4;
 			} else {
-				ng->tiles[i / 2] = static_cast<uint8_t>(m_pixels[i]);
+				ng->pixels[i / 2] = static_cast<uint8_t>(m_pixels[i]);
 			}
 		}
 	} else {
-		ng->tiles.resize(pixelCount);
-		for (std::size_t i = 0; i < ng->tiles.size(); ++i) {
-			ng->tiles[i] = static_cast<uint8_t>(m_pixels[i]);
+		ng->pixels.resize(pixelCount);
+		for (std::size_t i = 0; i < ng->pixels.size(); ++i) {
+			ng->pixels[i] = static_cast<uint8_t>(m_pixels[i]);
 		}
 	}
 	return ng;
@@ -788,17 +788,17 @@ void SheetData::updatePixels(const NostalgiaGraphic *ng) {
 	m_pixels.clear();
 	m_pixelSelected.clear();
 	if (ng->bpp == 8) {
-		for (std::size_t i = 0; i < ng->tiles.size(); i++) {
-			m_pixels.push_back(ng->tiles[i]);
+		for (std::size_t i = 0; i < ng->pixels.size(); i++) {
+			m_pixels.push_back(ng->pixels[i]);
 			m_pixelSelected.push_back(0);
 		}
 	} else {
-		for (std::size_t i = 0; i < ng->tiles.size() * 2; i++) {
+		for (std::size_t i = 0; i < ng->pixels.size() * 2; i++) {
 			uint8_t p;
 			if (i & 1) {
-				p = ng->tiles[i / 2] >> 4;
+				p = ng->pixels[i / 2] >> 4;
 			} else {
-				p = ng->tiles[i / 2] & 0xF;
+				p = ng->pixels[i / 2] & 0xF;
 			}
 			m_pixels.push_back(p);
 			m_pixelSelected.push_back(0);
@@ -1011,15 +1011,15 @@ QImage TileSheetEditor::toQImage(NostalgiaGraphic *ng, NostalgiaPalette *npal) c
 		dst.setPixel(pt.x, pt.y, color);
 	};
 	if (ng->bpp == 4) {
-		for (std::size_t i = 0; i < ng->tiles.size(); ++i) {
-			auto p1 = ng->tiles[i] & 0xF;
-			auto p2 = ng->tiles[i] >> 4;
+		for (std::size_t i = 0; i < ng->pixels.size(); ++i) {
+			auto p1 = ng->pixels[i] & 0xF;
+			auto p2 = ng->pixels[i] >> 4;
 			setPixel(i * 2 + 0, p1);
 			setPixel(i * 2 + 1, p2);
 		}
 	} else {
-		for (std::size_t i = 0; i < ng->tiles.size(); i++) {
-			const auto p = ng->tiles[i];
+		for (std::size_t i = 0; i < ng->pixels.size(); i++) {
+			const auto p = ng->pixels[i];
 			setPixel(i, p);
 		}
 	}
diff --git a/src/nostalgia/core/userland/CMakeLists.txt b/src/nostalgia/core/userland/CMakeLists.txt
index 33ba3bf6..5a92b6ca 100644
--- a/src/nostalgia/core/userland/CMakeLists.txt
+++ b/src/nostalgia/core/userland/CMakeLists.txt
@@ -1,5 +1,6 @@
 add_library(
-	NostalgiaCore-Userspace
+	NostalgiaCore-Userspace OBJECT
+		gfx_opengl.cpp
 		media.cpp
 )
 
@@ -7,10 +8,19 @@ if(NOT MSVC)
 	target_compile_options(NostalgiaCore-Userspace PRIVATE -Wsign-conversion)
 endif()
 
+if(APPLE)
+	find_package(OpenGL REQUIRED)
+else()
+	set(OPENGL_gl_LIBRARY GL)
+endif()
+
 target_link_libraries(
 	NostalgiaCore-Userspace PUBLIC
 		OxFS
 		OxStd
+		NostalgiaCore
+		#GLESv2
+		${OPENGL_gl_LIBRARY}
 )
 
 install(
diff --git a/src/nostalgia/core/userland/gfx.hpp b/src/nostalgia/core/userland/gfx.hpp
new file mode 100644
index 00000000..65b44bb3
--- /dev/null
+++ b/src/nostalgia/core/userland/gfx.hpp
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2016 - 2021 gary@drinkingtea.net
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#pragma once
+
+#include <ox/std/types.hpp>
+
+#include <nostalgia/core/context.hpp>
+
+namespace nostalgia::core::renderer {
+
+ox::Error init(Context *ctx);
+
+ox::Error shutdown(Context *ctx);
+
+ox::Error loadTexture(Context *ctx, int section, void *bytes, int w, int h);
+
+}
diff --git a/src/nostalgia/core/userland/gfx_opengl.cpp b/src/nostalgia/core/userland/gfx_opengl.cpp
new file mode 100644
index 00000000..42b4cd77
--- /dev/null
+++ b/src/nostalgia/core/userland/gfx_opengl.cpp
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2016 - 2021 gary@drinkingtea.net
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include <ox/std/fmt.hpp>
+#define GL_SILENCE_DEPRECATION
+
+#include <array>
+
+#include <ox/std/defines.hpp>
+#include <ox/std/defines.hpp>
+
+#define GL_GLEXT_PROTOTYPES 1
+//#include <GLES2/gl2.h>
+#ifdef OX_OS_Darwin
+#include <OpenGL/gl.h>
+#else
+#include <GL/gl.h>
+#endif
+
+#include <nostalgia/core/gfx.hpp>
+
+namespace nostalgia::core {
+
+using TileMap = std::array<std::array<int, 128>, 128>;
+
+struct GlImplData {
+	std::array<TileMap, 4> bgTileMaps{};
+	std::array<GLuint, 4> bgTextures{};
+	int64_t prevFpsCheckTime = 0;
+	uint64_t draws = 0;
+};
+
+
+namespace renderer {
+
+ox::Error init(Context *ctx) {
+	const auto id = new GlImplData;
+	ctx->setRendererData(id);
+	return OxError(0);
+}
+
+ox::Error shutdown(Context *ctx) {
+	const auto id = ctx->rendererData<GlImplData>();
+	ctx->setRendererData(nullptr);
+	delete id;
+	return OxError(0);
+}
+
+ox::Error loadTexture(Context *ctx, int section, void *pixels, int w, int h) {
+	oxTracef("nostalgia::core::gfx::gl", "loadTexture: { section: {}, w: {}, h: {} }", section, w, h);
+	const auto &id = ctx->rendererData<GlImplData>();
+	auto &texId = id->bgTextures[static_cast<std::size_t>(section)];
+	glGenTextures(1, &texId);
+	glActiveTexture(GL_TEXTURE0);
+	glBindTexture(GL_TEXTURE_2D, texId);
+	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0,  GL_RGBA, GL_UNSIGNED_BYTE, pixels);
+	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);
+	return OxError(0);
+}
+
+}
+
+
+void draw(Context *ctx) {
+	const auto id = ctx->rendererData<GlImplData>();
+	++id->draws;
+	if (id->draws >= 5000) {
+		using namespace std::chrono;
+		const auto now = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
+		const auto duration = static_cast<double>(now - id->prevFpsCheckTime) / 1000.0;
+		const auto fps = static_cast<int>(static_cast<double>(id->draws) / duration);
+#ifdef NOST_FPS_PRINT
+		std::cout << "FPS: " << fps << '\n';
+#endif
+		oxTracef("nostalgia::core::gfx::gl::fps", "FPS: {}", fps);
+		id->prevFpsCheckTime = now;
+		id->draws = 0;
+	}
+	glClearColor(0, 0, 0, 1);
+	glClear(GL_COLOR_BUFFER_BIT);
+	auto &texId = id->bgTextures[0];
+
+	glEnable(GL_TEXTURE_2D);
+	glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
+	glBindTexture(GL_TEXTURE_2D, texId);
+
+	// Draw a textured quad
+	glBegin(GL_QUADS);
+	glTexCoord2f(0, 0); glVertex2f(-0.01,  1);
+	glTexCoord2f(0, 1); glVertex2f(-0.01, -1);
+	glTexCoord2f(1, 1); glVertex2f( 0.01, -1);
+	glTexCoord2f(1, 0); glVertex2f( 0.01,  1);
+	glEnd();
+
+	glDisable(GL_TEXTURE_2D);
+
+	for (std::size_t i = 0; i < id->bgTileMaps.size(); i++) {
+	}
+}
+
+void puts(Context *ctx, int column, int row, const char *str) {
+	for (int i = 0; str[i]; ++i) {
+		setTile(ctx, 0, column + i, row, static_cast<uint8_t>(charMap[static_cast<int>(str[i])]));
+	}
+}
+
+void clearTileLayer(Context*, int) {
+}
+
+void hideSprite(Context*, unsigned) {
+}
+
+void setSprite(Context*,
+               unsigned,
+               unsigned,
+               unsigned,
+               unsigned,
+               unsigned,
+               unsigned,
+               unsigned) {
+}
+
+void setTile(Context *ctx, int layer, int column, int row, uint8_t tile) {
+	const auto id = ctx->rendererData<GlImplData>();
+	const auto z = static_cast<unsigned>(layer);
+	const auto y = static_cast<unsigned>(row);
+	const auto x = static_cast<unsigned>(column);
+	id->bgTileMaps[z][y][x] = tile;
+}
+
+}