Porytiles
Loading...
Searching...
No Matches
best_fusion_strategy.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4
9
10namespace {
11
12using namespace porytiles;
13
30[[nodiscard]] std::optional<std::size_t> find_best_palette(
31 const PackableTile &tile, const std::vector<PackedPalette> &palettes, const ShapeGroupMetadata *metadata)
32{
33 std::optional<std::size_t> best_idx;
34 double best_cost = std::numeric_limits<double>::max();
35
36 for (std::size_t i = 0; i < palettes.size(); ++i) {
37 const auto &pal = palettes[i];
38
39 // Skip palettes that can't fit the tile
40 if (!pal.can_fit(tile.color_set())) {
41 continue;
42 }
43
44 // Use fast metric function with cached color counts - O(colors) instead of O(tiles × colors)
45 double cost = compute_weighted_cost_in_palette_fast(tile.color_set(), pal);
46
47 // Add sharing penalty to deprioritize palettes that already contain a shape group sibling
48 if (metadata != nullptr) {
49 cost += compute_sharing_penalty(tile, pal, *metadata);
50 }
51
52 if (cost < best_cost) {
53 best_cost = cost;
54 best_idx = i;
55 }
56 }
57
58 // If best cost >= tile's color count, prefer creating a new palette
59 // (no significant overlap benefit - each color contributes 1.0 when count is 0)
60 if (best_idx.has_value() && best_cost >= static_cast<double>(tile.color_count())) {
61 return std::nullopt;
62 }
63
64 return best_idx;
65}
66
67} // namespace
68
69namespace porytiles {
70
72{
73 PackingOutput output;
74 PalettePool pal_pool = input.pal_pool_;
75
76 // Initialize output palettes from prefilled palettes
77 output.pals_ = initialize_packed_palettes(input.prefilled_pals_, pal_pool, input.pal_capacity_);
78
79 // Create additional empty palettes from the rest of the available PalettePool slots
80 while (pal_pool.has_available_pal()) {
81 output.pals_.emplace_back(pal_pool.checkout(), input.pal_capacity_);
82 }
83
84 // Extract shape group metadata pointer (nullptr when not sharing-aware)
85 const ShapeGroupMetadata *metadata =
86 input.shape_group_metadata_.has_value() ? &input.shape_group_metadata_.value() : nullptr;
87
88 // Helper to assign a tile
89 // Note: palette-local cost computation now uses cached color counts in PackedPalette,
90 // eliminating the need for a separate tile_colors_map
91 auto assign_tile = [&output, metadata](const PackableTile &tile) -> bool {
92 const auto maybe_best_idx = find_best_palette(tile, output.pals_, metadata);
93
94 if (maybe_best_idx.has_value()) {
95 // Add to existing palette
96 output.pals_[maybe_best_idx.value()].add_tile(tile);
97 output.tile_to_pal_[tile.id()] = output.pals_[maybe_best_idx.value()].hardware_index();
98 return true;
99 }
100
101 // Try to find an empty palette
102 for (std::size_t i = 0; i < output.pals_.size(); ++i) {
103 if (output.pals_[i].color_count() == 0 && output.pals_[i].can_fit(tile.color_set())) {
104 output.pals_[i].add_tile(tile);
105 output.tile_to_pal_[tile.id()] = output.pals_[i].hardware_index();
106 return true;
107 }
108 }
109
110 // Try to find ANY palette that can fit (even without good overlap).
111 // Sibling avoidance is intentionally not applied here. Packing success takes priority over sharing.
112 for (std::size_t i = 0; i < output.pals_.size(); ++i) {
113 if (output.pals_[i].can_fit(tile.color_set())) {
114 output.pals_[i].add_tile(tile);
115 output.tile_to_pal_[tile.id()] = output.pals_[i].hardware_index();
116 return true;
117 }
118 }
119
120 // Cannot fit - would need more palettes
121 return false;
122 };
123
124 // Create pool of tiles to be assigned
125 std::vector<PackableTile> tile_pool{};
126 for (const auto &hint : input.hints_) {
127 tile_pool.emplace_back(hint);
128 }
129 for (const auto &tile : input.tiles_) {
130 tile_pool.emplace_back(tile);
131 }
132
133 if (tile_pool.empty()) {
134 return output;
135 }
136
137 /*
138 * Right now, we mix together the hints and regular tiles before sorting. Do we want this? I think it's probably ok,
139 * since hints still guarantee that colors in the same hint will be in the same palette. And if the user supplied
140 * hints that are larger than any individual tile, they'll go first as expected. However, I think it makes sense to
141 * allow regular tiles that are large to go before smaller hints, since this probably helps to find an optimal
142 * result -- that larger tile *has to* get put somewhere in order for a solution to be found. No sense placing the
143 * hint first, only to block ourselves from finding a possible solution down the line. In other words, the promised
144 * hint postcondition is not violated, and we potentially get a better solution.
145 */
146
147 // Presort the tile pool so tiles with larger color counts come first (First Fit Decreasing heuristic)
148 std::ranges::sort(
149 tile_pool, [](const PackableTile &a, const PackableTile &b) { return a.color_count() > b.color_count(); });
150
151 for (const auto &tile : tile_pool) {
152 if (!assign_tile(tile)) {
153 return FormattableError{"Best Fusion: cannot assign tile - no palette has room."};
154 }
155 }
156
157 return output;
158}
159
160} // namespace porytiles
ChainableResult< PackingOutput > pack(const PackingInput &input) const override
Packs tiles into palettes using the Best Fusion algorithm.
A result type that maintains a chainable sequence of errors for debugging and error reporting.
General-purpose error implementation with formatted message support.
Definition error.hpp:63
Wraps a ColorSet with a tile ID for tracking during palette packing.
std::size_t color_count() const
const ColorSet & color_set() const
const Id & id() const
Manages allocation of hardware palette indexes with stack-based checkout semantics.
std::size_t checkout()
Checks out the next available hardware palette index.
bool has_available_pal() const
Checks if there is at least one available pal that can be checked out.
double compute_weighted_cost_in_palette_fast(const ColorSet &tile_colors, const PackedPalette &palette)
Computes the weighted cost of placing a tile in a palette using cached counts.
double compute_sharing_penalty(const PackableTile &tile, const PackedPalette &palette, const ShapeGroupMetadata &metadata, double sharing_weight=0.5)
Computes the sharing penalty for placing a tile in a palette.
std::vector< PackedPalette > initialize_packed_palettes(const std::set< PrefilledPalette > &prefilled_pals, PalettePool &pal_pool, std::size_t pal_capacity)
Initializes packed palettes from prefilled palettes.
Metrics for Bin Packing with Overlapping Items (Pagination problem).
Input data aggregate for the low-level palette packing algorithm.
std::optional< ShapeGroupMetadata > shape_group_metadata_
Optional shape group metadata for sharing-aware packing.
std::set< PrefilledPalette > prefilled_pals_
Pre-assigned palettes with fixed colors.
std::size_t pal_capacity_
Maximum number of colors per palette.
std::vector< PackableTile > hints_
Priority "hint" tiles that can be assigned before regular tiles.
std::vector< PackableTile > tiles_
Regular tiles to pack into palettes.
PalettePool pal_pool_
A bitset marking which palettes are available for editing.
The final palette assignments after a successful packing operation.
std::map< PackableTile::Id, std::size_t > tile_to_pal_
Maps tile IDs to their assigned hardware palette indices.
std::vector< PackedPalette > pals_
The packed palettes with their colors and assigned tiles.
Metadata that maps PackableTile IDs to shape group membership for sharing-aware packing.