/* * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. */ #include #include #include #include #include #include #include #include #include OX_ALLOW_UNSAFE_BUFFERS_BEGIN namespace nostalgia::gfx { struct BgCbbData { unsigned bpp = 4; }; class Context { public: turbine::Context &turbineCtx; ox::Array cbbData; explicit Context(turbine::Context &tctx) noexcept: turbineCtx{tctx} {} Context(Context &other) noexcept = delete; Context(Context const&other) noexcept = delete; Context(Context const&&other) noexcept = delete; virtual ~Context() noexcept = default; [[nodiscard]] ox::MemFS const&rom() const noexcept { return static_cast(*turbine::rom(turbineCtx)); } }; void safeDelete(Context *ctx) noexcept { delete ctx; } keel::Context &keelCtx(Context &ctx) noexcept { return turbine::keelCtx(ctx.turbineCtx); } turbine::Context &turbineCtx(Context &ctx) noexcept { return ctx.turbineCtx; } static constexpr auto SpriteCount = 128; static ox::Error initGfx() noexcept { for (auto bgCtl = ®_BG0CTL; bgCtl <= ®_BG3CTL; bgCtl += 2) { teagba::bgSetSbb(*bgCtl, 28); } for (uint16_t i = 0; i < SpriteCount; ++i) { auto &sa = teagba::spriteAttr(i); sa.idx = i; } return {}; } ox::Result> init(turbine::Context &tctx, InitParams const&) noexcept { auto ctx = ox::make_unique(tctx); OX_RETURN_ERROR(initGfx()); return ctx; } ox::Error loadBgPalette( Context&, size_t const palBank, CompactPalette const&palette, size_t const page) noexcept { if (palette.pages.empty()) { return {}; } auto const paletteMem = MEM_BG_PALETTE + palBank * 16; for (auto i = 0u; i < colorCnt(palette, page); ++i) { paletteMem[i] = color(palette, page, i); } return {}; } ox::Error loadSpritePalette( Context&, CompactPalette const&palette, size_t const page) noexcept { if (palette.pages.empty()) { return {}; } auto const paletteMem = MEM_SPRITE_PALETTE; for (auto i = 0u; i < colorCnt(palette, page); ++i) { paletteMem[i] = color(palette, page, i); } return {}; } void clearCbb(Context&, unsigned const cbb) noexcept { for (auto &v : MEM_BG_TILES[cbb]) { v = 0; } } void clearCbbs(Context &ctx) noexcept { clearCbb(ctx, 0); clearCbb(ctx, 1); clearCbb(ctx, 2); clearCbb(ctx, 3); } static ox::Error loadTileSheetSet( Context &ctx, ox::Span tileMapTargetMem, TileSheetSet const&set) noexcept { size_t tileWriteIdx = 0; size_t const bppMod = set.bpp == 4; for (auto const&entry : set.entries) { OX_REQUIRE(ts, keel::readObj(keelCtx(ctx), entry.tilesheet)); if (set.bpp != ts->bpp && ts->bpp == 8) { return ox::Error(1, "cannot load an 8 BPP tilesheet into a 4 BPP CBB"); } for (auto const&s : entry.sections) { auto const cnt = (static_cast(s.tiles) * PixelsPerTile) >> bppMod; for (size_t i = 0; i < cnt; ++i) { auto const begin = static_cast(s.begin) * (PixelsPerTile >> bppMod); auto const srcIdx = begin + i * 2; auto const v = static_cast( static_cast(ts->pixels[srcIdx]) | (static_cast(ts->pixels[srcIdx + 1]) << 8)); tileMapTargetMem[tileWriteIdx + i] = v; } tileWriteIdx += cnt >> bppMod; } } return {}; } ox::Error loadBgTileSheet( Context &ctx, unsigned const cbb, CompactTileSheet const&ts, size_t const dstTileIdx, size_t const srcTileIdx, size_t const tileCnt) noexcept { size_t const bppMod = ts.bpp == 4; size_t const bytesPerTile = PixelsPerTile >> bppMod; auto const cnt = (tileCnt * bytesPerTile) / 2; auto const srcPxIdx = srcTileIdx * bytesPerTile; auto const dstPxIdx = (dstTileIdx * bytesPerTile) / 2; for (size_t i = 0; i < cnt; ++i) { auto const srcIdx = srcPxIdx + i * 2; auto const p1 = static_cast(ts.pixels[srcIdx]); auto const p2 = static_cast(ts.pixels[srcIdx + 1]); MEM_BG_TILES[cbb][dstPxIdx + i] = static_cast(p1 | (p2 << 8)); } // update bpp of all bgs with the updated cbb auto const bpp = ctx.cbbData[cbb].bpp; teagba::iterateBgCtl([bpp, cbb](volatile BgCtl &bgCtl) { if (teagba::bgCbb(bgCtl) == cbb) { teagba::bgSetBpp(bgCtl, bpp); } }); return {}; } ox::Error loadBgTileSheet( Context &ctx, unsigned const cbb, CompactTileSheet const&ts, ox::Optional const&paletteBank) noexcept { auto const cnt = (ts.pixels.size() * PixelsPerTile) / (1 + (ts.bpp == 4)); for (size_t i = 0; i < cnt; ++i) { auto const srcIdx = i * 2; auto const p1 = static_cast(ts.pixels[srcIdx]); auto const p2 = static_cast(ts.pixels[srcIdx + 1]); MEM_BG_TILES[cbb][i] = static_cast(p1 | (p2 << 8)); } // update bpp of all bgs with the updated cbb auto const bpp = ctx.cbbData[cbb].bpp; teagba::iterateBgCtl([bpp, cbb](volatile BgCtl &bgCtl) { if (teagba::bgCbb(bgCtl) == cbb) { teagba::bgSetBpp(bgCtl, bpp); } }); if (paletteBank.has_value() && ts.defaultPalette) { OX_RETURN_ERROR(loadBgPalette(ctx, *paletteBank, ts.defaultPalette)); } return {}; } ox::Error loadBgTileSheet( Context &ctx, unsigned const cbb, TileSheetSet const&set) noexcept { auto const bpp = static_cast(set.bpp); OX_RETURN_ERROR(loadTileSheetSet(ctx, MEM_BG_TILES[cbb], set)); // update bpp of all bgs with the updated cbb ctx.cbbData[cbb].bpp = bpp; teagba::iterateBgCtl([bpp, cbb](volatile BgCtl &bgCtl) { if (teagba::bgCbb(bgCtl) == cbb) { teagba::bgSetBpp(bgCtl, bpp); } }); return {}; } static void setSpritesBpp(unsigned const bpp) noexcept { auto const eightBpp = static_cast(bpp == 8); for (auto i = 0u; i < SpriteCount; ++i) { auto &sa = teagba::spriteAttr(i); auto &a = sa.attr0; a |= static_cast((a & ~static_cast(1u << 13)) | (eightBpp << 13)); } } ox::Error loadSpriteTileSheet( Context &ctx, CompactTileSheet const&ts, bool const loadDefaultPalette) noexcept { for (size_t i = 0; i < ts.pixels.size(); i += 2) { uint16_t v = ts.pixels[i]; v |= static_cast(ts.pixels[i + 1] << 8); MEM_SPRITE_TILES[i] = v; } if (loadDefaultPalette && ts.defaultPalette) { OX_RETURN_ERROR(loadSpritePalette(ctx, ts.defaultPalette)); } setSpritesBpp(static_cast(ts.bpp)); return {}; } ox::Error loadSpriteTileSheet( Context &ctx, TileSheetSet const&set) noexcept { auto const bpp = static_cast(set.bpp); OX_RETURN_ERROR(loadTileSheetSet(ctx, {MEM_SPRITE_TILES, 32 * ox::units::KB}, set)); setSpritesBpp(bpp); return {}; } void setBgTile( Context &ctx, uint_t const bgIdx, int const column, int const row, BgTile const&tile) noexcept { auto const tileIdx = static_cast(row * tileColumns(ctx) + column); // see Tonc 9.3 MEM_BG_MAP[bgIdx][tileIdx] = static_cast(tile.tileIdx & 0b1'1111'1111) | static_cast(tile.flipX << 0xa) | static_cast(tile.flipY << 0xb) | static_cast(tile.palBank << 0xc); } void clearBg(Context &ctx, uint_t const bgIdx) noexcept { memset(MEM_BG_MAP[bgIdx].data(), 0, static_cast(tileRows(ctx) * tileColumns(ctx))); } uint8_t bgStatus(Context&) noexcept { return (REG_DISPCTL >> 8u) & 0b1111u; } void setBgStatus(Context&, uint32_t const status) noexcept { constexpr auto BgStatus = 8; REG_DISPCTL = (REG_DISPCTL & ~0b111100000000u) | status << BgStatus; } bool bgStatus(Context&, unsigned const bg) noexcept { return (REG_DISPCTL >> (8 + bg)) & 1; } void setBgStatus(Context&, unsigned const bg, bool const status) noexcept { constexpr auto Bg0Status = 8; const auto mask = static_cast(status) << (Bg0Status + bg); REG_DISPCTL = REG_DISPCTL | ((REG_DISPCTL & ~mask) | mask); } void setBgBpp(Context&, unsigned const bgIdx, unsigned const bpp) noexcept { auto &bgCtl = regBgCtl(bgIdx); teagba::bgSetBpp(bgCtl, bpp); } void setBgCbb(Context &ctx, unsigned const bgIdx, unsigned const cbbIdx) noexcept { auto &bgCtl = regBgCtl(bgIdx); const auto &cbbData = ctx.cbbData[cbbIdx]; teagba::bgSetBpp(bgCtl, cbbData.bpp); teagba::bgSetCbb(bgCtl, cbbIdx); } void setBgPriority(Context&, uint_t const bgIdx, uint_t const priority) noexcept { auto &bgCtl = regBgCtl(bgIdx); bgCtl = (bgCtl & 0b1111'1111'1111'1100u) | (priority & 0b11); } void hideSprite(Context&, unsigned const idx) noexcept { //oxAssert(g_spriteUpdates < config::GbaSpriteBufferLen, "Sprite update buffer overflow"); teagba::addSpriteUpdate({ .attr0 = uint16_t{0b11 << 8}, .idx = static_cast(idx), }); } void showSprite(Context&, unsigned const idx) noexcept { //oxAssert(g_spriteUpdates < config::GbaSpriteBufferLen, "Sprite update buffer overflow"); teagba::addSpriteUpdate({ .attr0 = 0, .idx = static_cast(idx), }); } void setSprite(Context&, uint_t const idx, Sprite const&s) noexcept { //oxAssert(g_spriteUpdates < config::GbaSpriteBufferLen, "Sprite update buffer overflow"); uint16_t const eightBpp = s.bpp == 8; teagba::addSpriteUpdate({ .attr0 = static_cast( (static_cast(s.y & ox::onMask(0b111'1111))) | (static_cast(1) << 10) // enable alpha | (static_cast(eightBpp) << 13) | (static_cast(s.spriteShape) << 14)), .attr1 = static_cast( (static_cast(s.x) & ox::onMask(8)) | (static_cast(s.flipX) << 12) | (static_cast(s.spriteSize) << 14)), .attr2 = static_cast( // double tileIdx if 8 bpp (static_cast((s.tileIdx * (1 + eightBpp)) & ox::onMask(8))) | (static_cast(s.priority & 0b11) << 10)), .idx = static_cast(idx), }); } uint_t spriteCount(Context const&) noexcept { return SpriteCount; } } namespace ox { void panic(const char *file, int line, const char *panicMsg, ox::Error const&err) noexcept { using namespace nostalgia::gfx; // reset heap to make sure we have enough memory to allocate context data OX_ALLOW_UNSAFE_BUFFERS_BEGIN const auto heapBegin = reinterpret_cast(MEM_EWRAM_BEGIN); const auto heapSz = (MEM_EWRAM_END - MEM_EWRAM_BEGIN) / 2; const auto heapEnd = reinterpret_cast(MEM_EWRAM_BEGIN + heapSz); ox::heapmgr::initHeap(heapBegin, heapEnd); OX_ALLOW_UNSAFE_BUFFERS_END auto tctx = turbine::init(keel::loadRomFs("").unwrap(), "Nostalgia").unwrap(); auto ctx = init(*tctx).unwrap(); std::ignore = initGfx(); std::ignore = initConsole(*ctx); setBgStatus(*ctx, 0, true); clearBg(*ctx, 0); auto const serr = ox::sfmt>("Error code: {}", static_cast(err)); consoleWrite(*ctx, 32 + 1, 1, "SADNESS..."); consoleWrite(*ctx, 32 + 1, 4, "UNEXPECTED STATE:"); consoleWrite(*ctx, 32 + 2, 6, panicMsg); if (err) { consoleWrite(*ctx, 32 + 2, 8, serr); } consoleWrite(*ctx, 32 + 1, 15, "PLEASE RESTART THE SYSTEM"); // print to terminal if in mGBA oxErrf("\033[31;1;1mPANIC:\033[0m [{}:{}]: {}\n", file, line, panicMsg); if (err.msg) { oxErrf("\tError Message:\t{}\n", err.msg); } oxErrf("\tError Code:\t{}\n", static_cast(err)); if (err.src.file_name() != nullptr) { oxErrf("\tError Location:\t{}:{}\n", err.src.file_name(), err.src.line()); } abort(); } } OX_ALLOW_UNSAFE_BUFFERS_END