15struct PaletteBuildState {
27[[nodiscard]] std::optional<std::size_t> try_resolve_indirect(
30 IndirectPosition current =
start;
32 if (!states.at(current.ref_palette_index).has_value()) {
35 const auto &ref_state = states.at(current.ref_palette_index).value();
36 if (!ref_state.color_positions.contains(current.ref_color)) {
39 const auto &ref_position = ref_state.color_positions.at(current.ref_color);
41 if (std::holds_alternative<AbsolutePosition>(ref_position)) {
42 return std::get<AbsolutePosition>(ref_position).slot;
44 else if (std::holds_alternative<IndirectPosition>(ref_position)) {
45 current = std::get<IndirectPosition>(ref_position);
52 panic(
"Indirect chain resolution exceeded maximum iterations (cycle detected)");
58 const std::vector<PackedPalette> &packed_palettes,
61 const Rgba32 &default_slot_zero,
62 const std::vector<IndirectLink> &indirect_links,
70 const std::size_t hw = packed_palette.hardware_index();
72 panic(
"invalid hardware index " + std::to_string(hw) +
": out of range");
75 PaletteBuildState state;
79 prefilled_palettes.
at(hw).has_value() ? &prefilled_palettes.at(hw).value() :
nullptr;
82 if (prefilled_ptr !=
nullptr) {
85 state.prefilled_slots.insert(i);
92 std::set<Rgba32> already_placed_colors;
95 if (prefilled_ptr !=
nullptr) {
98 already_placed_colors.insert(prefilled_ptr->
at(i));
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");
111 const auto &color = color_opt.value();
112 if (!already_placed_colors.contains(color)) {
117 states.at(hw) = std::move(state);
121 struct AppliedIndirect {
122 std::size_t source_palette;
124 std::size_t ref_palette;
126 std::size_t source_group_index;
128 std::vector<AppliedIndirect> applied_indirects;
131 for (
const auto &link : indirect_links) {
132 if (!states.at(link.source_palette).has_value()) {
135 auto &state = states.at(link.source_palette).value();
137 if (!state.color_positions.contains(link.source_color)) {
141 auto &position = state.color_positions.at(link.source_color);
143 if (std::holds_alternative<UndeterminedPosition>(position)) {
144 position = IndirectPosition{link.ref_palette, link.ref_color, link.source_group_index};
147 else if (std::holds_alternative<IndirectPosition>(position)) {
148 const auto &existing = std::get<IndirectPosition>(position);
150 if (existing.ref_palette_index == link.ref_palette && existing.ref_color == link.ref_color) {
153 applied_indirects.push_back(
159 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_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});
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_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;
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});
208 for (
auto &state_opt : states) {
209 if (!state_opt.has_value()) {
212 auto &state = state_opt.value();
215 std::set<std::size_t> used_slots;
216 used_slots.insert(0);
218 if (std::holds_alternative<AbsolutePosition>(position)) {
219 used_slots.insert(std::get<AbsolutePosition>(position).slot);
224 std::size_t next_slot = 1;
226 if (!std::holds_alternative<UndeterminedPosition>(position)) {
233 position = AbsolutePosition{next_slot};
234 used_slots.insert(next_slot);
238 panic(
"ran out of palette slots during sequential fill for palette " + std::to_string(state.hw_index));
252 for (std::size_t palette_index = 0; palette_index < states.size(); ++palette_index) {
253 if (!states.at(palette_index).has_value()) {
256 auto &state = states.at(palette_index).value();
259 struct IndirectResolution {
261 std::size_t target_slot;
262 std::size_t source_group_index;
263 std::size_t ref_palette_index;
266 std::vector<IndirectResolution> resolutions;
269 if (!std::holds_alternative<IndirectPosition>(position)) {
273 const auto &indirect_pos = std::get<IndirectPosition>(position);
274 auto resolved = try_resolve_indirect(indirect_pos, states);
275 if (!resolved.has_value()) {
277 "Indirect chain resolution returned nullopt for color in palette " + std::to_string(palette_index) +
278 ": internal invariant violated");
282 if (state.prefilled_slots.contains(resolved.value())) {
283 if (failure_counts !=
nullptr) {
285 Rgba32 locked_color{};
287 if (std::holds_alternative<AbsolutePosition>(p) &&
288 std::get<AbsolutePosition>(p).slot == resolved.value()) {
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});
304 resolutions.push_back(
308 indirect_pos.source_group_index,
309 indirect_pos.ref_palette_index,
310 indirect_pos.ref_color});
314 for (
const auto &[indirect_color, target_slot, source_group_index, res_ref_palette, res_ref_color] :
317 Rgba32 evicted_color{};
318 bool needs_eviction =
false;
321 if (color == indirect_color) {
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;
333 if (needs_eviction) {
336 std::set<std::size_t> all_used;
339 if (std::holds_alternative<AbsolutePosition>(p)) {
340 all_used.insert(std::get<AbsolutePosition>(p).slot);
343 std::size_t free_slot = 1;
348 state.color_positions.at(evicted_color) = AbsolutePosition{free_slot};
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.");
358 state.color_positions.at(indirect_color) = AbsolutePosition{target_slot};
361 applied_indirects.push_back(
362 AppliedIndirect{palette_index, indirect_color, res_ref_palette, res_ref_color, source_group_index});
371 for (
auto &state_opt : states) {
372 if (!state_opt.has_value()) {
375 auto &state = state_opt.value();
378 std::set<std::size_t> used_slots;
379 used_slots.insert(0);
381 if (std::holds_alternative<AbsolutePosition>(position)) {
382 used_slots.insert(std::get<AbsolutePosition>(position).slot);
387 std::size_t next_slot = 1;
389 if (!std::holds_alternative<IndirectPosition>(position)) {
396 position = AbsolutePosition{next_slot};
397 used_slots.insert(next_slot);
402 "ran out of palette slots during Indirect fallback fill for palette " +
403 std::to_string(state.hw_index));
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()) {
414 const auto &source_state = states.at(ai.source_palette).value();
415 const auto &ref_state = states.at(ai.ref_palette).value();
417 if (!source_state.color_positions.contains(ai.source_color) ||
418 !ref_state.color_positions.contains(ai.ref_color)) {
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);
425 if (!std::holds_alternative<AbsolutePosition>(source_pos) ||
426 !std::holds_alternative<AbsolutePosition>(ref_pos)) {
430 const auto source_slot = std::get<AbsolutePosition>(source_pos).slot;
431 const auto ref_slot = std::get<AbsolutePosition>(ref_pos).slot;
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});
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);
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);
464 for (
const auto &state_opt : states) {
465 if (!state_opt.has_value()) {
468 const auto &state = state_opt.value();
469 const std::size_t hw = state.hw_index;
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));
480 output.set(0, default_slot_zero);
484 if (prefilled_ptr !=
nullptr) {
486 if (!prefilled_ptr->is_wildcard(i)) {
487 output.set(i, prefilled_ptr->at(i));
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);
502 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 max_size
constexpr std::size_t num_palettes
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, 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::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.