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