Porytiles
Loading...
Searching...
No Matches
palette_builder.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <map>
5#include <set>
6
9
10namespace porytiles {
11
12namespace {
13
15struct PaletteBuildState {
16 std::size_t hw_index;
17 std::map<Rgba32, ColorPosition> color_positions;
18 std::set<std::size_t> prefilled_slots;
19};
20
27[[nodiscard]] std::optional<std::size_t> try_resolve_indirect(
28 const IndirectPosition &start, const std::array<std::optional<PaletteBuildState>, palette::num_palettes> &states)
29{
30 IndirectPosition current = start;
31 for (std::size_t iter = 0; iter < palette::num_palettes; ++iter) {
32 if (!states.at(current.ref_palette_index).has_value()) {
33 return std::nullopt;
34 }
35 const auto &ref_state = states.at(current.ref_palette_index).value();
36 if (!ref_state.color_positions.contains(current.ref_color)) {
37 return std::nullopt;
38 }
39 const auto &ref_position = ref_state.color_positions.at(current.ref_color);
40
41 if (std::holds_alternative<AbsolutePosition>(ref_position)) {
42 return std::get<AbsolutePosition>(ref_position).slot;
43 }
44 else if (std::holds_alternative<IndirectPosition>(ref_position)) {
45 current = std::get<IndirectPosition>(ref_position);
46 }
47 else {
48 // Undetermined: reference palette not yet sequentially filled
49 return std::nullopt;
50 }
51 }
52 panic("Indirect chain resolution exceeded maximum iterations (cycle detected)");
53}
54
55} // namespace
56
57std::array<std::optional<Palette<Rgba32, palette::max_size>>, palette::num_palettes> build_all_output_palettes(
58 const std::vector<PackedPalette> &packed_palettes,
59 const std::array<std::optional<Palette<Rgba32, palette::max_size>>, palette::num_palettes> &prefilled_palettes,
60 const ColorIndexMap<Rgba32> &color_map,
61 const Rgba32 &default_slot_zero,
62 const std::vector<IndirectLink> &indirect_links,
63 AlignmentFailureCounts *failure_counts)
64{
65 // Build per-palette state indexed by hardware index
66 std::array<std::optional<PaletteBuildState>, palette::num_palettes> states{};
67
68 // Phase 1: Initialize position maps
69 for (const PackedPalette &packed_palette : packed_palettes) {
70 const std::size_t hw = packed_palette.hardware_index();
71 if (hw >= palette::num_palettes) {
72 panic("invalid hardware index " + std::to_string(hw) + ": out of range");
73 }
74
75 PaletteBuildState state;
76 state.hw_index = hw;
77
78 const Palette<Rgba32, palette::max_size> *prefilled_ptr =
79 prefilled_palettes.at(hw).has_value() ? &prefilled_palettes.at(hw).value() : nullptr;
80
81 // Track prefilled (locked) slots
82 if (prefilled_ptr != nullptr) {
83 for (std::size_t i = 1; i < palette::max_size; ++i) {
84 if (!prefilled_ptr->is_wildcard(i)) {
85 state.prefilled_slots.insert(i);
86 }
87 }
88 }
89
90 // Collect colors from PackedPalette that need to be placed.
91 // Use for_each_color to iterate in the same order as the old build_output_palette.
92 std::set<Rgba32> already_placed_colors;
93
94 // Colors from prefilled slots are already placed
95 if (prefilled_ptr != nullptr) {
96 for (std::size_t i = 1; i < palette::max_size; ++i) {
97 if (!prefilled_ptr->is_wildcard(i)) {
98 already_placed_colors.insert(prefilled_ptr->at(i));
99 // Prefilled colors get Absolute positions at their locked slots
100 state.color_positions[prefilled_ptr->at(i)] = AbsolutePosition{i};
101 }
102 }
103 }
104
105 // Remaining colors from the packed palette start as Undetermined
106 for_each_color(packed_palette.color_set(), [&](const std::size_t color_index) {
107 const auto color_opt = color_map.color_at_index(ColorIndex{color_index});
108 if (!color_opt.has_value()) {
109 panic("color_index " + std::to_string(color_index) + " not found in color map");
110 }
111 const auto &color = color_opt.value();
112 if (!already_placed_colors.contains(color)) {
113 state.color_positions[color] = UndeterminedPosition{};
114 }
115 });
116
117 states.at(hw) = std::move(state);
118 }
119
120 // Track successfully applied indirect links for post-resolution verification
121 struct AppliedIndirect {
122 std::size_t source_palette;
123 Rgba32 source_color;
124 std::size_t ref_palette;
125 Rgba32 ref_color;
126 std::size_t source_group_index;
127 };
128 std::vector<AppliedIndirect> applied_indirects;
129
130 // Phase 2: Apply Indirect links
131 for (const auto &link : indirect_links) {
132 if (!states.at(link.source_palette).has_value()) {
133 continue;
134 }
135 auto &state = states.at(link.source_palette).value();
136
137 if (!state.color_positions.contains(link.source_color)) {
138 continue;
139 }
140
141 auto &position = state.color_positions.at(link.source_color);
142 // First-writer-wins: only set Indirect on Undetermined positions (prevents cycles)
143 if (std::holds_alternative<UndeterminedPosition>(position)) {
144 position = IndirectPosition{link.ref_palette, link.ref_color, link.source_group_index};
145 // Don't record here. Phase 4 will record if resolution succeeds.
146 }
147 else if (std::holds_alternative<IndirectPosition>(position)) {
148 const auto &existing = std::get<IndirectPosition>(position);
149 // Compatible: same reference, no actual conflict
150 if (existing.ref_palette_index == link.ref_palette && existing.ref_color == link.ref_color) {
151 // The existing IndirectPosition already satisfies this link. Record for post-verification
152 // (Phase 4 will only record the winning group, so compatible groups need recording here)
153 applied_indirects.push_back(
154 AppliedIndirect{
155 link.source_palette,
156 link.source_color,
157 link.ref_palette,
158 link.ref_color,
159 link.source_group_index});
160 continue;
161 }
162 if (failure_counts != nullptr) {
163 failure_counts->first_writer_wins_details.push_back(
164 FirstWriterWinsDetail{
165 .source_group_index = link.source_group_index,
166 .source_palette_index = link.source_palette,
167 .source_color = link.source_color,
168 .winning_group_index = existing.source_group_index,
169 .winning_ref_palette_index = existing.ref_palette_index,
170 .winning_ref_color = existing.ref_color,
171 .losing_ref_palette_index = link.ref_palette,
172 .losing_ref_color = link.ref_color});
173 }
174 }
175 else if (std::holds_alternative<AbsolutePosition>(position)) {
176 // Link dropped: source color is prefilled (locked). Phase 1 set it to AbsolutePosition.
177 // However, if the ref color in the ref palette is also AbsolutePosition at the same slot,
178 // alignment is naturally satisfied, so there is no conflict to report.
179 bool naturally_aligned = false;
180 const auto source_slot = std::get<AbsolutePosition>(position).slot;
181 if (states.at(link.ref_palette).has_value()) {
182 const auto &ref_state = states.at(link.ref_palette).value();
183 if (ref_state.color_positions.contains(link.ref_color)) {
184 const auto &ref_position = ref_state.color_positions.at(link.ref_color);
185 if (std::holds_alternative<AbsolutePosition>(ref_position) &&
186 std::get<AbsolutePosition>(ref_position).slot == source_slot) {
187 naturally_aligned = true;
188 }
189 }
190 }
191 if (!naturally_aligned && failure_counts != nullptr) {
192 failure_counts->prefilled_source_conflict_details.push_back(
193 PrefilledSourceConflictDetail{
194 .source_group_index = link.source_group_index,
195 .source_palette_index = link.source_palette,
196 .source_color = link.source_color,
197 .ref_palette_index = link.ref_palette,
198 .ref_color = link.ref_color});
199 }
200 }
201 }
202
203 // === Phase 3: Sequential fill ALL palettes (skipping Indirect) ===
204 //
205 // Every Undetermined color gets an Absolute slot. Indirect colors are left untouched; they'll be resolved in
206 // Phase 4. After this phase, all reference colors (which are Undetermined, not Indirect) have stable Absolute
207 // positions, enabling Indirect chain resolution.
208 for (auto &state_opt : states) {
209 if (!state_opt.has_value()) {
210 continue;
211 }
212 auto &state = state_opt.value();
213
214 // Collect slots already used by Absolute positions (prefilled)
215 std::set<std::size_t> used_slots;
216 used_slots.insert(0); // Slot 0 is always reserved
217 for (const auto &[color, position] : state.color_positions) {
218 if (std::holds_alternative<AbsolutePosition>(position)) {
219 used_slots.insert(std::get<AbsolutePosition>(position).slot);
220 }
221 }
222
223 // Assign next free slot to each Undetermined color; skip Indirect colors
224 std::size_t next_slot = 1;
225 for (auto &[color, position] : state.color_positions) {
226 if (!std::holds_alternative<UndeterminedPosition>(position)) {
227 continue;
228 }
229 while (next_slot < palette::max_size && used_slots.contains(next_slot)) {
230 ++next_slot;
231 }
232 if (next_slot < palette::max_size) {
233 position = AbsolutePosition{next_slot};
234 used_slots.insert(next_slot);
235 ++next_slot;
236 }
237 else {
238 panic("ran out of palette slots during sequential fill for palette " + std::to_string(state.hw_index));
239 }
240 }
241 }
242
243 // === Phase 4: Resolve Indirect chains with eviction ===
244 //
245 // Now all reference colors have Absolute positions (from Phase 3). Resolve each Indirect color to the reference
246 // color's slot. If the target slot is already occupied by a sequential-fill color, evict the occupant to the next
247 // free slot. Prefilled slots are never evicted.
248 //
249 // This handles cross-palette Indirect dependencies (where palette A links to B and B links to A for different
250 // shape groups) without deadlocking, because the reference colors are always Undetermined (not Indirect) and
251 // were assigned Absolute positions in Phase 3.
252 for (std::size_t palette_index = 0; palette_index < states.size(); ++palette_index) {
253 if (!states.at(palette_index).has_value()) {
254 continue;
255 }
256 auto &state = states.at(palette_index).value();
257
258 // Collect all Indirect colors and their resolved target slots
259 struct IndirectResolution {
260 Rgba32 color;
261 std::size_t target_slot;
262 std::size_t source_group_index;
263 std::size_t ref_palette_index;
264 Rgba32 ref_color;
265 };
266 std::vector<IndirectResolution> resolutions;
267
268 for (const auto &[color, position] : state.color_positions) {
269 if (!std::holds_alternative<IndirectPosition>(position)) {
270 continue;
271 }
272
273 const auto &indirect_pos = std::get<IndirectPosition>(position);
274 auto resolved = try_resolve_indirect(indirect_pos, states);
275 if (!resolved.has_value()) {
276 panic(
277 "Indirect chain resolution returned nullopt for color in palette " + std::to_string(palette_index) +
278 ": internal invariant violated");
279 }
280
281 // Skip if the target slot is prefilled (can't evict prefilled)
282 if (state.prefilled_slots.contains(resolved.value())) {
283 if (failure_counts != nullptr) {
284 // Capture detail: find the color occupying the locked slot
285 Rgba32 locked_color{};
286 for (const auto &[c, p] : state.color_positions) {
287 if (std::holds_alternative<AbsolutePosition>(p) &&
288 std::get<AbsolutePosition>(p).slot == resolved.value()) {
289 locked_color = c;
290 break;
291 }
292 }
293 failure_counts->prefilled_destination_conflict_details.push_back(
294 PrefilledDestinationConflictDetail{
295 .source_group_index = indirect_pos.source_group_index,
296 .palette_index = palette_index,
297 .target_slot = resolved.value(),
298 .blocked_color = color,
299 .locked_color = locked_color});
300 }
301 continue;
302 }
303
304 resolutions.push_back(
305 IndirectResolution{
306 color,
307 resolved.value(),
308 indirect_pos.source_group_index,
309 indirect_pos.ref_palette_index,
310 indirect_pos.ref_color});
311 }
312
313 // Apply resolutions with eviction
314 for (const auto &[indirect_color, target_slot, source_group_index, res_ref_palette, res_ref_color] :
315 resolutions) {
316 // Check if the target slot is occupied by a sequential-fill color
317 Rgba32 evicted_color{};
318 bool needs_eviction = false;
319
320 for (auto &[color, position] : state.color_positions) {
321 if (color == indirect_color) {
322 continue;
323 }
324 if (std::holds_alternative<AbsolutePosition>(position) &&
325 std::get<AbsolutePosition>(position).slot == target_slot &&
326 !state.prefilled_slots.contains(target_slot)) {
327 evicted_color = color;
328 needs_eviction = true;
329 break;
330 }
331 }
332
333 if (needs_eviction) {
334 // Find next free slot for the evicted color. Note: target_slot stays in all_used so the free
335 // slot search won't pick target_slot itself (that slot is being claimed by the Indirect color).
336 std::set<std::size_t> all_used;
337 all_used.insert(0);
338 for (const auto &[c, p] : state.color_positions) {
339 if (std::holds_alternative<AbsolutePosition>(p)) {
340 all_used.insert(std::get<AbsolutePosition>(p).slot);
341 }
342 }
343 std::size_t free_slot = 1;
344 while (free_slot < palette::max_size && all_used.contains(free_slot)) {
345 ++free_slot;
346 }
347 if (free_slot < palette::max_size) {
348 state.color_positions.at(evicted_color) = AbsolutePosition{free_slot};
349 }
350 else {
351 panic(
352 "no free slot for eviction in palette " + std::to_string(palette_index) +
353 ": internal invariant violated. Phase 3 guarantees one free slot per Indirect color.");
354 }
355 }
356
357 // Place the Indirect color at the target slot
358 state.color_positions.at(indirect_color) = AbsolutePosition{target_slot};
359
360 // Record successfully resolved link for post-resolution verification
361 applied_indirects.push_back(
362 AppliedIndirect{palette_index, indirect_color, res_ref_palette, res_ref_color, source_group_index});
363 }
364 }
365
366 // === Phase 5: Fallback, assign free slots to unresolved Indirect colors ===
367 //
368 // Phase 4 may leave colors in IndirectPosition if resolution failed (prefilled destination conflict). These colors
369 // still need placement in the final palette, so assign them sequential
370 // free slots, identical to Phase 3's logic but targeting IndirectPosition instead of UndeterminedPosition.
371 for (auto &state_opt : states) {
372 if (!state_opt.has_value()) {
373 continue;
374 }
375 auto &state = state_opt.value();
376
377 // Collect slots already used by Absolute positions
378 std::set<std::size_t> used_slots;
379 used_slots.insert(0); // Slot 0 is always reserved
380 for (const auto &[color, position] : state.color_positions) {
381 if (std::holds_alternative<AbsolutePosition>(position)) {
382 used_slots.insert(std::get<AbsolutePosition>(position).slot);
383 }
384 }
385
386 // Assign next free slot to each remaining Indirect color
387 std::size_t next_slot = 1;
388 for (auto &[color, position] : state.color_positions) {
389 if (!std::holds_alternative<IndirectPosition>(position)) {
390 continue;
391 }
392 while (next_slot < palette::max_size && used_slots.contains(next_slot)) {
393 ++next_slot;
394 }
395 if (next_slot < palette::max_size) {
396 position = AbsolutePosition{next_slot};
397 used_slots.insert(next_slot);
398 ++next_slot;
399 }
400 else {
401 panic(
402 "ran out of palette slots during Indirect fallback fill for palette " +
403 std::to_string(state.hw_index));
404 }
405 }
406 }
407
408 // Post-resolution verification: detect eviction displacement
409 if (failure_counts != nullptr) {
410 for (const auto &ai : applied_indirects) {
411 if (!states.at(ai.source_palette).has_value() || !states.at(ai.ref_palette).has_value()) {
412 continue;
413 }
414 const auto &source_state = states.at(ai.source_palette).value();
415 const auto &ref_state = states.at(ai.ref_palette).value();
416
417 if (!source_state.color_positions.contains(ai.source_color) ||
418 !ref_state.color_positions.contains(ai.ref_color)) {
419 continue;
420 }
421
422 const auto &source_pos = source_state.color_positions.at(ai.source_color);
423 const auto &ref_pos = ref_state.color_positions.at(ai.ref_color);
424
425 if (!std::holds_alternative<AbsolutePosition>(source_pos) ||
426 !std::holds_alternative<AbsolutePosition>(ref_pos)) {
427 continue;
428 }
429
430 const auto source_slot = std::get<AbsolutePosition>(source_pos).slot;
431 const auto ref_slot = std::get<AbsolutePosition>(ref_pos).slot;
432
433 if (source_slot != ref_slot) {
434 failure_counts->post_resolution_mismatch_details.push_back(
435 PostResolutionMismatchDetail{
436 .source_group_index = ai.source_group_index,
437 .source_palette_index = ai.source_palette,
438 .source_color = ai.source_color,
439 .source_final_slot = source_slot,
440 .ref_palette_index = ai.ref_palette,
441 .ref_color = ai.ref_color,
442 .ref_final_slot = ref_slot});
443 }
444 }
445 }
446
447 // Deduplicate detail records: multiple IndirectLinks for the same color pair (from different group members) can
448 // produce identical detail records. Sort + unique ensures counts and diagnostic output reflect unique failures.
449 if (failure_counts != nullptr) {
450 auto dedup = [](auto &vec) {
451 std::ranges::sort(vec);
452 auto [first, last] = std::ranges::unique(vec);
453 vec.erase(first, last);
454 };
455 dedup(failure_counts->prefilled_destination_conflict_details);
456 dedup(failure_counts->prefilled_source_conflict_details);
457 dedup(failure_counts->first_writer_wins_details);
458 dedup(failure_counts->post_resolution_mismatch_details);
459 }
460
461 // Phase 6: Build final palettes from resolved positions
462 std::array<std::optional<Palette<Rgba32, palette::max_size>>, palette::num_palettes> result{};
463
464 for (const auto &state_opt : states) {
465 if (!state_opt.has_value()) {
466 continue;
467 }
468 const auto &state = state_opt.value();
469 const std::size_t hw = state.hw_index;
470
471 Palette<Rgba32, palette::max_size> output{Rgba32{0, 0, 0, Rgba32::alpha_opaque}};
472
473 // Set slot 0
474 const Palette<Rgba32, palette::max_size> *prefilled_ptr =
475 prefilled_palettes.at(hw).has_value() ? &prefilled_palettes.at(hw).value() : nullptr;
476 if (prefilled_ptr != nullptr && !prefilled_ptr->is_wildcard(0)) {
477 output.set(0, prefilled_ptr->at(0));
478 }
479 else {
480 output.set(0, default_slot_zero);
481 }
482
483 // Place prefilled slots
484 if (prefilled_ptr != nullptr) {
485 for (std::size_t i = 1; i < palette::max_size; ++i) {
486 if (!prefilled_ptr->is_wildcard(i)) {
487 output.set(i, prefilled_ptr->at(i));
488 }
489 }
490 }
491
492 // Place all Absolute-position colors
493 for (const auto &[color, position] : state.color_positions) {
494 if (std::holds_alternative<AbsolutePosition>(position)) {
495 const std::size_t slot = std::get<AbsolutePosition>(position).slot;
496 if (!state.prefilled_slots.contains(slot)) {
497 output.set(slot, color);
498 }
499 }
500 }
501
502 result.at(hw) = output;
503 }
504
505 return result;
506}
507
508} // namespace porytiles
A bidirectional mapping between pixel color values and sequential integer indices.
Represents a hardware palette after packing with accumulated colors and assigned tiles.
A generic palette container for colors that support transparency checking.
Definition palette.hpp:45
ColorType at(std::size_t index) const
Gets the color at a specific index.
Definition palette.hpp:246
bool is_wildcard(std::size_t index) const
Checks if a slot is a wildcard.
Definition palette.hpp:176
Represents a 32-bit RGBA color.
Definition rgba32.hpp:21
static constexpr std::uint8_t alpha_opaque
Definition rgba32.hpp:24
std::size_t start
constexpr std::size_t max_size
Definition palette.hpp:19
constexpr std::size_t num_palettes
Definition palette.hpp:21
void for_each_color(const ColorSet &set, Func &&func)
Iterates over each color index in a ColorSet.
void panic(const StringViewSourceLoc &s)
Unconditionally terminates the program with a panic message.
Definition panic.cpp:43
std::array< std::optional< Palette< Rgba32, palette::max_size > >, palette::num_palettes > build_all_output_palettes(const std::vector< PackedPalette > &packed_palettes, const std::array< std::optional< Palette< Rgba32, palette::max_size > >, palette::num_palettes > &prefilled_palettes, const ColorIndexMap< Rgba32 > &color_map, const Rgba32 &default_slot_zero, const std::vector< IndirectLink > &indirect_links, AlignmentFailureCounts *failure_counts=nullptr)
Builds all output palettes from packed palettes and Indirect links in a single call.
std::size_t hw_index
std::map< Rgba32, ColorPosition > color_positions
std::set< std::size_t > prefilled_slots
Position state for a color assigned to a specific palette slot.
Detailed records of alignment failures during Indirect link application and chain resolution.
Position state for a color that has not yet been assigned a palette slot.