17struct PaletteBuildState {
31[[nodiscard]] std::optional<std::size_t> try_resolve_indirect(
32 const IndirectPosition &
start,
const std::array<std::optional<PaletteBuildState>,
pal::num_pals> &states)
34 IndirectPosition current =
start;
36 if (!states.at(current.ref_pal_index).has_value()) {
39 const auto &ref_state = states.at(current.ref_pal_index).value();
40 if (!ref_state.color_positions.contains(current.ref_color)) {
43 const auto &ref_position = ref_state.color_positions.at(current.ref_color);
45 if (std::holds_alternative<AbsolutePosition>(ref_position)) {
46 return std::get<AbsolutePosition>(ref_position).slot;
48 else if (std::holds_alternative<IndirectPosition>(ref_position)) {
49 current = std::get<IndirectPosition>(ref_position);
56 panic(
"Indirect chain resolution exceeded maximum iterations (cycle detected)");
62 const std::vector<PackedPalette> &packed_pals,
65 const Rgba32 &default_slot_zero,
66 const std::vector<IndirectLink> &indirect_links,
70 std::array<std::optional<PaletteBuildState>,
pal::num_pals> states{};
74 const std::size_t hw = packed_pal.hardware_index();
76 panic(
"invalid hardware index " + std::to_string(hw) +
": out of range");
79 PaletteBuildState state;
83 prefilled_pals.
at(hw).has_value() ? &prefilled_pals.at(hw).value() :
nullptr;
86 if (prefilled_ptr !=
nullptr) {
89 state.prefilled_slots.insert(i);
96 std::set<Rgba32> already_placed_colors;
99 if (prefilled_ptr !=
nullptr) {
102 already_placed_colors.insert(prefilled_ptr->
at(i));
110 for_each_color(packed_pal.color_set(), [&](
const std::size_t color_index) {
111 const auto color_opt = color_map.color_at_index(ColorIndex{color_index});
112 if (!color_opt.has_value()) {
113 panic(
"color_index " + std::to_string(color_index) +
" not found in color map");
115 const auto &color = color_opt.value();
116 if (!already_placed_colors.contains(color)) {
121 states.at(hw) = std::move(state);
125 struct AppliedIndirect {
126 std::size_t source_pal;
130 std::size_t source_group_index;
132 std::vector<AppliedIndirect> applied_indirects;
135 for (
const auto &link : indirect_links) {
136 if (!states.at(link.source_pal).has_value()) {
139 auto &state = states.at(link.source_pal).value();
141 if (!state.color_positions.contains(link.source_color)) {
145 auto &position = state.color_positions.at(link.source_color);
147 if (std::holds_alternative<UndeterminedPosition>(position)) {
148 position = IndirectPosition{link.ref_pal, link.ref_color, link.source_group_index};
151 else if (std::holds_alternative<IndirectPosition>(position)) {
152 const auto &existing = std::get<IndirectPosition>(position);
154 if (existing.ref_pal_index == link.ref_pal && existing.ref_color == link.ref_color) {
157 applied_indirects.push_back(
159 link.source_pal, link.source_color, link.ref_pal, link.ref_color, link.source_group_index});
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_pal_index = link.source_pal,
167 .source_color = link.source_color,
168 .winning_group_index = existing.source_group_index,
169 .winning_ref_pal_index = existing.ref_pal_index,
170 .winning_ref_color = existing.ref_color,
171 .losing_ref_pal_index = link.ref_pal,
172 .losing_ref_color = link.ref_color});
175 else if (std::holds_alternative<AbsolutePosition>(position)) {
179 bool naturally_aligned =
false;
180 const auto source_slot = std::get<AbsolutePosition>(position).slot;
181 if (states.at(link.ref_pal).has_value()) {
182 const auto &ref_state = states.at(link.ref_pal).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;
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_pal_index = link.source_pal,
196 .source_color = link.source_color,
197 .ref_pal_index = link.ref_pal,
198 .ref_color = link.ref_color});
210 for (
auto &state_opt : states) {
211 if (!state_opt.has_value()) {
214 auto &state = state_opt.value();
217 std::set<std::size_t> used_slots;
218 used_slots.insert(0);
220 if (std::holds_alternative<AbsolutePosition>(position)) {
221 used_slots.insert(std::get<AbsolutePosition>(position).slot);
226 std::size_t next_slot = 1;
228 if (!std::holds_alternative<UndeterminedPosition>(position)) {
231 while (next_slot <
pal::max_size && used_slots.contains(next_slot)) {
235 position = AbsolutePosition{next_slot};
236 used_slots.insert(next_slot);
240 panic(
"ran out of palette slots during sequential fill for palette " + std::to_string(state.hw_index));
256 for (std::size_t pal_index = 0; pal_index < states.size(); ++pal_index) {
257 if (!states.at(pal_index).has_value()) {
260 auto &state = states.at(pal_index).value();
263 struct IndirectResolution {
265 std::size_t target_slot;
266 std::size_t source_group_index;
267 std::size_t ref_pal_index;
270 std::vector<IndirectResolution> resolutions;
273 if (!std::holds_alternative<IndirectPosition>(position)) {
277 const auto &indirect_pos = std::get<IndirectPosition>(position);
278 auto resolved = try_resolve_indirect(indirect_pos, states);
279 if (!resolved.has_value()) {
281 "Indirect chain resolution returned nullopt for color in palette " + std::to_string(pal_index) +
282 ": internal invariant violated");
286 if (state.prefilled_slots.contains(resolved.value())) {
287 if (failure_counts !=
nullptr) {
289 Rgba32 locked_color{};
291 if (std::holds_alternative<AbsolutePosition>(p) &&
292 std::get<AbsolutePosition>(p).slot == resolved.value()) {
297 failure_counts->prefilled_destination_conflict_details.push_back(
298 PrefilledDestinationConflictDetail{
299 .source_group_index = indirect_pos.source_group_index,
300 .palette_index = pal_index,
301 .target_slot = resolved.value(),
302 .blocked_color = color,
303 .locked_color = locked_color});
308 resolutions.push_back(
312 indirect_pos.source_group_index,
313 indirect_pos.ref_pal_index,
314 indirect_pos.ref_color});
318 for (
const auto &[indirect_color, target_slot, source_group_index, res_ref_pal, res_ref_color] : resolutions) {
320 Rgba32 evicted_color{};
321 bool needs_eviction =
false;
324 if (color == indirect_color) {
327 if (std::holds_alternative<AbsolutePosition>(position) &&
328 std::get<AbsolutePosition>(position).slot == target_slot &&
329 !state.prefilled_slots.contains(target_slot)) {
330 evicted_color = color;
331 needs_eviction =
true;
336 if (needs_eviction) {
341 std::set<std::size_t> all_used;
344 if (std::holds_alternative<AbsolutePosition>(p)) {
345 all_used.insert(std::get<AbsolutePosition>(p).slot);
348 std::size_t free_slot = 1;
349 while (free_slot <
pal::max_size && all_used.contains(free_slot)) {
353 state.color_positions.at(evicted_color) = AbsolutePosition{free_slot};
357 "no free slot for eviction in palette " + std::to_string(pal_index) +
358 ": internal invariant violated. Phase 3 guarantees one free slot per Indirect color.");
363 state.color_positions.at(indirect_color) = AbsolutePosition{target_slot};
366 applied_indirects.push_back(
367 AppliedIndirect{pal_index, indirect_color, res_ref_pal, res_ref_color, source_group_index});
378 for (
auto &state_opt : states) {
379 if (!state_opt.has_value()) {
382 auto &state = state_opt.value();
385 std::set<std::size_t> used_slots;
386 used_slots.insert(0);
388 if (std::holds_alternative<AbsolutePosition>(position)) {
389 used_slots.insert(std::get<AbsolutePosition>(position).slot);
394 std::size_t next_slot = 1;
396 if (!std::holds_alternative<IndirectPosition>(position)) {
399 while (next_slot <
pal::max_size && used_slots.contains(next_slot)) {
403 position = AbsolutePosition{next_slot};
404 used_slots.insert(next_slot);
409 "ran out of palette slots during Indirect fallback fill for palette " +
410 std::to_string(state.hw_index));
416 if (failure_counts !=
nullptr) {
417 for (
const auto &ai : applied_indirects) {
418 if (!states.at(ai.source_pal).has_value() || !states.at(ai.ref_pal).has_value()) {
421 const auto &source_state = states.at(ai.source_pal).value();
422 const auto &ref_state = states.at(ai.ref_pal).value();
424 if (!source_state.color_positions.contains(ai.source_color) ||
425 !ref_state.color_positions.contains(ai.ref_color)) {
429 const auto &source_pos = source_state.color_positions.at(ai.source_color);
430 const auto &ref_pos = ref_state.color_positions.at(ai.ref_color);
432 if (!std::holds_alternative<AbsolutePosition>(source_pos) ||
433 !std::holds_alternative<AbsolutePosition>(ref_pos)) {
437 const auto source_slot = std::get<AbsolutePosition>(source_pos).slot;
438 const auto ref_slot = std::get<AbsolutePosition>(ref_pos).slot;
440 if (source_slot != ref_slot) {
441 failure_counts->post_resolution_mismatch_details.push_back(
442 PostResolutionMismatchDetail{
443 .source_group_index = ai.source_group_index,
444 .source_pal_index = ai.source_pal,
445 .source_color = ai.source_color,
446 .source_final_slot = source_slot,
447 .ref_pal_index = ai.ref_pal,
448 .ref_color = ai.ref_color,
449 .ref_final_slot = ref_slot});
456 if (failure_counts !=
nullptr) {
457 auto dedup = [](
auto &vec) {
458 std::ranges::sort(vec);
459 auto [first, last] = std::ranges::unique(vec);
460 vec.erase(first, last);
462 dedup(failure_counts->prefilled_destination_conflict_details);
463 dedup(failure_counts->prefilled_source_conflict_details);
464 dedup(failure_counts->first_writer_wins_details);
465 dedup(failure_counts->post_resolution_mismatch_details);
469 std::array<std::optional<Palette<Rgba32, pal::max_size>>,
pal::num_pals> result{};
471 for (
const auto &state_opt : states) {
472 if (!state_opt.has_value()) {
475 const auto &state = state_opt.value();
476 const std::size_t hw = state.hw_index;
481 const Palette<Rgba32, pal::max_size> *prefilled_ptr =
482 prefilled_pals.at(hw).has_value() ? &prefilled_pals.at(hw).value() :
nullptr;
483 if (prefilled_ptr !=
nullptr && !prefilled_ptr->is_wildcard(0)) {
484 output.set(0, prefilled_ptr->at(0));
487 output.set(0, default_slot_zero);
491 if (prefilled_ptr !=
nullptr) {
493 if (!prefilled_ptr->is_wildcard(i)) {
494 output.set(i, prefilled_ptr->at(i));
501 if (std::holds_alternative<AbsolutePosition>(position)) {
502 const std::size_t slot = std::get<AbsolutePosition>(position).slot;
503 if (!state.prefilled_slots.contains(slot)) {
504 output.set(slot, color);
509 result.at(hw) = output;
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.
ColorType at(std::size_t index) const
Gets the color at a specific index.
bool is_wildcard(std::size_t index) const
Checks if a slot is a wildcard.
Represents a 32-bit RGBA color.
static constexpr std::uint8_t alpha_opaque
constexpr std::size_t num_pals
constexpr std::size_t max_size
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.
std::array< std::optional< Palette< Rgba32, pal::max_size > >, pal::num_pals > build_all_output_palettes(const std::vector< PackedPalette > &packed_pals, const std::array< std::optional< Palette< Rgba32, pal::max_size > >, pal::num_pals > &prefilled_pals, 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::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.