/* * Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved. */ #include #include #include #include #include #include #include #include "context.hpp" namespace nostalgia::core { namespace gl { void drawMainView(core::Context *ctx, ox::Size const&renderSz) noexcept; } namespace renderer { void Drawer::draw(turbine::Context &tctx) noexcept { core::gl::drawMainView(m_ctx, turbine::getScreenSize(tctx)); } constexpr ox::StringView bgvshadTmpl = R"( {} in vec2 vTexCoord; in vec2 vPosition; out vec2 fTexCoord; uniform float vXScale; uniform float vTileHeight; void main() { float xScaleInvert = 1.0 - vXScale; gl_Position = vec4( vPosition.x * vXScale - xScaleInvert, vPosition.y, 0.0, 1.0); fTexCoord = vTexCoord * vec2(1, vTileHeight); })"; constexpr ox::StringView bgfshadTmpl = R"( {} out vec4 outColor; in vec2 fTexCoord; uniform sampler2D image; uniform vec4 fPalette[256]; void main() { int idx = int(texture(image, fTexCoord).rgb.r * 256); outColor = fPalette[idx]; //outColor = vec4(0.0, 0.7, 1.0, 1.0); })"; constexpr ox::StringView spritevshadTmpl = R"( {} in float vEnabled; in vec2 vTexCoord; in vec2 vPosition; out vec2 fTexCoord; uniform float vXScale; uniform float vTileHeight; void main() { float xScaleInvert = 1.0 - vXScale; gl_Position = vec4( vPosition.x * vXScale - xScaleInvert, vPosition.y, 0.0, 1.0); fTexCoord = vTexCoord * vec2(1, vTileHeight) * vec2(vEnabled, vEnabled); })"; constexpr ox::StringView spritefshadTmpl = bgfshadTmpl; [[nodiscard]] static constexpr auto bgVertexRow(unsigned x, unsigned y) noexcept { return y * TileRows + x; } static void setSpriteBufferObject(Context*, unsigned vi, float enabled, float x, float y, unsigned textureRow, unsigned flipX, float *vbo, GLuint *ebo) noexcept { // don't worry, this memcpy gets optimized to something much more ideal constexpr float xmod = 0.1f; constexpr float ymod = 0.1f; x *= xmod; y *= -ymod; x -= 1.f; y += 1.f - ymod; const auto textureRowf = static_cast(textureRow); const float L = flipX ? 1 : 0; const float R = flipX ? 0 : 1; const ox::Array vertices { enabled, x, y, L, textureRowf + 1, // bottom left enabled, x + xmod, y, R, textureRowf + 1, // bottom right enabled, x + xmod, y + ymod, R, textureRowf + 0, // top right enabled, x, y + ymod, L, textureRowf + 0, // top left }; memcpy(vbo, vertices.data(), sizeof(vertices)); const ox::Array elms { vi + 0, vi + 1, vi + 2, vi + 2, vi + 3, vi + 0, }; memcpy(ebo, elms.data(), sizeof(elms)); } static void setTileBufferObject(Context*, unsigned vi, float x, float y, unsigned textureRow, float *vbo, GLuint *ebo) noexcept { // don't worry, this memcpy gets optimized to something much more ideal constexpr float ymod = 0.1f; constexpr float xmod = 0.1f; x *= xmod; y *= -ymod; x -= 1.0f; y += 1.0f - ymod; const auto textureRowf = static_cast(textureRow); const ox::Array vertices { x, y, 0, textureRowf + 1, // bottom left x + xmod, y, 1, textureRowf + 1, // bottom right x + xmod, y + ymod, 1, textureRowf + 0, // top right x, y + ymod, 0, textureRowf + 0, // top left }; memcpy(vbo, vertices.data(), sizeof(vertices)); const ox::Array elms { vi + 0, vi + 1, vi + 2, vi + 2, vi + 3, vi + 0, }; memcpy(ebo, elms.data(), sizeof(elms)); } static void initSpriteBufferObjects(Context *ctx, glutils::BufferSet *bs) noexcept { for (auto i = 0u; i < SpriteCount; ++i) { auto vbo = &bs->vertices[i * static_cast(SpriteVertexVboLength)]; auto ebo = &bs->elements[i * static_cast(SpriteVertexEboLength)]; setSpriteBufferObject(ctx, i * SpriteVertexVboRows, 0, 0, 0, 0, false, vbo, ebo); } } static void initBackgroundBufferObjects(Context *ctx, glutils::BufferSet *bg) noexcept { for (auto x = 0u; x < TileColumns; ++x) { for (auto y = 0u; y < TileRows; ++y) { const auto i = bgVertexRow(x, y); auto vbo = &bg->vertices[i * static_cast(BgVertexVboLength)]; auto ebo = &bg->elements[i * static_cast(BgVertexEboLength)]; setTileBufferObject( ctx, static_cast(i * BgVertexVboRows), static_cast(x), static_cast(y), 0, vbo, ebo); } } } static void initSpritesBufferset(Context *ctx, GLuint shader, glutils::BufferSet *bs) noexcept { // vao bs->vao = glutils::generateVertexArrayObject(); glBindVertexArray(bs->vao); // vbo & ebo bs->vbo = glutils::generateBuffer(); bs->ebo = glutils::generateBuffer(); initSpriteBufferObjects(ctx, bs); glutils::sendVbo(*bs); glutils::sendEbo(*bs); // vbo layout auto enabledAttr = static_cast(glGetAttribLocation(shader, "vEnabled")); glEnableVertexAttribArray(enabledAttr); glVertexAttribPointer(enabledAttr, 1, GL_FLOAT, GL_FALSE, SpriteVertexVboRowLength * sizeof(float), nullptr); auto posAttr = static_cast(glGetAttribLocation(shader, "vPosition")); glEnableVertexAttribArray(posAttr); glVertexAttribPointer(posAttr, 2, GL_FLOAT, GL_FALSE, SpriteVertexVboRowLength * sizeof(float), reinterpret_cast(1 * sizeof(float))); auto texCoordAttr = static_cast(glGetAttribLocation(shader, "vTexCoord")); glEnableVertexAttribArray(texCoordAttr); glVertexAttribPointer(texCoordAttr, 2, GL_FLOAT, GL_FALSE, SpriteVertexVboRowLength * sizeof(float), reinterpret_cast(3 * sizeof(float))); } static void initBackgroundBufferset(Context *ctx, GLuint shader, glutils::BufferSet *bg) noexcept { // vao bg->vao = glutils::generateVertexArrayObject(); glBindVertexArray(bg->vao); // vbo & ebo bg->vbo = glutils::generateBuffer(); bg->ebo = glutils::generateBuffer(); initBackgroundBufferObjects(ctx, bg); glutils::sendVbo(*bg); glutils::sendEbo(*bg); // vbo layout auto posAttr = static_cast(glGetAttribLocation(shader, "vPosition")); glEnableVertexAttribArray(posAttr); glVertexAttribPointer(posAttr, 2, GL_FLOAT, GL_FALSE, BgVertexVboRowLength * sizeof(float), nullptr); auto texCoordAttr = static_cast(glGetAttribLocation(shader, "vTexCoord")); glEnableVertexAttribArray(texCoordAttr); glVertexAttribPointer(texCoordAttr, 2, GL_FLOAT, GL_FALSE, BgVertexVboRowLength * sizeof(float), reinterpret_cast(2 * sizeof(float))); } static glutils::GLTexture loadTexture(GLsizei w, GLsizei h, const void *pixels) noexcept { GLuint texId = 0; glGenTextures(1, &texId); glutils::GLTexture tex(texId); tex.width = w; tex.height = h; glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, tex.id); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, tex.width, tex.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 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 tex; } static void drawBackground(CBB *cbb) noexcept { glBindVertexArray(cbb->vao); if (cbb->updated) { cbb->updated = false; glutils::sendVbo(*cbb); } glBindTexture(GL_TEXTURE_2D, cbb->tex); glDrawElements(GL_TRIANGLES, static_cast(cbb->elements.size()), GL_UNSIGNED_INT, nullptr); } static void drawBackgrounds(GlContext *gctx, ox::Size const&renderSz) noexcept { // load background shader and its uniforms glUseProgram(gctx->bgShader); const auto uniformXScale = static_cast(glGetUniformLocation(gctx->bgShader, "vXScale")); const auto uniformTileHeight = static_cast(glGetUniformLocation(gctx->bgShader, "vTileHeight")); const auto [wi, hi] = renderSz; const auto wf = static_cast(wi); const auto hf = static_cast(hi); glUniform1f(uniformXScale, hf / wf); for (const auto &bg : gctx->backgrounds) { if (bg.enabled) { auto &cbb = gctx->cbbs[bg.cbbIdx]; const auto tileRows = cbb.tex.height / TileHeight; glUniform1f(uniformTileHeight, 1.0f / static_cast(tileRows)); drawBackground(&cbb); } } } static void drawSprites(GlContext *gctx, ox::Size const&renderSz) noexcept { glUseProgram(gctx->spriteShader); auto &sb = gctx->spriteBlocks; const auto uniformXScale = static_cast(glGetUniformLocation(gctx->bgShader, "vXScale")); const auto uniformTileHeight = static_cast(glGetUniformLocation(gctx->spriteShader, "vTileHeight")); const auto [wi, hi] = renderSz; const auto wf = static_cast(wi); const auto hf = static_cast(hi); glUniform1f(uniformXScale, hf / wf); // update vbo glBindVertexArray(sb.vao); if (sb.updated) { sb.updated = false; glutils::sendVbo(sb); } // set vTileHeight uniform const auto tileRows = sb.tex.height / TileHeight; glUniform1f(uniformTileHeight, 1.0f / static_cast(tileRows)); // draw glBindTexture(GL_TEXTURE_2D, sb.tex); glDrawElements(GL_TRIANGLES, static_cast(sb.elements.size()), GL_UNSIGNED_INT, nullptr); } static void loadPalette(GLuint shaderPgrm, const Palette &pal, bool firstIsTransparent = false) noexcept { static constexpr std::size_t ColorCnt = 256; ox::Array palette{}; for (auto i = 0u; const auto c : pal.colors) { palette[i++] = redf(c); palette[i++] = greenf(c); palette[i++] = bluef(c); palette[i++] = 255; } if (firstIsTransparent) { palette[3] = 0; } glUseProgram(shaderPgrm); const auto uniformPalette = static_cast(glGetUniformLocation(shaderPgrm, "fPalette")); glUniform4fv(uniformPalette, ColorCnt, palette.data()); } static void loadBgPalette(GlContext *gctx, const Palette &pal) noexcept { loadPalette(gctx->bgShader, pal); } static void loadSpritePalette(GlContext *gctx, const Palette &pal) noexcept { loadPalette(gctx->spriteShader, pal, true); } static void loadBgTexture(GlContext *gctx, unsigned cbbIdx, const void *pixels, int w, int h) noexcept { oxTracef("nostalgia::core::gfx::gl", "loadBgTexture: { cbbIdx: {}, w: {}, h: {} }", cbbIdx, w, h); gctx->cbbs[cbbIdx].tex = loadTexture(w, h, pixels); } static void loadSpriteTexture(GlContext *gctx, const void *pixels, int w, int h) noexcept { oxTracef("nostalgia::core::gfx::gl", "loadSpriteTexture: { w: {}, h: {} }", w, h); gctx->spriteBlocks.tex = loadTexture(w, h, pixels); } } ox::Error initGfx(Context *ctx, const InitParams &initParams) noexcept { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); const auto bgVshad = ox::sfmt(renderer::bgvshadTmpl, glutils::GlslVersion); const auto bgFshad = ox::sfmt(renderer::bgfshadTmpl, glutils::GlslVersion); const auto spriteVshad = ox::sfmt(renderer::spritevshadTmpl, glutils::GlslVersion); const auto spriteFshad = ox::sfmt(renderer::spritefshadTmpl, glutils::GlslVersion); auto &gctx = static_cast(*ctx); oxReturnError(glutils::buildShaderProgram(bgVshad.c_str(), bgFshad.c_str()).moveTo(&gctx.bgShader)); oxReturnError(glutils::buildShaderProgram(spriteVshad.c_str(), spriteFshad.c_str()).moveTo(&gctx.spriteShader)); for (auto &bg : gctx.cbbs) { initBackgroundBufferset(ctx, gctx.bgShader, &bg); } gctx.drawer.m_ctx = ctx; if (initParams.glInstallDrawer) { turbine::gl::addDrawer(*gctx.turbineCtx, &gctx.drawer); initSpritesBufferset(ctx, gctx.spriteShader, &gctx.spriteBlocks); } return {}; } void shutdownGfx(Context *ctx) noexcept { auto &gctx = static_cast(*ctx); turbine::gl::removeDrawer(*gctx.turbineCtx, &gctx.drawer); } ox::Error initConsole(Context *ctx) noexcept { constexpr ox::FileAddress TilesheetAddr("/TileSheets/Charset.ng"); constexpr ox::FileAddress PaletteAddr("/Palettes/Charset.npal"); setBgStatus(ctx, 0b0001); setBgCbb(ctx, 0, 0); return loadBgTileSheet(ctx, 0, TilesheetAddr, PaletteAddr); } struct TileSheetData { ox::Vector pixels; int width = 0; int height = 0; }; ox::Result loadTileSheet(Context *ctx, const CompactTileSheet &tilesheet) noexcept { auto &gctx = static_cast(*ctx); const unsigned bytesPerTile = tilesheet.bpp == 8 ? PixelsPerTile : PixelsPerTile / 2; const auto tiles = tilesheet.pixels.size() / bytesPerTile; constexpr int width = 8; const int height = 8 * static_cast(tiles); ox::Vector pixels; if (bytesPerTile == 64) { // 8 BPP pixels.resize(tilesheet.pixels.size()); for (std::size_t i = 0; i < tilesheet.pixels.size(); ++i) { pixels[i] = 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] = tilesheet.pixels[i] & 0xF; pixels[i * 2 + 1] = tilesheet.pixels[i] >> 4; } } renderer::loadSpriteTexture(&gctx, pixels.data(), width, height); return TileSheetData{std::move(pixels), width, height}; } ox::Error loadBgTileSheet(Context *ctx, unsigned cbb, const ox::FileAddress &tilesheetAddr, const ox::FileAddress &paletteAddr) noexcept { auto &gctx = static_cast(*ctx); auto &kctx = *gctx.turbineCtx; oxRequire(tilesheet, readObj(&kctx, tilesheetAddr)); oxRequire(palette, readObj(&kctx, paletteAddr ? paletteAddr : tilesheet->defaultPalette)); oxRequire(tsd, loadTileSheet(ctx, *tilesheet)); renderer::loadBgTexture(&gctx, cbb, tsd.pixels.data(), tsd.width, tsd.height); renderer::loadBgPalette(&gctx, *palette); return {}; } ox::Error loadSpriteTileSheet(Context *ctx, const ox::FileAddress &tilesheetAddr, const ox::FileAddress &paletteAddr) noexcept { auto &gctx = static_cast(*ctx); auto &kctx = *gctx.turbineCtx; oxRequire(tilesheet, readObj(&kctx, tilesheetAddr)); oxRequire(palette, readObj(&kctx, paletteAddr ? paletteAddr : tilesheet->defaultPalette)); oxRequire(tsd, loadTileSheet(ctx, *tilesheet)); renderer::loadSpriteTexture(&gctx, tsd.pixels.data(), tsd.width, tsd.height); renderer::loadSpritePalette(&gctx, *palette); return {}; } void puts(Context *ctx, int column, int row, ox::CRStringView str) noexcept { const auto col = static_cast(column); for (auto i = 0u; i < str.bytes(); ++i) { setTile(ctx, 0, static_cast(col + i), row, static_cast(charMap[static_cast(str[i])])); } } void setBgCbb(Context *ctx, unsigned bgIdx, unsigned cbbIdx) noexcept { auto &gctx = static_cast(*ctx); auto &bg = gctx.backgrounds[bgIdx]; bg.cbbIdx = cbbIdx; } uint8_t bgStatus(Context *ctx) noexcept { const auto &gctx = static_cast(*ctx); uint8_t out = 0; for (unsigned i = 0; i < gctx.cbbs.size(); ++i) { out |= static_cast(static_cast(gctx.backgrounds[i].enabled) << i); } return out; } void setBgStatus(Context *ctx, uint32_t status) noexcept { auto &gctx = static_cast(*ctx); for (unsigned i = 0; i < gctx.cbbs.size(); ++i) { gctx.backgrounds[i].enabled = (status >> i) & 1; } } bool bgStatus(Context *ctx, unsigned bg) noexcept { const auto &gctx = static_cast(*ctx); return gctx.backgrounds[bg].enabled; } void setBgStatus(Context *ctx, unsigned bg, bool status) noexcept { auto &gctx = static_cast(*ctx); gctx.backgrounds[bg].enabled = status; } void glearTileLayer(Context *ctx, unsigned bgIdx) noexcept { auto &gctx = static_cast(*ctx); auto &bg = gctx.cbbs[static_cast(bgIdx)]; initBackgroundBufferObjects(&gctx, &bg); bg.updated = true; } void hideSprite(Context *ctx, unsigned idx) noexcept { auto &gctx = static_cast(*ctx); auto vbo = &gctx.spriteBlocks.vertices[idx * renderer::SpriteVertexVboLength]; auto ebo = &gctx.spriteBlocks.elements[idx * renderer::SpriteVertexEboLength]; renderer::setSpriteBufferObject(ctx, idx * renderer::SpriteVertexVboRows, 0, 0, 0, 0, false, vbo, ebo); gctx.spriteBlocks.updated = true; } void setSprite(Context *ctx, unsigned idx, int x, int y, unsigned tileIdx, [[maybe_unused]] unsigned spriteShape, [[maybe_unused]] unsigned spriteSize, unsigned flipX) noexcept { //oxTracef("nostalgia::core::gfx::gl", "setSprite(ctx, {}, {}, {}, {}, {}, {}, {})", // idx, x, y, tileIdx, spriteShape, spriteSize, flipX); // Tonc Table 8.4 static constexpr ox::Array, 12> dimensions{ // col 0 {1, 1}, // 0, 0 {2, 2}, // 0, 1 {4, 4}, // 0, 2 {8, 8}, // 0, 3 // col 1 {2, 1}, // 1, 0 {4, 1}, // 1, 1 {4, 2}, // 1, 2 {8, 4}, // 1, 3 // col 2 {1, 1}, // 2, 0 {1, 4}, // 2, 1 {2, 4}, // 2, 2 {4, 8}, // 2, 3 }; const auto dim = dimensions[(spriteShape << 2) | spriteSize]; const auto uX = static_cast(x) % 255; const auto uY = static_cast(y + 8) % 255 - 8; auto &gctx = static_cast(*ctx); auto i = 0u; const auto set = [&](int xIt, int yIt) { const auto fX = static_cast(uX + xIt * 8) / 8; const auto fY = static_cast(uY + yIt * 8) / 8; const auto cidx = idx + i; auto vbo = &gctx.spriteBlocks.vertices[cidx * renderer::SpriteVertexVboLength]; auto ebo = &gctx.spriteBlocks.elements[cidx * renderer::SpriteVertexEboLength]; renderer::setSpriteBufferObject(ctx, cidx * renderer::SpriteVertexVboRows, 1, fX, fY, tileIdx + i, flipX, vbo, ebo); ++i; }; if (!flipX) { for (auto yIt = 0; yIt < static_cast(dim.y); ++yIt) { for (auto xIt = 0u; xIt < dim.x; ++xIt) { set(static_cast(xIt), static_cast(yIt)); } } } else { for (auto yIt = 0u; yIt < dim.y; ++yIt) { for (auto xIt = dim.x - 1; xIt < ~0u; --xIt) { set(static_cast(xIt), static_cast(yIt)); } } } gctx.spriteBlocks.updated = true; } void setTile(Context *ctx, unsigned bgIdx, int column, int row, uint8_t tile) noexcept { oxTracef( "nostalgia::core::gfx::setTile", "bgIdx: {}, column: {}, row: {}, tile: {}", bgIdx, column, row, tile); auto &gctx = static_cast(*ctx); const auto z = static_cast(bgIdx); const auto y = static_cast(row); const auto x = static_cast(column); const auto i = renderer::bgVertexRow(x, y); auto &bg = gctx.cbbs[z]; auto vbo = &bg.vertices[i * renderer::BgVertexVboLength]; auto ebo = &bg.elements[i * renderer::BgVertexEboLength]; renderer::setTileBufferObject( ctx, static_cast(i * renderer::BgVertexVboRows), static_cast(x), static_cast(y), tile, vbo, ebo); bg.updated = true; } namespace gl { void drawMainView(core::Context *ctx, ox::Size const&renderSz) noexcept { // clear screen glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT); auto &gctx = static_cast(*ctx); renderer::drawBackgrounds(&gctx, renderSz); if (gctx.spriteBlocks.tex) { renderer::drawSprites(&gctx, renderSz); } } } }