mirror of
https://github.com/gbdev/rgbds.git
synced 2025-11-20 10:12:06 +00:00
Reimplement basic RGBGFX features in C++
Currently missing from the old version:
- `-f` ("fixing" the input image to be indexed)
- `-m` (the code for detecting mirrored tiles is missing, but all of the
"plumbing" is otherwise there)
- `-C`
- `-d`
- `-x` (though I need to check the exact functionality the old one has)
- Also the man page is still a draft and needs to be fleshed out
More planned features are not implemented yet either:
- Explicit palette spec
- Better error messages, also error "images"
- Better 8x16 support, as well as other "dedup unit" sizes
- Support for arbitrary number of palettes & colors per palette
- Other output formats (for example, a "full" palette map for "streaming"
use cases like gb-open-world)
- Quantization?
Some things may also be bugged:
- Transparency support
- Tile offsets (not exposed yet)
- Tile counts per bank (not exposed yet)
...and performance remains to be checked.
We need to set up some tests, honestly.
This commit is contained in:
359
src/gfx/pal_packing.cpp
Normal file
359
src/gfx/pal_packing.cpp
Normal file
@@ -0,0 +1,359 @@
|
||||
/*
|
||||
* This file is part of RGBDS.
|
||||
*
|
||||
* Copyright (c) 2022, Eldred Habert and RGBDS contributors.
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#include "gfx/pal_packing.hpp"
|
||||
|
||||
#include <assert.h>
|
||||
#include <bitset>
|
||||
#include <inttypes.h>
|
||||
#include <numeric>
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "gfx/main.hpp"
|
||||
#include "gfx/proto_palette.hpp"
|
||||
|
||||
using std::swap;
|
||||
|
||||
namespace packing {
|
||||
|
||||
// The solvers here are picked from the paper at http://arxiv.org/abs/1605.00558:
|
||||
// "Algorithms for the Pagination Problem, a Bin Packing with Overlapping Items"
|
||||
// Their formulation of the problem consists in packing "tiles" into "pages"; here is a
|
||||
// correspondence table for our application of it:
|
||||
// Paper | RGBGFX
|
||||
// ------+-------
|
||||
// Tile | Proto-palette
|
||||
// Page | Palette
|
||||
|
||||
/**
|
||||
* A reference to a proto-palette, and attached attributes for sorting purposes
|
||||
*/
|
||||
struct ProtoPalAttrs {
|
||||
size_t const palIndex;
|
||||
/**
|
||||
* Pages from which we are banned (to prevent infinite loops)
|
||||
* This is dynamic because we wish not to hard-cap the amount of palettes
|
||||
*/
|
||||
std::vector<bool> bannedPages;
|
||||
|
||||
ProtoPalAttrs(size_t index) : palIndex(index) {}
|
||||
bool isBannedFrom(size_t index) const {
|
||||
return index < bannedPages.size() && bannedPages[index];
|
||||
}
|
||||
void banFrom(size_t index) {
|
||||
if (bannedPages.size() <= index) {
|
||||
bannedPages.resize(index + 1);
|
||||
}
|
||||
bannedPages[index] = true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A collection of proto-palettes assigned to a palette
|
||||
* Does not contain the actual color indices because we need to be able to remove elements
|
||||
*/
|
||||
class AssignedProtos {
|
||||
// We leave room for emptied slots to avoid copying the structs around on removal
|
||||
std::vector<std::optional<ProtoPalAttrs>> _assigned;
|
||||
// For resolving proto-palette indices
|
||||
std::vector<ProtoPalette> const &_protoPals;
|
||||
|
||||
public:
|
||||
template<typename... Ts>
|
||||
AssignedProtos(decltype(_protoPals) protoPals, Ts &&...elems)
|
||||
: _assigned{std::forward<Ts>(elems)...}, _protoPals{protoPals} {}
|
||||
|
||||
private:
|
||||
template<typename Inner, template<typename> typename Constness>
|
||||
class Iter {
|
||||
public:
|
||||
friend class AssignedProtos;
|
||||
// For `iterator_traits`
|
||||
using difference_type = typename std::iterator_traits<Inner>::difference_type;
|
||||
using value_type = ProtoPalAttrs;
|
||||
using pointer = Constness<value_type> *;
|
||||
using reference = Constness<value_type> &;
|
||||
using iterator_category = std::input_iterator_tag;
|
||||
|
||||
private:
|
||||
Constness<decltype(_assigned)> *_array = nullptr;
|
||||
Inner _iter{};
|
||||
|
||||
Iter(decltype(_array) array, decltype(_iter) &&iter) : _array(array), _iter(iter) {
|
||||
skipEmpty();
|
||||
}
|
||||
void skipEmpty() {
|
||||
while (_iter != _array->end() && !(*_iter).has_value()) {
|
||||
++_iter;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
Iter() = default;
|
||||
|
||||
bool operator==(Iter const &other) const { return _iter == other._iter; }
|
||||
bool operator!=(Iter const &other) const { return !(*this == other); }
|
||||
Iter &operator++() {
|
||||
++_iter;
|
||||
skipEmpty();
|
||||
return *this;
|
||||
}
|
||||
Iter operator++(int) {
|
||||
Iter it = *this;
|
||||
++(*this);
|
||||
return it;
|
||||
}
|
||||
reference operator*() const {
|
||||
assert((*_iter).has_value());
|
||||
return **_iter;
|
||||
}
|
||||
pointer operator->() const {
|
||||
return &(**this); // Invokes the operator above, not quite a no-op!
|
||||
}
|
||||
|
||||
friend void swap(Iter &lhs, Iter &rhs) {
|
||||
swap(lhs._array, rhs._array);
|
||||
swap(lhs._iter, rhs._iter);
|
||||
}
|
||||
};
|
||||
public:
|
||||
using iterator = Iter<decltype(_assigned)::iterator, std::remove_const_t>;
|
||||
iterator begin() { return iterator{&_assigned, _assigned.begin()}; }
|
||||
iterator end() { return iterator{&_assigned, _assigned.end()}; }
|
||||
using const_iterator = Iter<decltype(_assigned)::const_iterator, std::add_const_t>;
|
||||
const_iterator begin() const { return const_iterator{&_assigned, _assigned.begin()}; }
|
||||
const_iterator end() const { return const_iterator{&_assigned, _assigned.end()}; }
|
||||
|
||||
/**
|
||||
* Assigns a new ProtoPalAttrs in a free slot, assuming there is one
|
||||
* Args are passed to the `ProtoPalAttrs`'s constructor
|
||||
*/
|
||||
template<typename... Ts>
|
||||
auto assign(Ts &&...args) {
|
||||
auto freeSlot = std::find_if_not(
|
||||
_assigned.begin(), _assigned.end(),
|
||||
[](std::optional<ProtoPalAttrs> const &slot) { return slot.has_value(); });
|
||||
|
||||
if (freeSlot == _assigned.end()) { // We are full, use a new slot
|
||||
_assigned.emplace_back(std::forward<Ts>(args)...);
|
||||
} else { // Reuse a free slot
|
||||
(*freeSlot).emplace(std::forward<Ts>(args)...);
|
||||
}
|
||||
return freeSlot;
|
||||
}
|
||||
void remove(iterator const &iter) {
|
||||
(*iter._iter).reset(); // This time, we want to access the `optional` itself
|
||||
}
|
||||
void clear() { _assigned.clear(); }
|
||||
|
||||
/**
|
||||
* Computes the "relative size" of a proto-palette on this palette
|
||||
*/
|
||||
double relSizeOf(ProtoPalette const &protoPal) const {
|
||||
return std::transform_reduce(
|
||||
protoPal.begin(), protoPal.end(), .0, std::plus<>(), [this](uint16_t color) {
|
||||
// NOTE: The paper and the associated code disagree on this: the code has
|
||||
// this `1 +`, whereas the paper does not; its lack causes a division by 0
|
||||
// if the symbol is not found anywhere, so I'm assuming the paper is wrong.
|
||||
return 1.
|
||||
/ (1
|
||||
+ std::count_if(
|
||||
begin(), end(), [this, &color](ProtoPalAttrs const &attrs) {
|
||||
ProtoPalette const &pal = _protoPals[attrs.palIndex];
|
||||
return std::find(pal.begin(), pal.end(), color) != pal.end();
|
||||
}));
|
||||
});
|
||||
}
|
||||
private:
|
||||
std::unordered_set<uint16_t> &uniqueColors() const {
|
||||
// We check for *distinct* colors by stuffing them into a `set`; this should be
|
||||
// faster than "back-checking" on every element (O(n²))
|
||||
//
|
||||
// TODO: calc84maniac suggested another approach; try implementing it, see if it
|
||||
// performs better:
|
||||
// > So basically you make a priority queue that takes iterators into each of your sets
|
||||
// (paired with end iterators so you'll know where to stop), and the comparator tests the
|
||||
// values pointed to by each iterator > Then each iteration you pop from the queue,
|
||||
// optionally add one to your count, increment the iterator and push it back into the queue
|
||||
// if it didn't reach the end > and you do this until the priority queue is empty
|
||||
static std::unordered_set<uint16_t> colors;
|
||||
|
||||
colors.clear();
|
||||
for (ProtoPalAttrs const &attrs : *this) {
|
||||
for (uint16_t color : _protoPals[attrs.palIndex]) {
|
||||
colors.insert(color);
|
||||
}
|
||||
}
|
||||
return colors;
|
||||
}
|
||||
public:
|
||||
/**
|
||||
* Returns the number of distinct colors
|
||||
*/
|
||||
size_t volume() const { return uniqueColors().size(); }
|
||||
bool canFit(ProtoPalette const &protoPal) const {
|
||||
auto &colors = uniqueColors();
|
||||
for (uint16_t color : protoPal) {
|
||||
colors.insert(color);
|
||||
}
|
||||
return colors.size() <= 4;
|
||||
}
|
||||
};
|
||||
|
||||
std::tuple<DefaultInitVec<size_t>, size_t>
|
||||
overloadAndRemove(std::vector<ProtoPalette> const &protoPalettes) {
|
||||
options.verbosePrint("Paginating palettes using \"overload-and-remove\" strategy...\n");
|
||||
|
||||
struct Iota {
|
||||
using value_type = size_t;
|
||||
using difference_type = size_t;
|
||||
using pointer = value_type const *;
|
||||
using reference = value_type const &;
|
||||
using iterator_category = std::input_iterator_tag;
|
||||
|
||||
// Use aggregate init etc.
|
||||
value_type i;
|
||||
|
||||
bool operator!=(Iota const &other) { return i != other.i; }
|
||||
reference operator*() const { return i; }
|
||||
pointer operator->() const { return &i; }
|
||||
Iota operator++() {
|
||||
++i;
|
||||
return *this;
|
||||
}
|
||||
Iota operator++(int) {
|
||||
Iota copy = *this;
|
||||
++i;
|
||||
return copy;
|
||||
}
|
||||
};
|
||||
|
||||
// Begin with all proto-palettes queued up for insertion
|
||||
std::queue queue(std::deque<ProtoPalAttrs>(Iota{0}, Iota{protoPalettes.size()}));
|
||||
// Begin with no pages
|
||||
std::vector<AssignedProtos> assignments{};
|
||||
|
||||
for (; !queue.empty(); queue.pop()) {
|
||||
ProtoPalAttrs const &attrs = queue.front(); // Valid until the `queue.pop()`
|
||||
|
||||
ProtoPalette const &protoPal = protoPalettes[attrs.palIndex];
|
||||
size_t bestPalIndex = assignments.size();
|
||||
// We're looking for a palette where the proto-palette's relative size is less than
|
||||
// its actual size; so only overwrite the "not found" index on meeting that criterion
|
||||
double bestRelSize = protoPal.size();
|
||||
|
||||
for (size_t i = 0; i < assignments.size(); ++i) {
|
||||
// Skip the page if this one is banned from it
|
||||
if (attrs.isBannedFrom(i)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
options.verbosePrint("%zu: Rel size: %f (size = %zu)\n", i,
|
||||
assignments[i].relSizeOf(protoPal), protoPal.size());
|
||||
if (assignments[i].relSizeOf(protoPal) < bestRelSize) {
|
||||
bestPalIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestPalIndex == assignments.size()) {
|
||||
// Found nowhere to put it, create a new page containing just that one
|
||||
assignments.emplace_back(protoPalettes, std::move(attrs));
|
||||
} else {
|
||||
auto &bestPal = assignments[bestPalIndex];
|
||||
// Add the color to that palette
|
||||
bestPal.assign(std::move(attrs));
|
||||
|
||||
// If this overloads the palette, get it back to normal (if possible)
|
||||
while (bestPal.volume() > 4) {
|
||||
options.verbosePrint("Palette %zu is overloaded! (%zu > 4)\n", bestPalIndex,
|
||||
bestPal.volume());
|
||||
|
||||
// Look for a proto-pal minimizing "efficiency" (size / rel_size)
|
||||
auto efficiency = [&bestPal](ProtoPalette const &pal) {
|
||||
return pal.size() / bestPal.relSizeOf(pal);
|
||||
};
|
||||
auto [minEfficiencyIter, maxEfficiencyIter] =
|
||||
std::minmax_element(bestPal.begin(), bestPal.end(),
|
||||
[&efficiency, &protoPalettes](ProtoPalAttrs const &lhs,
|
||||
ProtoPalAttrs const &rhs) {
|
||||
return efficiency(protoPalettes[lhs.palIndex])
|
||||
< efficiency(protoPalettes[rhs.palIndex]);
|
||||
});
|
||||
|
||||
// All efficiencies are identical iff min equals max
|
||||
// TODO: maybe not ideal to re-compute these two?
|
||||
// TODO: yikes for float comparison! I *think* this threshold is OK?
|
||||
if (efficiency(protoPalettes[maxEfficiencyIter->palIndex])
|
||||
- efficiency(protoPalettes[minEfficiencyIter->palIndex])
|
||||
< .001) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Remove the proto-pal with minimal efficiency
|
||||
queue.emplace(std::move(*minEfficiencyIter));
|
||||
queue.back().banFrom(bestPalIndex); // Ban it from this palette
|
||||
bestPal.remove(minEfficiencyIter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deal with palettes still overloaded, by emptying them
|
||||
for (AssignedProtos &pal : assignments) {
|
||||
if (pal.volume() > 4) {
|
||||
for (ProtoPalAttrs &attrs : pal) {
|
||||
queue.emplace(std::move(attrs));
|
||||
}
|
||||
pal.clear();
|
||||
}
|
||||
}
|
||||
// Place back any proto-palettes now in the queue via first-fit
|
||||
while (!queue.empty()) {
|
||||
ProtoPalAttrs const &attrs = queue.front();
|
||||
ProtoPalette const &protoPal = protoPalettes[attrs.palIndex];
|
||||
auto iter =
|
||||
std::find_if(assignments.begin(), assignments.end(),
|
||||
[&protoPal](AssignedProtos const &pal) { return pal.canFit(protoPal); });
|
||||
if (iter == assignments.end()) { // No such page, create a new one
|
||||
options.verbosePrint("Adding new palette for overflow\n");
|
||||
assignments.emplace_back(protoPalettes, std::move(attrs));
|
||||
} else {
|
||||
options.verbosePrint("Assigning overflow to palette %zu\n", iter - assignments.begin());
|
||||
iter->assign(std::move(attrs));
|
||||
}
|
||||
queue.pop();
|
||||
}
|
||||
// Deal with any empty palettes left over from the "un-overloading" step
|
||||
// TODO (can there be any?)
|
||||
|
||||
if (options.beVerbose) {
|
||||
for (auto &&assignment : assignments) {
|
||||
options.verbosePrint("{ ");
|
||||
for (auto &&attrs : assignment) {
|
||||
for (auto &&colorIndex : protoPalettes[attrs.palIndex]) {
|
||||
options.verbosePrint("%04" PRIx16 ", ", colorIndex);
|
||||
}
|
||||
}
|
||||
options.verbosePrint("} (%zu)\n", assignment.volume());
|
||||
}
|
||||
}
|
||||
|
||||
DefaultInitVec<size_t> mappings(protoPalettes.size());
|
||||
for (size_t i = 0; i < assignments.size(); ++i) {
|
||||
for (ProtoPalAttrs const &attrs : assignments[i]) {
|
||||
mappings[attrs.palIndex] = i;
|
||||
}
|
||||
}
|
||||
return {mappings, assignments.size()};
|
||||
}
|
||||
|
||||
} // namespace packing
|
||||
Reference in New Issue
Block a user