Porytiles
Loading...
Searching...
No Matches
backtracking_strategy.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cstddef>
6#include <deque>
7#include <format>
8#include <limits>
9#include <map>
10#include <string>
11#include <unordered_set>
12#include <vector>
13
23
24namespace porytiles {
25
26namespace {
27
28enum class AssignResult { success, no_solution, cutoff_reached };
29
30struct SearchParams {
32 std::size_t node_cutoff;
33 std::size_t best_branches; // SIZE_MAX = unlimited
35};
36
37struct SearchContext {
38 std::vector<PackableTile> sorted_tiles;
39 std::vector<ColorSet> initial_palette_colors;
40 std::vector<std::size_t> palette_capacities;
41 std::vector<std::size_t> hardware_indices;
42
51 const ShapeGroupMetadata *shape_group_metadata = nullptr;
52
60 std::vector<std::vector<ColorSet>> sibling_color_sets;
61};
62
63struct BfsState {
64 std::vector<ColorSet> palette_colors;
65 std::size_t next_tile_index{};
66 bool operator==(const BfsState &) const = default;
67};
68
69struct BfsStateHash {
70 std::size_t operator()(const BfsState &s) const noexcept
71 {
72 std::size_t seed = std::hash<std::size_t>{}(s.next_tile_index);
73 for (const auto &cs : s.palette_colors) {
74 seed ^= std::hash<ColorSet>{}(cs) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
75 }
76 return seed;
77 }
78};
79
80[[nodiscard]] std::array<SearchParams, 48> build_preset_matrix()
81{
82 std::array<SearchParams, 48> matrix{};
83 std::size_t idx = 0;
84
85 constexpr std::array<std::size_t, 4> cutoffs = {1'000'000, 2'000'000, 4'000'000, 8'000'000};
86
87 for (std::size_t cutoff : cutoffs) {
88 for (auto algo : {SearchAlgorithm::dfs, SearchAlgorithm::bfs}) {
89 // Configuration 1: unlimited branches with smart pruning
90 matrix[idx++] = SearchParams{algo, cutoff, std::numeric_limits<std::size_t>::max(), true};
91
92 // Configurations 2-6: limited branches without smart pruning
93 for (std::size_t branches = 2; branches <= 6; ++branches) {
94 matrix[idx++] = SearchParams{algo, cutoff, branches, false};
95 }
96 }
97 }
98
99 return matrix;
100}
101
102[[nodiscard]] SearchContext build_search_context(const PackingInput &input)
103{
104 SearchContext ctx;
105
106 // Combine hints and regular tiles, sorted descending by color count (FFD)
107 ctx.sorted_tiles.reserve(input.hints_.size() + input.tiles_.size());
108 for (const auto &hint : input.hints_) {
109 ctx.sorted_tiles.push_back(hint);
110 }
111 for (const auto &tile : input.tiles_) {
112 ctx.sorted_tiles.push_back(tile);
113 }
114 std::ranges::sort(ctx.sorted_tiles, [](const PackableTile &a, const PackableTile &b) {
115 // Descending by color count, tiebreak by ID for determinism
116 if (a.color_count() != b.color_count()) {
117 return a.color_count() > b.color_count();
118 }
119 return a.id() < b.id();
120 });
121
122 // Initialize palettes from prefilled + available pool slots
123 PalettePool pool = input.pal_pool_;
124 auto prefilled_pals = initialize_packed_palettes(input.prefilled_pals_, pool, input.pal_capacity_);
125
126 // Build palette arrays: first prefilled, then empty slots from pool
127 for (const auto &pal : prefilled_pals) {
128 ctx.initial_palette_colors.push_back(pal.color_set());
129 ctx.palette_capacities.push_back(input.pal_capacity_);
130 ctx.hardware_indices.push_back(pal.hardware_index());
131 }
132
133 // Compute effective capacities for prefilled palettes
134 for (std::size_t i = 0; i < prefilled_pals.size(); ++i) {
135 // Account for wasted slots from duplicate colors in prefilled palettes
136 for (const auto &prefilled_pal : input.prefilled_pals_) {
137 if (prefilled_pal.hardware_index() == ctx.hardware_indices[i]) {
138 const std::size_t unique_colors = color_set_count(prefilled_pal.fixed_colors());
139 const std::size_t occupied = prefilled_pal.occupied_slots();
140 const std::size_t wasted = occupied - unique_colors;
141 ctx.palette_capacities[i] = input.pal_capacity_ - wasted;
142 break;
143 }
144 }
145 }
146
147 while (pool.has_available_pal()) {
148 std::size_t hw_idx = pool.checkout();
149 ctx.initial_palette_colors.emplace_back();
150 ctx.palette_capacities.push_back(input.pal_capacity_);
151 ctx.hardware_indices.push_back(hw_idx);
152 }
153
154 // Populate sharing metadata if available
155 if (input.shape_group_metadata_.has_value()) {
156 ctx.shape_group_metadata = &input.shape_group_metadata_.value();
157
158 // Build sibling color sets for each sorted tile
159 ctx.sibling_color_sets.resize(ctx.sorted_tiles.size());
160 for (std::size_t i = 0; i < ctx.sorted_tiles.size(); ++i) {
161 const auto &tile_id = ctx.sorted_tiles[i].id();
162 auto group_it = ctx.shape_group_metadata->tile_id_to_group.find(tile_id);
163 if (group_it == ctx.shape_group_metadata->tile_id_to_group.end()) {
164 continue;
165 }
166 std::size_t group_idx = group_it->second;
167 const auto &members = ctx.shape_group_metadata->group_members[group_idx];
168
169 // Find sibling color sets from the sorted_tiles vector
170 for (const auto &sibling_id : members) {
171 if (sibling_id == tile_id) {
172 continue;
173 }
174 // Look up sibling in sorted_tiles to get its color set
175 for (const auto &st : ctx.sorted_tiles) {
176 if (st.id() == sibling_id) {
177 ctx.sibling_color_sets[i].push_back(st.color_set());
178 break;
179 }
180 }
181 }
182 }
183 }
184
185 return ctx;
186}
187
188/*
189 * DFS with in-place mutation and undo. Porytiles1 copied the entire palette vector for each branch;
190 * we save and restore only the single modified ColorSet, reducing per-node allocation overhead.
191 *
192 * Candidate palettes are sorted by intersection_size (descending), then color_set_count (ascending).
193 * This "best-fit" heuristic tries palettes with the most color overlap first, preferring emptier
194 * palettes as a tiebreaker. smart_prune caps candidates after the first zero-intersection palette,
195 * and best_branches limits total branching factor.
196 */
197AssignResult assign_depth_first(
198 std::vector<ColorSet> &palette_colors,
199 const SearchContext &ctx,
200 const SearchParams &params,
201 std::size_t next_tile_index,
202 std::size_t &explored_nodes)
203{
204 ++explored_nodes;
205 if (explored_nodes > params.node_cutoff) {
206 return AssignResult::cutoff_reached;
207 }
208
209 // Base case: all tiles assigned
210 if (next_tile_index >= ctx.sorted_tiles.size()) {
211 return AssignResult::success;
212 }
213
214 const auto &tile = ctx.sorted_tiles[next_tile_index];
215 const auto &tile_colors = tile.color_set();
216
217 /*
218 * Authoritative subset shortcut (improvement over Porytiles1).
219 *
220 * If the tile's colors are already a subset of some palette, the tile is satisfied without adding
221 * any new colors. We recurse immediately and return the result directly (no fallthrough to the
222 * candidate loop).
223 *
224 * Why authoritative? If skipping the tile fails (remaining tiles can't be packed), trying explicit
225 * candidate assignments can only make things worse:
226 * - Assigning to the covering palette is a no-op (union doesn't change its ColorSet), so we'd
227 * re-explore the exact same subtree that already failed.
228 * - Assigning to a different palette ADDS colors to it, strictly reducing its remaining capacity.
229 *
230 * A non-authoritative version that fell through to candidates caused exponential blowup: at each
231 * of K levels with a subset match, the same subtree was explored twice (once via shortcut, once
232 * via the covering palette as first candidate), yielding O(2^K) redundant work. For tilesets like
233 * gTileset_General with many shared colors, K is large enough to make the search hang indefinitely.
234 */
235 for (std::size_t i = 0; i < palette_colors.size(); ++i) {
236 if (is_subset(tile_colors, palette_colors[i])) {
237 return assign_depth_first(palette_colors, ctx, params, next_tile_index + 1, explored_nodes);
238 }
239 }
240
241 // Build candidate list: (palette_index, intersection_size, color_set_count, has_sibling)
242 struct Candidate {
243 std::size_t pal_index;
244 std::size_t isect_size;
245 std::size_t cs_count;
246 bool has_sibling;
247 };
248 std::vector<Candidate> candidates;
249 candidates.reserve(palette_colors.size());
250
251 for (std::size_t i = 0; i < palette_colors.size(); ++i) {
252 std::size_t u_size = union_size(tile_colors, palette_colors[i]);
253 if (u_size <= ctx.palette_capacities[i]) {
254 std::size_t i_size = intersection_size(tile_colors, palette_colors[i]);
255 std::size_t c_count = color_set_count(palette_colors[i]);
256
257 /*
258 * Heuristic: check if this palette likely contains a sibling by testing whether any sibling's color set
259 * is a subset of the palette's accumulated colors. This is an approximation; false positives are possible
260 * when unrelated tiles contribute the same colors. False positives only cause suboptimal candidate ordering
261 * (deprioritizing a palette unnecessarily), not incorrect packing.
262 */
263 bool sibling = false;
264 if (!ctx.sibling_color_sets.empty() && next_tile_index < ctx.sibling_color_sets.size()) {
265 for (const auto &sibling_cs : ctx.sibling_color_sets[next_tile_index]) {
266 if (is_subset(sibling_cs, palette_colors[i])) {
267 sibling = true;
268 break;
269 }
270 }
271 }
272
273 candidates.push_back(Candidate{i, i_size, c_count, sibling});
274 }
275 }
276
277 // Sort: no_sibling < has_sibling, then descending by intersection_size, then ascending by color_set_count
278 std::ranges::sort(candidates, [](const Candidate &a, const Candidate &b) {
279 if (a.has_sibling != b.has_sibling) {
280 return !a.has_sibling;
281 }
282 if (a.isect_size != b.isect_size) {
283 return a.isect_size > b.isect_size;
284 }
285 return a.cs_count < b.cs_count;
286 });
287
288 // Smart prune: cap candidates after first zero-intersection palette
289 if (params.smart_prune) {
290 for (std::size_t i = 0; i < candidates.size(); ++i) {
291 if (candidates[i].isect_size == 0) {
292 // Keep this one but remove the rest after it
293 candidates.resize(i + 1);
294 break;
295 }
296 }
297 }
298
299 // Apply best_branches limit
300 if (candidates.size() > params.best_branches) {
301 candidates.resize(params.best_branches);
302 }
303
304 for (const auto &cand : candidates) {
305 // Save/restore single palette (Porytiles1 copied the entire vector per branch)
306 ColorSet saved = palette_colors[cand.pal_index];
307 palette_colors[cand.pal_index] = color_set_union(palette_colors[cand.pal_index], tile_colors);
308
309 auto result = assign_depth_first(palette_colors, ctx, params, next_tile_index + 1, explored_nodes);
310 if (result != AssignResult::no_solution) {
311 return result;
312 }
313
314 // Restore state (backtrack)
315 palette_colors[cand.pal_index] = saved;
316 }
317
318 return AssignResult::no_solution;
319}
320
321/*
322 * BFS with dual-queue heuristic and visited-state deduplication (matching Porytiles1's approach).
323 *
324 * Two queues partition the frontier: high_queue for states reached via overlapping assignments
325 * (intersection > 0), and low_queue for states reached via zero-overlap assignments. The high_queue
326 * is always drained first, focusing exploration on promising branches before resorting to "waste"
327 * assignments that consume fresh palette capacity.
328 *
329 * Improvement over Porytiles1: when ALL candidates for a tile have zero intersection (no palette has
330 * any color overlap), Porytiles1 routed them to the high queue via a `sawAssignmentWithIntersection`
331 * flag that stayed false. We preserve this behavior: zero-intersection candidates only go to the low
332 * queue after we've seen at least one candidate with overlap. This prevents starvation when a tile
333 * has entirely unique colors (common for early tiles assigned to empty palettes).
334 */
335AssignResult assign_breadth_first(
336 const std::vector<ColorSet> &initial_colors,
337 const SearchContext &ctx,
338 const SearchParams &params,
339 std::size_t &explored_nodes,
340 std::vector<ColorSet> &solution)
341{
342 std::deque<BfsState> high_queue;
343 std::deque<BfsState> low_queue;
344 std::unordered_set<BfsState, BfsStateHash> visited;
345
346 BfsState initial{initial_colors, 0};
347 visited.insert(initial);
348 high_queue.push_back(std::move(initial));
349
350 while (!high_queue.empty() || !low_queue.empty()) {
351 ++explored_nodes;
352 if (explored_nodes > params.node_cutoff) {
353 return AssignResult::cutoff_reached;
354 }
355
356 // Dequeue: prefer high_queue (overlap assignments)
357 BfsState current = [&]() {
358 if (!high_queue.empty()) {
359 BfsState s = std::move(high_queue.front());
360 high_queue.pop_front();
361 return s;
362 }
363 BfsState s = std::move(low_queue.front());
364 low_queue.pop_front();
365 return s;
366 }();
367
368 // Skip tiles whose colors are already subsets (advance next_tile_index)
369 std::size_t tile_idx = current.next_tile_index;
370 while (tile_idx < ctx.sorted_tiles.size()) {
371 const auto &tc = ctx.sorted_tiles[tile_idx].color_set();
372 bool already_covered = false;
373 for (const auto &pc : current.palette_colors) {
374 if (is_subset(tc, pc)) {
375 already_covered = true;
376 break;
377 }
378 }
379 if (!already_covered) {
380 break;
381 }
382 ++tile_idx;
383 }
384
385 // Base case: all tiles assigned
386 if (tile_idx >= ctx.sorted_tiles.size()) {
387 solution = std::move(current.palette_colors);
388 return AssignResult::success;
389 }
390
391 const auto &tile_colors = ctx.sorted_tiles[tile_idx].color_set();
392
393 // Build candidates
394 struct Candidate {
395 std::size_t pal_index;
396 std::size_t isect_size;
397 std::size_t cs_count;
398 bool has_sibling;
399 };
400 std::vector<Candidate> candidates;
401 candidates.reserve(current.palette_colors.size());
402
403 for (std::size_t i = 0; i < current.palette_colors.size(); ++i) {
404 std::size_t u_size = union_size(tile_colors, current.palette_colors[i]);
405 if (u_size <= ctx.palette_capacities[i]) {
406 std::size_t i_size = intersection_size(tile_colors, current.palette_colors[i]);
407 std::size_t c_count = color_set_count(current.palette_colors[i]);
408
409 // Check if this palette already contains a sibling
410 bool sibling = false;
411 if (!ctx.sibling_color_sets.empty() && tile_idx < ctx.sibling_color_sets.size()) {
412 for (const auto &sibling_cs : ctx.sibling_color_sets[tile_idx]) {
413 if (is_subset(sibling_cs, current.palette_colors[i])) {
414 sibling = true;
415 break;
416 }
417 }
418 }
419
420 candidates.push_back(Candidate{i, i_size, c_count, sibling});
421 }
422 }
423
424 // Sort: no_sibling < has_sibling, then descending by intersection_size, ascending by color_set_count
425 std::sort(candidates.begin(), candidates.end(), [](const Candidate &a, const Candidate &b) {
426 if (a.has_sibling != b.has_sibling) {
427 return !a.has_sibling;
428 }
429 if (a.isect_size != b.isect_size) {
430 return a.isect_size > b.isect_size;
431 }
432 return a.cs_count < b.cs_count;
433 });
434
435 // Smart prune
436 if (params.smart_prune) {
437 for (std::size_t i = 0; i < candidates.size(); ++i) {
438 if (candidates[i].isect_size == 0) {
439 candidates.resize(i + 1);
440 break;
441 }
442 }
443 }
444
445 // Apply best_branches limit
446 if (candidates.size() > params.best_branches) {
447 candidates.resize(params.best_branches);
448 }
449
450 // Track whether we've seen any candidate with color overlap, matching Porytiles1's
451 // dual-queue heuristic: when NO candidate has intersection, all go to high_queue
452 // (they're the only options). Only after seeing an intersection candidate do
453 // zero-intersection candidates go to low_queue.
454 bool saw_intersection = false;
455
456 for (const auto &cand : candidates) {
457 BfsState next_state;
458 next_state.palette_colors = current.palette_colors;
459 next_state.palette_colors[cand.pal_index] =
460 color_set_union(next_state.palette_colors[cand.pal_index], tile_colors);
461 next_state.next_tile_index = tile_idx + 1;
462
463 if (cand.isect_size > 0) {
464 saw_intersection = true;
465 }
466
467 if (!visited.contains(next_state)) {
468 visited.insert(next_state);
469 if (saw_intersection && cand.isect_size == 0) {
470 low_queue.push_back(std::move(next_state));
471 }
472 else {
473 high_queue.push_back(std::move(next_state));
474 }
475 }
476 }
477 }
478
479 return AssignResult::no_solution;
480}
481
482[[nodiscard]] PackingOutput
483build_packing_output(const std::vector<ColorSet> &solution_colors, const SearchContext &ctx, const PackingInput &input)
484{
485 PackingOutput output;
486
487 // Create PackedPalettes
488 for (std::size_t i = 0; i < ctx.hardware_indices.size(); ++i) {
489 PackedPalette pal{ctx.hardware_indices[i], ctx.palette_capacities[i]};
490
491 // Add system tile for prefilled palettes
492 for (const auto &prefilled : input.prefilled_pals_) {
493 if (prefilled.hardware_index() == ctx.hardware_indices[i] &&
494 color_set_count(prefilled.fixed_colors()) > 0) {
495 PackableTile system_tile{
496 PackableTile::PrefilledPaletteId{prefilled.hardware_index()}, prefilled.fixed_colors()};
497 pal.add_tile(system_tile);
498 break;
499 }
500 }
501
502 output.pals_.push_back(std::move(pal));
503 }
504
505 // Assign each tile to the first palette whose solution colors are a superset
506 for (const auto &tile : ctx.sorted_tiles) {
507 // Skip prefilled palette system tiles (already added above)
508 if (tile.is_prefilled_palette()) {
509 continue;
510 }
511
512 for (std::size_t i = 0; i < solution_colors.size(); ++i) {
513 if (is_subset(tile.color_set(), solution_colors[i])) {
514 output.pals_[i].add_tile(tile);
515 output.tile_to_pal_[tile.id()] = ctx.hardware_indices[i];
516 break;
517 }
518 }
519 }
520
521 return output;
522}
523
524[[nodiscard]] PackingOutput build_empty_output(const SearchContext &ctx, const PackingInput &input)
525{
526 PackingOutput output;
527 for (std::size_t i = 0; i < ctx.hardware_indices.size(); ++i) {
528 PackedPalette pal{ctx.hardware_indices[i], ctx.palette_capacities[i]};
529
530 for (const auto &prefilled : input.prefilled_pals_) {
531 if (prefilled.hardware_index() == ctx.hardware_indices[i] &&
532 color_set_count(prefilled.fixed_colors()) > 0) {
533 PackableTile system_tile{
534 PackableTile::PrefilledPaletteId{prefilled.hardware_index()}, prefilled.fixed_colors()};
535 pal.add_tile(system_tile);
536 break;
537 }
538 }
539
540 output.pals_.push_back(std::move(pal));
541 }
542 return output;
543}
544
545[[nodiscard]] std::string format_search_params_line(const SearchParams &params)
546{
547 std::string branches_str = params.best_branches == std::numeric_limits<std::size_t>::max()
548 ? "unlimited"
549 : std::to_string(params.best_branches);
550 return std::format(
551 "algorithm={}, node_cutoff={}, best_branches={}, smart_prune={}.",
552 to_string(params.algorithm),
553 params.node_cutoff,
554 branches_str,
555 params.smart_prune ? "true" : "false");
556}
557
558void emit_success_remark(const UserDiagnostics &diag, const SearchParams &params, bool is_preset)
559{
560 std::vector<std::string> lines;
561 if (is_preset) {
562 lines.emplace_back("Backtracking search succeeded with preset config:");
563 }
564 else {
565 lines.emplace_back("Backtracking search succeeded:");
566 }
567 lines.emplace_back(format_search_params_line(params));
568 diag.remark("backtracking-search", lines);
569}
570
571} // namespace
572
573ChainableResult<PackingOutput> BacktrackingStrategy::pack(const PackingInput &input) const
574{
575 auto ctx = build_search_context(input);
576
577 if (ctx.sorted_tiles.empty()) {
578 return build_empty_output(ctx, input);
579 }
580
581 if (use_preset_matrix_) {
582 auto matrix = build_preset_matrix();
583
584 for (const auto &params : matrix) {
585 std::size_t explored = 0;
586
587 if (params.algorithm == SearchAlgorithm::dfs) {
588 auto colors = ctx.initial_palette_colors;
589 if (assign_depth_first(colors, ctx, params, 0, explored) == AssignResult::success) {
590 if (diag_ != nullptr) {
591 emit_success_remark(*diag_, params, true);
592 }
593 return build_packing_output(colors, ctx, input);
594 }
595 }
596 else {
597 std::vector<ColorSet> solution;
598 if (assign_breadth_first(ctx.initial_palette_colors, ctx, params, explored, solution) ==
599 AssignResult::success) {
600 if (diag_ != nullptr) {
601 emit_success_remark(*diag_, params, true);
602 }
603 return build_packing_output(solution, ctx, input);
604 }
605 }
606 }
607
608 return FormattableError{
609 "Backtracking strategy failed to find a valid palette assignment after all preset configurations."};
610 }
611
612 // Single-config mode: run one search with the configured parameters
613 SearchParams params{algorithm_, node_cutoff_, best_branches_, smart_prune_};
614 std::size_t explored = 0;
615
616 if (algorithm_ == SearchAlgorithm::dfs) {
617 auto colors = ctx.initial_palette_colors;
618 if (assign_depth_first(colors, ctx, params, 0, explored) == AssignResult::success) {
619 if (diag_ != nullptr) {
620 emit_success_remark(*diag_, params, false);
621 }
622 return build_packing_output(colors, ctx, input);
623 }
624 }
625 else {
626 std::vector<ColorSet> solution;
627 if (assign_breadth_first(ctx.initial_palette_colors, ctx, params, explored, solution) ==
628 AssignResult::success) {
629 if (diag_ != nullptr) {
630 emit_success_remark(*diag_, params, false);
631 }
632 return build_packing_output(solution, ctx, input);
633 }
634 }
635
636 return FormattableError{
637 "Backtracking strategy failed to find a valid palette assignment with the configured parameters."};
638}
639
640} // namespace porytiles
std::size_t node_cutoff
std::vector< std::size_t > palette_capacities
std::vector< ColorSet > initial_palette_colors
std::size_t best_branches
SearchAlgorithm algorithm
std::vector< std::vector< ColorSet > > sibling_color_sets
Maps each sorted tile index to a list of sibling color sets from the same shape group.
std::vector< std::size_t > hardware_indices
std::size_t next_tile_index
const ShapeGroupMetadata * shape_group_metadata
Optional shape group metadata for sharing-aware candidate sorting.
std::vector< PackableTile > sorted_tiles
bool smart_prune
std::vector< ColorSet > palette_colors
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
bool is_subset(const ColorSet &a, const ColorSet &b)
Checks if one ColorSet is a subset of another.
Definition color_set.cpp:49
ColorSet color_set_union(const ColorSet &a, const ColorSet &b)
Computes the union of two ColorSets.
Definition color_set.cpp:20
std::size_t intersection_size(const ColorSet &a, const ColorSet &b)
Computes the intersection size between two ColorSets.
Definition color_set.cpp:58
SearchAlgorithm
Search algorithm used by BacktrackingStrategy.
@ dfs
Depth-first search with in-place mutation and undo.
@ bfs
Breadth-first search with dual-queue heuristic and visited-state deduplication.
std::size_t color_set_count(const ColorSet &set)
Counts the number of colors in a ColorSet.
Definition color_set.cpp:44
std::string to_string(const PrimaryPairingMode m)
Converts a PrimaryPairingMode to its canonical string representation.
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.
std::size_t union_size(const ColorSet &a, const ColorSet &b)
Computes the union size of two ColorSets.
Definition color_set.cpp:63
Input data aggregate for the low-level palette packing algorithm.