Squashed 'deps/nostalgia/' changes from 830f8fe3..672b92b3

672b92b3 [nostalgia/gfx/studio] Remove accidental version tag in default Palette
762a6517 [nostalgia] Rename core to gfx
d141154a Merge commit '38777cfac8868b3628332090260710d5ac26aba0'
6170647c [nostalgia,studio] Proper fix for input filtering
48e45c7d [studio] Cleanup
5d3d9229 [nostalgia/core/studio/paletteeditor] Ignore keyboard input when popup is open
d54e93d8 [studio] Cleanup
7b638538 Merge commit '8e0b6ffbabb10f8a6e9ad7e9f07e0ba1d039a02e'
240effd3 Merge commit '7e20f7200963cd0b22f84cc46e10db12b6c13806'
f6f2acd6 [nostalgia/core/studio/tilesheeteditor] Add back file type check for palette drop

git-subtree-dir: deps/nostalgia
git-subtree-split: 672b92b363a2047c4c8ce93fb3d88001a76da35f
This commit is contained in:
2025-01-20 03:14:04 -06:00
parent 38777cfac8
commit 1e92e0d134
102 changed files with 244 additions and 229 deletions

View File

@@ -0,0 +1,12 @@
add_subdirectory(src)
if(NOT BUILDCORE_TARGET STREQUAL "gba")
add_subdirectory(test)
endif()
install(
DIRECTORY
include/nostalgia
DESTINATION
include
)

View File

@@ -0,0 +1,164 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/math.hpp>
#include <ox/std/types.hpp>
namespace nostalgia::gfx {
using Color16 = uint16_t;
/**
* Nostalgia Core logically uses 16 bit colors, but must translate that to 32
* bit colors in some implementations.
*/
using Color32 = uint32_t;
[[nodiscard]]
constexpr Color32 toColor32(Color16 nc) noexcept {
const auto r = static_cast<Color32>(((nc & 0b0000000000011111) >> 0) * 8);
const auto g = static_cast<Color32>(((nc & 0b0000001111100000) >> 5) * 8);
const auto b = static_cast<Color32>(((nc & 0b0111110000000000) >> 10) * 8);
const auto a = static_cast<Color32>(255);
return r | (g << 8) | (b << 16) | (a << 24);
}
[[nodiscard]]
constexpr uint8_t red16(Color16 c) noexcept {
return c & 0b0000000000011111;
}
[[nodiscard]]
constexpr uint8_t green16(Color16 c) noexcept {
return (c & 0b0000001111100000) >> 5;
}
[[nodiscard]]
constexpr uint8_t blue16(Color16 c) noexcept {
return (c & 0b0111110000000000) >> 10;
}
[[nodiscard]]
constexpr uint8_t alpha16(Color16 c) noexcept {
return static_cast<uint8_t>(c >> 15);
}
[[nodiscard]]
constexpr uint8_t red32(Color16 c) noexcept {
return red16(c) * 8;
}
[[nodiscard]]
constexpr uint8_t green32(Color16 c) noexcept {
return green16(c) * 8;
}
[[nodiscard]]
constexpr uint8_t blue32(Color16 c) noexcept {
return blue16(c) * 8;
}
[[nodiscard]]
constexpr uint8_t alpha32(Color16 c) noexcept {
return static_cast<uint8_t>((c >> 15) * 255);
}
[[nodiscard]]
constexpr uint8_t red32(Color32 c) noexcept {
return (c & 0x000000ff) >> 0;
}
[[nodiscard]]
constexpr uint8_t green32(Color32 c) noexcept {
return (c & 0x0000ff00) >> 8;
}
[[nodiscard]]
constexpr uint8_t blue32(Color32 c) noexcept {
return (c & 0x00ff0000) >> 16;
}
[[nodiscard]]
constexpr Color32 color32(uint8_t r, uint8_t g, uint8_t b) noexcept {
return static_cast<Color32>(r | (g << 8) | (b << 16));
}
[[nodiscard]]
constexpr Color32 color32(Color16 c) noexcept {
return color32(red32(c), green32(c), blue32(c));
}
[[nodiscard]]
constexpr Color32 color32(float r, float g, float b) noexcept {
return static_cast<Color32>(static_cast<uint8_t>(r * 255) | (static_cast<uint8_t>(g * 255) << 8) | (static_cast<uint8_t>(b * 255) << 16));
}
[[nodiscard]]
constexpr float redf(Color16 c) noexcept {
return static_cast<float>(red16(c)) / 31.f;
}
[[nodiscard]]
constexpr float greenf(Color16 c) noexcept {
return static_cast<float>(green16(c)) / 31.f;
}
[[nodiscard]]
constexpr float bluef(Color16 c) noexcept {
return static_cast<float>(blue16(c)) / 31.f;
}
[[nodiscard]]
constexpr float redf(Color32 c) noexcept {
return static_cast<float>(red32(c)) / 255.f;
}
[[nodiscard]]
constexpr float greenf(Color32 c) noexcept {
return static_cast<float>(green32(c)) / 255.f;
}
[[nodiscard]]
constexpr float bluef(Color32 c) noexcept {
return static_cast<float>(blue32(c)) / 255.f;
}
[[nodiscard]]
constexpr Color16 color16(int r, int g, int b, int a = 0) noexcept {
return static_cast<Color16>(ox::min<uint8_t>(static_cast<uint8_t>(r), 31))
| static_cast<Color16>(ox::min<uint8_t>(static_cast<uint8_t>(g), 31) << 5)
| static_cast<Color16>(ox::min<uint8_t>(static_cast<uint8_t>(b), 31) << 10)
| static_cast<Color16>(a << 15);
}
[[nodiscard]]
constexpr Color16 color16(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 0) noexcept {
return static_cast<Color16>(ox::min<uint8_t>(r, 31))
| static_cast<Color16>(ox::min<uint8_t>(g, 31) << 5)
| static_cast<Color16>(ox::min<uint8_t>(b, 31) << 10)
| static_cast<Color16>(a << 15);
}
static_assert(color16(0, 31, 0) == 992);
static_assert(color16(16, 31, 0) == 1008);
static_assert(color16(16, 31, 8) == 9200);
static_assert(color16(16, 32, 8) == 9200);
[[nodiscard]]
constexpr Color16 applySelectionColor(Color16 const color) noexcept {
namespace core = nostalgia::gfx;
auto const r = core::red16(color) / 2;
auto const g = (core::green16(color) + 20) / 2;
auto const b = (core::blue16(color) + 31) / 2;
return core::color16(r, g, b);
}
}

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/stringliteral.hpp>
namespace nostalgia::gfx {
constexpr auto TileWidth = 8;
constexpr auto TileHeight = 8;
constexpr auto PixelsPerTile = TileWidth * TileHeight;
constexpr ox::StringLiteral FileExt_ng("ng");
constexpr ox::StringLiteral FileExt_npal("npal");
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/fs/fs.hpp>
#include <ox/model/desctypes.hpp>
#include <ox/std/buffer.hpp>
#include <ox/std/size.hpp>
#include <turbine/context.hpp>
#include "initparams.hpp"
namespace nostalgia::gfx {
class Context;
void safeDelete(Context *ctx) noexcept;
using ContextUPtr = ox::UPtr<Context>;
ox::Result<ContextUPtr> init(turbine::Context &tctx, InitParams const&params = {}) noexcept;
keel::Context &keelCtx(Context &ctx) noexcept;
turbine::Context &turbineCtx(Context &ctx) noexcept;
}

View File

@@ -0,0 +1,14 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "color.hpp"
#include "context.hpp"
#include "gfx.hpp"
#include "initparams.hpp"
#include "keelmodule.hpp"
#include "palette.hpp"
#include "ptidxconv.hpp"
#include "tilesheet.hpp"

View File

@@ -0,0 +1,256 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/array.hpp>
#include <ox/std/size.hpp>
#include <ox/std/types.hpp>
#include <ox/model/def.hpp>
#include "context.hpp"
#include "palette.hpp"
#include "tilesheet.hpp"
namespace nostalgia::gfx {
struct Sprite {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.Sprite";
static constexpr auto TypeVersion = 1;
bool enabled = false;
int x = 0;
int y = 0;
unsigned tileIdx = 0;
unsigned spriteShape = 0;
unsigned spriteSize = 0;
unsigned flipX = 0;
unsigned bpp = 0;
/**
* Valid priorities: 0-3
*/
unsigned priority = 0;
};
OX_MODEL_BEGIN(Sprite)
OX_MODEL_FIELD(idx)
OX_MODEL_FIELD(x)
OX_MODEL_FIELD(y)
OX_MODEL_FIELD(enabled)
OX_MODEL_FIELD(tileIdx)
OX_MODEL_FIELD(spriteShape)
OX_MODEL_FIELD(spriteSize)
OX_MODEL_FIELD(flipX)
OX_MODEL_FIELD(bpp)
OX_MODEL_FIELD(priority)
OX_MODEL_END()
struct BgTile {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.BgTile";
static constexpr auto TypeVersion = 1;
unsigned tileIdx = 0;
unsigned palBank = 0;
unsigned flipX = false;
unsigned flipY = false;
};
OX_MODEL_BEGIN(BgTile)
OX_MODEL_FIELD(tileIdx)
OX_MODEL_FIELD(palBank)
OX_MODEL_FIELD(horizontalFlip)
OX_MODEL_FIELD(verticalFlip)
OX_MODEL_END()
struct TileSheetSetEntrySection {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheetSetEntrySection";
static constexpr auto TypeVersion = 1;
int32_t begin = 0;
int32_t tiles = 0;
[[nodiscard]]
constexpr auto end() const noexcept {
return begin + tiles - 1;
}
};
OX_MODEL_BEGIN(TileSheetSetEntrySection)
OX_MODEL_FIELD(begin)
OX_MODEL_FIELD(tiles)
OX_MODEL_END()
struct TileSheetSetEntry {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheetSetEntry";
static constexpr auto TypeVersion = 1;
ox::FileAddress tilesheet;
ox::Vector<TileSheetSetEntrySection> sections;
};
OX_MODEL_BEGIN(TileSheetSetEntry)
OX_MODEL_FIELD(tilesheet)
OX_MODEL_FIELD(sections)
OX_MODEL_END()
struct TileSheetSet {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheetSet";
static constexpr auto TypeVersion = 1;
static constexpr auto Preloadable = true;
int32_t bpp = 0;
ox::Vector<TileSheetSetEntry> entries;
};
OX_MODEL_BEGIN(TileSheetSet)
OX_MODEL_FIELD(bpp)
OX_MODEL_FIELD(entries)
OX_MODEL_END()
[[nodiscard]]
int tileColumns(Context&) noexcept;
[[nodiscard]]
int tileRows(Context&) noexcept;
ox::Error loadBgPalette(
Context &ctx,
size_t palBank,
CompactPalette const&palette,
size_t page = 0) noexcept;
ox::Error loadSpritePalette(
Context &ctx,
CompactPalette const&palette,
size_t page = 0) noexcept;
ox::Error loadBgPalette(
Context &ctx,
size_t palBank,
ox::StringViewCR palettePath) noexcept;
ox::Error loadBgPalette(
Context &ctx,
size_t palBank,
ox::FileAddress const&paletteAddr) noexcept;
ox::Error loadSpritePalette(
Context &ctx,
ox::StringViewCR palettePath) noexcept;
ox::Error loadSpritePalette(
Context &ctx,
ox::FileAddress const&paletteAddr) noexcept;
ox::Error loadBgTileSheet(
Context &ctx,
unsigned cbb,
TileSheetSet const&set) noexcept;
void clearCbb(Context &ctx, unsigned cbb) noexcept;
void clearCbbs(Context &ctx) noexcept;
ox::Error loadBgTileSheet(
Context &ctx,
unsigned cbb,
CompactTileSheet const&ts,
size_t dstTileIdx,
size_t srcTileIdx,
size_t tileCnt) noexcept;
ox::Error loadBgTileSheet(
Context &ctx,
unsigned cbb,
ox::StringViewCR tsPath,
size_t dstTileIdx,
size_t srcTileIdx,
size_t tileCnt) noexcept;
ox::Error loadBgTileSheet(
Context &ctx,
unsigned cbb,
ox::FileAddress const&tsAddr,
size_t dstTileIdx,
size_t srcTileIdx,
size_t tileCnt) noexcept;
ox::Error loadBgTileSheet(
Context &ctx,
unsigned cbb,
CompactTileSheet const&ts,
ox::Optional<unsigned> const&paletteBank = {}) noexcept;
ox::Error loadBgTileSheet(
Context &ctx,
unsigned cbb,
ox::StringViewCR tilesheetPath,
ox::Optional<unsigned> const&paletteBank) noexcept;
ox::Error loadBgTileSheet(
Context &ctx,
unsigned cbb,
ox::FileAddress const&tilesheetAddr,
ox::Optional<unsigned> const&paletteBank = {}) noexcept;
ox::Error loadSpriteTileSheet(
Context &ctx,
CompactTileSheet const&ts,
bool loadDefaultPalette) noexcept;
ox::Error loadSpriteTileSheet(
Context &ctx,
ox::StringViewCR tilesheetPath,
bool loadDefaultPalette = false) noexcept;
ox::Error loadSpriteTileSheet(
Context &ctx,
ox::FileAddress const&tilesheetAddr,
bool loadDefaultPalette = false) noexcept;
ox::Error loadSpriteTileSheet(
Context &ctx,
TileSheetSet const&set) noexcept;
void setBgTile(Context &ctx, uint_t bgIdx, int column, int row, unsigned tile, unsigned palBank = 0) noexcept;
void setBgTile(Context &ctx, uint_t bgIdx, int column, int row, BgTile const&tile) noexcept;
void clearBg(Context &ctx, uint_t bgIdx) noexcept;
[[nodiscard]]
uint8_t bgStatus(Context &ctx) noexcept;
void setBgStatus(Context &ctx, uint32_t status) noexcept;
[[nodiscard]]
bool bgStatus(Context &ctx, unsigned bg) noexcept;
void setBgStatus(Context &ctx, unsigned bg, bool status) noexcept;
void setBgCbb(Context &ctx, unsigned bgIdx, unsigned cbb) noexcept;
void setBgPriority(Context &ctx, uint_t bgIdx, uint_t priority) noexcept;
void hideSprite(Context &ctx, unsigned) noexcept;
void showSprite(Context &ctx, unsigned) noexcept;
void setSprite(Context &c, uint_t idx, Sprite const&s) noexcept;
[[nodiscard]]
uint_t spriteCount(Context &ctx) noexcept;
ox::Error initConsole(Context &ctx) noexcept;
void puts(Context &ctx, int column, int row, ox::StringViewCR str) noexcept;
}
namespace nostalgia::gfx::gl {
constexpr ox::CStringView GlslVersion = "#version 330";
[[nodiscard]]
ox::Size drawSize(int scale = 5) noexcept;
void draw(gfx::Context &ctx, ox::Size const&renderSz) noexcept;
void draw(gfx::Context&, int scale = 5) noexcept;
}

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/types.hpp>
namespace nostalgia::gfx {
struct InitParams {
bool glInstallDrawer = true;
uint_t glSpriteCount = 128;
uint_t glBlocksPerSprite = 64;
};
}

View File

@@ -0,0 +1,13 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <keel/module.hpp>
namespace nostalgia::gfx {
const keel::Module *keelModule() noexcept;
}

View File

@@ -0,0 +1,289 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/vector.hpp>
#include <ox/model/def.hpp>
#include <turbine/turbine.hpp>
#include "color.hpp"
namespace nostalgia::gfx {
struct PaletteColorV1 {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.PaletteColor";
static constexpr auto TypeVersion = 1;
uint8_t r{}, g{}, b{}, a{};
constexpr PaletteColorV1() noexcept = default;
constexpr PaletteColorV1(Color16 const c) noexcept:
r{red16(c)},
g{green16(c)},
b{blue16(c)},
a{alpha16(c)} {}
constexpr operator Color16() const noexcept { return color16(r, g, b, a); }
};
OX_MODEL_BEGIN(PaletteColorV1)
OX_MODEL_FIELD(r)
OX_MODEL_FIELD(g)
OX_MODEL_FIELD(b)
OX_MODEL_FIELD(a)
OX_MODEL_END()
using PaletteColor = PaletteColorV1;
struct PalettePageV1 {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.Palette.PalettePage";
static constexpr auto TypeVersion = 1;
ox::String name;
ox::Vector<PaletteColorV1> colors;
constexpr PalettePageV1() noexcept = default;
constexpr PalettePageV1(ox::StringParam pName, ox::Vector<PaletteColorV1> pColors) noexcept:
name(std::move(pName)), colors(std::move(pColors)) {}
constexpr PalettePageV1(ox::StringParam pName, ox::Vector<Color16> const&pColors) noexcept:
name(std::move(pName)) {
colors.reserve(pColors.size());
for (auto const c : pColors) {
colors.emplace_back(c);
}
}
};
OX_MODEL_BEGIN(PalettePageV1)
OX_MODEL_FIELD(name)
OX_MODEL_FIELD(colors)
OX_MODEL_END()
using PalettePage = PalettePageV1;
struct NostalgiaPalette {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.NostalgiaPalette";
static constexpr auto TypeVersion = 1;
ox::Vector<Color16> colors = {};
};
OX_MODEL_BEGIN(NostalgiaPalette)
OX_MODEL_FIELD(colors)
OX_MODEL_END()
struct PaletteV1 {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.Palette";
static constexpr auto TypeVersion = 1;
ox::Vector<Color16> colors;
};
OX_MODEL_BEGIN(PaletteV1)
OX_MODEL_FIELD(colors)
OX_MODEL_END()
struct PaletteV2 {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.Palette";
static constexpr auto TypeVersion = 2;
static constexpr auto Preloadable = true;
ox::Vector<ox::Vector<Color16>> pages;
};
OX_MODEL_BEGIN(PaletteV2)
OX_MODEL_FIELD(pages)
OX_MODEL_END()
struct PaletteV3 {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.Palette";
static constexpr auto TypeVersion = 3;
static constexpr auto Preloadable = true;
struct ColorInfo {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.Palette.ColorInfo";
static constexpr auto TypeVersion = 3;
ox::String name;
constexpr ColorInfo() noexcept = default;
constexpr ColorInfo(ox::StringParam pName) noexcept: name{std::move(pName)} {}
};
ox::Vector<ColorInfo> colorInfo;
ox::Vector<ox::Vector<Color16>> pages;
};
OX_MODEL_BEGIN(PaletteV3::ColorInfo)
OX_MODEL_FIELD(name)
OX_MODEL_END()
OX_MODEL_BEGIN(PaletteV3)
OX_MODEL_FIELD(colorInfo)
OX_MODEL_FIELD(pages)
OX_MODEL_END()
[[nodiscard]]
constexpr bool valid(PaletteV3 const&p) noexcept {
auto const colors = p.colorInfo.size();
return ox::all_of(p.pages.begin(), p.pages.end(), [colors](auto const&page) {
return page.size() == colors;
});
}
constexpr ox::Error repair(PaletteV3 &p) noexcept {
auto const colors = p.colorInfo.size();
for (auto &page : p.pages) {
page.resize(colors);
}
return {};
}
struct PaletteV4 {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.Palette";
static constexpr auto TypeVersion = 4;
static constexpr auto Preloadable = true;
ox::Vector<ox::String> colorNames;
ox::Vector<PalettePageV1> pages;
};
OX_MODEL_BEGIN(PaletteV4)
OX_MODEL_FIELD(colorNames)
OX_MODEL_FIELD(pages)
OX_MODEL_END()
[[nodiscard]]
constexpr bool valid(PaletteV4 const&p) noexcept {
auto const colors = p.colorNames.size();
return ox::all_of(p.pages.begin(), p.pages.end(), [colors](PalettePageV1 const&page) {
return page.colors.size() == colors;
});
}
constexpr ox::Error repair(PaletteV4 &p) noexcept {
auto const colors = p.colorNames.size();
for (auto &page : p.pages) {
page.colors.resize(colors);
}
return {};
}
using Palette = PaletteV4;
struct CompactPaletteV1 {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.CompactPalette";
static constexpr auto TypeVersion = 1;
static constexpr auto Preloadable = true;
ox::Vector<ox::Vector<Color16>> pages{};
};
OX_MODEL_BEGIN(CompactPaletteV1)
OX_MODEL_FIELD(pages)
OX_MODEL_END()
[[nodiscard]]
constexpr bool valid(CompactPaletteV1 const&p) noexcept {
size_t colors{};
for (auto const&page : p.pages) {
colors = ox::max(colors, page.size());
}
return ox::all_of(p.pages.begin(), p.pages.end(), [colors](ox::Vector<Color16> const&page) {
return page.size() == colors;
});
}
constexpr ox::Error repair(CompactPaletteV1 &p) noexcept {
size_t colors{};
for (auto const&page : p.pages) {
colors = ox::max(colors, page.size());
}
for (auto &page : p.pages) {
page.resize(colors);
}
return {};
}
using CompactPalette = CompactPaletteV1;
[[nodiscard]]
constexpr Color16 color(Palette const&pal, size_t page, size_t idx) noexcept {
if (page < pal.pages.size() && idx < pal.pages[page].colors.size()) [[likely]] {
return pal.pages[page].colors[idx];
}
return 0;
}
[[nodiscard]]
constexpr Color16 color(CompactPalette const&pal, size_t page, size_t idx) noexcept {
if (page < pal.pages.size() && idx < pal.pages[page].size()) [[likely]] {
return pal.pages[page][idx];
}
return 0;
}
[[nodiscard]]
constexpr Color16 color(Palette const&pal, size_t idx) noexcept {
return color(pal, 0, idx);
}
[[nodiscard]]
constexpr Color16 color(CompactPalette const&pal, size_t idx) noexcept {
return color(pal, 0, idx);
}
[[nodiscard]]
constexpr auto &colors(Palette &pal, size_t page = 0) noexcept {
return pal.pages[page].colors;
}
[[nodiscard]]
constexpr auto &colors(CompactPalette &pal, size_t page = 0) noexcept {
return pal.pages[page];
}
[[nodiscard]]
constexpr auto &colors(Palette const&pal, size_t page = 0) noexcept {
return pal.pages[page];
}
[[nodiscard]]
constexpr auto &colors(CompactPalette const&pal, size_t page = 0) noexcept {
return pal.pages[page];
}
[[nodiscard]]
constexpr size_t colorCnt(Palette const&pal, size_t page = 0) noexcept {
if (page < pal.pages.size()) [[likely]] {
return pal.pages[page].colors.size();
}
return 0;
}
[[nodiscard]]
constexpr size_t colorCnt(CompactPalette const&pal, size_t page = 0) noexcept {
if (page < pal.pages.size()) [[likely]] {
return pal.pages[page].size();
}
return 0;
}
[[nodiscard]]
constexpr size_t largestPage(Palette const&pal) noexcept {
size_t out{};
for (auto const&page : pal.pages) {
out = ox::max(out, page.colors.size());
}
return out;
}
[[nodiscard]]
constexpr size_t largestPage(CompactPalette const&pal) noexcept {
size_t out{};
for (auto const&page : pal.pages) {
out = ox::max(out, page.size());
}
return out;
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/point.hpp>
#include "consts.hpp"
namespace nostalgia::gfx {
[[nodiscard]]
constexpr std::size_t ptToIdx(int x, int y, int c, int scale = 1) noexcept {
const auto tileWidth = TileWidth * scale;
const auto tileHeight = TileHeight * scale;
const auto pixelsPerTile = tileWidth * tileHeight;
const auto colLength = static_cast<std::size_t>(pixelsPerTile);
const auto rowLength = static_cast<std::size_t>(static_cast<std::size_t>(c / tileWidth) * colLength);
const auto colStart = static_cast<std::size_t>(colLength * static_cast<std::size_t>(x / tileWidth));
const auto rowStart = static_cast<std::size_t>(rowLength * static_cast<std::size_t>(y / tileHeight));
const auto colOffset = static_cast<std::size_t>(x % tileWidth);
const auto rowOffset = static_cast<std::size_t>((y % tileHeight) * tileHeight);
return static_cast<std::size_t>(colStart + colOffset + rowStart + rowOffset);
}
[[nodiscard]]
constexpr std::size_t ptToIdx(const ox::Point &pt, int c, int scale = 1) noexcept {
return ptToIdx(pt.x, pt.y, c * TileWidth, scale);
}
[[nodiscard]]
constexpr ox::Point idxToPt(int i, int c, int scale = 1) noexcept {
const auto tileWidth = TileWidth * scale;
const auto tileHeight = TileHeight * scale;
const auto pixelsPerTile = tileWidth * tileHeight;
// prevent divide by zeros
if (!c) {
++c;
}
const auto t = i / pixelsPerTile; // tile number
const auto iti = i % pixelsPerTile; // in tile index
const auto tc = t % c; // tile column
const auto tr = t / c; // tile row
const auto itx = iti % tileWidth; // in tile x
const auto ity = iti / tileHeight; // in tile y
return {
itx + tc * tileWidth,
ity + tr * tileHeight,
};
}
static_assert(idxToPt(4, 1) == ox::Point{4, 0});
static_assert(idxToPt(8, 1) == ox::Point{0, 1});
static_assert(idxToPt(8, 2) == ox::Point{0, 1});
static_assert(idxToPt(64, 2) == ox::Point{8, 0});
static_assert(idxToPt(128, 2) == ox::Point{0, 8});
static_assert(idxToPt(129, 2) == ox::Point{1, 8});
static_assert(idxToPt(192, 2) == ox::Point{8, 8});
static_assert(idxToPt(384, 8) == ox::Point{48, 0});
}

View File

@@ -0,0 +1,11 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/studio.hpp>
namespace nostalgia::core {
}

View File

@@ -0,0 +1,13 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/studio.hpp>
namespace nostalgia::gfx {
const studio::Module *studioModule() noexcept;
}

View File

@@ -0,0 +1,536 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/fs/fs.hpp>
#include <ox/std/array.hpp>
#include <ox/std/point.hpp>
#include <ox/std/size.hpp>
#include <ox/std/span.hpp>
#include <ox/std/types.hpp>
#include <ox/model/def.hpp>
#include <nostalgia/gfx/ptidxconv.hpp>
#include "palette.hpp"
namespace nostalgia::gfx {
struct SubSheetTemplate {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.gfx.SubSheetTemplate";
static constexpr auto TypeVersion = 1;
ox::String name;
int32_t width{};
int32_t height{};
ox::Vector<SubSheetTemplate> subsheets;
};
OX_MODEL_BEGIN(SubSheetTemplate)
OX_MODEL_FIELD(name)
OX_MODEL_FIELD(width)
OX_MODEL_FIELD(height)
OX_MODEL_FIELD(subsheets)
OX_MODEL_END()
// Predecessor to TileSheet, kept for backward compatibility
struct TileSheetV1 {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.NostalgiaGraphic";
static constexpr auto TypeVersion = 1;
int8_t bpp = 0;
// rows and columns are really only used by TileSheetEditor
int rows = 1;
int columns = 1;
ox::FileAddress defaultPalette;
PaletteV1 pal;
ox::Vector<uint8_t> pixels = {};
};
[[nodiscard]]
constexpr bool valid(TileSheetV1 const&ts) noexcept {
auto const bytes = static_cast<size_t>(ts.columns * ts.rows * PixelsPerTile) / (ts.bpp == 4 ? 2 : 1);
return (ts.bpp == 4 || ts.bpp == 8) && ts.pixels.size() == bytes;
}
constexpr ox::Error repair(TileSheetV1 &ts, int bpp) noexcept {
auto const bytes = static_cast<size_t>(ts.columns * ts.rows * PixelsPerTile) / (bpp == 4 ? 2 : 1);
ts.pixels.resize(bytes);
return {};
}
struct TileSheetV2 {
using SubSheetIdx = ox::Vector<std::size_t, 4>;
struct SubSheet {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheet.SubSheet";
static constexpr auto TypeVersion = 1;
ox::String name;
int columns = 0;
int rows = 0;
ox::Vector<SubSheet> subsheets;
ox::Vector<uint8_t> pixels;
constexpr SubSheet() noexcept = default;
constexpr SubSheet(ox::StringParam pName, int pColumns, int pRows, int bpp) noexcept:
name(std::move(pName)),
columns(pColumns),
rows(pRows),
pixels(static_cast<size_t>(columns * rows * PixelsPerTile) / (bpp == 4 ? 2u : 1u)) {
}
};
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheet";
static constexpr auto TypeVersion = 2;
int8_t bpp = 4;
ox::FileAddress defaultPalette;
SubSheet subsheet{"Root", 1, 1, bpp};
};
[[nodiscard]]
constexpr bool valid(TileSheetV2::SubSheet const&ss, int bpp) noexcept {
auto const bytes = static_cast<size_t>(ss.columns * ss.rows * PixelsPerTile) / (bpp == 4 ? 2 : 1);
return ox::all_of(ss.subsheets.begin(), ss.subsheets.end(),
[bpp, bytes](TileSheetV2::SubSheet const&s) {
return bytes == s.pixels.size() && valid(s, bpp);
});
}
[[nodiscard]]
constexpr bool valid(TileSheetV2 const&ts) noexcept {
return (ts.bpp == 4 || ts.bpp == 8) && valid(ts.subsheet, ts.bpp);
}
constexpr void repair(TileSheetV2::SubSheet &ss, int bpp) noexcept {
auto const bytes = static_cast<size_t>(ss.columns * ss.rows * PixelsPerTile) / (bpp == 4 ? 2 : 1);
ss.pixels.resize(bytes);
for (auto &s : ss.subsheets) {
repair(s, bpp);
}
}
constexpr ox::Error repair(TileSheetV2 &ts) noexcept {
if (ts.bpp != 4 && ts.bpp != 8) {
return ox::Error(1, "Unable to repair TileSheet");
}
repair(ts.subsheet, ts.bpp);
return {};
}
using SubSheetId = int32_t;
struct TileSheetV3 {
using SubSheetIdx = ox::Vector<std::size_t, 4>;
struct SubSheet {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheet.SubSheet";
static constexpr auto TypeVersion = 3;
SubSheetId id = 0;
ox::String name;
int columns = 0;
int rows = 0;
ox::Vector<SubSheet> subsheets;
ox::Vector<uint8_t> pixels;
constexpr SubSheet() noexcept = default;
SubSheet(
SubSheetId pId,
ox::StringParam pName,
int pColumns,
int pRows,
int bpp) noexcept:
id(pId),
name(std::move(pName)),
columns(pColumns),
rows(pRows),
pixels(static_cast<std::size_t>(columns * rows * PixelsPerTile) / (bpp == 4 ? 2u : 1u)) {
}
};
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheet";
static constexpr auto TypeVersion = 3;
int8_t bpp = 4;
SubSheetId idIt = 0;
ox::FileAddress defaultPalette;
SubSheet subsheet{0, "Root", 1, 1, bpp};
};
[[nodiscard]]
constexpr bool valid(TileSheetV3::SubSheet const&ss, int bpp) noexcept {
auto const bytes = static_cast<size_t>(ss.columns * ss.rows * PixelsPerTile) / (bpp == 4 ? 2 : 1);
return ox::all_of(ss.subsheets.begin(), ss.subsheets.end(),
[bpp, bytes](TileSheetV3::SubSheet const&s) {
return bytes == s.pixels.size() && valid(s, bpp);
});
}
[[nodiscard]]
constexpr bool valid(TileSheetV3 const&ts) noexcept {
return (ts.bpp == 4 || ts.bpp == 8) && valid(ts.subsheet, ts.bpp);
}
constexpr void repair(TileSheetV3::SubSheet &ss, int bpp) noexcept {
auto const bytes = static_cast<size_t>(ss.columns * ss.rows * PixelsPerTile) / (bpp == 4 ? 2 : 1);
ss.pixels.resize(bytes);
for (auto &s : ss.subsheets) {
repair(s, bpp);
}
}
constexpr ox::Error repair(TileSheetV3 &ts) noexcept {
if (ts.bpp != 4 && ts.bpp != 8) {
return ox::Error(1, "Unable to repair TileSheet");
}
repair(ts.subsheet, ts.bpp);
return {};
}
struct TileSheetV4 {
using SubSheetIdx = ox::Vector<std::size_t, 4>;
struct SubSheet {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheet.SubSheet";
static constexpr auto TypeVersion = 4;
SubSheetId id = 0;
ox::String name;
int columns = 0;
int rows = 0;
ox::Vector<SubSheet> subsheets;
ox::Vector<uint8_t> pixels;
constexpr SubSheet() noexcept = default;
SubSheet(
SubSheetId pId,
ox::StringParam pName,
int pColumns,
int pRows,
int bpp) noexcept:
id(pId),
name(std::move(pName)),
columns(pColumns),
rows(pRows),
pixels(static_cast<std::size_t>(columns * rows * PixelsPerTile) / (bpp == 4 ? 2u : 1u)) {
}
SubSheet(
SubSheetId pId,
ox::StringParam pName,
int pColumns,
int pRows,
ox::Vector<uint8_t> pPixels) noexcept:
id(pId),
name(std::move(pName)),
columns(pColumns),
rows(pRows),
pixels(std::move(pPixels)) {
}
/**
*
* @return the dimensional size of the SubSheet (e.g. width * height)
*/
[[nodiscard]]
constexpr std::size_t size() const noexcept {
return static_cast<std::size_t>(columns) * static_cast<std::size_t>(rows);
}
};
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheet";
static constexpr auto TypeVersion = 4;
int8_t bpp = 4;
SubSheetId idIt = 0;
ox::FileAddress defaultPalette;
SubSheet subsheet{0, "Root", 1, 1, bpp};
constexpr TileSheetV4() noexcept = default;
};
[[nodiscard]]
constexpr bool valid(TileSheetV4::SubSheet const&ss, int bpp) noexcept {
auto const bytes = static_cast<size_t>(ss.columns * ss.rows * PixelsPerTile) / (bpp == 4 ? 2 : 1);
return
(ss.pixels.empty() || ss.subsheets.empty()) &&
ox::all_of(ss.subsheets.begin(), ss.subsheets.end(),
[bpp, bytes](TileSheetV4::SubSheet const&s) {
return bytes == s.pixels.size() && valid(s, bpp);
});
}
[[nodiscard]]
constexpr bool valid(TileSheetV4 const&ts) noexcept {
return (ts.bpp == 4 || ts.bpp == 8) && valid(ts.subsheet, ts.bpp);
}
constexpr void repair(TileSheetV4::SubSheet &ss, int bpp) noexcept {
if (ss.subsheets.empty()) {
auto const bytes = static_cast<size_t>(ss.columns * ss.rows * PixelsPerTile) / (bpp == 4 ? 2 : 1);
ss.pixels.resize(bytes);
} else {
ss.pixels.clear();
ss.columns = -1;
ss.rows = -1;
}
for (auto &s : ss.subsheets) {
repair(s, bpp);
}
}
constexpr ox::Error repair(TileSheetV4 &ts) noexcept {
if (ts.bpp != 4 && ts.bpp != 8) {
return ox::Error(1, "Unable to repair TileSheet");
}
repair(ts.subsheet, ts.bpp);
return {};
}
using TileSheet = TileSheetV4;
[[nodiscard]]
std::size_t idx(TileSheet::SubSheet const&ss, ox::Point const&pt) noexcept;
[[nodiscard]]
size_t getTileCnt(TileSheet const&ts) noexcept;
[[nodiscard]]
TileSheet::SubSheet const*getSubsheet(TileSheet const&ts, SubSheetId id) noexcept;
[[nodiscard]]
ox::Optional<size_t> getTileIdx(TileSheet const&ts, SubSheetId id) noexcept;
[[nodiscard]]
uint8_t getPixel4Bpp(TileSheet::SubSheet const&ss, std::size_t idx) noexcept;
[[nodiscard]]
uint8_t getPixel8Bpp(TileSheet::SubSheet const&ss, std::size_t idx) noexcept;
[[nodiscard]]
uint8_t getPixel(TileSheet::SubSheet const&ss, int8_t pBpp, std::size_t idx) noexcept;
[[nodiscard]]
uint8_t getPixel4Bpp(TileSheet::SubSheet const&ss, ox::Point const&pt) noexcept;
[[nodiscard]]
uint8_t getPixel8Bpp(TileSheet::SubSheet const&ss, ox::Point const&pt) noexcept;
[[nodiscard]]
uint8_t getPixel(TileSheet::SubSheet const&ss, int8_t pBpp, ox::Point const&pt) noexcept;
constexpr void walkPixels(TileSheet::SubSheet const&ss, int8_t pBpp, auto callback) noexcept {
if (pBpp == 4) {
const auto pixelCnt = ox::min<std::size_t>(
static_cast<std::size_t>(ss.columns * ss.rows * PixelsPerTile) / 2,
ss.pixels.size());
//oxAssert(pixels.size() == pixelCnt, "Pixel count does not match rows and columns");
for (std::size_t i = 0; i < pixelCnt; ++i) {
const auto colorIdx1 = static_cast<uint8_t>(ss.pixels[i] & 0xF);
const auto colorIdx2 = static_cast<uint8_t>(ss.pixels[i] >> 4);
callback(i * 2 + 0, colorIdx1);
callback(i * 2 + 1, colorIdx2);
}
} else {
const auto pixelCnt = ox::min<std::size_t>(
static_cast<std::size_t>(ss.columns * ss.rows * PixelsPerTile),
ss.pixels.size());
for (std::size_t i = 0; i < pixelCnt; ++i) {
const auto p = ss.pixels[i];
callback(i, p);
}
}
}
void setPixel(TileSheet::SubSheet &ss, int8_t pBpp, uint64_t idx, uint8_t palIdx) noexcept;
void setPixel(TileSheet::SubSheet &ss, int8_t pBpp, ox::Point const&pt, uint8_t palIdx) noexcept;
ox::Error setPixelCount(TileSheet::SubSheet &ss, int8_t pBpp, std::size_t cnt) noexcept;
/**
* Gets a count of the pixels in this sheet, and not that of its children.
* @param pBpp bits per pixel, need for knowing how to count the pixels
* @return a count of the pixels in this sheet
*/
[[nodiscard]]
unsigned pixelCnt(TileSheet::SubSheet const&ss, int8_t pBpp) noexcept;
/**
*
* @param ss
* @param pBpp
* @param sz size of Subsheet in tiles (not pixels)
*/
ox::Error resizeSubsheet(TileSheet::SubSheet &ss, int8_t pBpp, ox::Size const&sz) noexcept;
/**
* validateSubSheetIdx takes a SubSheetIdx and moves the index to the
* preceding or parent sheet if the current corresponding sheet does
* not exist.
* @param idx SubSheetIdx to validate and correct
* @return a valid version of idx
*/
[[nodiscard]]
TileSheet::SubSheetIdx validateSubSheetIdx(TileSheet const&ts, TileSheet::SubSheetIdx idx) noexcept;
[[nodiscard]]
TileSheet::SubSheet const&getSubSheet(
TileSheet::SubSheetIdx const&idx,
std::size_t idxIt,
TileSheet::SubSheet const&pSubsheet) noexcept;
[[nodiscard]]
TileSheet::SubSheet &getSubSheet(
TileSheet::SubSheetIdx const&idx,
std::size_t idxIt,
TileSheet::SubSheet &pSubsheet) noexcept;
[[nodiscard]]
TileSheet::SubSheet const&getSubSheet(TileSheet const&ts, TileSheet::SubSheetIdx const&idx) noexcept;
[[nodiscard]]
TileSheet::SubSheet &getSubSheet(TileSheet &ts, TileSheet::SubSheetIdx const&idx) noexcept;
ox::Error addSubSheet(TileSheet &ts, TileSheet::SubSheetIdx const&idx) noexcept;
ox::Error rmSubSheet(
TileSheet &ts,
TileSheet::SubSheetIdx const&idx,
std::size_t idxIt,
TileSheet::SubSheet &pSubsheet) noexcept;
ox::Error rmSubSheet(TileSheet &ts, TileSheet::SubSheetIdx const&idx) noexcept;
[[nodiscard]]
uint8_t getPixel4Bpp(
TileSheet const&ts,
ox::Point const&pt,
TileSheet::SubSheetIdx const&subsheetIdx) noexcept;
[[nodiscard]]
uint8_t getPixel8Bpp(
TileSheet const&ts,
ox::Point const&pt,
TileSheet::SubSheetIdx const&subsheetIdx) noexcept;
ox::Result<SubSheetId> getIdFor(TileSheet const&ts, ox::StringViewCR path) noexcept;
ox::Result<unsigned> getTileOffset(TileSheet const&ts, ox::StringViewCR pNamePath) noexcept;
ox::Result<uint32_t> getTileOffset(TileSheet const&ts, SubSheetId pId) noexcept;
ox::Result<ox::StringView> getNameFor(TileSheet::SubSheet const&ss, SubSheetId pId) noexcept;
ox::Result<ox::StringView> getNameFor(TileSheet const&ts, SubSheetId pId) noexcept;
[[nodiscard]]
ox::Vector<uint8_t> pixels(TileSheet &ts) noexcept;
struct CompactTileSheetV1 {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.CompactTileSheet";
static constexpr auto TypeVersion = 1;
static constexpr auto Preloadable = true;
int8_t bpp = 0;
ox::FileAddress defaultPalette;
ox::Vector<uint8_t> pixels;
};
[[nodiscard]]
constexpr bool valid(CompactTileSheetV1 const&ts) noexcept {
return ts.bpp == 4 || ts.bpp == 8;
}
using CompactTileSheet = CompactTileSheetV1;
[[nodiscard]]
uint8_t getPixel4Bpp(
CompactTileSheet const&ts,
size_t idx) noexcept;
[[nodiscard]]
uint8_t getPixel8Bpp(
CompactTileSheet const&ts,
size_t idx) noexcept;
[[nodiscard]]
ox::Pair<uint8_t> get2Pixels4Bpp(
CompactTileSheet const&ts,
size_t idx) noexcept;
[[nodiscard]]
ox::Pair<uint8_t> get2Pixels8Bpp(
CompactTileSheet const&ts,
size_t idx) noexcept;
OX_MODEL_BEGIN(TileSheetV1)
OX_MODEL_FIELD(bpp)
OX_MODEL_FIELD(rows)
OX_MODEL_FIELD(columns)
OX_MODEL_FIELD(defaultPalette)
OX_MODEL_FIELD(pal)
OX_MODEL_FIELD(pixels)
OX_MODEL_END()
OX_MODEL_BEGIN(TileSheetV2::SubSheet)
OX_MODEL_FIELD(name)
OX_MODEL_FIELD(rows)
OX_MODEL_FIELD(columns)
OX_MODEL_FIELD(subsheets)
OX_MODEL_FIELD(pixels)
OX_MODEL_END()
OX_MODEL_BEGIN(TileSheetV2)
OX_MODEL_FIELD(bpp)
OX_MODEL_FIELD(defaultPalette)
OX_MODEL_FIELD(subsheet)
OX_MODEL_END()
OX_MODEL_BEGIN(TileSheetV3::SubSheet)
OX_MODEL_FIELD(name)
OX_MODEL_FIELD(rows)
OX_MODEL_FIELD(columns)
OX_MODEL_FIELD(subsheets)
OX_MODEL_FIELD(pixels)
OX_MODEL_END()
OX_MODEL_BEGIN(TileSheetV3)
OX_MODEL_FIELD(bpp)
OX_MODEL_FIELD(idIt)
OX_MODEL_FIELD(defaultPalette)
OX_MODEL_FIELD(subsheet)
OX_MODEL_END()
OX_MODEL_BEGIN(TileSheetV4::SubSheet)
OX_MODEL_FIELD(id)
OX_MODEL_FIELD(name)
OX_MODEL_FIELD(rows)
OX_MODEL_FIELD(columns)
OX_MODEL_FIELD(subsheets)
OX_MODEL_FIELD(pixels)
OX_MODEL_END()
OX_MODEL_BEGIN(TileSheetV4)
OX_MODEL_FIELD(bpp)
OX_MODEL_FIELD(idIt)
OX_MODEL_FIELD(defaultPalette)
OX_MODEL_FIELD(subsheet)
OX_MODEL_END()
OX_MODEL_BEGIN(CompactTileSheetV1)
OX_MODEL_FIELD(bpp)
OX_MODEL_FIELD(defaultPalette)
OX_MODEL_FIELD(pixels)
OX_MODEL_END()
ox::Vector<uint32_t> resizeTileSheetData(
ox::Vector<uint32_t> const&srcPixels,
ox::Size const&srcSize,
int scale = 2) noexcept;
}

View File

@@ -0,0 +1,33 @@
add_library(
NostalgiaCore
gfx.cpp
tilesheet.cpp
)
add_subdirectory(gba)
if(NOT BUILDCORE_TARGET STREQUAL "gba")
add_subdirectory(opengl)
endif()
target_include_directories(
NostalgiaCore PUBLIC
../include
)
target_link_libraries(
NostalgiaCore PUBLIC
Turbine
)
add_subdirectory(keel)
if(NOSTALGIA_BUILD_STUDIO)
add_subdirectory(studio)
endif()
install(
TARGETS
NostalgiaCore
DESTINATION
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)

View File

@@ -0,0 +1,21 @@
add_library(
NostalgiaCore-GBA OBJECT
context.cpp
gfx.cpp
panic.cpp
)
target_include_directories(
NostalgiaCore-GBA PUBLIC
../../include
)
target_link_libraries(
NostalgiaCore-GBA PUBLIC
TeaGBA
Keel
Turbine
)
if(BUILDCORE_TARGET STREQUAL "gba")
set_source_files_properties(gfx.cpp PROPERTIES COMPILE_FLAGS -marm)
target_link_libraries(NostalgiaCore PUBLIC NostalgiaCore-GBA)
endif()

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <turbine/turbine.hpp>
#include <nostalgia/gfx/gfx.hpp>
#include "context.hpp"
namespace nostalgia::gfx {
void safeDelete(Context *ctx) noexcept {
delete ctx;
}
Context::Context(turbine::Context &tctx) noexcept: turbineCtx(tctx) {
}
ox::Error initGfx(Context &ctx, InitParams const&) noexcept;
ox::Result<ContextUPtr> init(turbine::Context &tctx, InitParams const&params) noexcept {
auto ctx = ox::make_unique<Context>(tctx);
OX_RETURN_ERROR(initGfx(*ctx, params));
return ContextUPtr(std::move(ctx));
}
keel::Context &keelCtx(Context &ctx) noexcept {
return turbine::keelCtx(ctx.turbineCtx);
}
turbine::Context &turbineCtx(Context &ctx) noexcept {
return ctx.turbineCtx;
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <nostalgia/gfx/context.hpp>
namespace nostalgia::gfx {
struct BgCbbData {
unsigned bpp = 4;
};
class Context {
public:
turbine::Context &turbineCtx;
ox::Array<BgCbbData, 4> cbbData;
explicit Context(turbine::Context &tctx) noexcept;
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<ox::MemFS const&>(*turbine::rom(turbineCtx));
}
};
}

View File

@@ -0,0 +1,302 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/fs/fs.hpp>
#include <ox/std/array.hpp>
#include <teagba/addresses.hpp>
#include <teagba/gfx.hpp>
#include <teagba/registers.hpp>
#include <keel/keel.hpp>
#include <nostalgia/gfx/context.hpp>
#include <nostalgia/gfx/gfx.hpp>
#include <nostalgia/gfx/tilesheet.hpp>
#include "context.hpp"
OX_ALLOW_UNSAFE_BUFFERS_BEGIN
namespace nostalgia::gfx {
static constexpr auto SpriteCount = 128;
ox::Error initGfx(Context&, InitParams const&) noexcept {
for (auto bgCtl = &REG_BG0CTL; bgCtl <= &REG_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::Error loadBgPalette(
Context&,
size_t palBank,
CompactPalette const&palette,
size_t 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 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<uint16_t> 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<CompactTileSheet>(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<size_t>(s.tiles) * PixelsPerTile) >> bppMod;
for (size_t i = 0; i < cnt; ++i) {
auto const begin = static_cast<size_t>(s.begin) * (PixelsPerTile >> bppMod);
auto const srcIdx = begin + i * 2;
auto const v = static_cast<uint16_t>(
static_cast<uint16_t>(ts->pixels[srcIdx]) |
(static_cast<uint16_t>(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<uint16_t>(ts.pixels[srcIdx]);
auto const p2 = static_cast<uint16_t>(ts.pixels[srcIdx + 1]);
MEM_BG_TILES[cbb][dstPxIdx + i] = static_cast<uint16_t>(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 cbb,
CompactTileSheet const&ts,
ox::Optional<unsigned> 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<uint16_t>(ts.pixels[srcIdx]);
auto const p2 = static_cast<uint16_t>(ts.pixels[srcIdx + 1]);
MEM_BG_TILES[cbb][i] = static_cast<uint16_t>(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<unsigned>(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<uint16_t >(bpp == 8);
for (auto i = 0u; i < SpriteCount; ++i) {
auto &sa = teagba::spriteAttr(i);
auto &a = sa.attr0;
a |= static_cast<uint16_t>((a & ~static_cast<uint16_t>(1u << 13)) | (eightBpp << 13));
}
}
ox::Error loadSpriteTileSheet(
Context &ctx,
CompactTileSheet const&ts,
bool loadDefaultPalette) noexcept {
for (size_t i = 0; i < ts.pixels.size(); i += 2) {
uint16_t v = ts.pixels[i];
v |= static_cast<uint16_t>(ts.pixels[i + 1] << 8);
MEM_SPRITE_TILES[i] = v;
}
if (loadDefaultPalette && ts.defaultPalette) {
OX_RETURN_ERROR(loadSpritePalette(ctx, ts.defaultPalette));
}
setSpritesBpp(static_cast<unsigned>(ts.bpp));
return {};
}
ox::Error loadSpriteTileSheet(
Context &ctx,
TileSheetSet const&set) noexcept {
auto const bpp = static_cast<unsigned>(set.bpp);
OX_RETURN_ERROR(loadTileSheetSet(ctx, {MEM_SPRITE_TILES, 32 * ox::units::KB}, set));
setSpritesBpp(bpp);
return {};
}
void setBgTile(Context &ctx, uint_t bgIdx, int column, int row, BgTile const&tile) noexcept {
auto const tileIdx = static_cast<std::size_t>(row * tileColumns(ctx) + column);
// see Tonc 9.3
MEM_BG_MAP[bgIdx][tileIdx] =
static_cast<uint16_t>(tile.tileIdx & 0b1'1111'1111) |
static_cast<uint16_t>(tile.flipX << 0xa) |
static_cast<uint16_t>(tile.flipY << 0xb) |
static_cast<uint16_t>(tile.palBank << 0xc);
}
void clearBg(Context &ctx, uint_t bgIdx) noexcept {
memset(MEM_BG_MAP[bgIdx].data(), 0, static_cast<size_t>(tileRows(ctx) * tileColumns(ctx)));
}
uint8_t bgStatus(Context&) noexcept {
return (REG_DISPCTL >> 8u) & 0b1111u;
}
void setBgStatus(Context&, uint32_t status) noexcept {
constexpr auto BgStatus = 8;
REG_DISPCTL = (REG_DISPCTL & ~0b111100000000u) | status << BgStatus;
}
bool bgStatus(Context&, unsigned bg) noexcept {
return (REG_DISPCTL >> (8 + bg)) & 1;
}
void setBgStatus(Context&, unsigned bg, bool status) noexcept {
constexpr auto Bg0Status = 8;
const auto mask = static_cast<uint32_t>(status) << (Bg0Status + bg);
REG_DISPCTL = REG_DISPCTL | ((REG_DISPCTL & ~mask) | mask);
}
void setBgBpp(Context&, unsigned bgIdx, unsigned bpp) noexcept {
auto &bgCtl = regBgCtl(bgIdx);
teagba::bgSetBpp(bgCtl, bpp);
}
void setBgCbb(Context &ctx, unsigned bgIdx, unsigned cbb) noexcept {
auto &bgCtl = regBgCtl(bgIdx);
const auto &cbbData = ctx.cbbData[cbb];
teagba::bgSetBpp(bgCtl, cbbData.bpp);
teagba::bgSetCbb(bgCtl, cbb);
}
void setBgPriority(Context&, uint_t bgIdx, uint_t priority) noexcept {
auto &bgCtl = regBgCtl(bgIdx);
bgCtl = (bgCtl & 0b1111'1111'1111'1100u) | (priority & 0b11);
}
void hideSprite(Context&, unsigned idx) noexcept {
//oxAssert(g_spriteUpdates < config::GbaSpriteBufferLen, "Sprite update buffer overflow");
teagba::addSpriteUpdate({
.attr0 = uint16_t{0b11 << 8},
.idx = static_cast<uint16_t>(idx),
});
}
void showSprite(Context&, unsigned idx) noexcept {
//oxAssert(g_spriteUpdates < config::GbaSpriteBufferLen, "Sprite update buffer overflow");
teagba::addSpriteUpdate({
.attr0 = 0,
.idx = static_cast<uint16_t>(idx),
});
}
void setSprite(Context&, uint_t 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<uint16_t>(
(static_cast<uint16_t>(s.y & ox::onMask<uint8_t>(0b111'1111)))
| (static_cast<uint16_t>(1) << 10) // enable alpha
| (static_cast<uint16_t>(eightBpp) << 13)
| (static_cast<uint16_t>(s.spriteShape) << 14)),
.attr1 = static_cast<uint16_t>(
(static_cast<uint16_t>(s.x) & ox::onMask<uint8_t>(8))
| (static_cast<uint16_t>(s.flipX) << 12)
| (static_cast<uint16_t>(s.spriteSize) << 14)),
.attr2 = static_cast<uint16_t>(
// double tileIdx if 8 bpp
(static_cast<uint16_t>((s.tileIdx * (1 + eightBpp)) & ox::onMask<uint16_t>(8)))
| (static_cast<uint16_t>(s.priority & 0b11) << 10)),
.idx = static_cast<uint16_t>(idx),
});
}
uint_t spriteCount(Context&) noexcept {
return SpriteCount;
}
}
OX_ALLOW_UNSAFE_BUFFERS_END

View File

@@ -0,0 +1,11 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <nostalgia/gfx/context.hpp>
namespace nostalgia::gfx {
ox::Error initGfx(Context &ctx, InitParams const&) noexcept;
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/std/def.hpp>
#include <ox/std/realstd.hpp>
#include <keel/media.hpp>
#include <turbine/turbine.hpp>
#include <teagba/addresses.hpp>
#include <teagba/bios.hpp>
#include <nostalgia/gfx/core.hpp>
#include "gfx.hpp"
#define HEAP_BEGIN (reinterpret_cast<char*>(MEM_EWRAM_BEGIN))
#define HEAP_SIZE ((MEM_EWRAM_END - MEM_EWRAM_BEGIN) / 2)
#define HEAP_END (reinterpret_cast<char*>(MEM_EWRAM_BEGIN + HEAP_SIZE))
namespace ox {
using namespace nostalgia::gfx;
void panic(const char *file, int line, const char *panicMsg, ox::Error const&err) noexcept {
// reset heap to make sure we have enough memory to allocate context data
OX_ALLOW_UNSAFE_BUFFERS_BEGIN
ox::heapmgr::initHeap(HEAP_BEGIN, HEAP_END);
OX_ALLOW_UNSAFE_BUFFERS_END
auto tctx = turbine::init(keel::loadRomFs("").unwrap(), "Nostalgia").unwrap();
auto ctx = init(*tctx).unwrap();
std::ignore = initGfx(*ctx, {});
std::ignore = initConsole(*ctx);
setBgStatus(*ctx, 0, true);
clearBg(*ctx, 0);
auto const serr = ox::sfmt<ox::IString<23>>("Error code: {}", static_cast<int64_t>(err));
puts(*ctx, 32 + 1, 1, "SADNESS...");
puts(*ctx, 32 + 1, 4, "UNEXPECTED STATE:");
puts(*ctx, 32 + 2, 6, panicMsg);
if (err) {
puts(*ctx, 32 + 2, 8, serr);
}
puts(*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<ErrorCode>(err));
if (err.src.file_name() != nullptr) {
oxErrf("\tError Location:\t{}:{}\n", err.src.file_name(), err.src.line());
}
abort();
}
}

View File

@@ -0,0 +1,269 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <keel/media.hpp>
#include <nostalgia/gfx/gfx.hpp>
namespace nostalgia::gfx {
constexpr auto GbaTileColumns = 32;
constexpr auto GbaTileRows = 32;
int tileColumns(Context&) noexcept {
return GbaTileColumns;
}
int tileRows(Context&) noexcept {
return GbaTileRows;
}
ox::Error loadBgPalette(
Context &ctx,
size_t palBank,
ox::StringViewCR palettePath) noexcept {
OX_REQUIRE(pal, keel::readObj<CompactPalette>(keelCtx(ctx), palettePath));
return loadBgPalette(ctx, palBank, *pal, 0);
}
ox::Error loadBgPalette(
Context &ctx,
size_t palBank,
ox::FileAddress const&paletteAddr) noexcept {
OX_REQUIRE(pal, keel::readObj<CompactPalette>(keelCtx(ctx), paletteAddr));
return loadBgPalette(ctx, palBank, *pal, 0);
}
ox::Error loadSpritePalette(
Context &ctx,
ox::StringViewCR palettePath) noexcept {
OX_REQUIRE(pal, keel::readObj<CompactPalette>(keelCtx(ctx), palettePath));
return loadSpritePalette(ctx, *pal, 0);
}
ox::Error loadSpritePalette(
Context &ctx,
ox::FileAddress const&paletteAddr) noexcept {
OX_REQUIRE(pal, keel::readObj<CompactPalette>(keelCtx(ctx), paletteAddr));
return loadSpritePalette(ctx, *pal, 0);
}
ox::Error loadBgTileSheet(
Context &ctx,
unsigned cbb,
ox::FileAddress const&tsAddr,
size_t dstTileIdx,
size_t srcTileIdx,
size_t tileCnt) noexcept {
OX_REQUIRE(ts, keel::readObj<CompactTileSheet>(keelCtx(ctx), tsAddr));
return loadBgTileSheet(ctx, cbb, *ts, dstTileIdx, srcTileIdx, tileCnt);
}
ox::Error loadBgTileSheet(
Context &ctx,
unsigned cbb,
ox::StringViewCR tsPath,
size_t dstTileIdx,
size_t srcTileIdx,
size_t tileCnt) noexcept {
OX_REQUIRE(ts, keel::readObj<CompactTileSheet>(keelCtx(ctx), tsPath));
return loadBgTileSheet(ctx, cbb, *ts, dstTileIdx, srcTileIdx, tileCnt);
}
ox::Error loadBgTileSheet(
Context &ctx,
unsigned cbb,
ox::StringViewCR tilesheetPath,
ox::Optional<unsigned> const&paletteBank) noexcept {
OX_REQUIRE(ts, keel::readObj<CompactTileSheet>(keelCtx(ctx), tilesheetPath));
return loadBgTileSheet(ctx, cbb, *ts, paletteBank);
}
ox::Error loadBgTileSheet(
Context &ctx,
unsigned cbb,
ox::FileAddress const&tilesheetAddr,
ox::Optional<unsigned> const&paletteBank) noexcept {
OX_REQUIRE(ts, keel::readObj<CompactTileSheet>(keelCtx(ctx), tilesheetAddr));
return loadBgTileSheet(ctx, cbb, *ts, paletteBank);
}
ox::Error loadSpriteTileSheet(
Context &ctx,
ox::StringViewCR tilesheetPath,
bool loadDefaultPalette) noexcept {
OX_REQUIRE(ts, readObj<CompactTileSheet>(keelCtx(ctx), tilesheetPath));
return loadSpriteTileSheet(ctx, *ts, loadDefaultPalette);
}
ox::Error loadSpriteTileSheet(
Context &ctx,
ox::FileAddress const&tilesheetAddr,
bool loadDefaultPalette) noexcept {
OX_REQUIRE(ts, readObj<CompactTileSheet>(keelCtx(ctx), tilesheetAddr));
return loadSpriteTileSheet(ctx, *ts, loadDefaultPalette);
}
// map ASCII values to the nostalgia charset
constexpr ox::Array<char, 128> charMap = {
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0, // space
38, // !
0, // "
49, // #
0, // $
0, // %
0, // &
0, // '
42, // (
43, // )
0, // *
0, // +
37, // ,
0, // -
39, // .
0, // /
27, // 0
28, // 1
29, // 2
30, // 3
31, // 4
32, // 5
33, // 6
34, // 7
35, // 8
36, // 9
40, // :
0, // ;
0, // <
41, // =
0, // >
0, // ?
0, // @
1, // A
2, // B
3, // C
4, // D
5, // E
6, // F
7, // G
8, // H
9, // I
10, // J
11, // K
12, // L
13, // M
14, // N
15, // O
16, // P
17, // Q
18, // R
19, // S
20, // T
21, // U
22, // V
23, // W
24, // X
25, // Y
26, // Z
44, // [
0, // backslash
45, // ]
0, // ^
0, // _
0, // `
1, // a
2, // b
3, // c
4, // d
5, // e
6, // f
7, // g
8, // h
9, // i
10, // j
11, // k
12, // l
13, // m
14, // n
15, // o
16, // p
17, // q
18, // r
19, // s
20, // t
21, // u
22, // v
23, // w
24, // x
25, // y
26, // z
46, // {
51, // |
48, // }
50, // ~
};
void setBgTile(Context &ctx, uint_t bgIdx, int column, int row, unsigned tile, unsigned palBank) noexcept {
setBgTile(ctx, bgIdx, column, row, {
.tileIdx = tile,
.palBank = palBank,
});
}
ox::Error initConsole(Context &ctx) noexcept {
constexpr ox::FileAddress TilesheetAddr = ox::StringLiteral("/TileSheets/Charset.ng");
constexpr ox::FileAddress PaletteAddr = ox::StringLiteral("/Palettes/Charset.npal");
setBgStatus(ctx, 0b0001);
setBgCbb(ctx, 0, 0);
OX_RETURN_ERROR(loadBgTileSheet(ctx, 0, TilesheetAddr));
return loadBgPalette(ctx, 0, PaletteAddr);
}
void puts(
Context &ctx,
int const column,
int const row,
ox::StringViewCR str) noexcept {
for (auto i = 0u; i < str.bytes(); ++i) {
setBgTile(
ctx,
0,
column + static_cast<int>(i),
row,
static_cast<uint8_t>(charMap[static_cast<uint8_t>(str[i])]));
}
}
}

View File

@@ -0,0 +1,19 @@
add_library(
NostalgiaCore-Keel
keelmodule.cpp
typeconv.cpp
)
target_link_libraries(
NostalgiaCore-Keel PUBLIC
Keel
NostalgiaCore
)
install(
TARGETS
NostalgiaCore-Keel
DESTINATION
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/model/model.hpp>
#include <keel/asset.hpp>
#include <keel/module.hpp>
#include <nostalgia/gfx/palette.hpp>
#include <nostalgia/gfx/tilesheet.hpp>
#include "typeconv.hpp"
namespace nostalgia::gfx {
static class: public keel::Module {
private:
NostalgiaPaletteToPaletteV1Converter m_nostalgiaPaletteToPaletteV1Converter;
PaletteV1ToPaletteV2Converter m_paletteV1ToPaletteV2Converter;
PaletteV2ToPaletteV3Converter m_paletteV2ToPaletteV3Converter;
PaletteV3ToPaletteV4Converter m_paletteV3ToPaletteV4Converter;
PaletteToCompactPaletteConverter m_paletteToCompactPaletteConverter;
TileSheetV1ToTileSheetV2Converter m_tileSheetV1ToTileSheetV2Converter;
TileSheetV2ToTileSheetV3Converter m_tileSheetV2ToTileSheetV3Converter;
TileSheetV3ToTileSheetV4Converter m_tileSheetV3ToTileSheetV4Converter;
TileSheetToCompactTileSheetConverter m_tileSheetToCompactTileSheetConverter;
public:
[[nodiscard]]
ox::String id() const noexcept override {
return ox::String("net.drinkingtea.nostalgia.core");
}
[[nodiscard]]
ox::Vector<keel::TypeDescGenerator> types() const noexcept final {
return {
keel::generateTypeDesc<TileSheetV1>,
keel::generateTypeDesc<TileSheetV2>,
keel::generateTypeDesc<TileSheetV3>,
keel::generateTypeDesc<TileSheetV4>,
keel::generateTypeDesc<CompactTileSheetV1>,
keel::generateTypeDesc<PaletteV1>,
keel::generateTypeDesc<PaletteV2>,
keel::generateTypeDesc<PaletteV3>,
keel::generateTypeDesc<PaletteV4>,
keel::generateTypeDesc<CompactPaletteV1>,
};
}
[[nodiscard]]
ox::Vector<keel::BaseConverter const*> converters() const noexcept final {
return {
&m_nostalgiaPaletteToPaletteV1Converter,
&m_paletteV1ToPaletteV2Converter,
&m_paletteV2ToPaletteV3Converter,
&m_paletteV3ToPaletteV4Converter,
&m_paletteToCompactPaletteConverter,
&m_tileSheetV1ToTileSheetV2Converter,
&m_tileSheetV2ToTileSheetV3Converter,
&m_tileSheetV3ToTileSheetV4Converter,
&m_tileSheetToCompactTileSheetConverter,
};
}
[[nodiscard]]
ox::Vector<keel::PackTransform> packTransforms() const noexcept final {
return {
// convert tilesheets to CompactTileSheets
[](keel::Context &ctx, ox::Buffer &buff, ox::StringViewCR typeId) -> ox::Result<bool> {
if (typeId == ox::ModelTypeId_v<TileSheetV1> ||
typeId == ox::ModelTypeId_v<TileSheetV2> ||
typeId == ox::ModelTypeId_v<TileSheetV3> ||
typeId == ox::ModelTypeId_v<TileSheetV4>) {
OX_RETURN_ERROR(keel::convertBuffToBuff<CompactTileSheet>(
ctx, buff, ox::ClawFormat::Metal).moveTo(buff));
return true;
}
return false;
},
[](keel::Context &ctx, ox::Buffer &buff, ox::StringViewCR typeId) -> ox::Result<bool> {
if (typeId == ox::ModelTypeId_v<NostalgiaPalette> ||
typeId == ox::ModelTypeId_v<PaletteV1> ||
typeId == ox::ModelTypeId_v<PaletteV2> ||
typeId == ox::ModelTypeId_v<PaletteV3> ||
typeId == ox::ModelTypeId_v<PaletteV4>) {
OX_RETURN_ERROR(keel::convertBuffToBuff<CompactPalette>(
ctx, buff, ox::ClawFormat::Metal).moveTo(buff));
return true;
}
return false;
},
};
}
} const mod;
keel::Module const*keelModule() noexcept {
return &mod;
}
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "typeconv.hpp"
namespace nostalgia::gfx {
ox::Error NostalgiaPaletteToPaletteV1Converter::convert(
keel::Context&,
NostalgiaPalette &src,
PaletteV1 &dst) const noexcept {
dst.colors = std::move(src.colors);
return {};
}
ox::Error PaletteV1ToPaletteV2Converter::convert(
keel::Context&,
PaletteV1 &src,
PaletteV2 &dst) const noexcept {
dst.pages.emplace_back(std::move(src.colors));
return {};
}
ox::Error PaletteV2ToPaletteV3Converter::convert(
keel::Context&,
PaletteV2 &src,
PaletteV3 &dst) const noexcept {
dst.pages = std::move(src.pages);
if (!dst.pages.empty()) {
dst.colorInfo.reserve(dst.pages[0].size());
for (size_t i = 0; i < dst.pages[0].size(); ++i) {
dst.colorInfo.emplace_back(ox::sfmt("Color {}", i + 1));
}
}
return {};
}
ox::Error PaletteV3ToPaletteV4Converter::convert(
keel::Context&,
PaletteV3 &src,
PaletteV4 &dst) const noexcept {
dst.pages.reserve(src.pages.size());
for (auto i = 1; auto &page : src.pages) {
dst.pages.emplace_back(ox::sfmt("Page {}", i), std::move(page));
++i;
}
dst.colorNames.reserve(src.colorInfo.size());
for (auto &ci : src.colorInfo) {
dst.colorNames.emplace_back(std::move(ci.name));
}
return {};
}
ox::Error PaletteToCompactPaletteConverter::convert(
keel::Context&,
Palette &src,
CompactPalette &dst) const noexcept {
dst.pages.reserve(src.pages.size());
for (auto &page : src.pages) {
auto &p = dst.pages.emplace_back();
for (auto const c : page.colors) {
p.emplace_back(c);
}
}
return {};
}
ox::Error TileSheetV1ToTileSheetV2Converter::convert(
keel::Context&,
TileSheetV1 &src,
TileSheetV2 &dst) const noexcept {
dst.bpp = src.bpp;
dst.defaultPalette = std::move(src.defaultPalette);
dst.subsheet.name = "Root";
dst.subsheet.rows = src.rows;
dst.subsheet.columns = src.columns;
dst.subsheet.pixels = std::move(src.pixels);
return {};
}
void TileSheetV2ToTileSheetV3Converter::convertSubsheet(
TileSheetV2::SubSheet &src,
TileSheetV3::SubSheet &dst,
SubSheetId &idIt) noexcept {
dst.id = idIt;
dst.name = std::move(src.name);
dst.columns = src.columns;
dst.rows = src.rows;
dst.pixels = std::move(src.pixels);
++idIt;
dst.subsheets.resize(src.subsheets.size());
for (auto i = 0u; i < src.subsheets.size(); ++i) {
convertSubsheet(src.subsheets[i], dst.subsheets[i], idIt);
}
}
ox::Error TileSheetV2ToTileSheetV3Converter::convert(
keel::Context&,
TileSheetV2 &src,
TileSheetV3 &dst) const noexcept {
dst.bpp = src.bpp;
dst.defaultPalette = std::move(src.defaultPalette);
convertSubsheet(src.subsheet, dst.subsheet, dst.idIt);
return {};
}
void TileSheetV3ToTileSheetV4Converter::convertSubsheet(
TileSheetV3::SubSheet &src,
TileSheetV4::SubSheet &dst,
SubSheetId &idIt) noexcept {
dst.id = idIt;
dst.name = std::move(src.name);
dst.columns = src.columns;
dst.rows = src.rows;
dst.pixels = std::move(src.pixels);
++idIt;
dst.subsheets.resize(src.subsheets.size());
for (auto i = 0u; i < src.subsheets.size(); ++i) {
convertSubsheet(src.subsheets[i], dst.subsheets[i], idIt);
}
}
ox::Error TileSheetV3ToTileSheetV4Converter::convert(
keel::Context&,
TileSheetV3 &src,
TileSheetV4 &dst) const noexcept {
dst.bpp = src.bpp;
dst.idIt = src.idIt;
dst.defaultPalette = std::move(src.defaultPalette);
convertSubsheet(src.subsheet, dst.subsheet, dst.idIt);
return {};
}
ox::Error TileSheetToCompactTileSheetConverter::convert(
keel::Context&,
TileSheet &src,
CompactTileSheet &dst) const noexcept {
dst.bpp = src.bpp;
dst.defaultPalette = std::move(src.defaultPalette);
dst.pixels = pixels(src);
return {};
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/def.hpp>
#include <keel/typeconv.hpp>
#include <nostalgia/gfx/context.hpp>
#include <nostalgia/gfx/palette.hpp>
#include <nostalgia/gfx/tilesheet.hpp>
namespace nostalgia::gfx {
// Type converters
class NostalgiaPaletteToPaletteV1Converter: public keel::Converter<NostalgiaPalette, PaletteV1> {
ox::Error convert(keel::Context&, NostalgiaPalette &src, PaletteV1 &dst) const noexcept final;
};
class PaletteV1ToPaletteV2Converter: public keel::Converter<PaletteV1, PaletteV2> {
ox::Error convert(keel::Context&, PaletteV1 &src, PaletteV2 &dst) const noexcept final;
};
class PaletteV2ToPaletteV3Converter: public keel::Converter<PaletteV2, PaletteV3> {
ox::Error convert(keel::Context&, PaletteV2 &src, PaletteV3 &dst) const noexcept final;
};
class PaletteV3ToPaletteV4Converter: public keel::Converter<PaletteV3, PaletteV4> {
ox::Error convert(keel::Context&, PaletteV3 &src, PaletteV4 &dst) const noexcept final;
};
class PaletteToCompactPaletteConverter: public keel::Converter<Palette, CompactPalette> {
ox::Error convert(keel::Context&, Palette &src, CompactPalette &dst) const noexcept final;
};
class TileSheetV1ToTileSheetV2Converter: public keel::Converter<TileSheetV1, TileSheetV2> {
ox::Error convert(keel::Context&, TileSheetV1 &src, TileSheetV2 &dst) const noexcept final;
};
class TileSheetV2ToTileSheetV3Converter: public keel::Converter<TileSheetV2, TileSheetV3> {
static void convertSubsheet(
TileSheetV2::SubSheet &src,
TileSheetV3::SubSheet &dst,
SubSheetId &idIt) noexcept;
ox::Error convert(keel::Context&, TileSheetV2 &src, TileSheetV3 &dst) const noexcept final;
};
class TileSheetV3ToTileSheetV4Converter: public keel::Converter<TileSheetV3, TileSheetV4> {
static void convertSubsheet(
TileSheetV3::SubSheet &src,
TileSheetV4::SubSheet &dst,
SubSheetId &idIt) noexcept;
ox::Error convert(keel::Context&, TileSheetV3 &src, TileSheetV4 &dst) const noexcept final;
};
class TileSheetToCompactTileSheetConverter: public keel::Converter<TileSheet, CompactTileSheet> {
ox::Error convert(keel::Context&, TileSheet &src, CompactTileSheet &dst) const noexcept final;
};
}

View File

@@ -0,0 +1,9 @@
target_sources(
NostalgiaCore PRIVATE
context.cpp
gfx.cpp
)
target_link_libraries(
NostalgiaCore PUBLIC
GlUtils
)

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "context.hpp"
#include "gfx.hpp"
namespace nostalgia::gfx {
void safeDelete(Context *ctx) noexcept {
delete ctx;
}
Context::Context(turbine::Context &tctx, InitParams const&params) noexcept:
turbineCtx(tctx),
spriteBlocks(params.glSpriteCount, params.glBlocksPerSprite),
drawer(*this),
spriteCount(params.glSpriteCount),
blocksPerSprite(params.glBlocksPerSprite) {
}
Context::~Context() noexcept {
shutdownGfx(*this);
}
ox::Result<ContextUPtr> init(turbine::Context &tctx, InitParams const&params) noexcept {
auto ctx = ox::make_unique<Context>(tctx, params);
OX_RETURN_ERROR(initGfx(*ctx, params));
return ContextUPtr(ctx.release());
}
keel::Context &keelCtx(Context &ctx) noexcept {
return turbine::keelCtx(ctx.turbineCtx);
}
turbine::Context &turbineCtx(Context &ctx) noexcept {
return ctx.turbineCtx;
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/types.hpp>
#include <glutils/glutils.hpp>
#include <nostalgia/gfx/gfx.hpp>
#include <nostalgia/gfx/context.hpp>
#include "gfx.hpp"
namespace nostalgia::gfx {
class Context {
public:
turbine::Context &turbineCtx;
glutils::GLProgram bgShader;
glutils::GLProgram spriteShader;
ox::Array<renderer::CBB, 4> cbbs;
renderer::SpriteBlockset spriteBlocks;
ox::Array<Sprite, 128> spriteStates;
ox::Array<GLfloat, 1024> bgPalette;
ox::Array<renderer::Background, 4> backgrounds;
renderer::Drawer drawer;
uint_t spriteCount = 0;
uint_t blocksPerSprite = 0;
explicit Context(turbine::Context &tctx, InitParams const&params) noexcept;
Context(Context const&) = delete;
Context(Context&&) = delete;
Context &operator=(Context const&) = delete;
Context &operator=(Context&&) = delete;
~Context() noexcept;
};
}

View File

@@ -0,0 +1,792 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/std/array.hpp>
#include <ox/std/fmt.hpp>
#include <ox/std/vec.hpp>
#include <keel/media.hpp>
#include <glutils/glutils.hpp>
#include <nostalgia/gfx/context.hpp>
#include <nostalgia/gfx/gfx.hpp>
#include <nostalgia/gfx/palette.hpp>
#include <nostalgia/gfx/tilesheet.hpp>
#include "context.hpp"
#include "gfx.hpp"
namespace nostalgia::gfx {
namespace renderer {
static constexpr auto Scale = 1;
static constexpr auto PriorityScale = 0.01f;
Drawer::Drawer(Context &ctx) noexcept: m_ctx(ctx) {}
void Drawer::draw(turbine::Context &tctx) noexcept {
gfx::gl::draw(m_ctx, turbine::getScreenSize(tctx));
}
static constexpr ox::CStringView bgvshadTmpl = R"glsl(
{}
in vec2 vTexCoord;
in vec3 vPosition;
in float vTileIdx;
in float vPalOffset;
out vec2 fTexCoord;
out float fPalOffset;
uniform float vXScale;
uniform float vTileHeight;
uniform float vBgIdx;
void main() {
float xScaleInvert = 1.0 - vXScale;
gl_Position = vec4(
vPosition.x * vXScale - xScaleInvert,
vPosition.y,
vPosition.z - 0.001 * vBgIdx,
1.0);
fTexCoord = vec2(
vTexCoord.x,
vTexCoord.y * vTileHeight + vTileIdx * vTileHeight);
fPalOffset = vPalOffset;
})glsl";
static constexpr ox::CStringView bgfshadTmpl = R"glsl(
{}
out vec4 outColor;
in float fPalOffset;
in vec2 fTexCoord;
uniform sampler2D image;
uniform vec2 fSrcImgSz;
uniform vec4 fPalette[256];
void main() {
outColor = fPalette[int(texture(image, fTexCoord).rgb.r * 256) + int(fPalOffset)];
//outColor = vec4(0.0, 0.7, 1.0, 1.0);
if (outColor.a == 0) {
discard;
}
})glsl";
static constexpr ox::CStringView spritevshadTmpl = R"glsl(
{}
in float vEnabled;
in vec3 vPosition;
in vec2 vTexCoord;
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,
// offset to ensure sprites draw on top of BGs by default
vPosition.z - 0.004,
1.0) * vEnabled;
fTexCoord = vTexCoord * vec2(1, vTileHeight);
})glsl";
static constexpr ox::CStringView spritefshadTmpl = R"glsl(
{}
out vec4 outColor;
in vec2 fTexCoord;
uniform sampler2D image;
uniform vec2 fSrcImgSz;
uniform vec4 fPalette[256];
void main() {
outColor = fPalette[int(texture(image, fTexCoord).rgb.r * 256)];
//outColor = vec4(0.0, 0.7, 1.0, 1.0);
if (outColor.a == 0) {
discard;
}
})glsl";
[[nodiscard]]
static constexpr auto bgVertexRow(uint_t x, uint_t y) noexcept {
return y * TileRows + x;
}
static void setSpriteBufferObject(
uint_t const vi,
float const enabled,
float x,
float y,
uint_t const textureRow,
uint_t const flipX,
uint_t const priority,
ox::Span<float> const vbo,
ox::Span<GLuint> const 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;
auto const prif = static_cast<float>(priority) * PriorityScale;
auto const textureRowf = static_cast<float>(textureRow);
float const L = flipX ? 1 : 0;
float const R = flipX ? 0 : 1;
ox::Array<float, SpriteVertexVboLength> const vertices {
// vEnabled| vPosition | vTexCoord
enabled, x, y, prif, L, textureRowf + 1, // bottom left
enabled, x + xmod, y, prif, R, textureRowf + 1, // bottom right
enabled, x + xmod, y + ymod, prif, R, textureRowf + 0, // top right
enabled, x, y + ymod, prif, L, textureRowf + 0, // top left
};
ox::spancpy<float>(vbo, vertices);
ox::Array<GLuint, SpriteVertexEboLength> const elms {
vi + 0, vi + 1, vi + 2,
vi + 2, vi + 3, vi + 0,
};
ox::spancpy<GLuint>(ebo, elms);
}
static void setTileBufferObject(
uint_t const vi,
float x,
float y,
float const textureTileIdx,
float const priority,
float const palOffset,
bool const flipX,
bool const flipY,
ox::Span<float> const vbo,
ox::Span<GLuint> const 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;
auto const prif = priority * PriorityScale;
float const L = flipX ? 1 : 0;
float const R = flipX ? 0 : 1;
float const T = flipY ? 1 : 0;
float const B = flipY ? 0 : 1;
ox::Array<float, BgVertexVboLength> const vertices {
x, y, prif, L, B, textureTileIdx, palOffset, // bottom left
x + xmod, y, prif, R, B, textureTileIdx, palOffset, // bottom right
x + xmod, y + ymod, prif, R, T, textureTileIdx, palOffset, // top right
x, y + ymod, prif, L, T, textureTileIdx, palOffset, // top left
};
ox::spancpy<float>(vbo, vertices);
ox::Array<GLuint, BgVertexEboLength> const elms {
vi + 0, vi + 1, vi + 2,
vi + 2, vi + 3, vi + 0,
};
ox::spancpy<GLuint>(ebo, elms);
}
static void initSpriteBufferObjects(Context &ctx, glutils::BufferSet &bs) noexcept {
for (auto i = 0u; i < ctx.spriteCount; ++i) {
auto const vbo = ox::Span{bs.vertices}
+ i * static_cast<std::size_t>(SpriteVertexVboLength);
auto const ebo = ox::Span{bs.elements}
+ i * static_cast<std::size_t>(SpriteVertexEboLength);
setSpriteBufferObject(
i * static_cast<uint_t>(SpriteVertexVboRows) * ctx.blocksPerSprite,
0,
0,
0,
0,
false,
0,
vbo,
ebo);
}
}
static void initBackgroundBufferObjects(glutils::BufferSet &bs) noexcept {
for (auto x = 0u; x < TileColumns; ++x) {
for (auto y = 0u; y < TileRows; ++y) {
const auto i = bgVertexRow(x, y);
auto const vbo = ox::Span{bs.vertices}
+ i * static_cast<std::size_t>(BgVertexVboLength);
auto const ebo = ox::Span{bs.elements}
+ i * static_cast<std::size_t>(BgVertexEboLength);
setTileBufferObject(
static_cast<uint_t>(i * BgVertexVboRows),
static_cast<float>(x),
static_cast<float>(y),
0,
0,
0,
false,
false,
vbo,
ebo);
}
}
}
static void initSpritesBufferset(Context &ctx) noexcept {
auto const shader = ctx.spriteShader.id;
auto &bs = ctx.spriteBlocks;
// 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
// in float vEnabled;
auto const enabledAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vEnabled"));
glEnableVertexAttribArray(enabledAttr);
glVertexAttribPointer(enabledAttr, 1, GL_FLOAT, GL_FALSE, SpriteVertexVboRowLength * sizeof(float), nullptr);
// in vec3 vPosition;
auto const posAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vPosition"));
glEnableVertexAttribArray(posAttr);
glVertexAttribPointer(posAttr, 3, GL_FLOAT, GL_FALSE, SpriteVertexVboRowLength * sizeof(float),
std::bit_cast<void*>(uintptr_t{1 * sizeof(float)}));
// in vec2 vTexCoord;
auto const texCoordAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vTexCoord"));
glEnableVertexAttribArray(texCoordAttr);
glVertexAttribPointer(texCoordAttr, 2, GL_FLOAT, GL_FALSE, SpriteVertexVboRowLength * sizeof(float),
std::bit_cast<void*>(uintptr_t{4 * sizeof(float)}));
glBindVertexArray(0);
}
static void initBackgroundBufferset(
GLuint shader,
glutils::BufferSet &bs) noexcept {
// vao
bs.vao = glutils::generateVertexArrayObject();
glBindVertexArray(bs.vao);
// vbo & ebo
bs.vbo = glutils::generateBuffer();
bs.ebo = glutils::generateBuffer();
initBackgroundBufferObjects(bs);
glutils::sendVbo(bs);
glutils::sendEbo(bs);
// vbo layout
auto const posAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vPosition"));
glEnableVertexAttribArray(posAttr);
glVertexAttribPointer(posAttr, 3, GL_FLOAT, GL_FALSE, BgVertexVboRowLength * sizeof(float), nullptr);
auto const texCoordAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vTexCoord"));
glEnableVertexAttribArray(texCoordAttr);
glVertexAttribPointer(
texCoordAttr, 2, GL_FLOAT, GL_FALSE, BgVertexVboRowLength * sizeof(float),
std::bit_cast<void*>(uintptr_t{3 * sizeof(float)}));
auto const heightMultAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vTileIdx"));
glEnableVertexAttribArray(heightMultAttr);
glVertexAttribPointer(
heightMultAttr, 1, GL_FLOAT, GL_FALSE, BgVertexVboRowLength * sizeof(float),
std::bit_cast<void*>(uintptr_t{5 * sizeof(float)}));
auto const palBankAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vPalOffset"));
glEnableVertexAttribArray(palBankAttr);
glVertexAttribPointer(
palBankAttr, 1, GL_FLOAT, GL_FALSE, BgVertexVboRowLength * sizeof(float),
std::bit_cast<void*>(uintptr_t{6 * sizeof(float)}));
glBindVertexArray(0);
}
static glutils::GLTexture createTexture(
GLsizei w,
GLsizei h,
void const*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<GLsizei>(cbb.elements.size()), GL_UNSIGNED_INT, nullptr);
glBindVertexArray(0);
}
static void drawBackgrounds(
Context &ctx,
ox::Size const&renderSz) noexcept {
// load background shader and its uniforms
glUseProgram(ctx.bgShader);
const auto uniformSrcImgSz = glGetUniformLocation(ctx.bgShader, "fSrcImgSz");
const auto uniformXScale = static_cast<GLint>(glGetUniformLocation(ctx.bgShader, "vXScale"));
const auto uniformTileHeight = static_cast<GLint>(glGetUniformLocation(ctx.bgShader, "vTileHeight"));
const auto uniformBgIdx = static_cast<GLint>(glGetUniformLocation(ctx.bgShader, "vBgIdx"));
const auto [wi, hi] = renderSz;
const auto wf = static_cast<float>(wi);
const auto hf = static_cast<float>(hi);
glUniform1f(uniformXScale, hf / wf);
auto bgIdx = 0.f;
for (const auto &bg : ctx.backgrounds) {
if (bg.enabled) {
auto &cbb = ctx.cbbs[bg.cbbIdx];
const auto tileRows = cbb.tex.height / (TileHeight * Scale);
glUniform1f(uniformTileHeight, 1.0f / static_cast<float>(tileRows));
glUniform2f(
uniformSrcImgSz,
static_cast<float>(cbb.tex.width),
static_cast<float>(cbb.tex.height));
glUniform1f(uniformBgIdx, bgIdx);
drawBackground(cbb);
++bgIdx;
}
}
}
static void drawSprites(Context &ctx, ox::Size const&renderSz) noexcept {
glUseProgram(ctx.spriteShader);
auto &sb = ctx.spriteBlocks;
const auto uniformXScale = glGetUniformLocation(ctx.bgShader, "vXScale");
const auto uniformTileHeight = glGetUniformLocation(ctx.spriteShader, "vTileHeight");
const auto [wi, hi] = renderSz;
const auto wf = static_cast<float>(wi);
const auto hf = static_cast<float>(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 * Scale);
glUniform1f(uniformTileHeight, 1.0f / static_cast<float>(tileRows));
// draw
glBindTexture(GL_TEXTURE_2D, sb.tex);
glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(sb.elements.size()), GL_UNSIGNED_INT, nullptr);
glBindVertexArray(0);
}
static void loadPalette(
ox::Array<GLfloat, 1024> &palette,
size_t palOffset,
GLuint shaderPgrm,
CompactPalette const&pal,
size_t page = 0) noexcept {
static constexpr std::size_t ColorCnt = 256;
for (auto i = palOffset; auto const c : pal.pages[page]) {
palette[i++] = redf(c);
palette[i++] = greenf(c);
palette[i++] = bluef(c);
palette[i++] = 255;
}
// make first color transparent
palette[palOffset + 3] = 0;
glUseProgram(shaderPgrm);
const auto uniformPalette = static_cast<GLint>(glGetUniformLocation(shaderPgrm, "fPalette"));
glUniform4fv(uniformPalette, ColorCnt, palette.data());
}
static void setSprite(
Context &ctx,
uint_t const idx,
Sprite const&s) noexcept {
// Tonc Table 8.4
struct Sz { uint_t x{}, y{}; };
static constexpr ox::Array<Sz, 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
};
oxAssert(idx < ctx.spriteStates.size(), "overflow");
auto const dim = dimensions[(s.spriteShape << 2) | s.spriteSize];
auto const uX = static_cast<int>(s.x) % 255;
auto const uY = static_cast<int>(s.y + 8) % 255 - 8;
oxAssert(1 < ctx.spriteBlocks.vertices.size(), "vbo overflow");
oxAssert(1 < ctx.spriteBlocks.elements.size(), "ebo overflow");
const auto spriteVboSz = ctx.blocksPerSprite * renderer::SpriteVertexVboLength;
const auto spriteEboSz = ctx.blocksPerSprite * renderer::SpriteVertexEboLength;
auto const vboBase = spriteVboSz * idx;
auto const eboBase = spriteEboSz * idx;
auto i = 0u;
const auto set = [&](int xIt, int yIt, bool enabled) {
auto const fX = static_cast<float>(uX + xIt * 8) / 8;
auto const fY = static_cast<float>(uY + yIt * 8) / 8;
auto const vboIdx = vboBase + renderer::SpriteVertexVboLength * i;
auto const eboIdx = eboBase + renderer::SpriteVertexEboLength * i;
oxAssert(vboIdx < ctx.spriteBlocks.vertices.size(), "vbo overflow");
oxAssert(eboIdx < ctx.spriteBlocks.elements.size(), "ebo overflow");
auto const vbo = ox::Span{ctx.spriteBlocks.vertices} + vboIdx;
auto const ebo = ox::Span{ctx.spriteBlocks.elements} + eboIdx;
renderer::setSpriteBufferObject(
static_cast<uint_t>(vboIdx),
enabled,
fX,
fY,
s.tileIdx + i,
s.flipX,
s.priority & 0b11,
vbo,
ebo);
++i;
};
if (!s.flipX) {
for (auto yIt = 0; yIt < static_cast<int>(dim.y); ++yIt) {
for (auto xIt = 0u; xIt < dim.x; ++xIt) {
set(static_cast<int>(xIt), static_cast<int>(yIt), s.enabled);
}
}
} else {
for (auto yIt = 0u; yIt < dim.y; ++yIt) {
for (auto xIt = dim.x - 1; xIt < ~0u; --xIt) {
set(static_cast<int>(xIt), static_cast<int>(yIt), s.enabled);
}
}
}
// clear remaining blocks in the sprite
for (; i < ctx.blocksPerSprite; ++i) {
set(0, 0, false);
}
ctx.spriteBlocks.updated = true;
}
}
ox::Error initGfx(
Context &ctx,
InitParams const&initParams) noexcept {
const auto bgVshad = ox::sfmt(renderer::bgvshadTmpl, gl::GlslVersion);
const auto bgFshad = ox::sfmt(renderer::bgfshadTmpl, gl::GlslVersion);
const auto spriteVshad = ox::sfmt(renderer::spritevshadTmpl, gl::GlslVersion);
const auto spriteFshad = ox::sfmt(renderer::spritefshadTmpl, gl::GlslVersion);
OX_RETURN_ERROR(glutils::buildShaderProgram(bgVshad, bgFshad).moveTo(ctx.bgShader));
OX_RETURN_ERROR(
glutils::buildShaderProgram(spriteVshad, spriteFshad).moveTo(ctx.spriteShader));
for (auto &cbb : ctx.cbbs) {
initBackgroundBufferset(ctx.bgShader, cbb);
}
renderer::initSpritesBufferset(ctx);
if (initParams.glInstallDrawer) {
turbine::gl::addDrawer(ctx.turbineCtx, &ctx.drawer);
}
return {};
}
void shutdownGfx(Context &ctx) noexcept {
turbine::gl::removeDrawer(ctx.turbineCtx, &ctx.drawer);
}
struct TileSheetData {
ox::Vector<uint32_t> pixels;
int width = 0;
int height = 0;
[[nodiscard]]
constexpr ox::Size size() const noexcept {
return {width, height};
}
};
static ox::Result<TileSheetData> normalizeTileSheet(
CompactTileSheet const&ts) noexcept {
const uint_t bytesPerTile = ts.bpp == 8 ? PixelsPerTile : PixelsPerTile / 2;
const auto tiles = ts.pixels.size() / bytesPerTile;
constexpr int width = 8;
const int height = 8 * static_cast<int>(tiles);
ox::Vector<uint32_t> pixels;
if (bytesPerTile == 64) { // 8 BPP
pixels.resize(ts.pixels.size());
for (std::size_t i = 0; i < ts.pixels.size(); ++i) {
pixels[i] = ts.pixels[i];
}
} else { // 4 BPP
pixels.resize(ts.pixels.size() * 2);
for (std::size_t i = 0; i < ts.pixels.size(); ++i) {
pixels[i * 2 + 0] = ts.pixels[i] & 0xF;
pixels[i * 2 + 1] = ts.pixels[i] >> 4;
}
}
return TileSheetData{std::move(pixels), width, height};
}
ox::Error loadBgPalette(
Context &ctx,
size_t palBank,
CompactPalette const&palette,
size_t page) noexcept {
renderer::loadPalette(ctx.bgPalette, palBank * 16 * 4, ctx.bgShader, palette, page);
return {};
}
ox::Error loadSpritePalette(
Context &ctx,
CompactPalette const&palette,
size_t page) noexcept {
ox::Array<GLfloat, 1024> pal;
renderer::loadPalette(pal, 0, ctx.spriteShader, palette, page);
return {};
}
static ox::Result<TileSheetData> buildSetTsd(
Context &ctx,
TileSheetSet const&set) noexcept {
auto &kctx = keelCtx(ctx.turbineCtx);
TileSheetData setTsd;
setTsd.width = TileWidth;
for (auto const&entry : set.entries) {
OX_REQUIRE(tilesheet, readObj<CompactTileSheet>(kctx, entry.tilesheet));
OX_REQUIRE(tsd, normalizeTileSheet(*tilesheet));
for (auto const&s : entry.sections) {
auto const size = s.tiles * PixelsPerTile;
for (auto i = 0; i < size; ++i) {
auto const srcIdx = static_cast<size_t>(i) + static_cast<size_t>(s.begin * PixelsPerTile);
setTsd.pixels.push_back(tsd.pixels[srcIdx]);
}
setTsd.height += TileHeight * s.tiles;
}
}
return setTsd;
}
static void copyPixels(
CompactTileSheet const&ts,
ox::Span<uint32_t> dst,
size_t const srcPxIdx,
size_t const pxlCnt) noexcept {
size_t idx{};
if (ts.bpp == 4) {
for (size_t i = 0; i < pxlCnt; i += 2) {
auto const [a, b] = get2Pixels4Bpp(ts, i + srcPxIdx);
dst[idx++] = a;
dst[idx++] = b;
}
} else if (ts.bpp == 8) {
for (size_t i = 0; i < pxlCnt; i += 2) {
auto const [a, b] = get2Pixels8Bpp(ts, i + srcPxIdx);
dst[idx++] = a;
dst[idx++] = b;
}
}
}
void clearCbb(Context &ctx, unsigned const cbb) noexcept {
for (auto &v : ctx.cbbs[cbb].pixels) {
v = 0;
}
}
void clearCbbs(Context &ctx) noexcept {
for (unsigned i = 0 ; i < ctx.cbbs.size(); ++i) {
clearCbb(ctx, i);
}
}
ox::Error loadBgTileSheet(
Context &ctx,
unsigned const cbb,
CompactTileSheet const&ts,
size_t const dstTileIdx,
size_t const srcTileIdx,
size_t const tileCnt) noexcept {
auto &cbbPxls = ctx.cbbs[cbb].pixels;
auto const bytesPerTile = static_cast<uint64_t>(PixelsPerTile / (1 + (ts.bpp == 4)));
auto const pxlCnt = tileCnt * PixelsPerTile;
auto const srcPxIdx = srcTileIdx * PixelsPerTile;
auto const dstPxIdx = dstTileIdx * PixelsPerTile;
if (dstPxIdx + pxlCnt >= cbbPxls.size()) {
return ox::Error(1, "video mem dst overflow");
}
auto const dst = ox::Span{cbbPxls} + dstPxIdx;
copyPixels(ts, dst, srcPxIdx, pxlCnt);
auto const cbbTiles = cbbPxls.size() / bytesPerTile;
int constexpr cbbWidth = 8;
int const cbbHeight = 8 * static_cast<int>(cbbTiles);
ctx.cbbs[cbb].tex = renderer::createTexture(cbbWidth, cbbHeight, cbbPxls.data());
return {};
}
ox::Error loadBgTileSheet(
Context &ctx,
uint_t cbb,
CompactTileSheet const&ts,
ox::Optional<unsigned> const&paletteBank) noexcept {
auto const bytesPerTile = static_cast<uint64_t>(PixelsPerTile / (1 + (ts.bpp == 4)));
auto const tiles = ts.pixels.size() / bytesPerTile;
OX_RETURN_ERROR(loadBgTileSheet(ctx, cbb, ts, 0, 0, tiles));
if (paletteBank.has_value() && ts.defaultPalette) {
OX_RETURN_ERROR(loadBgPalette(ctx, *paletteBank, ts.defaultPalette));
}
return {};
}
ox::Error loadBgTileSheet(
Context &ctx,
unsigned cbb,
TileSheetSet const&set) noexcept {
OX_REQUIRE(setTsd, buildSetTsd(ctx, set));
ctx.cbbs[cbb].tex = renderer::createTexture(setTsd.width, setTsd.height, setTsd.pixels.data());
return {};
}
ox::Error loadSpriteTileSheet(
Context &ctx,
CompactTileSheet const&ts,
bool loadDefaultPalette) noexcept {
OX_REQUIRE(tsd, normalizeTileSheet(ts));
oxTracef("nostalgia.core.gfx.gl", "loadSpriteTexture: { w: {}, h: {} }", tsd.width, tsd.height);
ctx.spriteBlocks.tex = renderer::createTexture(tsd.width, tsd.height, tsd.pixels.data());
if (loadDefaultPalette) {
OX_RETURN_ERROR(loadSpritePalette(ctx, ts.defaultPalette));
}
return {};
}
ox::Error loadSpriteTileSheet(
Context &ctx,
TileSheetSet const&set) noexcept {
OX_REQUIRE(setTsd, buildSetTsd(ctx, set));
ctx.spriteBlocks.tex = renderer::createTexture(setTsd.width, setTsd.height, setTsd.pixels.data());
return {};
}
void setBgTile(
Context &ctx,
uint_t bgIdx,
int column,
int row,
BgTile const&tile) noexcept {
oxTracef(
"nostalgia.core.gfx.setBgTile",
"bgIdx: {}, column: {}, row: {}, tile: {}, palBank: {}",
bgIdx, column, row, tile.tileIdx, tile.palBank);
const auto z = static_cast<uint_t>(bgIdx);
const auto y = static_cast<uint_t>(row);
const auto x = static_cast<uint_t>(column);
const auto i = renderer::bgVertexRow(x, y);
auto &cbb = ctx.cbbs[z];
const auto vbo = ox::Span{cbb.vertices} + i * renderer::BgVertexVboLength;
const auto ebo = ox::Span{cbb.elements} + i * renderer::BgVertexEboLength;
auto &bg = ctx.backgrounds[bgIdx];
renderer::setTileBufferObject(
static_cast<uint_t>(i * renderer::BgVertexVboRows),
static_cast<float>(x),
static_cast<float>(y),
static_cast<float>(tile.tileIdx),
bg.priority,
static_cast<float>(tile.palBank * 16),
tile.flipX,
tile.flipY,
vbo,
ebo);
cbb.updated = true;
}
void clearBg(Context &ctx, uint_t bgIdx) noexcept {
auto &cbb = ctx.cbbs[static_cast<std::size_t>(bgIdx)];
initBackgroundBufferObjects(cbb);
cbb.updated = true;
auto &bg = ctx.backgrounds[static_cast<std::size_t>(bgIdx)];
bg.priority = 0;
}
uint8_t bgStatus(Context &ctx) noexcept {
uint8_t out = 0;
for (uint_t i = 0; i < ctx.cbbs.size(); ++i) {
out |= static_cast<uint8_t>(static_cast<uint_t>(ctx.backgrounds[i].enabled) << i);
}
return out;
}
void setBgStatus(Context &ctx, uint32_t status) noexcept {
for (uint_t i = 0; i < ctx.cbbs.size(); ++i) {
ctx.backgrounds[i].enabled = (status >> i) & 1;
}
}
bool bgStatus(Context &ctx, uint_t bg) noexcept {
return ctx.backgrounds[bg].enabled;
}
void setBgStatus(Context&ctx, uint_t bg, bool status) noexcept {
ctx.backgrounds[bg].enabled = status;
}
void setBgBpp(Context&, unsigned, unsigned) noexcept {}
void setBgCbb(Context &ctx, uint_t bgIdx, uint_t cbbIdx) noexcept {
auto &bg = ctx.backgrounds[bgIdx];
bg.cbbIdx = cbbIdx;
}
void setBgPriority(Context &ctx, uint_t bgIdx, uint_t priority) noexcept {
auto &bg = ctx.backgrounds[bgIdx];
bg.priority = static_cast<float>(priority & 0b11);
}
void hideSprite(Context &ctx, uint_t idx) noexcept {
auto &s = ctx.spriteStates[idx];
s.enabled = false;
renderer::setSprite(ctx, idx, s);
}
void showSprite(Context &ctx, uint_t idx) noexcept {
auto &s = ctx.spriteStates[idx];
s.enabled = true;
renderer::setSprite(ctx, idx, s);
}
void setSprite(Context &ctx, uint_t idx, Sprite const&sprite) noexcept {
auto &s = ctx.spriteStates[idx];
s = sprite;
renderer::setSprite(ctx, idx, s);
}
uint_t spriteCount(Context &ctx) noexcept {
return ctx.spriteCount;
}
namespace gl {
ox::Size drawSize(int scale) noexcept {
return {240 * scale, 160 * scale};
}
void draw(gfx::Context &ctx, ox::Size const&renderSz) noexcept {
glViewport(0, 0, renderSz.width, renderSz.height);
glEnable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
renderer::drawBackgrounds(ctx, renderSz);
if (ctx.spriteBlocks.tex) {
renderer::drawSprites(ctx, renderSz);
}
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
}
void draw(gfx::Context &ctx, int scale) noexcept {
draw(ctx, drawSize(scale));
}
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/types.hpp>
#include <turbine/gfx.hpp>
#include <glutils/glutils.hpp>
#include <nostalgia/gfx/context.hpp>
namespace nostalgia::gfx::renderer {
constexpr uint64_t TileRows = 128;
constexpr uint64_t TileColumns = 128;
constexpr uint64_t TileCount = TileRows * TileColumns;
constexpr uint64_t BgVertexVboRows = 4;
constexpr uint64_t BgVertexVboRowLength = 7;
constexpr uint64_t BgVertexVboLength = BgVertexVboRows * BgVertexVboRowLength;
constexpr uint64_t BgVertexEboLength = 6;
constexpr uint64_t SpriteVertexVboRows = 4;
constexpr uint64_t SpriteVertexVboRowLength = 6;
constexpr uint64_t SpriteVertexVboLength = SpriteVertexVboRows * SpriteVertexVboRowLength;
constexpr uint64_t SpriteVertexEboLength = 6;
struct CBB: public glutils::BufferSet {
bool updated = false;
ox::Array<uint32_t, 32768> pixels;
constexpr CBB() noexcept {
vertices.resize(TileCount * BgVertexVboLength);
elements.resize(TileCount * BgVertexEboLength);
}
};
struct SpriteBlockset: public glutils::BufferSet {
bool updated = false;
constexpr SpriteBlockset(uint64_t spriteCount, uint64_t blocksPerSprite) noexcept {
vertices.resize(spriteCount * SpriteVertexVboLength * blocksPerSprite);
elements.resize(spriteCount * SpriteVertexEboLength * blocksPerSprite);
}
};
struct Background {
float priority = 0;
bool enabled = false;
unsigned cbbIdx = 0;
};
class Drawer: public turbine::gl::Drawer {
private:
Context &m_ctx;
public:
explicit Drawer(Context &ctx) noexcept;
void draw(turbine::Context&) noexcept final;
};
}
namespace nostalgia::gfx {
ox::Error initGfx(Context &ctx, InitParams const&) noexcept;
void shutdownGfx(Context &ctx) noexcept;
}

View File

@@ -0,0 +1,28 @@
add_library(NostalgiaCore-Studio)
add_library(
NostalgiaCore-Studio-ImGui
studiomodule.cpp
)
target_link_libraries(
NostalgiaCore-Studio PUBLIC
NostalgiaCore
Studio
)
target_link_libraries(
NostalgiaCore-Studio-ImGui PUBLIC
NostalgiaCore-Studio
)
install(
TARGETS
NostalgiaCore-Studio-ImGui
NostalgiaCore-Studio
LIBRARY DESTINATION
${NOSTALGIA_DIST_MODULE}
)
add_subdirectory(paletteeditor)
add_subdirectory(tilesheeteditor)

View File

@@ -0,0 +1,18 @@
target_sources(
NostalgiaCore-Studio PRIVATE
commands/addcolorcommand.cpp
commands/addpagecommand.cpp
commands/applycolorallpagescommand.cpp
commands/duplicatepagecommand.cpp
commands/movecolorcommand.cpp
commands/removecolorcommand.cpp
commands/removepagecommand.cpp
commands/renamepagecommand.cpp
commands/updatecolorcommand.cpp
commands/updatecolorinfocommand.cpp
)
target_sources(
NostalgiaCore-Studio-ImGui PRIVATE
paletteeditor-imgui.cpp
)

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "commands.hpp"
#include "addcolorcommand.hpp"
namespace nostalgia::gfx {
AddColorCommand::AddColorCommand(Palette &pal, Color16 const color, size_t const idx) noexcept:
m_pal(pal),
m_color(color),
m_idx(idx) {}
int AddColorCommand::commandId() const noexcept {
return static_cast<int>(PaletteEditorCommandId::AddColor);
}
ox::Error AddColorCommand::redo() noexcept {
m_pal.colorNames.emplace(m_idx, ox::sfmt("Color {}", m_pal.colorNames.size() + 1));
for (auto &page : m_pal.pages) {
page.colors.emplace(m_idx, m_color);
}
return {};
}
ox::Error AddColorCommand::undo() noexcept {
OX_RETURN_ERROR(m_pal.colorNames.erase(m_idx));
for (auto &page : m_pal.pages) {
OX_RETURN_ERROR(page.colors.erase(m_idx));
}
return {};
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/studio.hpp>
#include <nostalgia/gfx/palette.hpp>
namespace nostalgia::gfx {
class AddColorCommand: public studio::UndoCommand {
private:
Palette &m_pal;
Color16 m_color = 0;
size_t const m_idx = 0;
public:
AddColorCommand(Palette &pal, Color16 color, size_t idx) noexcept;
~AddColorCommand() noexcept override = default;
[[nodiscard]]
int commandId() const noexcept override;
ox::Error redo() noexcept override;
ox::Error undo() noexcept override;
};
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "commands.hpp"
#include "addpagecommand.hpp"
namespace nostalgia::gfx {
AddPageCommand::AddPageCommand(Palette &pal) noexcept:
m_pal(pal) {}
int AddPageCommand::commandId() const noexcept {
return static_cast<int>(PaletteEditorCommandId::AddPage);
}
ox::Error AddPageCommand::redo() noexcept {
m_pal.pages.emplace_back(ox::sfmt("Page {}", m_pal.pages.size() + 1), ox::Vector<PaletteColor>{});
return {};
}
ox::Error AddPageCommand::undo() noexcept {
m_pal.pages.pop_back();
return {};
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/studio.hpp>
#include <nostalgia/gfx/palette.hpp>
namespace nostalgia::gfx {
class AddPageCommand: public studio::UndoCommand {
private:
Palette &m_pal;
public:
explicit AddPageCommand(Palette &pal) noexcept;
~AddPageCommand() noexcept override = default;
[[nodiscard]]
int commandId() const noexcept final;
ox::Error redo() noexcept final;
ox::Error undo() noexcept final;
};
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "commands.hpp"
#include "applycolorallpagescommand.hpp"
namespace nostalgia::gfx {
ApplyColorAllPagesCommand::ApplyColorAllPagesCommand(Palette &pal, size_t const page, size_t const idx):
m_pal(pal),
m_page(page),
m_idx(idx),
m_origColors([this] {
ox::Vector<Color16> colors;
colors.reserve(m_pal.pages.size());
for (auto const&p : m_pal.pages) {
colors.emplace_back(p.colors[m_idx]);
}
return colors;
}()) {
auto const c = color(m_pal, m_page, m_idx);
if (ox::all_of(m_pal.pages.begin(), m_pal.pages.end(), [this, c](PalettePage const&page) {
return page.colors[m_idx] == c;
})) {
throw studio::NoChangesException();
}
}
int ApplyColorAllPagesCommand::commandId() const noexcept {
return static_cast<int>(PaletteEditorCommandId::ApplyColorAllPages);
}
ox::Error ApplyColorAllPagesCommand::redo() noexcept {
auto const c = color(m_pal, m_page, m_idx);
for (auto &page : m_pal.pages) {
page.colors[m_idx] = c;
}
return {};
}
ox::Error ApplyColorAllPagesCommand::undo() noexcept {
for (size_t p = 0u; auto &page : m_pal.pages) {
page.colors[m_idx] = m_origColors[p];
++p;
}
return {};
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/studio.hpp>
#include <nostalgia/gfx/palette.hpp>
namespace nostalgia::gfx {
class ApplyColorAllPagesCommand: public studio::UndoCommand {
private:
Palette &m_pal;
size_t const m_page{};
size_t const m_idx{};
ox::Vector<Color16> const m_origColors;
public:
ApplyColorAllPagesCommand(Palette &pal, size_t page, size_t idx);
~ApplyColorAllPagesCommand() noexcept override = default;
[[nodiscard]]
int commandId() const noexcept final;
ox::Error redo() noexcept final;
ox::Error undo() noexcept final;
};
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
namespace nostalgia::gfx {
enum class PaletteEditorCommandId {
ApplyColorAllPages,
RenamePage,
AddPage,
DuplicatePage,
RemovePage,
AddColor,
RemoveColor,
UpdateColorInfo,
UpdateColor,
MoveColor,
};
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "commands.hpp"
#include "duplicatepagecommand.hpp"
namespace nostalgia::gfx {
DuplicatePageCommand::DuplicatePageCommand(Palette &pal, size_t srcIdx, size_t dstIdx) noexcept:
m_pal(pal),
m_dstIdx(dstIdx) {
auto const&src = m_pal.pages[srcIdx];
m_page.reserve(src.colors.size());
for (auto const&s : src.colors) {
m_page.emplace_back(s);
}
}
int DuplicatePageCommand::commandId() const noexcept {
return static_cast<int>(PaletteEditorCommandId::DuplicatePage);
}
ox::Error DuplicatePageCommand::redo() noexcept {
m_pal.pages.emplace(m_dstIdx, ox::sfmt("Page {}", m_pal.pages.size() + 1), std::move(m_page));
return {};
}
ox::Error DuplicatePageCommand::undo() noexcept {
m_page = std::move(m_pal.pages[m_dstIdx].colors);
return m_pal.pages.erase(m_dstIdx).error;
}
size_t DuplicatePageCommand::insertIdx() const noexcept {
return m_dstIdx;
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/studio.hpp>
#include <nostalgia/gfx/palette.hpp>
namespace nostalgia::gfx {
class DuplicatePageCommand: public studio::UndoCommand {
private:
Palette &m_pal;
size_t m_dstIdx = 0;
ox::Vector<PaletteColor> m_page;
public:
DuplicatePageCommand(Palette &pal, size_t srcIdx, size_t dstIdx) noexcept;
~DuplicatePageCommand() noexcept override = default;
[[nodiscard]]
int commandId() const noexcept final;
ox::Error redo() noexcept final;
ox::Error undo() noexcept final;
[[nodiscard]]
size_t insertIdx() const noexcept;
};
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "commands.hpp"
#include "movecolorcommand.hpp"
namespace nostalgia::gfx {
MoveColorCommand::MoveColorCommand(
Palette &pal, size_t page, size_t srcIdx, size_t dstIdx) noexcept:
m_pal(pal),
m_page(page),
m_srcIdx(srcIdx),
m_dstIdx(dstIdx) {}
int MoveColorCommand::commandId() const noexcept {
return static_cast<int>(PaletteEditorCommandId::MoveColor);
}
ox::Error MoveColorCommand::redo() noexcept {
moveColor(m_srcIdx, m_dstIdx);
return {};
}
ox::Error MoveColorCommand::undo() noexcept {
moveColor(m_dstIdx, m_srcIdx);
return {};
}
void MoveColorCommand::moveColor(size_t srcIdx, size_t dstIdx) noexcept {
auto const c = color(m_pal, m_page, srcIdx);
std::ignore = colors(m_pal, m_page).erase(srcIdx);
colors(m_pal, m_page).emplace(dstIdx, c);
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/studio.hpp>
#include <nostalgia/gfx/palette.hpp>
namespace nostalgia::gfx {
class MoveColorCommand: public studio::UndoCommand {
private:
Palette &m_pal;
size_t const m_page = 0;
std::size_t const m_srcIdx = 0;
std::size_t const m_dstIdx = 0;
public:
MoveColorCommand(Palette &pal, size_t page, size_t srcIdx, size_t dstIdx) noexcept;
~MoveColorCommand() noexcept override = default;
[[nodiscard]]
int commandId() const noexcept override;
ox::Error redo() noexcept override;
ox::Error undo() noexcept override;
private:
void moveColor(size_t srcIdx, size_t dstIdx) noexcept;
};
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "commands.hpp"
#include "removecolorcommand.hpp"
namespace nostalgia::gfx {
RemoveColorCommand::RemoveColorCommand(Palette &pal, size_t const idx) noexcept:
m_pal(pal),
m_idx(idx),
m_colors([this] {
ox::Vector<Color16> colors;
colors.reserve(m_pal.pages.size());
for (auto const&p : m_pal.pages) {
colors.emplace_back(p.colors[m_idx]);
}
return colors;
}()) {}
int RemoveColorCommand::commandId() const noexcept {
return static_cast<int>(PaletteEditorCommandId::RemoveColor);
}
ox::Error RemoveColorCommand::redo() noexcept {
m_colorInfo = std::move(m_pal.colorNames[m_idx]);
OX_RETURN_ERROR(m_pal.colorNames.erase(m_idx));
for (auto &page : m_pal.pages) {
OX_RETURN_ERROR(page.colors.erase(m_idx));
}
return {};
}
ox::Error RemoveColorCommand::undo() noexcept {
m_pal.colorNames.emplace(m_idx, std::move(m_colorInfo));
for (size_t p = 0; auto &page : m_pal.pages) {
page.colors.emplace(m_idx, m_colors[p]);
++p;
}
return {};
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/studio.hpp>
#include <nostalgia/gfx/palette.hpp>
namespace nostalgia::gfx {
class RemoveColorCommand: public studio::UndoCommand {
private:
Palette &m_pal;
size_t const m_idx = 0;
ox::String m_colorInfo;
ox::Vector<Color16> const m_colors;
public:
RemoveColorCommand(Palette &pal, size_t idx) noexcept;
~RemoveColorCommand() noexcept override = default;
[[nodiscard]]
int commandId() const noexcept override;
ox::Error redo() noexcept override;
ox::Error undo() noexcept override;
};
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "commands.hpp"
#include "removepagecommand.hpp"
namespace nostalgia::gfx {
RemovePageCommand::RemovePageCommand(Palette &pal, size_t idx) noexcept:
m_pal(pal),
m_idx(idx) {}
int RemovePageCommand::commandId() const noexcept {
return static_cast<int>(PaletteEditorCommandId::RemovePage);
}
ox::Error RemovePageCommand::redo() noexcept {
m_page = std::move(m_pal.pages[m_idx]);
return m_pal.pages.erase(m_idx).error;
}
ox::Error RemovePageCommand::undo() noexcept {
m_pal.pages.emplace(m_idx, std::move(m_page));
return {};
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/studio.hpp>
#include <nostalgia/gfx/palette.hpp>
namespace nostalgia::gfx {
class RemovePageCommand: public studio::UndoCommand {
private:
Palette &m_pal;
size_t m_idx = 0;
PalettePage m_page;
public:
RemovePageCommand(Palette &pal, size_t idx) noexcept;
~RemovePageCommand() noexcept override = default;
[[nodiscard]]
int commandId() const noexcept final;
ox::Error redo() noexcept final;
ox::Error undo() noexcept final;
};
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "renamepagecommand.hpp"
namespace nostalgia::gfx {
RenamePageCommand::RenamePageCommand(Palette &pal, size_t const page, ox::StringParam name) noexcept:
m_pal(pal),
m_page(page),
m_name{std::move(name)} {}
int RenamePageCommand::commandId() const noexcept {
return static_cast<int>(PaletteEditorCommandId::RenamePage);
}
ox::Error RenamePageCommand::redo() noexcept {
std::swap(m_pal.pages[m_page].name, m_name);
return {};
}
ox::Error RenamePageCommand::undo() noexcept {
std::swap(m_pal.pages[m_page].name, m_name);
return {};
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/studio.hpp>
#include <nostalgia/gfx/palette.hpp>
#include "commands.hpp"
namespace nostalgia::gfx {
class RenamePageCommand: public studio::UndoCommand {
private:
Palette &m_pal;
size_t m_page = 0;
ox::String m_name;
public:
RenamePageCommand(Palette &pal, size_t page, ox::StringParam name) noexcept;
~RenamePageCommand() noexcept override = default;
[[nodiscard]]
int commandId() const noexcept final;
ox::Error redo() noexcept final;
ox::Error undo() noexcept final;
};
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "commands.hpp"
#include "updatecolorcommand.hpp"
namespace nostalgia::gfx {
UpdateColorCommand::UpdateColorCommand(
Palette &pal,
size_t page,
size_t idx,
Color16 newColor):
m_pal(pal),
m_page(page),
m_idx(idx),
m_altColor(newColor) {
if (color(m_pal, m_page, m_idx) == newColor) {
throw studio::NoChangesException();
}
}
bool UpdateColorCommand::mergeWith(UndoCommand &cmd) noexcept {
if (cmd.commandId() != static_cast<int>(PaletteEditorCommandId::UpdateColor)) {
return false;
}
auto ucCmd = dynamic_cast<UpdateColorCommand const*>(&cmd);
if (m_idx != ucCmd->m_idx) {
return false;
}
return true;
}
int UpdateColorCommand::commandId() const noexcept {
return static_cast<int>(PaletteEditorCommandId::UpdateColor);
}
ox::Error UpdateColorCommand::redo() noexcept {
swap();
return {};
}
ox::Error UpdateColorCommand::undo() noexcept {
swap();
return {};
}
void UpdateColorCommand::swap() noexcept {
auto &dst = colors(m_pal, m_page)[m_idx];
std::swap(dst, m_altColor);
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/studio.hpp>
#include <nostalgia/gfx/palette.hpp>
namespace nostalgia::gfx {
class UpdateColorCommand: public studio::UndoCommand {
private:
Palette &m_pal;
size_t const m_page = 0;
size_t const m_idx{};
PaletteColor m_altColor{};
public:
UpdateColorCommand(
Palette &pal,
size_t page,
size_t idx,
Color16 newColor);
~UpdateColorCommand() noexcept override = default;
[[nodiscard]]
bool mergeWith(UndoCommand &cmd) noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
ox::Error redo() noexcept final;
ox::Error undo() noexcept final;
private:
void swap() noexcept;
};
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "commands.hpp"
#include "updatecolorinfocommand.hpp"
namespace nostalgia::gfx {
UpdateColorInfoCommand::UpdateColorInfoCommand(
Palette &pal,
size_t idx,
ox::StringParam newColorInfo):
m_pal(pal),
m_idx(idx),
m_altColorInfo(std::move(newColorInfo)) {
if (m_pal.colorNames[m_idx] == m_altColorInfo) {
throw studio::NoChangesException();
}
}
bool UpdateColorInfoCommand::mergeWith(UndoCommand &cmd) noexcept {
if (cmd.commandId() != static_cast<int>(PaletteEditorCommandId::UpdateColorInfo)) {
return false;
}
auto ucCmd = dynamic_cast<UpdateColorInfoCommand const*>(&cmd);
if (m_idx != ucCmd->m_idx) {
return false;
}
m_pal.colorNames[m_idx] = std::move(ucCmd->m_pal.colorNames[m_idx]);
setObsolete(m_altColorInfo == m_pal.colorNames[m_idx]);
return true;
}
int UpdateColorInfoCommand::commandId() const noexcept {
return static_cast<int>(PaletteEditorCommandId::UpdateColorInfo);
}
ox::Error UpdateColorInfoCommand::redo() noexcept {
swap();
return {};
}
ox::Error UpdateColorInfoCommand::undo() noexcept {
swap();
return {};
}
void UpdateColorInfoCommand::swap() noexcept {
std::swap(m_pal.colorNames[m_idx], m_altColorInfo);
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/studio.hpp>
#include <nostalgia/gfx/palette.hpp>
namespace nostalgia::gfx {
class UpdateColorInfoCommand: public studio::UndoCommand {
private:
Palette &m_pal;
size_t const m_idx{};
ox::String m_altColorInfo;
public:
UpdateColorInfoCommand(
Palette &pal,
size_t idx,
ox::StringParam newColorInfo);
~UpdateColorInfoCommand() noexcept override = default;
[[nodiscard]]
bool mergeWith(UndoCommand &cmd) noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
ox::Error redo() noexcept final;
ox::Error undo() noexcept final;
private:
void swap() noexcept;
};
}

View File

@@ -0,0 +1,291 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <imgui.h>
#include <keel/media.hpp>
#include "commands/addcolorcommand.hpp"
#include "commands/addpagecommand.hpp"
#include "commands/applycolorallpagescommand.hpp"
#include "commands/duplicatepagecommand.hpp"
#include "commands/movecolorcommand.hpp"
#include "commands/removecolorcommand.hpp"
#include "commands/removepagecommand.hpp"
#include "commands/renamepagecommand.hpp"
#include "commands/updatecolorcommand.hpp"
#include "commands/updatecolorinfocommand.hpp"
#include "paletteeditor-imgui.hpp"
namespace nostalgia::gfx {
namespace ig = studio::ig;
void PaletteEditorImGui::PageRenameDialog::draw(turbine::Context &tctx) noexcept {
if (!m_show) {
return;
}
if (ig::BeginPopup(tctx, "Rename Page", m_show)) {
ig::InputText("Name", m_name);
switch (ig::PopupControlsOkCancel(m_show)) {
case ig::PopupResponse::OK:
inputSubmitted.emit(m_name);
[[fallthrough]];
case ig::PopupResponse::Cancel:
close();
default:
break;
}
ImGui::EndPopup();
}
}
PaletteEditorImGui::PaletteEditorImGui(studio::StudioContext &sctx, ox::StringParam path):
Editor(std::move(path)),
m_sctx(sctx),
m_tctx(sctx.tctx),
m_pal(*keel::readObj<Palette>(keelCtx(m_tctx), itemPath()).unwrapThrow()) {
undoStack()->changeTriggered.connect(this, &PaletteEditorImGui::handleCommand);
m_pageRenameDlg.inputSubmitted.connect(this, &PaletteEditorImGui::renamePage);
}
void PaletteEditorImGui::draw(studio::StudioContext&) noexcept {
auto const paneSize = ImGui::GetContentRegionAvail();
{
ImGui::BeginChild("Pages", {280, paneSize.y}, true);
drawPagesEditor();
ImGui::EndChild();
}
ImGui::SameLine();
{
ImGui::BeginChild("Colors", {-1, paneSize.y}, true);
drawColorsEditor();
ImGui::EndChild();
}
m_pageRenameDlg.draw(m_tctx);
}
ox::Error PaletteEditorImGui::saveItem() noexcept {
return m_sctx.project->writeObj(itemPath(), m_pal, ox::ClawFormat::Organic);
}
void PaletteEditorImGui::drawColumnLeftAlign(ox::CStringView txt) noexcept {
ImGui::TableNextColumn();
ImGui::Text("%s", txt.c_str());
}
void PaletteEditorImGui::drawColumn(ox::CStringView txt) noexcept {
ImGui::TableNextColumn();
ImGui::SetCursorPosX(
ImGui::GetCursorPosX() + ImGui::GetColumnWidth() - ImGui::CalcTextSize(txt.data()).x);
ImGui::Text("%s", txt.c_str());
}
void PaletteEditorImGui::numShortcuts(size_t &val, size_t const sizeRange) noexcept {
auto const lastElem = sizeRange - 1;
if (ImGui::IsKeyPressed(ImGuiKey_0)) {
val = ox::min<size_t>(9, lastElem);
} else for (auto i = 9u; i < 10; --i) {
auto const key = static_cast<ImGuiKey>(ImGuiKey_1 + i);
if (ImGui::IsKeyPressed(key)) {
val = ox::min<size_t>(i, lastElem);
break;
}
}
}
void PaletteEditorImGui::colorInput(ox::CStringView label, int &v, bool &inputFocused) noexcept {
ImGui::InputInt(label.c_str(), &v, 1, 5);
inputFocused = inputFocused || ImGui::IsItemFocused();
v = ox::max(v, 0);
}
void PaletteEditorImGui::drawColorsEditor() noexcept {
constexpr auto tableFlags = ImGuiTableFlags_RowBg;
auto const colorsSz = ImGui::GetContentRegionAvail();
auto colorEditor = m_selectedColorRow < colorCnt(m_pal, m_page);
auto const colorEditorWidth = 220;
static constexpr auto toolbarHeight = 40;
{
auto constexpr sz = ImVec2{70, 24};
if (ImGui::Button("Add", sz)) {
auto const colorSz = colorCnt(m_pal, m_page);
constexpr Color16 c = 0;
std::ignore = pushCommand<AddColorCommand>(m_pal, c, colorSz);
}
ImGui::SameLine();
ImGui::BeginDisabled(m_selectedColorRow >= colorCnt(m_pal, m_page));
{
if (ImGui::Button("Remove", sz)) {
std::ignore = pushCommand<RemoveColorCommand>(m_pal, m_selectedColorRow);
m_selectedColorRow = ox::min(colorCnt(m_pal, m_page) - 1, m_selectedColorRow);
colorEditor = m_selectedColorRow < colorCnt(m_pal, m_page);
}
ImGui::SameLine();
ImGui::BeginDisabled(m_selectedColorRow <= 0);
{
if (ImGui::Button("Move Up", sz)) {
std::ignore = pushCommand<MoveColorCommand>(
m_pal, m_page, m_selectedColorRow, m_selectedColorRow - 1);
--m_selectedColorRow;
}
}
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::BeginDisabled(m_selectedColorRow >= colorCnt(m_pal, m_page) - 1);
{
if (ImGui::Button("Move Down", sz)) {
std::ignore = pushCommand<MoveColorCommand>(
m_pal, m_page, m_selectedColorRow, m_selectedColorRow + 1);
++m_selectedColorRow;
}
}
ImGui::EndDisabled();
}
ImGui::EndDisabled();
}
auto const tblWidth = (colorsSz.x - static_cast<float>(colorEditorWidth) - 8.f)
* static_cast<float>(colorEditor);
ImGui::BeginTable(
"Colors",
6,
tableFlags,
{tblWidth, colorsSz.y - (toolbarHeight + 5)});
{
ImGui::TableSetupColumn("Idx", ImGuiTableColumnFlags_WidthFixed, 25);
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 100);
ImGui::TableSetupColumn("Red", ImGuiTableColumnFlags_WidthFixed, 40);
ImGui::TableSetupColumn("Green", ImGuiTableColumnFlags_WidthFixed, 40);
ImGui::TableSetupColumn("Blue", ImGuiTableColumnFlags_WidthFixed, 40);
ImGui::TableSetupColumn("Preview", ImGuiTableColumnFlags_NoHide);
ImGui::TableHeadersRow();
if (m_page < m_pal.pages.size()) {
for (auto i = 0u; auto const &c: m_pal.pages[m_page].colors) {
ig::IDStackItem const idStackItem(static_cast<int>(i));
ImGui::TableNextRow();
drawColumn(i + 1);
drawColumnLeftAlign(m_pal.colorNames[i]);
drawColumn(red16(c));
drawColumn(green16(c));
drawColumn(blue16(c));
ImGui::TableNextColumn();
auto const ic = ImGui::GetColorU32({redf(c), greenf(c), bluef(c), 1});
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, ic);
if (ImGui::Selectable(
"##ColorRow", i == m_selectedColorRow, ImGuiSelectableFlags_SpanAllColumns)) {
m_selectedColorRow = i;
}
++i;
}
}
}
ImGui::EndTable();
if (colorEditor) {
ImGui::SameLine();
ImGui::BeginChild("ColorEditor", {colorEditorWidth, -1}, true);
drawColorEditor();
ImGui::EndChild();
}
}
void PaletteEditorImGui::drawPagesEditor() noexcept {
constexpr auto tableFlags = ImGuiTableFlags_RowBg;
auto const paneSz = ImGui::GetContentRegionAvail();
constexpr auto toolbarHeight = 40;
auto const btnSz = ImVec2{paneSz.x / 4 - 5.5f, 24};
if (ImGui::Button("Add", btnSz)) {
if (m_pal.pages.empty()) {
std::ignore = pushCommand<AddPageCommand>(m_pal);
} else {
std::ignore = pushCommand<DuplicatePageCommand>(m_pal, 0u, m_pal.pages.size());
}
m_page = m_pal.pages.size() - 1;
}
ImGui::SameLine();
if (ImGui::Button("Remove", btnSz)) {
std::ignore = pushCommand<RemovePageCommand>(m_pal, m_page);
m_page = std::min(m_page, m_pal.pages.size() - 1);
}
ImGui::SameLine();
if (ImGui::Button("Clone", btnSz)) {
std::ignore = pushCommand<DuplicatePageCommand>(m_pal, m_page, m_pal.pages.size());
}
ImGui::SameLine();
if (ImGui::Button("Rename", btnSz)) {
m_pageRenameDlg.show(m_pal.pages[m_page].name);
}
ImGui::BeginTable(
"PageSelect",
2,
tableFlags,
{paneSz.x, paneSz.y - (toolbarHeight + 5)});
{
ImGui::TableSetupColumn("Page", ImGuiTableColumnFlags_WidthFixed, 60);
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 200);
ImGui::TableHeadersRow();
for (auto i = 0u; i < m_pal.pages.size(); ++i) {
ig::IDStackItem const idStackItem(static_cast<int>(i));
ImGui::TableNextRow();
drawColumn(i + 1);
drawColumnLeftAlign(m_pal.pages[i].name);
ImGui::SameLine();
if (ImGui::Selectable("##PageRow", i == m_page, ImGuiSelectableFlags_SpanAllColumns)) {
m_page = i;
}
}
}
ImGui::EndTable();
}
void PaletteEditorImGui::drawColorEditor() noexcept {
auto const c = color(m_pal, m_page, m_selectedColorRow);
int r = red16(c);
int g = green16(c);
int b = blue16(c);
int const a = alpha16(c);
auto const newName = ig::InputText<50>(
"Name", m_pal.colorNames[m_selectedColorRow]);
bool inputFocused = ImGui::IsItemFocused();
ImGui::Separator();
colorInput("Red", r, inputFocused);
colorInput("Green", g, inputFocused);
colorInput("Blue", b, inputFocused);
if (ig::PushButton("Apply to all pages", {-1, ig::BtnSz.y})) {
std::ignore = pushCommand<ApplyColorAllPagesCommand>(
m_pal, m_page, m_selectedColorRow);
}
if (ig::mainWinHasFocus()) {
if (!ImGui::IsKeyDown(ImGuiKey_ModAlt)) {
numShortcuts(m_selectedColorRow, largestPage(m_pal));
} else {
numShortcuts(m_page, m_pal.pages.size());
}
}
auto const newColor = color16(r, g, b, a);
if (c != newColor) {
std::ignore = pushCommand<UpdateColorCommand>(m_pal, m_page, m_selectedColorRow, newColor);
}
if (newName) {
std::ignore = pushCommand<UpdateColorInfoCommand>(
m_pal, m_selectedColorRow, static_cast<ox::String>(newName.text));
}
}
ox::Error PaletteEditorImGui::renamePage(ox::StringView name) noexcept {
return pushCommand<RenamePageCommand>(m_pal, m_page, name);
}
ox::Error PaletteEditorImGui::handleCommand(studio::UndoCommand const*cmd) noexcept {
if (dynamic_cast<RemovePageCommand const*>(cmd)) {
m_page = ox::min(m_page, m_pal.pages.size() - 1);
} else if (auto const dupPageCmd = dynamic_cast<DuplicatePageCommand const*>(cmd)) {
m_page = ox::clamp<size_t>(dupPageCmd->insertIdx(), 0, m_pal.pages.size() - 1);
}
return {};
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/studio.hpp>
#include <nostalgia/gfx/gfx.hpp>
#include <nostalgia/gfx/palette.hpp>
namespace nostalgia::gfx {
class PaletteEditorImGui: public studio::Editor {
private:
class PageRenameDialog {
private:
ox::IString<50> m_name;
bool m_show = false;
public:
ox::Signal<ox::Error(ox::StringView name)> inputSubmitted;
constexpr void show(ox::StringView const&name) noexcept {
m_show = true;
m_name = name;
}
constexpr void close() noexcept {
m_show = false;
}
[[nodiscard]]
constexpr bool isOpen() const noexcept { return m_show; }
void draw(turbine::Context &tctx) noexcept;
} m_pageRenameDlg;
studio::StudioContext &m_sctx;
turbine::Context &m_tctx;
Palette m_pal;
size_t m_selectedColorRow = 0;
size_t m_page = 0;
public:
PaletteEditorImGui(studio::StudioContext &sctx, ox::StringParam path);
void draw(studio::StudioContext&) noexcept final;
protected:
ox::Error saveItem() noexcept final;
private:
static void drawColumnLeftAlign(ox::CStringView txt) noexcept;
static void drawColumn(ox::CStringView txt) noexcept;
static void drawColumn(ox::Integer_c auto i) noexcept {
drawColumn(ox::itoa(i));
}
static void numShortcuts(size_t &val, size_t sizeRange) noexcept;
static void colorInput(ox::CStringView label, int &v, bool &inputFocused) noexcept;
void drawColorsEditor() noexcept;
void drawPagesEditor() noexcept;
void drawColorEditor() noexcept;
ox::Error renamePage(ox::StringView name) noexcept;
ox::Error handleCommand(studio::UndoCommand const*) noexcept;
};
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/std/memory.hpp>
#include <studio/studio.hpp>
#include "paletteeditor/paletteeditor-imgui.hpp"
#include "tilesheeteditor/tilesheeteditor-imgui.hpp"
namespace nostalgia::gfx {
static class: public studio::Module {
ox::Vector<studio::EditorMaker> editors(studio::StudioContext &ctx) const noexcept final {
return {
studio::editorMaker<TileSheetEditorImGui>(ctx, FileExt_ng),
studio::editorMaker<PaletteEditorImGui>(ctx, FileExt_npal),
};
}
ox::Vector<ox::UPtr<studio::ItemMaker>> itemMakers(studio::StudioContext&) const noexcept final {
ox::Vector<ox::UniquePtr<studio::ItemMaker>> out;
out.emplace_back(ox::make<studio::ItemMakerT<TileSheet>>("Tile Sheet", "TileSheets", FileExt_ng));
out.emplace_back(ox::make<studio::ItemMakerT<Palette>>("Palette", "Palettes", FileExt_npal, Palette{
.colorNames = {},
.pages = {{"Page 1", ox::Vector<PaletteColor>{}}},
}, ox::ClawFormat::Organic));
return out;
}
} const mod;
const studio::Module *studioModule() noexcept {
return &mod;
}
}

View File

@@ -0,0 +1,19 @@
target_sources(
NostalgiaCore-Studio PRIVATE
tilesheeteditorview.cpp
tilesheeteditormodel.cpp
tilesheetpixelgrid.cpp
tilesheetpixels.cpp
)
target_sources(
NostalgiaCore-Studio-ImGui PRIVATE
tilesheeteditor-imgui.cpp
)
target_link_libraries(
NostalgiaCore-Studio-ImGui PUBLIC
lodepng
)
add_subdirectory(commands)

View File

@@ -0,0 +1,11 @@
target_sources(
NostalgiaCore-Studio PRIVATE
addsubsheetcommand.cpp
cutpastecommand.cpp
deletetilescommand.cpp
drawcommand.cpp
inserttilescommand.cpp
palettechangecommand.cpp
rmsubsheetcommand.cpp
updatesubsheetcommand.cpp
)

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "addsubsheetcommand.hpp"
namespace nostalgia::gfx {
AddSubSheetCommand::AddSubSheetCommand(
TileSheet &img,
TileSheet::SubSheetIdx parentIdx) noexcept:
m_img(img), m_parentIdx(std::move(parentIdx)) {
auto &parent = getSubSheet(m_img, m_parentIdx);
if (!parent.subsheets.empty()) {
auto idx = m_parentIdx;
idx.emplace_back(parent.subsheets.size());
m_addedSheets.push_back(idx);
} else {
auto idx = m_parentIdx;
idx.emplace_back(0u);
m_addedSheets.push_back(idx);
*idx.back().value = 1;
m_addedSheets.push_back(idx);
}
}
ox::Error AddSubSheetCommand::redo() noexcept {
auto &parent = getSubSheet(m_img, m_parentIdx);
if (m_addedSheets.size() < 2) {
auto i = parent.subsheets.size();
parent.subsheets.emplace_back(m_img.idIt++, ox::sfmt("Subsheet {}", i), 1, 1, m_img.bpp);
} else {
parent.subsheets.emplace_back(m_img.idIt++, "Subsheet 0", parent.columns, parent.rows, std::move(parent.pixels));
parent.rows = 0;
parent.columns = 0;
parent.subsheets.emplace_back(m_img.idIt++, "Subsheet 1", 1, 1, m_img.bpp);
}
return {};
}
ox::Error AddSubSheetCommand::undo() noexcept {
auto &parent = getSubSheet(m_img, m_parentIdx);
if (parent.subsheets.size() == 2) {
auto s = parent.subsheets[0];
parent.rows = s.rows;
parent.columns = s.columns;
parent.pixels = std::move(s.pixels);
parent.subsheets.clear();
--m_img.idIt;
} else {
for (auto idx = m_addedSheets.rbegin(); idx != m_addedSheets.rend(); ++idx) {
OX_RETURN_ERROR(rmSubSheet(m_img, *idx));
--m_img.idIt;
}
}
return {};
}
int AddSubSheetCommand::commandId() const noexcept {
return static_cast<int>(CommandId::AddSubSheet);
}
TileSheet::SubSheetIdx const&AddSubSheetCommand::subsheetIdx() const noexcept {
return m_parentIdx;
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "commands.hpp"
namespace nostalgia::gfx {
class AddSubSheetCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_parentIdx;
ox::Vector<TileSheet::SubSheetIdx, 2> m_addedSheets;
public:
AddSubSheetCommand(TileSheet &img, TileSheet::SubSheetIdx parentIdx) noexcept;
ox::Error redo() noexcept final;
ox::Error undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/undostack.hpp>
#include <nostalgia/gfx/tilesheet.hpp>
namespace nostalgia::gfx {
// Command IDs to use with QUndoCommand::id()
enum class CommandId {
Draw = 1,
AddSubSheet = 2,
RmSubSheet = 3,
DeleteTile = 4,
InsertTile = 4,
UpdateSubSheet = 5,
Cut = 6,
Paste = 7,
PaletteChange = 8,
};
constexpr bool operator==(CommandId c, int i) noexcept {
return static_cast<int>(c) == i;
}
constexpr bool operator==(int i, CommandId c) noexcept {
return static_cast<int>(c) == i;
}
class TileSheetCommand: public studio::UndoCommand {
public:
[[nodiscard]]
virtual TileSheet::SubSheetIdx const&subsheetIdx() const noexcept = 0;
};
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "cutpastecommand.hpp"
namespace nostalgia::gfx {
TileSheetClipboard::Pixel::Pixel(uint16_t pColorIdx, ox::Point pPt) noexcept {
colorIdx = pColorIdx;
pt = pPt;
}
void TileSheetClipboard::addPixel(ox::Point const&pt, uint16_t colorIdx) noexcept {
m_pixels.emplace_back(colorIdx, pt);
}
const ox::Vector<TileSheetClipboard::Pixel> &TileSheetClipboard::pixels() const noexcept {
return m_pixels;
}
CutPasteCommand::CutPasteCommand(
CommandId commandId,
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
ox::Point const&dstStart,
ox::Point dstEnd,
TileSheetClipboard const&cb) noexcept:
m_commandId(commandId),
m_img(img),
m_subSheetIdx(std::move(subSheetIdx)) {
auto const&ss = getSubSheet(m_img, m_subSheetIdx);
dstEnd.x = std::min(ss.columns * TileWidth - 1, dstEnd.x);
dstEnd.y = std::min(ss.rows * TileHeight - 1, dstEnd.y);
for (auto const&p : cb.pixels()) {
auto const dstPt = p.pt + dstStart;
if (dstPt.x <= dstEnd.x && dstPt.y <= dstEnd.y) {
auto const idx = gfx::idx(ss, dstPt);
m_changes.emplace_back(static_cast<uint32_t>(idx), p.colorIdx, getPixel(ss, m_img.bpp, idx));
}
}
}
ox::Error CutPasteCommand::redo() noexcept {
auto &subsheet = getSubSheet(m_img, m_subSheetIdx);
for (const auto &c : m_changes) {
setPixel(subsheet, m_img.bpp, c.idx, static_cast<uint8_t>(c.newPalIdx));
}
return {};
}
ox::Error CutPasteCommand::undo() noexcept {
auto &subsheet = getSubSheet(m_img, m_subSheetIdx);
for (const auto &c : m_changes) {
setPixel(subsheet, m_img.bpp, c.idx, static_cast<uint8_t>(c.oldPalIdx));
}
return {};
}
int CutPasteCommand::commandId() const noexcept {
return static_cast<int>(m_commandId);
}
TileSheet::SubSheetIdx const&CutPasteCommand::subsheetIdx() const noexcept {
return m_subSheetIdx;
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <turbine/clipboard.hpp>
#include "commands.hpp"
namespace nostalgia::gfx {
OX_MODEL_FWD_DECL(class TileSheetClipboard);
class TileSheetClipboard: public turbine::ClipboardObject<TileSheetClipboard> {
public:
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.studio.TileSheetClipboard";
static constexpr auto TypeVersion = 1;
OX_MODEL_FRIEND(TileSheetClipboard);
struct Pixel {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.studio.TileSheetClipboard.Pixel";
static constexpr auto TypeVersion = 1;
uint16_t colorIdx = 0;
ox::Point pt;
Pixel(uint16_t pColorIdx, ox::Point pPt) noexcept;
};
protected:
ox::Vector<Pixel> m_pixels;
public:
void addPixel(ox::Point const&pt, uint16_t colorIdx) noexcept;
[[nodiscard]]
ox::Vector<Pixel> const&pixels() const noexcept;
};
OX_MODEL_BEGIN(TileSheetClipboard::Pixel)
OX_MODEL_FIELD(colorIdx)
OX_MODEL_FIELD(pt)
OX_MODEL_END()
OX_MODEL_BEGIN(TileSheetClipboard)
OX_MODEL_FIELD_RENAME(m_pixels, pixels)
OX_MODEL_END()
class CutPasteCommand: public TileSheetCommand {
private:
struct Change {
uint32_t idx = 0;
uint16_t newPalIdx = 0;
uint16_t oldPalIdx = 0;
constexpr Change(uint32_t pIdx, uint16_t pNewPalIdx, uint16_t pOldPalIdx) noexcept {
idx = pIdx;
newPalIdx = pNewPalIdx;
oldPalIdx = pOldPalIdx;
}
};
CommandId m_commandId;
TileSheet &m_img;
TileSheet::SubSheetIdx m_subSheetIdx;
ox::Vector<Change> m_changes;
public:
CutPasteCommand(
CommandId commandId,
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
ox::Point const&dstStart,
ox::Point dstEnd,
TileSheetClipboard const&cb) noexcept;
ox::Error redo() noexcept final;
ox::Error undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/std/algorithm.hpp>
#include "deletetilescommand.hpp"
namespace nostalgia::gfx {
gfx::DeleteTilesCommand::DeleteTilesCommand(
TileSheet &img,
TileSheet::SubSheetIdx idx,
std::size_t tileIdx,
std::size_t tileCnt) noexcept:
m_img(img),
m_idx(std::move(idx)) {
const unsigned bytesPerTile = m_img.bpp == 4 ? PixelsPerTile / 2 : PixelsPerTile;
m_deletePos = tileIdx * bytesPerTile;
m_deleteSz = tileCnt * bytesPerTile;
m_deletedPixels.resize(m_deleteSz);
// copy pixels to be erased
{
auto &s = getSubSheet(m_img, m_idx);
auto dst = m_deletedPixels.begin();
auto src = s.pixels.begin() + m_deletePos;
ox::copy_n(src, m_deleteSz, dst);
}
}
ox::Error gfx::DeleteTilesCommand::redo() noexcept {
auto &s = getSubSheet(m_img, m_idx);
auto &p = s.pixels;
auto srcPos = m_deletePos + m_deleteSz;
auto const src = &p[srcPos];
auto const dst1 = &p[m_deletePos];
auto const dst2 = &p[(p.size() - m_deleteSz)];
ox::memmove(dst1, src, p.size() - srcPos);
ox::memset(dst2, 0, m_deleteSz * sizeof(decltype(p[0])));
return {};
}
ox::Error DeleteTilesCommand::undo() noexcept {
auto &s = getSubSheet(m_img, m_idx);
auto &p = s.pixels;
auto const src = &p[m_deletePos];
auto const dst1 = &p[m_deletePos + m_deleteSz];
auto const dst2 = src;
auto const sz = p.size() - m_deletePos - m_deleteSz;
ox::memmove(dst1, src, sz);
ox::memcpy(dst2, m_deletedPixels.data(), m_deletedPixels.size());
return {};
}
int DeleteTilesCommand::commandId() const noexcept {
return static_cast<int>(CommandId::DeleteTile);
}
TileSheet::SubSheetIdx const&DeleteTilesCommand::subsheetIdx() const noexcept {
return m_idx;
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "commands.hpp"
namespace nostalgia::gfx {
class DeleteTilesCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
std::size_t m_deletePos = 0;
std::size_t m_deleteSz = 0;
ox::Vector<uint8_t> m_deletedPixels = {};
public:
DeleteTilesCommand(
TileSheet &img,
TileSheet::SubSheetIdx idx,
std::size_t tileIdx,
std::size_t tileCnt) noexcept;
ox::Error redo() noexcept final;
ox::Error undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "drawcommand.hpp"
namespace nostalgia::gfx {
DrawCommand::DrawCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
std::size_t idx,
int palIdx) noexcept:
m_img(img),
m_subSheetIdx(std::move(subSheetIdx)),
m_palIdx(palIdx) {
auto &subsheet = getSubSheet(m_img, m_subSheetIdx);
m_changes.emplace_back(static_cast<uint32_t>(idx), getPixel(subsheet, m_img.bpp, idx));
}
DrawCommand::DrawCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
ox::Vector<std::size_t> const&idxList,
int palIdx) noexcept:
m_img(img),
m_subSheetIdx(std::move(subSheetIdx)),
m_palIdx(palIdx) {
auto &subsheet = getSubSheet(m_img, m_subSheetIdx);
for (auto const idx : idxList) {
m_changes.emplace_back(static_cast<uint32_t>(idx), getPixel(subsheet, m_img.bpp, idx));
}
}
bool DrawCommand::append(std::size_t idx) noexcept {
auto &subsheet = getSubSheet(m_img, m_subSheetIdx);
if (m_changes.back().value->idx != idx && getPixel(subsheet, m_img.bpp, idx) != m_palIdx) {
// duplicate entries are bad
auto existing = ox::find_if(m_changes.cbegin(), m_changes.cend(), [idx](auto const&c) {
return c.idx == idx;
});
if (existing == m_changes.cend()) {
m_changes.emplace_back(static_cast<uint32_t>(idx), getPixel(subsheet, m_img.bpp, idx));
setPixel(subsheet, m_img.bpp, idx, static_cast<uint8_t>(m_palIdx));
return true;
}
}
return false;
}
bool DrawCommand::append(ox::Vector<std::size_t> const&idxList) noexcept {
auto out = false;
for (auto idx : idxList) {
out = append(idx) || out;
}
return out;
}
ox::Error DrawCommand::redo() noexcept {
auto &subsheet = getSubSheet(m_img, m_subSheetIdx);
for (auto const&c : m_changes) {
setPixel(subsheet, m_img.bpp, c.idx, static_cast<uint8_t>(m_palIdx));
}
return {};
}
ox::Error DrawCommand::undo() noexcept {
auto &subsheet = getSubSheet(m_img, m_subSheetIdx);
for (auto const&c : m_changes) {
setPixel(subsheet, m_img.bpp, c.idx, static_cast<uint8_t>(c.oldPalIdx));
}
return {};
}
int DrawCommand::commandId() const noexcept {
return static_cast<int>(CommandId::Draw);
}
TileSheet::SubSheetIdx const&DrawCommand::subsheetIdx() const noexcept {
return m_subSheetIdx;
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "commands.hpp"
namespace nostalgia::gfx {
class DrawCommand: public TileSheetCommand {
private:
struct Change {
uint32_t idx = 0;
uint16_t oldPalIdx = 0;
constexpr Change(uint32_t pIdx, uint16_t pOldPalIdx) noexcept {
idx = pIdx;
oldPalIdx = pOldPalIdx;
}
};
TileSheet &m_img;
TileSheet::SubSheetIdx m_subSheetIdx;
ox::Vector<Change, 8> m_changes;
int m_palIdx = 0;
public:
DrawCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
std::size_t idx,
int palIdx) noexcept;
DrawCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
ox::Vector<std::size_t> const&idxList,
int palIdx) noexcept;
bool append(std::size_t idx) noexcept;
bool append(ox::Vector<std::size_t> const&idxList) noexcept;
ox::Error redo() noexcept final;
ox::Error undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "inserttilescommand.hpp"
namespace nostalgia::gfx {
InsertTilesCommand::InsertTilesCommand(
TileSheet &img,
TileSheet::SubSheetIdx idx,
std::size_t tileIdx,
std::size_t tileCnt) noexcept:
m_img(img),
m_idx(std::move(idx)) {
const unsigned bytesPerTile = m_img.bpp == 4 ? PixelsPerTile / 2 : PixelsPerTile;
m_insertPos = tileIdx * bytesPerTile;
m_insertCnt = tileCnt * bytesPerTile;
m_deletedPixels.resize(m_insertCnt);
// copy pixels to be erased
{
auto &s = getSubSheet(m_img, m_idx);
auto &p = s.pixels;
auto dst = m_deletedPixels.begin();
auto src = p.begin() + p.size() - m_insertCnt;
ox::copy_n(src, m_insertCnt, dst);
}
}
ox::Error InsertTilesCommand::redo() noexcept {
auto &s = getSubSheet(m_img, m_idx);
auto &p = s.pixels;
auto dstPos = m_insertPos + m_insertCnt;
auto const src = &p[m_insertPos];
if (dstPos < p.size()) {
auto const dst = &p[dstPos];
ox::memmove(dst, src, p.size() - dstPos);
}
ox::memset(src, 0, m_insertCnt * sizeof(decltype(p[0])));
return {};
}
ox::Error InsertTilesCommand::undo() noexcept {
auto &s = getSubSheet(m_img, m_idx);
auto &p = s.pixels;
auto const dst1 = &p[m_insertPos];
auto const dst2 = &p[p.size() - m_insertCnt];
auto const srcIdx = m_insertPos + m_insertCnt;
if (srcIdx < p.size()) {
auto const sz = p.size() - srcIdx;
auto const src = &p[srcIdx];
ox::memmove(dst1, src, sz);
}
ox::memcpy(dst2, m_deletedPixels.data(), m_deletedPixels.size());
return {};
}
int InsertTilesCommand::commandId() const noexcept {
return static_cast<int>(CommandId::InsertTile);
}
TileSheet::SubSheetIdx const&InsertTilesCommand::subsheetIdx() const noexcept {
return m_idx;
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "commands.hpp"
namespace nostalgia::gfx {
class InsertTilesCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
std::size_t m_insertPos = 0;
std::size_t m_insertCnt = 0;
ox::Vector<uint8_t> m_deletedPixels = {};
public:
InsertTilesCommand(
TileSheet &img,
TileSheet::SubSheetIdx idx,
std::size_t tileIdx,
std::size_t tileCnt) noexcept;
ox::Error redo() noexcept final;
ox::Error undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "palettechangecommand.hpp"
namespace nostalgia::gfx {
gfx::PaletteChangeCommand::PaletteChangeCommand(
TileSheet::SubSheetIdx idx,
TileSheet &img,
ox::StringViewCR newPalette) noexcept:
m_img(img),
m_idx(std::move(idx)),
m_oldPalette(m_img.defaultPalette),
m_newPalette(ox::FileAddress(ox::sfmt<ox::IString<43>>("uuid://{}", newPalette))) {
}
ox::Error PaletteChangeCommand::redo() noexcept {
m_img.defaultPalette = m_newPalette;
return {};
}
ox::Error PaletteChangeCommand::undo() noexcept {
m_img.defaultPalette = m_oldPalette;
return {};
}
int PaletteChangeCommand::commandId() const noexcept {
return static_cast<int>(CommandId::PaletteChange);
}
TileSheet::SubSheetIdx const&PaletteChangeCommand::subsheetIdx() const noexcept {
return m_idx;
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "commands.hpp"
namespace nostalgia::gfx {
class PaletteChangeCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
ox::FileAddress m_oldPalette;
ox::FileAddress m_newPalette;
public:
PaletteChangeCommand(
TileSheet::SubSheetIdx idx,
TileSheet &img,
ox::StringViewCR newPalette) noexcept;
ox::Error redo() noexcept final;
ox::Error undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "rmsubsheetcommand.hpp"
namespace nostalgia::gfx {
gfx::RmSubSheetCommand::RmSubSheetCommand(TileSheet &img, TileSheet::SubSheetIdx idx) noexcept:
m_img(img),
m_idx(std::move(idx)),
m_parentIdx(m_idx) {
m_parentIdx.pop_back();
}
ox::Error RmSubSheetCommand::redo() noexcept {
auto &parent = getSubSheet(m_img, m_parentIdx);
m_sheet = std::move(parent.subsheets[*m_idx.back().value]);
OX_RETURN_ERROR(parent.subsheets.erase(*m_idx.back().value).error);
return {};
}
ox::Error RmSubSheetCommand::undo() noexcept {
auto &parent = getSubSheet(m_img, m_parentIdx);
auto const i = *m_idx.back().value;
parent.subsheets.insert(i, std::move(m_sheet));
return {};
}
int RmSubSheetCommand::commandId() const noexcept {
return static_cast<int>(CommandId::RmSubSheet);
}
TileSheet::SubSheetIdx const&RmSubSheetCommand::subsheetIdx() const noexcept {
return m_idx;
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "commands.hpp"
namespace nostalgia::gfx {
class RmSubSheetCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
TileSheet::SubSheetIdx m_parentIdx;
TileSheet::SubSheet m_sheet;
public:
RmSubSheetCommand(TileSheet &img, TileSheet::SubSheetIdx idx) noexcept;
ox::Error redo() noexcept final;
ox::Error undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "updatesubsheetcommand.hpp"
namespace nostalgia::gfx {
gfx::UpdateSubSheetCommand::UpdateSubSheetCommand(
TileSheet &img,
TileSheet::SubSheetIdx idx,
ox::StringParam name,
int const cols,
int const rows):
m_img{img},
m_idx{std::move(idx)},
m_sheet{getSubSheet(m_img, m_idx)} {
m_sheet = getSubSheet(m_img, m_idx);
m_sheet.name = std::move(name);
OX_THROW_ERROR(resizeSubsheet(m_sheet, m_img.bpp, {cols, rows}));
}
ox::Error UpdateSubSheetCommand::redo() noexcept {
std::swap(m_sheet, getSubSheet(m_img, m_idx));
return {};
}
ox::Error UpdateSubSheetCommand::undo() noexcept {
std::swap(m_sheet, getSubSheet(m_img, m_idx));
return {};
}
int UpdateSubSheetCommand::commandId() const noexcept {
return static_cast<int>(CommandId::UpdateSubSheet);
}
TileSheet::SubSheetIdx const&UpdateSubSheetCommand::subsheetIdx() const noexcept {
return m_idx;
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "commands.hpp"
namespace nostalgia::gfx {
class UpdateSubSheetCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
TileSheet::SubSheet m_sheet;
public:
UpdateSubSheetCommand(
TileSheet &img,
TileSheet::SubSheetIdx idx,
ox::StringParam name,
int cols,
int rows);
ox::Error redo() noexcept final;
ox::Error undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@@ -0,0 +1,598 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <imgui.h>
#include <lodepng.h>
#include <ox/std/point.hpp>
#include <keel/media.hpp>
#include <studio/studio.hpp>
#include "tilesheeteditor-imgui.hpp"
namespace nostalgia::gfx {
namespace ig = studio::ig;
struct TileSheetEditorConfig {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.studio.TileSheetEditorConfig";
static constexpr auto TypeVersion = 1;
TileSheet::SubSheetIdx activeSubsheet{};
};
OX_MODEL_BEGIN(TileSheetEditorConfig)
OX_MODEL_FIELD_RENAME(activeSubsheet, active_subsheet)
OX_MODEL_END()
static ox::Vector<uint32_t> normalizePixelSizes(
ox::Vector<uint8_t> const&inPixels,
int const bpp) noexcept {
uint_t const bytesPerTile = bpp == 8 ? PixelsPerTile : PixelsPerTile / 2;
ox::Vector<uint32_t> outPixels;
if (bytesPerTile == 64) { // 8 BPP
outPixels.resize(inPixels.size());
for (std::size_t i = 0; i < inPixels.size(); ++i) {
outPixels[i] = inPixels[i];
}
} else { // 4 BPP
outPixels.resize(inPixels.size() * 2);
for (std::size_t i = 0; i < inPixels.size(); ++i) {
outPixels[i * 2 + 0] = inPixels[i] & 0xF;
outPixels[i * 2 + 1] = inPixels[i] >> 4;
}
}
return outPixels;
}
static ox::Vector<uint32_t> normalizePixelArrangement(
ox::Vector<uint32_t> const&inPixels,
int cols,
int scale) {
auto const scalePt = ox::Point{scale, scale};
auto const width = cols * TileWidth;
auto const height = static_cast<int>(inPixels.size()) / width;
auto const dstWidth = width * scale;
ox::Vector<uint32_t> outPixels(static_cast<size_t>((width * scale) * (height * scale)));
for (std::size_t dstIdx = 0; dstIdx < outPixels.size(); ++dstIdx) {
auto const dstPt = ox::Point{
static_cast<int>(dstIdx) % dstWidth,
static_cast<int>(dstIdx) / dstWidth};
auto const srcPt = dstPt / scalePt;
auto const srcIdx = ptToIdx(srcPt, cols);
outPixels[dstIdx] = inPixels[srcIdx];
}
return outPixels;
}
static ox::Error toPngFile(
ox::CStringView const&path,
ox::Vector<uint32_t> &&pixels,
Palette const&pal,
size_t page,
unsigned width,
unsigned height) noexcept {
for (auto &c : pixels) {
c = color32(color(pal, page, c)) | static_cast<Color32>(0XFF << 24);
}
constexpr auto fmt = LCT_RGBA;
return ox::Error(static_cast<ox::ErrorCode>(
lodepng_encode_file(
path.c_str(),
reinterpret_cast<uint8_t const*>(pixels.data()),
width,
height,
fmt,
8)));
}
TileSheetEditorImGui::TileSheetEditorImGui(studio::StudioContext &sctx, ox::StringParam path):
Editor(std::move(path)),
m_sctx(sctx),
m_tctx(m_sctx.tctx),
m_view(m_sctx, itemPath(), *undoStack()),
m_model(m_view.model()) {
std::ignore = setPaletteSelection();
// connect signal/slots
m_subsheetEditor.inputSubmitted.connect(this, &TileSheetEditorImGui::updateActiveSubsheet);
m_exportMenu.inputSubmitted.connect(this, &TileSheetEditorImGui::exportSubhseetToPng);
m_model.paletteChanged.connect(this, &TileSheetEditorImGui::setPaletteSelection);
// load config
auto const&config = studio::readConfig<TileSheetEditorConfig>(
keelCtx(m_sctx), itemPath());
if (config.ok()) {
m_model.setActiveSubsheet(validateSubSheetIdx(m_model.img(), config.value.activeSubsheet));
}
}
void TileSheetEditorImGui::exportFile() {
m_exportMenu.show();
}
void TileSheetEditorImGui::cut() {
m_model.cut();
}
void TileSheetEditorImGui::copy() {
m_model.copy();
}
void TileSheetEditorImGui::paste() {
m_model.paste();
}
bool TileSheetEditorImGui::acceptsClipboardPayload() const noexcept {
return m_model.acceptsClipboardPayload();
}
void TileSheetEditorImGui::keyStateChanged(turbine::Key key, bool down) {
if (!down) {
return;
}
if (key == turbine::Key::Escape) {
m_subsheetEditor.close();
m_exportMenu.close();
}
auto const popupOpen = m_subsheetEditor.isOpen() || m_exportMenu.isOpen();
auto const pal = m_model.pal();
if (!popupOpen) {
auto const colorCnt = gfx::colorCnt(pal, m_model.palettePage());
if (key == turbine::Key::Alpha_D) {
m_tool = TileSheetTool::Draw;
setCopyEnabled(false);
setCutEnabled(false);
setPasteEnabled(false);
m_model.clearSelection();
} else if (key == turbine::Key::Alpha_S) {
m_tool = TileSheetTool::Select;
setCopyEnabled(true);
setCutEnabled(true);
setPasteEnabled(true);
} else if (key == turbine::Key::Alpha_F) {
m_tool = TileSheetTool::Fill;
setCopyEnabled(false);
setCutEnabled(false);
setPasteEnabled(false);
m_model.clearSelection();
} else if (key >= turbine::Key::Num_1 && key <= turbine::Key::Num_9) {
if (turbine::buttonDown(m_tctx, turbine::Key::Mod_Alt)) {
auto const idx = ox::min<std::size_t>(
static_cast<uint32_t>(key - turbine::Key::Num_1),
m_model.pal().pages.size() - 1);
m_model.setPalettePage(idx);
} else if (key <= turbine::Key::Num_9) {
auto const idx = ox::min<std::size_t>(
static_cast<uint32_t>(key - turbine::Key::Num_1),
colorCnt - 1);
m_view.setPalIdx(idx);
}
} else if (key == turbine::Key::Num_0) {
if (turbine::buttonDown(m_tctx, turbine::Key::Mod_Alt)) {
auto const idx = ox::min<std::size_t>(
static_cast<uint32_t>(key - turbine::Key::Num_1 + 9),
m_model.pal().pages.size() - 1);
m_model.setPalettePage(idx);
} else {
auto const idx = ox::min<std::size_t>(
static_cast<uint32_t>(key - turbine::Key::Num_1 + 9),
colorCnt - 1);
m_view.setPalIdx(idx);
}
}
}
}
void TileSheetEditorImGui::draw(studio::StudioContext&) noexcept {
auto const popupOpen = m_subsheetEditor.isOpen() || m_exportMenu.isOpen();
if (!popupOpen && m_tool == TileSheetTool::Select) {
if (ImGui::IsKeyDown(ImGuiKey_ModCtrl)) {
if (ImGui::IsKeyPressed(ImGuiKey_A)) {
auto const&img = m_model.activeSubSheet();
m_model.setSelection({{}, {img.columns * TileWidth - 1, img.rows * TileHeight - 1}});
} else if (ImGui::IsKeyPressed(ImGuiKey_G)) {
m_model.clearSelection();
}
}
}
auto const paneSize = ImGui::GetContentRegionAvail();
auto const tileSheetParentSize = ImVec2{paneSize.x - s_palViewWidth, paneSize.y};
auto const fbSize = ox::Vec2{tileSheetParentSize.x - 16, tileSheetParentSize.y - 16};
ImGui::BeginChild("TileSheetView", tileSheetParentSize, true);
{
drawTileSheet(fbSize);
}
ImGui::EndChild();
ImGui::SameLine();
ImGui::BeginChild("Controls", {s_palViewWidth - 8, paneSize.y}, true);
{
auto const controlsSize = ImGui::GetContentRegionAvail();
ImGui::BeginChild("ToolBox", {s_palViewWidth - 24, 30}, true);
{
auto const btnSz = ImVec2{45, 14};
if (ImGui::Selectable("Select", m_tool == TileSheetTool::Select, 0, btnSz)) {
m_tool = TileSheetTool::Select;
}
ImGui::SameLine();
if (ImGui::Selectable("Draw", m_tool == TileSheetTool::Draw, 0, btnSz)) {
m_tool = TileSheetTool::Draw;
m_model.clearSelection();
}
ImGui::SameLine();
if (ImGui::Selectable("Fill", m_tool == TileSheetTool::Fill, 0, btnSz)) {
m_tool = TileSheetTool::Fill;
m_model.clearSelection();
}
}
ImGui::EndChild();
auto const ySize = controlsSize.y - 38;
// draw palette/color picker
ImGui::BeginChild("Palette", {s_palViewWidth - 24, ySize / 2.f}, true);
{
drawPaletteMenu();
}
ImGui::EndChild();
ImGui::BeginChild("SubSheets", {s_palViewWidth - 24, ySize / 2.f}, true);
{
static constexpr auto btnHeight = ig::BtnSz.y;
auto constexpr btnSize = ImVec2{btnHeight, btnHeight};
if (ig::PushButton("+", btnSize)) {
auto insertOnIdx = m_model.activeSubSheetIdx();
auto const&parent = m_model.activeSubSheet();
m_model.addSubsheet(insertOnIdx);
insertOnIdx.emplace_back(parent.subsheets.size() - 1);
setActiveSubsheet(insertOnIdx);
}
ImGui::SameLine();
if (ig::PushButton("-", btnSize)) {
auto const&activeSubsheetIdx = m_model.activeSubSheetIdx();
if (!activeSubsheetIdx.empty()) {
m_model.rmSubsheet(activeSubsheetIdx);
}
}
ImGui::SameLine();
if (ig::PushButton("Edit")) {
showSubsheetEditor();
}
ImGui::SameLine();
if (ig::PushButton("Export")) {
m_exportMenu.show();
}
TileSheet::SubSheetIdx path;
static constexpr auto flags =
ImGuiTableFlags_RowBg |
ImGuiTableFlags_NoBordersInBody |
ImGuiTableFlags_ScrollY;
if (ImGui::BeginTable("Subsheets", 4, flags)) {
ImGui::TableSetupColumn("Subsheet", ImGuiTableColumnFlags_NoHide);
ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 25);
ImGui::TableSetupColumn("Columns", ImGuiTableColumnFlags_WidthFixed, 50);
ImGui::TableSetupColumn("Rows", ImGuiTableColumnFlags_WidthFixed, 50);
ImGui::TableHeadersRow();
drawSubsheetSelector(m_view.img().subsheet, path);
ImGui::EndTable();
}
}
ImGui::EndChild();
}
ImGui::EndChild();
m_subsheetEditor.draw(m_tctx);
m_exportMenu.draw(m_tctx);
}
void TileSheetEditorImGui::drawSubsheetSelector(
TileSheet::SubSheet &subsheet, TileSheet::SubSheetIdx &path) {
constexpr auto indentReduce = 14;
ImGui::TableNextRow(0, 5);
using Str = ox::BasicString<100>;
auto pathStr = ox::join<Str>("##", path).value;
auto lbl = ox::sfmt<Str>("{}##{}", subsheet.name, pathStr);
auto const rowSelected = path == m_model.activeSubSheetIdx();
auto const flags = ImGuiTreeNodeFlags_SpanFullWidth
| ImGuiTreeNodeFlags_OpenOnArrow
| ImGuiTreeNodeFlags_DefaultOpen
| (subsheet.subsheets.empty() ? ImGuiTreeNodeFlags_Leaf : 0)
| (rowSelected ? ImGuiTreeNodeFlags_Selected : 0);
ImGui::TableNextColumn();
auto const open = ImGui::TreeNodeEx(lbl.c_str(), flags);
ImGui::SameLine();
if (ImGui::IsItemClicked()) {
setActiveSubsheet(path);
}
if (ImGui::IsMouseDoubleClicked(0) && ImGui::IsItemHovered()) {
showSubsheetEditor();
}
ImGui::TableNextColumn();
ImGui::Text("%d", subsheet.id);
if (subsheet.subsheets.empty()) {
ImGui::TableNextColumn();
ImGui::Text("%d", subsheet.columns);
ImGui::TableNextColumn();
ImGui::Text("%d", subsheet.rows);
} else {
ImGui::TableNextColumn();
ImGui::Text("--");
ImGui::TableNextColumn();
ImGui::Text("--");
}
if (open) {
for (auto i = 0ul; auto &child : subsheet.subsheets) {
path.push_back(i);
ImGui::PushID(static_cast<int>(i));
ig::IndentStackItem const indentStackItem{-indentReduce};
drawSubsheetSelector(child, path);
ImGui::PopID();
path.pop_back();
++i;
}
ImGui::TreePop();
}
}
[[nodiscard]]
ox::Vec2 TileSheetEditorImGui::clickPos(ImVec2 const&winPos, ox::Vec2 clickPos) noexcept {
clickPos.x -= winPos.x + 10;
clickPos.y -= winPos.y + 10;
return clickPos;
}
ox::Error TileSheetEditorImGui::saveItem() noexcept {
return m_model.saveFile();
}
void TileSheetEditorImGui::showSubsheetEditor() noexcept {
auto const&sheet = m_model.activeSubSheet();
if (!sheet.subsheets.empty()) {
m_subsheetEditor.show(sheet.name, -1, -1);
} else {
m_subsheetEditor.show(sheet.name, sheet.columns, sheet.rows);
}
}
ox::Error TileSheetEditorImGui::exportSubhseetToPng(int const scale) const noexcept {
OX_REQUIRE(path, studio::saveFile({{"PNG", "png"}}));
// subsheet to png
auto const&img = m_model.img();
auto const&s = m_model.activeSubSheet();
auto const&pal = m_model.pal();
auto const width = s.columns * TileWidth;
auto const height = s.rows * TileHeight;
auto pixels = normalizePixelSizes(s.pixels, img.bpp);
pixels = normalizePixelArrangement(pixels, s.columns, scale);
auto const err = toPngFile(
path,
std::move(pixels),
pal,
m_model.palettePage(),
static_cast<unsigned>(width * scale),
static_cast<unsigned>(height * scale));
if (err) {
oxErrorf("Tilesheet export failed: {}", toStr(err));
}
return err;
}
void TileSheetEditorImGui::drawTileSheet(ox::Vec2 const&fbSize) noexcept {
auto const winPos = ImGui::GetWindowPos();
auto const fbSizei = ox::Size{static_cast<int>(fbSize.x), static_cast<int>(fbSize.y)};
if (m_framebuffer.width != fbSizei.width || m_framebuffer.height != fbSizei.height) {
glutils::resizeInitFrameBuffer(m_framebuffer, fbSizei.width, fbSizei.height);
m_view.resizeView(fbSize);
} else if (m_view.updated()) {
m_view.ackUpdate();
}
{
glutils::FrameBufferBind const frameBufferBind(m_framebuffer);
// clear screen and draw
glViewport(0, 0, fbSizei.width, fbSizei.height);
m_view.draw();
}
ImGui::Image(
ig::toImTextureID(m_framebuffer.color.id),
static_cast<ImVec2>(fbSize),
{0, 1},
{1, 0});
// handle input, this must come after drawing
auto const&io = ImGui::GetIO();
auto const mousePos = ox::Vec2{ImGui::GetMousePos()};
if (ImGui::IsItemHovered()) {
auto const wheel = io.MouseWheel;
auto const wheelh = io.MouseWheelH;
if (wheel != 0) {
m_view.scrollV(fbSize, wheel, ImGui::IsKeyDown(ImGuiKey_ModCtrl));
}
if (wheelh != 0) {
m_view.scrollH(fbSize, wheelh);
}
if (ImGui::IsMouseDown(0) && m_prevMouseDownPos != mousePos) {
m_prevMouseDownPos = mousePos;
switch (m_tool) {
case TileSheetTool::Draw:
m_view.clickDraw(fbSize, clickPos(winPos, mousePos));
break;
case TileSheetTool::Fill:
m_view.clickFill(fbSize, clickPos(winPos, mousePos));
break;
case TileSheetTool::Select:
m_view.clickSelect(fbSize, clickPos(winPos, mousePos));
break;
case TileSheetTool::None:
break;
}
}
}
if (ImGui::BeginPopupContextItem("TileMenu", ImGuiPopupFlags_MouseButtonRight)) {
auto const popupPos = ox::Vec2{ImGui::GetWindowPos()};
if (ImGui::MenuItem("Insert Tile")) {
m_view.insertTile(fbSize, clickPos(winPos, popupPos));
}
if (ImGui::MenuItem("Delete Tile")) {
m_view.deleteTile(fbSize, clickPos(winPos, popupPos));
}
ImGui::EndPopup();
}
if (io.MouseReleased[0]) {
m_prevMouseDownPos = {-1, -1};
m_view.releaseMouseButton(m_tool);
}
}
void TileSheetEditorImGui::drawPaletteMenu() noexcept {
auto constexpr comboWidthSub = 62;
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - comboWidthSub);
auto constexpr palTags = ImGuiInputTextFlags_ReadOnly;
if (ig::InputText("Palette", m_selectedPalette, palTags)) {
oxLogError(m_model.setPalette(m_selectedPalette));
}
if (ig::DragDropTarget const dragDropTarget; dragDropTarget) {
auto const [ref, err] = ig::getDragDropPayload<studio::FileRef>("FileRef");
if (!err && endsWith(ref.path, FileExt_npal)) {
if (ref.path != m_selectedPalette) {
oxLogError(m_model.setPalette(ref.path));
}
}
}
auto const pages = m_model.pal().pages.size();
if (pages > 1) {
ig::IndentStackItem const indentStackItem{20};
using Str = ox::IString<55>;
auto numStr = ox::sfmt<Str>(
"{} - {}", m_model.palettePage() + 1, m_model.pal().pages[m_model.palettePage()].name);
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - comboWidthSub);
if (ImGui::BeginCombo("Page", numStr.c_str(), 0)) {
for (auto n = 0u; n < pages; ++n) {
auto const selected = (m_model.palettePage() == n);
numStr = ox::sfmt<Str>("{} - {}", n + 1, m_model.pal().pages[n].name);
if (ImGui::Selectable(numStr.c_str(), selected) && m_model.palettePage() != n) {
m_model.setPalettePage(n);
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
}
// header
auto constexpr palTblFlags =
ImGuiTableFlags_RowBg |
ImGuiTableFlags_SizingStretchProp |
ImGuiTableFlags_ScrollY;
if (ImGui::BeginTable(
"PaletteTable", 4, palTblFlags)) {
ImGui::TableSetupColumn("Idx", 0, 0.6f);
ImGui::TableSetupColumn("", 0, 0.22f);
ImGui::TableSetupColumn("Name", 0, 3);
ImGui::TableSetupColumn("Color16", 0, 3);
ImGui::TableHeadersRow();
{
auto const&pal = m_model.pal();
if (pal.pages.size() > m_model.palettePage()) {
for (auto i = 0u; auto const&c: pal.pages[m_model.palettePage()].colors) {
ImGui::TableNextRow();
ImGui::PushID(static_cast<int>(i));
// Column: color idx
ImGui::TableNextColumn();
auto const label = ox::itoa(i + 1);
auto const rowSelected = i == m_view.palIdx();
if (ImGui::Selectable(
label.c_str(), rowSelected, ImGuiSelectableFlags_SpanAllColumns)) {
m_view.setPalIdx(i);
}
// Column: color RGB
ImGui::TableNextColumn();
auto ic = ImGui::GetColorU32(ImVec4(redf(c), greenf(c), bluef(c), 1));
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, ic);
ImGui::TableNextColumn();
auto const&name = i < pal.colorNames.size() ? pal.colorNames[i].c_str() : "";
ImGui::Text("%s", name);
ImGui::TableNextColumn();
ImGui::Text("(%02d, %02d, %02d)", red16(c), green16(c), blue16(c));
ImGui::PopID();
++i;
}
}
}
ImGui::EndTable();
}
}
ox::Error TileSheetEditorImGui::updateActiveSubsheet(ox::StringView const&name, int cols, int rows) noexcept {
return m_model.updateSubsheet(m_model.activeSubSheetIdx(), name, cols, rows);
}
ox::Error TileSheetEditorImGui::setPaletteSelection() noexcept {
m_selectedPalette = m_model.palPath();
return {};
}
void TileSheetEditorImGui::setActiveSubsheet(TileSheet::SubSheetIdx path) noexcept {
m_model.setActiveSubsheet(path);
studio::editConfig<TileSheetEditorConfig>(keelCtx(m_sctx), itemPath(),
[&path](TileSheetEditorConfig &config) {
config.activeSubsheet = std::move(path);
});
}
void TileSheetEditorImGui::SubSheetEditor::show(ox::StringViewCR name, int const cols, int const rows) noexcept {
m_show = true;
m_name = name;
m_cols = cols;
m_rows = rows;
}
void TileSheetEditorImGui::SubSheetEditor::draw(turbine::Context &tctx) noexcept {
constexpr auto popupName = "Edit Subsheet";
if (!m_show) {
return;
}
auto const modSize = m_cols > 0;
auto constexpr popupWidth = 235.f;
auto const popupHeight = modSize ? 130.f : 85.f;
auto const popupSz = ImVec2{popupWidth, popupHeight};
if (ig::BeginPopup(tctx, popupName, m_show, popupSz)) {
ig::InputText("Name", m_name);
if (modSize) {
ImGui::InputInt("Columns", &m_cols);
ImGui::InputInt("Rows", &m_rows);
}
if (ig::PopupControlsOkCancel(popupWidth, m_show) == ig::PopupResponse::OK) {
inputSubmitted.emit(m_name, m_cols, m_rows);
}
ImGui::EndPopup();
}
}
void TileSheetEditorImGui::SubSheetEditor::close() noexcept {
m_show = false;
}
void TileSheetEditorImGui::ExportMenu::show() noexcept {
m_show = true;
m_scale = 5;
}
void TileSheetEditorImGui::ExportMenu::draw(turbine::Context &tctx) noexcept {
constexpr auto popupName = "Export Tile Sheet";
if (!m_show) {
return;
}
constexpr auto popupWidth = 235.f;
constexpr auto popupHeight = 85.f;
constexpr auto popupSz = ImVec2{popupWidth, popupHeight};
if (ig::BeginPopup(tctx, popupName, m_show, popupSz)) {
ImGui::InputInt("Scale", &m_scale);
m_scale = ox::clamp(m_scale, 1, 50);
if (ig::PopupControlsOkCancel(popupWidth, m_show) == ig::PopupResponse::OK) {
inputSubmitted.emit(m_scale);
}
ImGui::EndPopup();
}
}
void TileSheetEditorImGui::ExportMenu::close() noexcept {
m_show = false;
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/model/def.hpp>
#include <ox/std/vec.hpp>
#include <glutils/glutils.hpp>
#include <studio/editor.hpp>
#include "tilesheetpixelgrid.hpp"
#include "tilesheetpixels.hpp"
#include "tilesheeteditorview.hpp"
namespace nostalgia::gfx {
class TileSheetEditorImGui: public studio::Editor {
private:
class SubSheetEditor {
private:
ox::IString<100> m_name;
int m_cols = 0;
int m_rows = 0;
bool m_show = false;
public:
ox::Signal<ox::Error(ox::StringViewCR name, int cols, int rows)> inputSubmitted;
void show(ox::StringViewCR name, int cols, int rows) noexcept;
void draw(turbine::Context &sctx) noexcept;
void close() noexcept;
[[nodiscard]]
constexpr bool isOpen() const noexcept { return m_show; }
};
class ExportMenu {
private:
int m_scale = 0;
bool m_show = false;
public:
ox::Signal<ox::Error(int scale)> inputSubmitted;
void show() noexcept;
void draw(turbine::Context &sctx) noexcept;
void close() noexcept;
[[nodiscard]]
constexpr bool isOpen() const noexcept { return m_show; }
};
static constexpr float s_palViewWidth = 300;
ox::String m_selectedPalette;
studio::StudioContext &m_sctx;
turbine::Context &m_tctx;
ox::Vector<ox::String> m_paletteList;
SubSheetEditor m_subsheetEditor;
ExportMenu m_exportMenu;
glutils::FrameBuffer m_framebuffer;
TileSheetEditorView m_view;
TileSheetEditorModel &m_model;
ox::Vec2 m_prevMouseDownPos;
TileSheetTool m_tool = TileSheetTool::Draw;
public:
TileSheetEditorImGui(studio::StudioContext &sctx, ox::StringParam path);
~TileSheetEditorImGui() override = default;
void exportFile() override;
void cut() override;
void copy() override;
void paste() override;
[[nodiscard]]
bool acceptsClipboardPayload() const noexcept override;
void keyStateChanged(turbine::Key key, bool down) override;
void draw(studio::StudioContext&) noexcept override;
void drawSubsheetSelector(TileSheet::SubSheet&, TileSheet::SubSheetIdx &path);
[[nodiscard]]
static ox::Vec2 clickPos(ImVec2 const&winPos, ox::Vec2 clickPos) noexcept;
protected:
ox::Error saveItem() noexcept override;
private:
void showSubsheetEditor() noexcept;
ox::Error exportSubhseetToPng(int scale) const noexcept;
void drawTileSheet(ox::Vec2 const&fbSize) noexcept;
void drawPaletteMenu() noexcept;
ox::Error updateActiveSubsheet(ox::StringView const&name, int cols, int rows) noexcept;
ox::Error setPaletteSelection() noexcept;
// slots
private:
void setActiveSubsheet(TileSheet::SubSheetIdx path) noexcept;
};
}

View File

@@ -0,0 +1,330 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/claw/read.hpp>
#include <ox/std/algorithm.hpp>
#include <ox/std/buffer.hpp>
#include <ox/std/memory.hpp>
#include <turbine/clipboard.hpp>
#include <keel/media.hpp>
#include <nostalgia/gfx/ptidxconv.hpp>
#include "commands/commands.hpp"
#include "commands/addsubsheetcommand.hpp"
#include "commands/cutpastecommand.hpp"
#include "commands/deletetilescommand.hpp"
#include "commands/drawcommand.hpp"
#include "commands/inserttilescommand.hpp"
#include "commands/palettechangecommand.hpp"
#include "commands/rmsubsheetcommand.hpp"
#include "commands/updatesubsheetcommand.hpp"
#include "tilesheeteditormodel.hpp"
namespace nostalgia::gfx {
// delete pixels of all non-leaf nodes
static void normalizeSubsheets(TileSheet::SubSheet &ss) noexcept {
if (ss.subsheets.empty()) {
for (auto &child : ss.subsheets) {
normalizeSubsheets(child);
}
} else {
ss.pixels.clear();
}
}
Palette const TileSheetEditorModel::s_defaultPalette = {
.colorNames = {ox::Vector<ox::String>{{}}},
.pages = {{"Page 1", ox::Vector<Color16>(128)}},
};
TileSheetEditorModel::TileSheetEditorModel(
studio::StudioContext &sctx, ox::StringViewCR path, studio::UndoStack &undoStack):
m_sctx(sctx),
m_tctx(m_sctx.tctx),
m_path(path),
m_img(*readObj<TileSheet>(keelCtx(m_tctx), m_path).unwrapThrow()),
// ignore failure to load palette
m_pal(readObj<Palette>(keelCtx(m_tctx), m_img.defaultPalette).value),
m_undoStack(undoStack) {
normalizeSubsheets(m_img.subsheet);
m_pal.updated.connect(this, &TileSheetEditorModel::markUpdated);
m_undoStack.changeTriggered.connect(this, &TileSheetEditorModel::markUpdatedCmdId);
}
void TileSheetEditorModel::cut() {
if (!m_selection) {
return;
}
TileSheetClipboard blankCb;
auto cb = ox::make_unique<TileSheetClipboard>();
auto const&s = activeSubSheet();
iterateSelectionRows(*m_selection, [&](int const x, int const y) {
auto pt = ox::Point{x, y};
auto const idx = gfx::idx(s, pt);
auto const c = getPixel(s, m_img.bpp, idx);
pt -= m_selection->a;
cb->addPixel(pt, c);
blankCb.addPixel(pt, 0);
});
auto const pt1 = m_selection->a;
auto const pt2 = ox::Point{s.columns * TileWidth, s.rows * TileHeight};
turbine::setClipboardObject(m_tctx, std::move(cb));
pushCommand(ox::make<CutPasteCommand>(
CommandId::Cut, m_img, m_activeSubsSheetIdx, pt1, pt2, blankCb));
}
void TileSheetEditorModel::copy() {
if (!m_selection) {
return;
}
auto cb = ox::make_unique<TileSheetClipboard>();
iterateSelectionRows(*m_selection, [&](int const x, int const y) {
auto pt = ox::Point{x, y};
const auto&s = activeSubSheet();
const auto idx = gfx::idx(s, pt);
const auto c = getPixel(s, m_img.bpp, idx);
pt -= m_selection->a;
cb->addPixel(pt, c);
});
turbine::setClipboardObject(m_tctx, std::move(cb));
}
void TileSheetEditorModel::paste() {
if (!m_selection) {
return;
}
auto [cb, err] = turbine::getClipboardObject<TileSheetClipboard>(m_tctx);
if (err) {
oxLogError(err);
oxErrf("Could not read clipboard: {}", toStr(err));
return;
}
auto const&s = activeSubSheet();
auto const pt1 = m_selection->a;
auto const pt2 = ox::Point{s.columns * TileWidth, s.rows * TileHeight};
pushCommand(ox::make<CutPasteCommand>(
CommandId::Paste, m_img, m_activeSubsSheetIdx, pt1, pt2, *cb));
}
bool TileSheetEditorModel::acceptsClipboardPayload() const noexcept {
auto const cb = getClipboardObject<TileSheetClipboard>(m_tctx);
return cb.ok();
}
ox::StringView TileSheetEditorModel::palPath() const noexcept {
auto [path, err] = m_img.defaultPalette.getPath();
if (err) {
return {};
}
constexpr ox::StringView uuidPrefix = "uuid://";
if (ox::beginsWith(path, uuidPrefix)) {
auto const uuid = ox::StringView(&path[uuidPrefix.bytes()], path.bytes() - uuidPrefix.bytes());
auto const out = keelCtx(m_tctx).uuidToPath.at(uuid);
if (out.error) {
return {};
}
return *out.value;
} else {
return path;
}
}
ox::Error TileSheetEditorModel::setPalette(ox::StringViewCR path) noexcept {
OX_REQUIRE(uuid, keelCtx(m_tctx).pathToUuid.at(path));
pushCommand(ox::make<PaletteChangeCommand>(
activeSubSheetIdx(), m_img, uuid->toString()));
return {};
}
void TileSheetEditorModel::setPalettePage(size_t const pg) noexcept {
m_palettePage = ox::clamp<size_t>(pg, 0, m_pal->pages.size() - 1);
m_updated = true;
}
size_t TileSheetEditorModel::palettePage() const noexcept {
return m_palettePage;
}
void TileSheetEditorModel::drawCommand(ox::Point const&pt, std::size_t const palIdx) noexcept {
const auto &activeSubSheet = getSubSheet(m_img, m_activeSubsSheetIdx);
if (pt.x >= activeSubSheet.columns * TileWidth || pt.y >= activeSubSheet.rows * TileHeight) {
return;
}
const auto idx = gfx::idx(activeSubSheet, pt);
if (m_ongoingDrawCommand) {
m_updated = m_updated || m_ongoingDrawCommand->append(idx);
} else if (getPixel(activeSubSheet, m_img.bpp, idx) != palIdx) {
pushCommand(ox::make<DrawCommand>(
m_img, m_activeSubsSheetIdx, idx, static_cast<int>(palIdx)));
}
}
void TileSheetEditorModel::endDrawCommand() noexcept {
m_ongoingDrawCommand = nullptr;
}
void TileSheetEditorModel::addSubsheet(TileSheet::SubSheetIdx const&parentIdx) noexcept {
pushCommand(ox::make<AddSubSheetCommand>(m_img, parentIdx));
}
void TileSheetEditorModel::rmSubsheet(TileSheet::SubSheetIdx const&idx) noexcept {
pushCommand(ox::make<RmSubSheetCommand>(m_img, idx));
}
void TileSheetEditorModel::insertTiles(
TileSheet::SubSheetIdx const&idx, std::size_t const tileIdx, std::size_t const tileCnt) noexcept {
pushCommand(ox::make<InsertTilesCommand>(m_img, idx, tileIdx, tileCnt));
}
void TileSheetEditorModel::deleteTiles(
TileSheet::SubSheetIdx const&idx, std::size_t const tileIdx, std::size_t const tileCnt) noexcept {
pushCommand(ox::make<DeleteTilesCommand>(m_img, idx, tileIdx, tileCnt));
}
ox::Error TileSheetEditorModel::updateSubsheet(
TileSheet::SubSheetIdx const&idx, ox::StringViewCR name, int const cols, int const rows) noexcept {
OX_REQUIRE(cmd, ox::makeCatch<UpdateSubSheetCommand>(m_img, idx, name, cols, rows));
pushCommand(cmd);
return {};
}
void TileSheetEditorModel::setActiveSubsheet(TileSheet::SubSheetIdx const&idx) noexcept {
m_activeSubsSheetIdx = idx;
this->activeSubsheetChanged.emit(m_activeSubsSheetIdx);
}
void TileSheetEditorModel::fill(ox::Point const&pt, int const palIdx) noexcept {
auto const&activeSubSheet = getSubSheet(m_img, m_activeSubsSheetIdx);
// build idx list
if (pt.x >= activeSubSheet.columns * TileWidth || pt.y >= activeSubSheet.rows * TileHeight) {
return;
}
ox::Array<bool, PixelsPerTile> updateMap = {};
auto const oldColor = getPixel(activeSubSheet, m_img.bpp, pt);
getFillPixels(activeSubSheet, updateMap, pt, oldColor);
ox::Vector<std::size_t> idxList;
auto i = gfx::idx(activeSubSheet, pt) / PixelsPerTile * PixelsPerTile;
for (auto const u : updateMap) {
if (u) {
idxList.emplace_back(i);
}
++i;
}
// do updates to sheet
if (m_ongoingDrawCommand) {
m_updated = m_updated || m_ongoingDrawCommand->append(idxList);
} else if (getPixel(activeSubSheet, m_img.bpp, pt) != palIdx) {
pushCommand(ox::make<DrawCommand>(m_img, m_activeSubsSheetIdx, idxList, palIdx));
}
}
void TileSheetEditorModel::setSelection(studio::Selection const&sel) noexcept {
m_selection.emplace(sel);
m_updated = true;
}
void TileSheetEditorModel::select(ox::Point const&pt) noexcept {
if (m_selTracker.updateCursorPoint(pt)) {
setSelection(m_selTracker.selection());
}
}
void TileSheetEditorModel::completeSelection() noexcept {
if (m_selTracker.selectionOngoing()) {
m_selTracker.finishSelection();
m_selection.emplace(m_selTracker.selection());
auto&pt = m_selection->b;
auto const&s = activeSubSheet();
pt.x = ox::min(s.columns * TileWidth - 1, pt.x);
pt.y = ox::min(s.rows * TileHeight - 1, pt.y);
}
}
void TileSheetEditorModel::clearSelection() noexcept {
m_updated = true;
m_selTracker.reset();
m_selection.reset();
}
bool TileSheetEditorModel::updated() const noexcept {
return m_updated;
}
ox::Error TileSheetEditorModel::markUpdatedCmdId(studio::UndoCommand const*cmd) noexcept {
m_updated = true;
const auto cmdId = cmd->commandId();
if (static_cast<CommandId>(cmdId) == CommandId::PaletteChange) {
OX_RETURN_ERROR(readObj<Palette>(keelCtx(m_tctx), m_img.defaultPalette).moveTo(m_pal));
m_palettePage = ox::min<size_t>(m_pal->pages.size(), 0);
paletteChanged.emit();
}
auto tsCmd = dynamic_cast<const TileSheetCommand*>(cmd);
auto idx = validateSubSheetIdx(m_img, tsCmd->subsheetIdx());
if (idx != m_activeSubsSheetIdx) {
setActiveSubsheet(idx);
}
return {};
}
ox::Error TileSheetEditorModel::markUpdated() noexcept {
m_updated = true;
return {};
}
void TileSheetEditorModel::ackUpdate() noexcept {
m_updated = false;
}
ox::Error TileSheetEditorModel::saveFile() noexcept {
return m_sctx.project->writeObj(m_path, m_img, ox::ClawFormat::Metal);
}
bool TileSheetEditorModel::pixelSelected(std::size_t const idx) const noexcept {
auto const&s = activeSubSheet();
auto const pt = idxToPt(static_cast<int>(idx), s.columns);
return m_selection && m_selection->contains(pt);
}
void TileSheetEditorModel::getFillPixels(
TileSheet::SubSheet const&activeSubSheet,
ox::Span<bool> pixels,
ox::Point const&pt,
int const oldColor) const noexcept {
auto const idx = ptToIdx(pt, activeSubSheet.columns);
auto const relIdx = idx % PixelsPerTile;
if (pixels[relIdx] || getPixel(activeSubSheet, m_img.bpp, idx) != oldColor) {
return;
}
// mark pixels to update
pixels[relIdx] = true;
if (pt.x % TileWidth != 0) {
auto const leftPt = pt + ox::Point{-1, 0};
getFillPixels(activeSubSheet, pixels, leftPt, oldColor);
}
if (pt.x % TileWidth != TileWidth - 1) {
auto const rightPt = pt + ox::Point{1, 0};
getFillPixels(activeSubSheet, pixels, rightPt, oldColor);
}
if (pt.y % TileHeight != 0) {
auto const topPt = pt + ox::Point{0, -1};
getFillPixels(activeSubSheet, pixels, topPt, oldColor);
}
if (pt.y % TileHeight != TileHeight - 1) {
auto const bottomPt = pt + ox::Point{0, 1};
getFillPixels(activeSubSheet, pixels, bottomPt, oldColor);
}
}
void TileSheetEditorModel::pushCommand(studio::UndoCommand *cmd) noexcept {
std::ignore = m_undoStack.push(ox::UPtr<studio::UndoCommand>{cmd});
m_ongoingDrawCommand = dynamic_cast<DrawCommand*>(cmd);
m_updated = true;
}
}

View File

@@ -0,0 +1,158 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/point.hpp>
#include <ox/std/string.hpp>
#include <studio/studio.hpp>
#include <nostalgia/gfx/gfx.hpp>
#include <nostalgia/gfx/tilesheet.hpp>
namespace nostalgia::gfx {
class TileSheetEditorModel: public ox::SignalHandler {
public:
ox::Signal<ox::Error(const TileSheet::SubSheetIdx&)> activeSubsheetChanged;
ox::Signal<ox::Error()> paletteChanged;
private:
static Palette const s_defaultPalette;
studio::StudioContext &m_sctx;
turbine::Context &m_tctx;
ox::String m_path;
TileSheet m_img;
TileSheet::SubSheetIdx m_activeSubsSheetIdx;
keel::AssetRef<Palette> m_pal;
size_t m_palettePage{};
studio::UndoStack &m_undoStack;
class DrawCommand *m_ongoingDrawCommand = nullptr;
studio::SelectionTracker m_selTracker;
ox::Optional<studio::Selection> m_selection;
bool m_updated = false;
public:
TileSheetEditorModel(studio::StudioContext &sctx, ox::StringViewCR path, studio::UndoStack &undoStack);
~TileSheetEditorModel() override = default;
void cut();
void copy();
void paste();
[[nodiscard]]
bool acceptsClipboardPayload() const noexcept;
[[nodiscard]]
constexpr TileSheet const&img() const noexcept;
[[nodiscard]]
constexpr TileSheet &img() noexcept;
[[nodiscard]]
constexpr Palette const&pal() const noexcept;
[[nodiscard]]
ox::StringView palPath() const noexcept;
ox::Error setPalette(ox::StringViewCR path) noexcept;
void setPalettePage(size_t pg) noexcept;
[[nodiscard]]
size_t palettePage() const noexcept;
void drawCommand(ox::Point const&pt, std::size_t palIdx) noexcept;
void endDrawCommand() noexcept;
void addSubsheet(TileSheet::SubSheetIdx const&parentIdx) noexcept;
void rmSubsheet(TileSheet::SubSheetIdx const&idx) noexcept;
void insertTiles(TileSheet::SubSheetIdx const&idx, std::size_t tileIdx, std::size_t tileCnt) noexcept;
void deleteTiles(TileSheet::SubSheetIdx const&idx, std::size_t tileIdx, std::size_t tileCnt) noexcept;
ox::Error updateSubsheet(TileSheet::SubSheetIdx const&idx, ox::StringView const&name, int cols, int rows) noexcept;
void setActiveSubsheet(TileSheet::SubSheetIdx const&) noexcept;
[[nodiscard]]
TileSheet::SubSheet const&activeSubSheet() const noexcept {
return getSubSheet(m_img, m_activeSubsSheetIdx);
}
[[nodiscard]]
TileSheet::SubSheet &activeSubSheet() noexcept {
return getSubSheet(m_img, m_activeSubsSheetIdx);
}
[[nodiscard]]
constexpr TileSheet::SubSheetIdx const&activeSubSheetIdx() const noexcept {
return m_activeSubsSheetIdx;
}
void fill(ox::Point const&pt, int palIdx) noexcept;
void setSelection(studio::Selection const&sel) noexcept;
void select(ox::Point const&pt) noexcept;
void completeSelection() noexcept;
void clearSelection() noexcept;
[[nodiscard]]
bool updated() const noexcept;
ox::Error markUpdatedCmdId(studio::UndoCommand const*cmd) noexcept;
ox::Error markUpdated() noexcept;
void ackUpdate() noexcept;
ox::Error saveFile() noexcept;
[[nodiscard]]
constexpr studio::UndoStack *undoStack() noexcept;
bool pixelSelected(std::size_t idx) const noexcept;
private:
void getFillPixels(
TileSheet::SubSheet const&activeSubSheet,
ox::Span<bool> pixels,
ox::Point const&pt,
int oldColor) const noexcept;
void pushCommand(studio::UndoCommand *cmd) noexcept;
};
constexpr TileSheet const&TileSheetEditorModel::img() const noexcept {
return m_img;
}
constexpr TileSheet &TileSheetEditorModel::img() noexcept {
return m_img;
}
constexpr Palette const&TileSheetEditorModel::pal() const noexcept {
if (m_pal) {
return *m_pal;
}
return s_defaultPalette;
}
constexpr studio::UndoStack *TileSheetEditorModel::undoStack() noexcept {
return &m_undoStack;
}
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/std/point.hpp>
#include <keel/media.hpp>
#include <nostalgia/gfx/consts.hpp>
#include "tilesheeteditorview.hpp"
namespace nostalgia::gfx {
TileSheetEditorView::TileSheetEditorView(studio::StudioContext &sctx, ox::StringView path, studio::UndoStack &undoStack):
m_model(sctx, path, undoStack),
m_pixelsDrawer(m_model) {
glBindVertexArray(0);
// build shaders
OX_THROW_ERROR(m_pixelsDrawer.buildShader());
OX_THROW_ERROR(m_pixelGridDrawer.buildShader());
m_model.activeSubsheetChanged.connect(this, &TileSheetEditorView::setActiveSubsheet);
}
void TileSheetEditorView::draw() noexcept {
glClearColor(0.37f, 0.37f, 0.37f, 1.f);
glClear(GL_COLOR_BUFFER_BIT);
m_pixelsDrawer.draw(updated(), m_scrollOffset);
m_pixelGridDrawer.draw(updated(), m_scrollOffset);
}
void TileSheetEditorView::scrollV(ox::Vec2 const&paneSz, float wheel, bool zoomMod) noexcept {
auto const&s = m_model.activeSubSheet();
auto const pixelSize = m_pixelsDrawer.pixelSize(paneSz);
ImVec2 const sheetSize(pixelSize.x * static_cast<float>(s.columns) * TileWidth,
pixelSize.y * static_cast<float>(s.rows) * TileHeight);
if (zoomMod) {
m_pixelSizeMod = ox::clamp(m_pixelSizeMod + wheel * 0.02f, 0.55f, 2.f);
m_pixelsDrawer.setPixelSizeMod(m_pixelSizeMod);
m_pixelGridDrawer.setPixelSizeMod(m_pixelSizeMod);
m_updated = true;
} else {
m_scrollOffset.y -= wheel * 0.1f;
}
// adjust scroll offset in both cases because the image can be zoomed
// or scrolled off screen
m_scrollOffset.y = ox::clamp(m_scrollOffset.y, 0.f, sheetSize.y / 2);
}
void TileSheetEditorView::scrollH(ox::Vec2 const&paneSz, float wheelh) noexcept {
auto const&s = m_model.activeSubSheet();
auto const pixelSize = m_pixelsDrawer.pixelSize(paneSz);
ImVec2 const sheetSize(pixelSize.x * static_cast<float>(s.columns) * TileWidth,
pixelSize.y * static_cast<float>(s.rows) * TileHeight);
m_scrollOffset.x += wheelh * 0.1f;
m_scrollOffset.x = ox::clamp(m_scrollOffset.x, -(sheetSize.x / 2), 0.f);
}
void TileSheetEditorView::insertTile(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept {
auto pt = clickPoint(paneSize, clickPos);
auto const&s = m_model.activeSubSheet();
pt.x = ox::min(pt.x, s.columns * TileWidth - 1);
pt.y = ox::min(pt.y, s.rows * TileHeight - 1);
auto const tileIdx = ptToIdx(pt, s.columns) / PixelsPerTile;
m_model.insertTiles(m_model.activeSubSheetIdx(), tileIdx, 1);
}
void TileSheetEditorView::deleteTile(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept {
auto const pt = clickPoint(paneSize, clickPos);
auto const&s = m_model.activeSubSheet();
auto const tileIdx = ptToIdx(pt, s.columns) / PixelsPerTile;
m_model.deleteTiles(m_model.activeSubSheetIdx(), tileIdx, 1);
}
void TileSheetEditorView::clickDraw(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept {
auto const pt = clickPoint(paneSize, clickPos);
m_model.drawCommand(pt, m_palIdx);
}
void TileSheetEditorView::clickSelect(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept {
auto const pt = clickPoint(paneSize, clickPos);
m_model.select(pt);
}
void TileSheetEditorView::clickFill(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept {
auto const pt = clickPoint(paneSize, clickPos);
m_model.fill(pt, static_cast<int>(m_palIdx));
}
void TileSheetEditorView::releaseMouseButton(TileSheetTool tool) noexcept {
switch (tool) {
case TileSheetTool::Draw:
case TileSheetTool::Fill:
m_model.endDrawCommand();
break;
case TileSheetTool::Select:
m_model.completeSelection();
break;
case TileSheetTool::None:
break;
}
}
void TileSheetEditorView::resizeView(ox::Vec2 const&sz) noexcept {
m_viewSize = sz;
initView();
}
bool TileSheetEditorView::updated() const noexcept {
return m_updated || m_model.updated();
}
ox::Error TileSheetEditorView::markUpdated() noexcept {
m_updated = true;
return {};
}
void TileSheetEditorView::ackUpdate() noexcept {
m_updated = false;
m_pixelsDrawer.update(m_viewSize);
m_pixelGridDrawer.update(m_viewSize, m_model.activeSubSheet());
m_model.ackUpdate();
}
void TileSheetEditorView::initView() noexcept {
m_pixelsDrawer.initBufferSet(m_viewSize);
m_pixelGridDrawer.initBufferSet(m_viewSize, m_model.activeSubSheet());
}
ox::Point TileSheetEditorView::clickPoint(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) const noexcept {
auto [x, y] = clickPos;
const auto pixDrawSz = m_pixelsDrawer.pixelSize(paneSize);
x /= paneSize.x;
y /= paneSize.y;
x += -m_scrollOffset.x / 2;
y += m_scrollOffset.y / 2;
x /= pixDrawSz.x;
y /= pixDrawSz.y;
return {static_cast<int>(x * 2), static_cast<int>(y * 2)};
}
ox::Error TileSheetEditorView::setActiveSubsheet(TileSheet::SubSheetIdx const&) noexcept {
initView();
return {};
}
}

View File

@@ -0,0 +1,129 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/vec.hpp>
#include <ox/model/def.hpp>
#include <glutils/glutils.hpp>
#include <studio/studio.hpp>
#include <nostalgia/gfx/gfx.hpp>
#include "tilesheeteditormodel.hpp"
#include "tilesheetpixelgrid.hpp"
#include "tilesheetpixels.hpp"
namespace nostalgia::gfx {
enum class TileSheetTool {
None,
Select,
Draw,
Fill,
};
[[nodiscard]]
constexpr auto toString(TileSheetTool t) noexcept {
switch (t) {
case TileSheetTool::Select:
return "Select";
case TileSheetTool::Draw:
return "Draw";
case TileSheetTool::Fill:
return "Fill";
case TileSheetTool::None:
return "None";
}
return "";
}
class TileSheetEditorView: public ox::SignalHandler {
private:
TileSheetEditorModel m_model;
TileSheetGrid m_pixelGridDrawer;
TileSheetPixels m_pixelsDrawer;
ox::Vec2 m_viewSize;
float m_pixelSizeMod = 1;
bool m_updated = false;
ox::Vec2 m_scrollOffset;
std::size_t m_palIdx = 0;
public:
TileSheetEditorView(studio::StudioContext &sctx, ox::StringView path, studio::UndoStack &undoStack);
~TileSheetEditorView() override = default;
void draw() noexcept;
void insertTile(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept;
void deleteTile(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept;
void clickDraw(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept;
void clickSelect(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept;
void clickFill(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept;
void releaseMouseButton(TileSheetTool tool) noexcept;
void scrollV(ox::Vec2 const&paneSz, float wheel, bool zoomMod) noexcept;
void scrollH(ox::Vec2 const&paneSz, float wheel) noexcept;
void resizeView(ox::Vec2 const&sz) noexcept;
[[nodiscard]]
constexpr TileSheet const&img() const noexcept;
[[nodiscard]]
constexpr TileSheet &img() noexcept;
[[nodiscard]]
constexpr auto &model() noexcept {
return m_model;
}
[[nodiscard]]
constexpr auto &model() const noexcept {
return m_model;
}
constexpr auto setPalIdx(auto palIdx) noexcept {
m_palIdx = palIdx;
}
[[nodiscard]]
constexpr auto palIdx() const noexcept {
return m_palIdx;
}
[[nodiscard]]
bool updated() const noexcept;
ox::Error markUpdated() noexcept;
void ackUpdate() noexcept;
private:
void initView() noexcept;
ox::Point clickPoint(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) const noexcept;
ox::Error setActiveSubsheet(TileSheet::SubSheetIdx const&idx) noexcept;
};
constexpr TileSheet const&TileSheetEditorView::img() const noexcept {
return m_model.img();
}
constexpr TileSheet &TileSheetEditorView::img() noexcept {
return m_model.img();
}
}

View File

@@ -0,0 +1,130 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/claw/write.hpp>
#include <nostalgia/gfx/consts.hpp>
#include "tilesheetpixelgrid.hpp"
namespace nostalgia::gfx {
void TileSheetGrid::setPixelSizeMod(float sm) noexcept {
m_pixelSizeMod = sm;
}
ox::Error TileSheetGrid::buildShader() noexcept {
auto const pixelLineVshad = ox::sfmt(VShad, gl::GlslVersion);
auto const pixelLineFshad = ox::sfmt(FShad, gl::GlslVersion);
auto const pixelLineGshad = ox::sfmt(GShad, gl::GlslVersion);
return glutils::buildShaderProgram(pixelLineVshad, pixelLineFshad, pixelLineGshad).moveTo(m_shader);
}
void TileSheetGrid::draw(bool update, ox::Vec2 const&scroll) noexcept {
glLineWidth(3 * m_pixelSizeMod * 0.5f);
glUseProgram(m_shader);
glBindVertexArray(m_bufferSet.vao);
if (update) {
glutils::sendVbo(m_bufferSet);
}
auto const uniformScroll = glGetUniformLocation(m_shader, "gScroll");
glUniform2f(uniformScroll, scroll.x, scroll.y);
glDrawArrays(GL_POINTS, 0, static_cast<GLsizei>(m_bufferSet.vertices.size() / VertexVboRowLength));
glBindVertexArray(0);
glUseProgram(0);
}
void TileSheetGrid::initBufferSet(ox::Vec2 const&paneSize, TileSheet::SubSheet const&subsheet) noexcept {
// vao
m_bufferSet.vao = glutils::generateVertexArrayObject();
glBindVertexArray(m_bufferSet.vao);
// vbo
m_bufferSet.vbo = glutils::generateBuffer();
setBufferObjects(paneSize, subsheet);
glutils::sendVbo(m_bufferSet);
// vbo layout
auto const pt1Attr = static_cast<GLuint>(glGetAttribLocation(m_shader, "vPt1"));
glEnableVertexAttribArray(pt1Attr);
glVertexAttribPointer(pt1Attr, 2, GL_FLOAT, GL_FALSE, VertexVboRowLength * sizeof(float), nullptr);
auto const pt2Attr = static_cast<GLuint>(glGetAttribLocation(m_shader, "vPt2"));
glEnableVertexAttribArray(pt2Attr);
glVertexAttribPointer(pt2Attr, 2, GL_FLOAT, GL_FALSE, VertexVboRowLength * sizeof(float),
std::bit_cast<void*>(uintptr_t{2 * sizeof(float)}));
auto const colorAttr = static_cast<GLuint>(glGetAttribLocation(m_shader, "vColor"));
glEnableVertexAttribArray(colorAttr);
glVertexAttribPointer(colorAttr, 3, GL_FLOAT, GL_FALSE, VertexVboRowLength * sizeof(float),
std::bit_cast<void*>(uintptr_t{4 * sizeof(float)}));
}
void TileSheetGrid::update(ox::Vec2 const&paneSize, TileSheet::SubSheet const&subsheet) noexcept {
glBindVertexArray(m_bufferSet.vao);
setBufferObjects(paneSize, subsheet);
glutils::sendVbo(m_bufferSet);
glutils::sendEbo(m_bufferSet);
}
void TileSheetGrid::setBufferObject(
ox::Point pt1,
ox::Point pt2,
Color32 c,
float *vbo,
ox::Vec2 const&pixSize) noexcept {
auto const x1 = static_cast<float>(pt1.x) * pixSize.x - 1.f;
auto const y1 = 1.f - static_cast<float>(pt1.y) * pixSize.y;
auto const x2 = static_cast<float>(pt2.x) * pixSize.x - 1.f;
auto const y2 = 1.f - static_cast<float>(pt2.y) * pixSize.y;
// don't worry, this memcpy gets optimized to something much more ideal
ox::Array<float, VertexVboLength> const vertices = {x1, y1, x2, y2, redf(c), greenf(c), bluef(c)};
memcpy(vbo, vertices.data(), sizeof(vertices));
}
void TileSheetGrid::setBufferObjects(ox::Vec2 const&paneSize, TileSheet::SubSheet const&subsheet) noexcept {
if (subsheet.columns < 1 || subsheet.rows < 1) {
m_bufferSet.elements.clear();
m_bufferSet.vertices.clear();
return;
}
auto const pixSize = pixelSize(paneSize);
auto const set = [&](std::size_t i, ox::Point pt1, ox::Point pt2, Color32 c) {
auto const vbo = &m_bufferSet.vertices[i * VertexVboLength];
setBufferObject(pt1, pt2, c, vbo, pixSize);
};
// set buffer length
auto const width = subsheet.columns * TileWidth;
auto const height = subsheet.rows * TileHeight;
auto const tileCnt = static_cast<unsigned>(subsheet.columns + subsheet.rows);
auto const pixelCnt = static_cast<unsigned>(width + height);
m_bufferSet.vertices.resize(static_cast<std::size_t>(tileCnt + pixelCnt + 4) * VertexVboLength);
// set buffer
std::size_t i = 0;
// pixel outlines
constexpr auto pixOutlineColor = color32(0.4431f, 0.4901f, 0.4941f);
for (auto x = 0; x < subsheet.columns * TileWidth + 1; ++x) {
set(i, {x, 0}, {x, subsheet.rows * TileHeight}, pixOutlineColor);
++i;
}
for (auto y = 0; y < subsheet.rows * TileHeight + 1; ++y) {
set(i, {0, y}, {subsheet.columns * TileWidth, y}, pixOutlineColor);
++i;
}
// tile outlines
constexpr auto tileOutlineColor = color32(0.f, 0.f, 0.f);
for (auto x = 0; x < subsheet.columns * TileWidth + 1; x += TileWidth) {
set(i, {x, 0}, {x, subsheet.rows * TileHeight}, tileOutlineColor);
++i;
}
for (auto y = 0; y < subsheet.rows * TileHeight + 1; y += TileHeight) {
set(i, {0, y}, {subsheet.columns * TileWidth, y}, tileOutlineColor);
++i;
}
}
ox::Vec2 TileSheetGrid::pixelSize(ox::Vec2 const&paneSize) const noexcept {
auto const [sw, sh] = paneSize;
constexpr float ymod = 0.35f / 10.0f;
auto const xmod = ymod * sh / sw;
return {xmod * m_pixelSizeMod, ymod * m_pixelSizeMod};
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <glutils/glutils.hpp>
#include <studio/studio.hpp>
#include <nostalgia/gfx/gfx.hpp>
#include <nostalgia/gfx/tilesheet.hpp>
namespace nostalgia::gfx {
class TileSheetGrid {
private:
static constexpr auto VertexVboRows = 1;
static constexpr auto VertexVboRowLength = 7;
static constexpr auto VertexVboLength = VertexVboRows * VertexVboRowLength;
static constexpr auto VShad = R"glsl(
{}
in vec2 vPt1;
in vec2 vPt2;
in vec3 vColor;
out vec2 gPt2;
out vec3 gColor;
void main() {
gColor = vColor;
gl_Position = vec4(vPt1, 0.0, 1.0);
gPt2 = vPt2;
})glsl";
static constexpr auto FShad = R"glsl(
{}
in vec3 fColor;
out vec4 outColor;
void main() {
outColor = vec4(fColor, 1);
//outColor = vec4(0.4431, 0.4901, 0.4941, 1.0);
})glsl";
static constexpr auto GShad = R"glsl(
{}
layout(points) in;
layout(line_strip, max_vertices = 2) out;
in vec3 gColor[];
in vec2 gPt2[];
out vec3 fColor;
uniform vec2 gScroll;
void main() {
fColor = gColor[0];
gl_Position = gl_in[0].gl_Position + vec4(gScroll, 0, 0);
EmitVertex();
gl_Position = vec4(gPt2[0] + gScroll, 0, 1);
EmitVertex();
EndPrimitive();
})glsl";
glutils::GLProgram m_shader;
glutils::BufferSet m_bufferSet;
float m_pixelSizeMod = 1;
public:
void setPixelSizeMod(float sm) noexcept;
ox::Error buildShader() noexcept;
void draw(bool update, ox::Vec2 const&scroll) noexcept;
void initBufferSet(ox::Vec2 const&paneSize, TileSheet::SubSheet const&subsheet) noexcept;
void update(ox::Vec2 const&paneSize, TileSheet::SubSheet const&subsheet) noexcept;
private:
static void setBufferObject(ox::Point pt1, ox::Point pt2, Color32 c, float *vbo, ox::Vec2 const&pixSize) noexcept;
void setBufferObjects(ox::Vec2 const&paneSize, TileSheet::SubSheet const&subsheet) noexcept;
[[nodiscard]]
ox::Vec2 pixelSize(ox::Vec2 const&paneSize) const noexcept;
};
}

View File

@@ -0,0 +1,153 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <nostalgia/gfx/consts.hpp>
#include <nostalgia/gfx/ptidxconv.hpp>
#include "tilesheeteditormodel.hpp"
#include "tilesheetpixels.hpp"
namespace nostalgia::gfx {
const glutils::ProgramSource TileSheetPixels::s_programSrc = {
.shaderParams = {
{
.len = 2,
.name = ox::String("vPosition"),
},
{
.len = 3,
.name = ox::String("vColor"),
},
},
.vertShader = ox::sfmt(R"(
{}
in vec2 vPosition;
in vec3 vColor;
out vec3 fColor;
uniform vec2 vScroll;
void main() {
gl_Position = vec4(vPosition + vScroll, 0.0, 1.0);
fColor = vColor;
})", gfx::gl::GlslVersion),
.fragShader = ox::sfmt(R"(
{}
in vec3 fColor;
out vec4 outColor;
void main() {
//outColor = vec4(0.0, 0.7, 1.0, 1.0);
outColor = vec4(fColor, 1.0);
})", gfx::gl::GlslVersion),
};
TileSheetPixels::TileSheetPixels(TileSheetEditorModel &model) noexcept: m_model(model) {
}
void TileSheetPixels::setPixelSizeMod(float sm) noexcept {
m_pixelSizeMod = sm;
}
ox::Error TileSheetPixels::buildShader() noexcept {
return glutils::buildShaderProgram(s_programSrc).moveTo(m_shader);
}
void TileSheetPixels::draw(bool const update, ox::Vec2 const&scroll) noexcept {
glUseProgram(m_shader);
glBindVertexArray(m_bufferSet.vao);
if (update) {
glutils::sendVbo(m_bufferSet);
}
auto const uniformScroll = glGetUniformLocation(m_shader, "vScroll");
glUniform2f(uniformScroll, scroll.x, scroll.y);
glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(m_bufferSet.elements.size()), GL_UNSIGNED_INT, nullptr);
glBindVertexArray(0);
glUseProgram(0);
}
void TileSheetPixels::initBufferSet(ox::Vec2 const&paneSize) noexcept {
m_bufferSet.vao = glutils::generateVertexArrayObject();
m_bufferSet.vbo = glutils::generateBuffer();
m_bufferSet.ebo = glutils::generateBuffer();
update(paneSize);
glutils::setupShaderParams(m_shader, s_programSrc.shaderParams);
}
void TileSheetPixels::update(ox::Vec2 const&paneSize) noexcept {
glBindVertexArray(m_bufferSet.vao);
setBufferObjects(paneSize);
glutils::sendVbo(m_bufferSet);
glutils::sendEbo(m_bufferSet);
}
ox::Vec2 TileSheetPixels::pixelSize(ox::Vec2 const&paneSize) const noexcept {
auto const [sw, sh] = paneSize;
constexpr float ymod = 0.35f / 10.0f;
auto const xmod = ymod * sh / sw;
return {xmod * m_pixelSizeMod, ymod * m_pixelSizeMod};
}
void TileSheetPixels::setPixelBufferObject(
ox::Vec2 const&paneSize,
unsigned vertexRow,
float x, float y,
Color16 color,
float *vbo,
GLuint *ebo) const noexcept {
auto const [xmod, ymod] = pixelSize(paneSize);
x *= xmod;
y *= -ymod;
x -= 1.0f;
y += 1.0f - ymod;
auto const r = redf(color), g = greenf(color), b = bluef(color);
// don't worry, these memcpys gets optimized to something much more ideal
std::array const vertices = {
x, y, r, g, b, // bottom left
x + xmod, y, r, g, b, // bottom right
x + xmod, y + ymod, r, g, b, // top right
x, y + ymod, r, g, b, // top left
};
memcpy(vbo, vertices.data(), sizeof(vertices));
std::array const elms = {
vertexRow + 0, vertexRow + 1, vertexRow + 2,
vertexRow + 2, vertexRow + 3, vertexRow + 0,
};
memcpy(ebo, elms.data(), sizeof(elms));
}
void TileSheetPixels::setBufferObjects(ox::Vec2 const&paneSize) noexcept {
// set buffer lengths
auto const&subSheet = m_model.activeSubSheet();
if (subSheet.columns < 1 || subSheet.rows < 1) {
m_bufferSet.vertices.clear();
m_bufferSet.elements.clear();
return;
}
auto const&pal = m_model.pal();
auto const width = subSheet.columns * TileWidth;
auto const height = subSheet.rows * TileHeight;
auto const pixels = static_cast<size_t>(width) * static_cast<size_t>(height);
auto const vboLen = static_cast<size_t>(s_programSrc.vboLen);
m_bufferSet.vertices.resize(pixels * vboLen);
m_bufferSet.elements.resize(pixels * VertexEboLength);
// set pixels
walkPixels(subSheet, m_model.img().bpp, [&](std::size_t i, uint8_t p) {
auto color = gfx::color(pal, m_model.palettePage(), p);
auto const pt = idxToPt(static_cast<int>(i), subSheet.columns);
auto const fx = static_cast<float>(pt.x);
auto const fy = static_cast<float>(pt.y);
auto const vbo = &m_bufferSet.vertices[i * vboLen];
auto const ebo = &m_bufferSet.elements[i * VertexEboLength];
if (i * vboLen + vboLen > m_bufferSet.vertices.size()) {
return;
}
if (i * VertexEboLength + VertexEboLength > m_bufferSet.elements.size()) {
return;
}
if (m_model.pixelSelected(i)) {
color = applySelectionColor(color);
}
setPixelBufferObject(paneSize, static_cast<unsigned>(i * VertexVboRows), fx, fy, color, vbo, ebo);
});
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/vec.hpp>
#include <glutils/glutils.hpp>
#include <studio/studio.hpp>
#include <nostalgia/gfx/color.hpp>
#include <nostalgia/gfx/gfx.hpp>
namespace nostalgia::gfx {
class TileSheetPixels {
private:
static constexpr auto VertexVboRows = 4;
static constexpr auto VertexEboLength = 6;
static const glutils::ProgramSource s_programSrc;
float m_pixelSizeMod = 1;
glutils::GLProgram m_shader;
glutils::BufferSet m_bufferSet;
class TileSheetEditorModel const&m_model;
public:
explicit TileSheetPixels(class TileSheetEditorModel &model) noexcept;
void setPixelSizeMod(float sm) noexcept;
ox::Error buildShader() noexcept;
void draw(bool update, ox::Vec2 const&scroll) noexcept;
void initBufferSet(ox::Vec2 const&paneSize) noexcept;
void update(ox::Vec2 const&paneSize) noexcept;
[[nodiscard]]
ox::Vec2 pixelSize(ox::Vec2 const&paneSize) const noexcept;
private:
void setPixelBufferObject(
ox::Vec2 const&paneS,
unsigned vertexRow,
float x,
float y,
Color16 color,
float *vbo,
GLuint *ebo) const noexcept;
void setBufferObjects(ox::Vec2 const&paneS) noexcept;
};
}

View File

@@ -0,0 +1,434 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/std/size.hpp>
#include <ox/std/vector.hpp>
#include <nostalgia/gfx/ptidxconv.hpp>
#include <nostalgia/gfx/tilesheet.hpp>
namespace nostalgia::gfx {
std::size_t idx(TileSheet::SubSheet const&ss, ox::Point const&pt) noexcept {
return ptToIdx(pt, ss.columns);
}
[[nodiscard]]
static TileSheet::SubSheet const *getSubsheet(TileSheet::SubSheet const&ss, SubSheetId const id) noexcept {
if (ss.id == id) {
return &ss;
}
for (auto const&child: ss.subsheets) {
if (auto out = getSubsheet(child, id)) {
return out;
}
}
return {};
}
[[nodiscard]]
static size_t getTileCnt(TileSheet::SubSheet const&ss, int const bpp) noexcept {
if (ss.subsheets.empty()) {
auto const bytesPerTile = bpp == 4 ? 32u : 64u;
return ss.pixels.size() / bytesPerTile;
} else {
size_t out{};
for (auto const&child: ss.subsheets) {
out += getTileCnt(child, bpp);
}
return out;
}
}
size_t getTileCnt(TileSheet const&ts) noexcept {
return getTileCnt(ts.subsheet, ts.bpp);
}
TileSheet::SubSheet const *getSubsheet(TileSheet const&ts, SubSheetId const id) noexcept {
return getSubsheet(ts.subsheet, id);
}
static ox::Optional<size_t> getPixelIdx(
TileSheet::SubSheet const&ss,
SubSheetId const id,
size_t &idx,
int8_t const bpp) noexcept {
for (auto const&child: ss.subsheets) {
if (child.id == id) {
return ox::Optional<size_t>(ox::in_place, idx);
}
if (auto out = getPixelIdx(child, id, idx, bpp)) {
return out;
}
idx += pixelCnt(child, bpp);
}
return ox::Optional<size_t>{};
}
ox::Optional<size_t> getTileIdx(TileSheet const&ts, SubSheetId const id) noexcept {
size_t idx{};
auto const out = getPixelIdx(ts.subsheet, id, idx, ts.bpp);
return out ? ox::Optional<size_t>{ox::in_place, *out / PixelsPerTile} : out;
}
uint8_t getPixel4Bpp(TileSheet::SubSheet const&ss, std::size_t idx) noexcept {
if (idx & 1) {
return ss.pixels[idx / 2] >> 4;
} else {
return ss.pixels[idx / 2] & 0b0000'1111;
}
}
uint8_t getPixel8Bpp(TileSheet::SubSheet const&ss, std::size_t idx) noexcept {
return ss.pixels[idx];
}
uint8_t getPixel(TileSheet::SubSheet const&ss, int8_t pBpp, std::size_t idx) noexcept {
if (pBpp == 4) {
return getPixel4Bpp(ss, idx);
} else {
return getPixel8Bpp(ss, idx);
}
}
uint8_t getPixel4Bpp(TileSheet::SubSheet const&ss, ox::Point const&pt) noexcept {
const auto idx = ptToIdx(pt, ss.columns);
return getPixel4Bpp(ss, idx);
}
uint8_t getPixel8Bpp(TileSheet::SubSheet const&ss, ox::Point const&pt) noexcept {
const auto idx = ptToIdx(pt, ss.columns);
return getPixel8Bpp(ss, idx);
}
uint8_t getPixel(TileSheet::SubSheet const&ss, int8_t pBpp, ox::Point const&pt) noexcept {
const auto idx = ptToIdx(pt, ss.columns);
return getPixel(ss, pBpp, idx);
}
static void setPixel(ox::Vector<uint8_t> &pixels, int8_t pBpp, uint64_t idx, uint8_t palIdx) noexcept {
auto &pixel = pixels[static_cast<std::size_t>(idx / 2)];
if (pBpp == 4) {
if (idx & 1) {
pixel = static_cast<uint8_t>((pixel & 0b0000'1111) | (palIdx << 4));
} else {
pixel = (pixel & 0b1111'0000) | (palIdx);
}
} else {
pixel = palIdx;
}
}
void setPixel(TileSheet::SubSheet &ss, int8_t pBpp, uint64_t idx, uint8_t palIdx) noexcept {
setPixel(ss.pixels, pBpp, idx, palIdx);
}
static void setPixel(ox::Vector<uint8_t> &pixels, int columns, int8_t pBpp, ox::Point const&pt, uint8_t palIdx) noexcept {
const auto idx = ptToIdx(pt, columns);
setPixel(pixels, pBpp, idx, palIdx);
}
void setPixel(TileSheet::SubSheet &ss, int8_t pBpp, ox::Point const&pt, uint8_t palIdx) noexcept {
const auto idx = ptToIdx(pt, ss.columns);
setPixel(ss, pBpp, idx, palIdx);
}
static ox::Error setPixelCount(ox::Vector<uint8_t> &pixels, int8_t pBpp, std::size_t cnt) noexcept {
size_t sz{};
switch (pBpp) {
case 4:
sz = cnt / 2;
break;
case 8:
sz = cnt;
break;
default:
return ox::Error(1, "Invalid pBpp used for TileSheet::SubSheet::setPixelCount");
}
pixels.reserve(sz);
pixels.resize(sz);
return {};
}
ox::Error setPixelCount(TileSheet::SubSheet &ss, int8_t pBpp, std::size_t cnt) noexcept {
return setPixelCount(ss.pixels, pBpp, cnt);
}
unsigned pixelCnt(TileSheet::SubSheet const&ss, int8_t pBpp) noexcept {
const auto pixelsSize = static_cast<unsigned>(ss.pixels.size());
return pBpp == 4 ? pixelsSize * 2 : pixelsSize;
}
ox::Error resizeSubsheet(TileSheet::SubSheet &ss, int8_t pBpp, ox::Size const&sz) noexcept {
ox::Vector<uint8_t> out;
OX_RETURN_ERROR(setPixelCount(out, pBpp, static_cast<size_t>(sz.width * sz.height) * PixelsPerTile));
auto const w = ox::min<int32_t>(ss.columns, sz.width) * TileWidth;
auto const h = ox::min<int32_t>(ss.rows, sz.height) * TileHeight;
for (auto x = 0; x < w; ++x) {
for (auto y = 0; y < h; ++y) {
auto const palIdx = getPixel(ss, pBpp, {x, y});
setPixel(out, sz.width, pBpp, {x, y}, palIdx);
}
}
ss.columns = sz.width;
ss.rows = sz.height;
ss.pixels = std::move(out);
return {};
}
ox::Result<ox::StringView> getNameFor(TileSheet::SubSheet const&ss, SubSheetId pId) noexcept {
if (ss.id == pId) {
return ox::StringView(ss.name);
}
for (const auto &sub : ss.subsheets) {
const auto [name, err] = getNameFor(sub, pId);
if (!err) {
return name;
}
}
return ox::Error(1, "SubSheet not found");
}
TileSheet::SubSheetIdx validateSubSheetIdx(
TileSheet::SubSheetIdx &&pIdx,
std::size_t pIdxIt,
TileSheet::SubSheet const&pSubsheet) noexcept {
if (pIdxIt >= pIdx.size()) {
return std::move(pIdx);
}
auto &currentIdx = pIdx[pIdxIt];
if (pSubsheet.subsheets.size() <= currentIdx) {
if (pSubsheet.subsheets.empty()) {
// currentIdx could not be repaired, remove
// this and all succeeding idxs and return
pIdx.resize(pIdxIt);
return std::move(pIdx);
} else {
currentIdx = pSubsheet.subsheets.size() - 1;
}
}
return validateSubSheetIdx(std::move(pIdx), pIdxIt + 1, pSubsheet.subsheets[currentIdx]);
}
TileSheet::SubSheetIdx validateSubSheetIdx(TileSheet const&ts, TileSheet::SubSheetIdx idx) noexcept {
return validateSubSheetIdx(std::move(idx), 0, ts.subsheet);
}
const TileSheet::SubSheet &getSubSheet(
TileSheet::SubSheetIdx const&idx,
std::size_t idxIt,
TileSheet::SubSheet const&pSubsheet) noexcept {
if (idxIt == idx.size()) {
return pSubsheet;
}
const auto currentIdx = idx[idxIt];
if (pSubsheet.subsheets.size() < currentIdx) {
return pSubsheet;
}
return getSubSheet(idx, idxIt + 1, pSubsheet.subsheets[currentIdx]);
}
TileSheet::SubSheet &getSubSheet(
TileSheet::SubSheetIdx const&idx,
std::size_t idxIt,
TileSheet::SubSheet &pSubsheet) noexcept {
if (idxIt == idx.size()) {
return pSubsheet;
}
return getSubSheet(idx, idxIt + 1, pSubsheet.subsheets[idx[idxIt]]);
}
TileSheet::SubSheet const&getSubSheet(TileSheet const&ts, TileSheet::SubSheetIdx const&idx) noexcept {
return gfx::getSubSheet(idx, 0, ts.subsheet);
}
TileSheet::SubSheet &getSubSheet(TileSheet &ts, TileSheet::SubSheetIdx const&idx) noexcept {
return gfx::getSubSheet(idx, 0, ts.subsheet);
}
ox::Error addSubSheet(TileSheet &ts, TileSheet::SubSheetIdx const&idx) noexcept {
auto &parent = getSubSheet(ts, idx);
if (parent.subsheets.size() < 2) {
parent.subsheets.emplace_back(++ts.idIt, ox::sfmt("Subsheet {}", parent.subsheets.size()), 1, 1, ts.bpp);
} else {
parent.subsheets.emplace_back(++ts.idIt, "Subsheet 0", parent.columns, parent.rows, ts.bpp);
parent.subsheets.emplace_back(++ts.idIt, "Subsheet 1", 1, 1, ts.bpp);
}
return ox::Error(0);
}
ox::Error rmSubSheet(
TileSheet &ts,
TileSheet::SubSheetIdx const&idx,
std::size_t idxIt,
TileSheet::SubSheet &pSubsheet) noexcept {
if (idxIt == idx.size() - 1) {
return pSubsheet.subsheets.erase(idx[idxIt]).error;
}
return rmSubSheet(ts, idx, idxIt + 1, pSubsheet.subsheets[idx[idxIt]]);
}
ox::Error rmSubSheet(TileSheet &ts, TileSheet::SubSheetIdx const&idx) noexcept {
return rmSubSheet(ts, idx, 0, ts.subsheet);
}
uint8_t getPixel4Bpp(
TileSheet const&ts,
ox::Point const&pt,
TileSheet::SubSheetIdx const&subsheetIdx) noexcept {
oxAssert(ts.bpp == 4, "TileSheet::getPixel4Bpp: wrong bpp");
auto &s = getSubSheet(ts, subsheetIdx);
auto const idx = ptToIdx(pt, s.columns);
return getPixel4Bpp(s, idx);
}
uint8_t getPixel8Bpp(
TileSheet const&ts,
ox::Point const&pt,
TileSheet::SubSheetIdx const&subsheetIdx) noexcept {
oxAssert(ts.bpp == 8, "TileSheet::getPixel8Bpp: wrong bpp");
auto &s = getSubSheet(ts, subsheetIdx);
auto const idx = ptToIdx(pt, s.columns);
return getPixel8Bpp(s, idx);
}
uint8_t getPixel4Bpp(
CompactTileSheet const&ts,
size_t const idx) noexcept {
oxAssert(ts.bpp == 4, "TileSheet::getPixel4Bpp: wrong bpp");
if (idx & 1) {
return ts.pixels[idx / 2] >> 4;
} else {
return ts.pixels[idx / 2] & 0b0000'1111;
}
}
uint8_t getPixel8Bpp(
CompactTileSheet const&ts,
size_t const idx) noexcept {
oxAssert(ts.bpp == 8, "TileSheet::getPixel8Bpp: wrong bpp");
return ts.pixels[idx];
}
ox::Pair<uint8_t> get2Pixels4Bpp(
CompactTileSheet const&ts,
size_t const idx) noexcept {
oxAssert(ts.bpp == 4, "TileSheet::getPixel4Bpp: wrong bpp");
auto const out = ts.pixels[idx / 2];
return {
static_cast<uint8_t>(out & 0x0f),
static_cast<uint8_t>(out >> 4),
};
}
ox::Pair<uint8_t> get2Pixels8Bpp(
CompactTileSheet const&ts,
size_t const idx) noexcept {
oxAssert(ts.bpp == 8, "TileSheet::getPixel8Bpp: wrong bpp");
return {
static_cast<uint8_t>(ts.pixels[idx]),
static_cast<uint8_t>(ts.pixels[idx + 1]),
};
}
static ox::Result<SubSheetId> getIdFor(
TileSheet::SubSheet const&ss,
ox::SpanView<ox::StringView> const&pNamePath,
std::size_t pIt = 0) noexcept {
for (auto &sub : ss.subsheets) {
if (sub.name == pNamePath[pIt]) {
if (pIt == pNamePath.size()) {
return ss.id;
}
return getIdFor(ss, pNamePath, pIt + 1);
}
}
return ox::Error(1, "SubSheet not found");
}
ox::Result<SubSheetId> getIdFor(TileSheet const&ts, ox::StringViewCR path) noexcept {
return getIdFor(ts.subsheet, ox::split<8>(path, '.'));
}
/**
* Gets the offset in tiles of the desired subsheet.
*/
static ox::Result<uint32_t> getTileOffset(
TileSheet::SubSheet const&ss,
ox::SpanView<ox::StringView> const&pNamePath,
int8_t pBpp,
std::size_t pIt = 0,
uint32_t pCurrentTotal = 0) noexcept {
// pIt == pNamePath.size() - 1 &&
if (ss.name != pNamePath[pIt]) {
return ox::Error(2, "Wrong branch");
}
if (pIt == pNamePath.size() - 1) {
return pCurrentTotal;
}
for (auto &sub : ss.subsheets) {
auto [offset, err] = getTileOffset(
sub, pNamePath, pBpp, pIt + 1, pCurrentTotal);
if (!err) {
return offset;
}
// Possible bug? Shoud this be usinga a recursive version of
// pixelCnt will count pixels in subsheets of sub as well.
pCurrentTotal += pixelCnt(sub, pBpp) / PixelsPerTile;
}
return ox::Error(1, "SubSheet not found");
}
ox::Result<uint32_t> getTileOffset(TileSheet const&ts, ox::StringViewCR pNamePath) noexcept {
return gfx::getTileOffset(ts.subsheet, ox::split<8>(pNamePath, '.'), ts.bpp);
}
ox::Result<ox::StringView> getNameFor(TileSheet &ts, SubSheetId pId) noexcept {
return gfx::getNameFor(ts.subsheet, pId);
}
ox::Result<ox::StringView> getNameFor(TileSheet const&ts, SubSheetId pId) noexcept {
return gfx::getNameFor(ts.subsheet, pId);
}
static void readPixelsTo(TileSheet::SubSheet &ss, ox::Vector<uint8_t> &pPixels) noexcept {
if (!ss.subsheets.empty()) {
for (auto &s: ss.subsheets) {
readPixelsTo(s, pPixels);
}
} else {
for (auto p : ss.pixels) {
pPixels.emplace_back(p);
}
}
}
ox::Vector<uint8_t> pixels(TileSheet &ts) noexcept {
ox::Vector<uint8_t> out;
readPixelsTo(ts.subsheet, out);
return out;
}
ox::Vector<uint32_t> resizeTileSheetData(
ox::Vector<uint32_t> const&srcPixels,
ox::Size const&srcSize,
int scale) noexcept {
ox::Vector<uint32_t> dst;
auto dstWidth = srcSize.width * scale;
auto dstHeight = srcSize.height * scale;
const auto pixelCnt = dstWidth * dstHeight;
dst.resize(static_cast<std::size_t>(pixelCnt));
for (auto i = 0; i < pixelCnt; ++i) {
const auto dstPt = idxToPt(i, 1, scale);
const auto srcPt = dstPt / ox::Point{scale, scale};
const auto srcIdx = ptToIdx(srcPt, 1);
const auto srcPixel = srcPixels[srcIdx];
dst[static_cast<std::size_t>(i)] = srcPixel;
}
return dst;
}
}

View File

@@ -0,0 +1,11 @@
add_executable(
NostalgiaCoreTest
tests.cpp
)
target_link_libraries(
NostalgiaCoreTest
NostalgiaCore
)
add_test("[NostalgiaCore] readWriteTileSheet" ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/NostalgiaCoreTest readWriteTileSheet)

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#undef NDEBUG
#include <map>
#include <ox/std/error.hpp>
#include <ox/mc/mc.hpp>
#include <nostalgia/gfx/core.hpp>
using namespace nostalgia;
static std::map<ox::StringView, ox::Error(*)()> tests = {
{
"readWriteTileSheet",
[]() -> ox::Error {
gfx::TileSheet in;
OX_REQUIRE(buff, ox::writeMC(in));
OX_REQUIRE(out, ox::readMC<gfx::TileSheet>(buff));
oxAssert(in.subsheet.name == out.subsheet.name, "subsheet.name serialization broken");
return {};
}
},
};
int main(int argc, const char **argv) {
int retval = -1;
if (argc > 0) {
auto const args = ox::Span{argv, static_cast<size_t>(argc)};
auto const testName = ox::StringView(args[1]);
if (tests.find(testName) != tests.end()) {
retval = static_cast<int>(tests[testName]());
} else {
retval = 1;
}
}
return retval;
}