/* * Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved. */ #include #include #include #include #include #include #include #include namespace nostalgia::core { void ImGui_Impl_NewFrame() noexcept; namespace renderer { constexpr uint64_t TileRows = 128; constexpr uint64_t TileColumns = 128; constexpr uint64_t TileCount = TileRows * TileColumns; constexpr uint64_t SpriteCount = 128; constexpr uint64_t BgVertexVboRows = 4; constexpr uint64_t BgVertexVboRowLength = 4; constexpr uint64_t BgVertexVboLength = BgVertexVboRows * BgVertexVboRowLength; constexpr uint64_t BgVertexEboLength = 6; constexpr uint64_t SpriteVertexVboRows = 256; constexpr uint64_t SpriteVertexVboRowLength = 5; constexpr uint64_t SpriteVertexVboLength = SpriteVertexVboRows * SpriteVertexVboRowLength; constexpr uint64_t SpriteVertexEboLength = 6; struct CBB: public glutils::BufferSet { bool updated = false; constexpr CBB() noexcept { vertices.resize(TileCount * BgVertexVboLength); elements.resize(TileCount * BgVertexEboLength); } }; struct SpriteBlockset: public glutils::BufferSet { bool updated = false; constexpr SpriteBlockset() noexcept { vertices.resize(SpriteCount * SpriteVertexVboLength); elements.resize(SpriteCount * SpriteVertexEboLength); } }; struct Background { bool enabled = false; unsigned cbbIdx = 0; }; struct Sprite { bool enabled = false; }; struct GlImplData { glutils::GLProgram bgShader; glutils::GLProgram spriteShader; int64_t prevFpsCheckTime = 0; uint64_t draws = 0; bool mainViewEnabled = true; ox::Array cbbs; SpriteBlockset spriteBlocks; ox::Array spriteStates; ox::Array backgrounds; ox::Optional renderSize; }; 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, 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 tickFps(GlImplData *id) noexcept { ++id->draws; if (id->draws >= 500) { using namespace std::chrono; const auto now = duration_cast(system_clock::now().time_since_epoch()).count(); const auto duration = static_cast(now - id->prevFpsCheckTime) / 1000.0; const auto fps = static_cast(static_cast(id->draws) / duration); if constexpr(config::UserlandFpsPrint) { oxOutf("FPS: {}\n", fps); } oxTracef("nostalgia::core::gfx::fps", "FPS: {}", fps); id->prevFpsCheckTime = now; id->draws = 0; } } 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(core::Context *ctx, GlImplData *id) noexcept { // load background shader and its uniforms glUseProgram(id->bgShader); const auto uniformXScale = static_cast(glGetUniformLocation(id->bgShader, "vXScale")); const auto uniformTileHeight = static_cast(glGetUniformLocation(id->bgShader, "vTileHeight")); const auto [wi, hi] = gl::getRenderSize(ctx); const auto wf = static_cast(wi); const auto hf = static_cast(hi); glUniform1f(uniformXScale, hf / wf); for (const auto &bg : id->backgrounds) { if (bg.enabled) { auto &cbb = id->cbbs[bg.cbbIdx]; const auto tileRows = cbb.tex.height / TileHeight; glUniform1f(uniformTileHeight, 1.0f / static_cast(tileRows)); drawBackground(&cbb); } } } static void drawSprites(core::Context *ctx, GlImplData *id) noexcept { glUseProgram(id->spriteShader); auto &sb = id->spriteBlocks; const auto uniformXScale = static_cast(glGetUniformLocation(id->bgShader, "vXScale")); const auto uniformTileHeight = static_cast(glGetUniformLocation(id->spriteShader, "vTileHeight")); const auto [wi, hi] = gl::getRenderSize(ctx); 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); } ox::Error init(Context *ctx) noexcept { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); const auto bgVshad = ox::sfmt(bgvshadTmpl, glutils::GlslVersion); const auto bgFshad = ox::sfmt(bgfshadTmpl, glutils::GlslVersion); const auto spriteVshad = ox::sfmt(spritevshadTmpl, glutils::GlslVersion); const auto spriteFshad = ox::sfmt(spritefshadTmpl, glutils::GlslVersion); const auto id = ox::make(); ctx->setRendererData(id); oxReturnError(glutils::buildShaderProgram(bgVshad.c_str(), bgFshad.c_str()).moveTo(&id->bgShader)); oxReturnError(glutils::buildShaderProgram(spriteVshad.c_str(), spriteFshad.c_str()).moveTo(&id->spriteShader)); for (auto &bg : id->cbbs) { initBackgroundBufferset(ctx, id->bgShader, &bg); } initSpritesBufferset(ctx, id->spriteShader, &id->spriteBlocks); ImGui_ImplOpenGL3_Init(glutils::GlslVersion); return {}; } void shutdown(Context*, void *rendererData) noexcept { const auto id = reinterpret_cast(rendererData); ox::safeDelete(id); } 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()); } void loadBgPalette(void *rendererData, const Palette &pal) noexcept { const auto id = static_cast(rendererData); loadPalette(id->bgShader, pal); } void loadSpritePalette(void *rendererData, const Palette &pal) noexcept { const auto id = static_cast(rendererData); loadPalette(id->spriteShader, pal, true); } void loadBgTexture(void *rendererData, unsigned cbbIdx, const void *pixels, int w, int h) noexcept { oxTracef("nostalgia::core::gfx::gl", "loadBgTexture: { cbbIdx: {}, w: {}, h: {} }", cbbIdx, w, h); const auto id = static_cast(rendererData); id->cbbs[cbbIdx].tex = loadTexture(w, h, pixels); } void loadSpriteTexture(void *rendererData, const void *pixels, int w, int h) noexcept { oxTracef("nostalgia::core::gfx::gl", "loadSpriteTexture: { w: {}, h: {} }", w, h); const auto id = static_cast(rendererData); id->spriteBlocks.tex = loadTexture(w, h, pixels); } } void setBgCbb(Context *ctx, unsigned bgIdx, unsigned cbbIdx) noexcept { const auto id = ctx->rendererData(); auto &bg = id->backgrounds[bgIdx]; bg.cbbIdx = cbbIdx; } uint8_t bgStatus(Context *ctx) noexcept { const auto id = ctx->rendererData(); uint8_t out = 0; for (unsigned i = 0; i < id->cbbs.size(); ++i) { out |= id->backgrounds[i].enabled << i; } return out; } void setBgStatus(Context *ctx, uint32_t status) noexcept { const auto id = ctx->rendererData(); for (unsigned i = 0; i < id->cbbs.size(); ++i) { id->backgrounds[i].enabled = (status >> i) & 1; } } bool bgStatus(Context *ctx, unsigned bg) noexcept { const auto id = ctx->rendererData(); return id->backgrounds[bg].enabled; } void setBgStatus(Context *ctx, unsigned bg, bool status) noexcept { const auto id = ctx->rendererData(); id->backgrounds[bg].enabled = status; } void draw(Context *ctx) noexcept { const auto id = ctx->rendererData(); renderer::tickFps(id); ImGui_ImplOpenGL3_NewFrame(); ImGui_Impl_NewFrame(); if constexpr(config::ImGuiEnabled) { ImGui::NewFrame(); } // clear screen glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT); // render if (id->mainViewEnabled) { gl::drawMainView(ctx); } for (const auto cd : ctx->drawers) { cd->draw(ctx); } if constexpr(config::ImGuiEnabled) { ImGui::Render(); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); } } void clearTileLayer(Context *ctx, unsigned bgIdx) noexcept { const auto id = ctx->rendererData(); auto &bg = id->cbbs[static_cast(bgIdx)]; initBackgroundBufferObjects(ctx, &bg); bg.updated = true; } void hideSprite(Context *ctx, unsigned idx) noexcept { auto &id = *ctx->rendererData(); auto vbo = &id.spriteBlocks.vertices[idx * renderer::SpriteVertexVboLength]; auto ebo = &id.spriteBlocks.elements[idx * renderer::SpriteVertexEboLength]; renderer::setSpriteBufferObject(ctx, idx * renderer::SpriteVertexVboRows, 0, 0, 0, 0, false, vbo, ebo); id.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 &id = *ctx->rendererData(); 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 = &id.spriteBlocks.vertices[cidx * renderer::SpriteVertexVboLength]; auto ebo = &id.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)); } } } id.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); const auto id = ctx->rendererData(); 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 = id->cbbs[z]; auto vbo = &bg.vertices[i * renderer::BgVertexVboLength]; auto ebo = &bg.elements[i * renderer::BgVertexEboLength]; renderer::setTileBufferObject( ctx, i * renderer::BgVertexVboRows, static_cast(x), static_cast(y), tile, vbo, ebo); bg.updated = true; } namespace gl { void setMainViewEnabled(core::Context *ctx, bool enabled) noexcept { const auto id = ctx->rendererData(); id->mainViewEnabled = enabled; } void drawMainView(core::Context *ctx) noexcept { const auto id = ctx->rendererData(); renderer::drawBackgrounds(ctx, id); if (id->spriteBlocks.tex) { renderer::drawSprites(ctx, id); } } void setRenderSize(core::Context *ctx, int width, int height) noexcept { const auto id = ctx->rendererData(); id->renderSize.emplace(width, height); } void clearRenderSize(core::Context *ctx) noexcept { auto id = ctx->rendererData(); id->renderSize.reset(); } ox::Size getRenderSize(core::Context *ctx) noexcept { const auto id = ctx->rendererData(); if (id->renderSize.has_value()) { return id->renderSize.value(); } else { return core::getScreenSize(ctx); } } } }