yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
palette_debug.cc
Go to the documentation of this file.
2
4
5#include <algorithm>
6#include <chrono>
7#include <climits>
8#include <map>
9#include <sstream>
10
11#include "absl/strings/str_format.h"
12#include "util/log.h"
13
14namespace {
15uint64_t GetCurrentTimeMs() {
16 return std::chrono::duration_cast<std::chrono::milliseconds>(
17 std::chrono::steady_clock::now().time_since_epoch())
18 .count();
19}
20
22#ifdef YAZE_PALETTE_DEBUG_VERBOSE
23 return true;
24#else
25 return false;
26#endif
27}
28
31 LOG_ERROR("PaletteDebug", "%s: %s", event.location.c_str(),
32 event.message.c_str());
33 return;
34 }
36 LOG_WARN("PaletteDebug", "%s: %s", event.location.c_str(),
37 event.message.c_str());
38 return;
39 }
41 LOG_DEBUG("PaletteDebug", "%s: %s", event.location.c_str(),
42 event.message.c_str());
43 }
44}
45} // namespace
46
47namespace yaze::zelda3 {
48
50 static PaletteDebugger instance;
51 return instance;
52}
53
54void PaletteDebugger::LogPaletteLoad(const std::string& location,
55 int palette_id,
56 const gfx::SnesPalette& palette) {
58 event.location = location;
59 event.palette_id = palette_id;
60 event.color_count = palette.size();
61 event.level = PaletteDebugLevel::INFO;
62 event.timestamp_ms = GetCurrentTimeMs();
63 event.sequence_number = sequence_counter_++;
64
65 // Sample first 3 colors
66 for (size_t i = 0; i < std::min(size_t(3), palette.size()); i++) {
67 auto rgb = palette[i].rgb();
68 event.sample_colors.push_back(static_cast<uint8_t>(rgb.x));
69 event.sample_colors.push_back(static_cast<uint8_t>(rgb.y));
70 event.sample_colors.push_back(static_cast<uint8_t>(rgb.z));
71 }
72
73 std::string sample_str;
74 if (event.sample_colors.size() >= 3) {
75 sample_str =
76 absl::StrFormat("[Sample: R=%d G=%d B=%d]", event.sample_colors[0],
77 event.sample_colors[1], event.sample_colors[2]);
78 }
79
80 event.message = absl::StrFormat("Loaded palette %d with %d colors %s",
81 palette_id, event.color_count, sample_str);
82
83 AddEvent(event);
84 LogPaletteDebugEvent(event);
85}
86
87void PaletteDebugger::LogPaletteApplication(const std::string& location,
88 int palette_id, bool success,
89 const std::string& reason) {
91 event.location = location;
92 event.palette_id = palette_id;
93 event.color_count = !current_render_palette_.empty()
94 ? static_cast<int>(current_render_palette_.size())
95 : static_cast<int>(current_palette_.size());
96 event.level = success ? PaletteDebugLevel::INFO : PaletteDebugLevel::ERROR;
97 event.timestamp_ms = GetCurrentTimeMs();
98 event.sequence_number = sequence_counter_++;
99 event.message =
100 success ? absl::StrFormat("Applied palette %d successfully", palette_id)
101 : absl::StrFormat("Failed to apply palette %d: %s", palette_id,
102 reason);
103
104 AddEvent(event);
105 LogPaletteDebugEvent(event);
106}
107
108void PaletteDebugger::LogTextureCreation(const std::string& location,
109 bool has_palette, int color_count) {
110 PaletteDebugEvent event;
111 event.location = location;
112 event.color_count = color_count;
113 event.level =
115 event.timestamp_ms = GetCurrentTimeMs();
116 event.sequence_number = sequence_counter_++;
117 event.message =
118 has_palette
119 ? absl::StrFormat("Creating texture with %d-color palette",
120 color_count)
121 : "WARNING: Creating texture WITHOUT palette - will use default "
122 "colors!";
123
124 AddEvent(event);
125 LogPaletteDebugEvent(event);
126}
127
128void PaletteDebugger::LogSurfaceState(const std::string& location,
129 SDL_Surface* surface) {
130 PaletteDebugEvent event;
131 event.location = location;
132 event.timestamp_ms = GetCurrentTimeMs();
133 event.sequence_number = sequence_counter_++;
134
135 if (!surface) {
136 event.level = PaletteDebugLevel::ERROR;
137 event.message = "Surface is NULL!";
138 AddEvent(event);
139 LogPaletteDebugEvent(event);
140 return;
141 }
142
143 Uint32 fmt = platform::GetSurfaceFormat(surface);
144 if (fmt == SDL_PIXELFORMAT_UNKNOWN) {
145 event.level = PaletteDebugLevel::ERROR;
146 event.message = "Surface format is unknown!";
147 AddEvent(event);
148 LogPaletteDebugEvent(event);
149 return;
150 }
151
152 bool is_indexed = (fmt == SDL_PIXELFORMAT_INDEX8);
153 SDL_Palette* palette = platform::GetSurfacePalette(surface);
154 bool has_palette = (palette != nullptr);
155 int ncolors = has_palette ? palette->ncolors : 0;
156
157 event.color_count = ncolors;
158
159 if (!is_indexed) {
160 event.level = PaletteDebugLevel::INFO;
161 event.message = absl::StrFormat(
162 "Surface OK: direct-color format=0x%08X; no palette required", fmt);
163 } else if (!has_palette) {
164 event.level = PaletteDebugLevel::WARNING;
165 event.message = "Surface is indexed but has NO palette attached!";
166 } else {
167 event.level = PaletteDebugLevel::INFO;
168 event.message = absl::StrFormat(
169 "Surface OK: indexed format, %d-color palette attached", ncolors);
170
171 // Sample color 56 (palette 7, offset 0) for verification
172 if (palette && ncolors > 56) {
173 auto& c = palette->colors[56];
174 event.sample_colors = {c.r, c.g, c.b};
175 event.message +=
176 absl::StrFormat(" [Color56: R=%d G=%d B=%d]", c.r, c.g, c.b);
177 }
178 }
179
180 AddEvent(event);
181 LogPaletteDebugEvent(event);
182}
183
187
189 const std::vector<SDL_Color>& palette) {
190 current_render_palette_ = palette;
191}
192
196
198 gfx::Bitmap* bitmap, const gfx::SnesPalette& palette,
199 const std::vector<SDL_Color>& render_palette) {
200 current_bitmap_ = bitmap;
201 current_palette_ = palette;
202 current_render_palette_ = render_palette;
203}
204
206 if (current_bitmap_ == bitmap) {
207 current_bitmap_ = nullptr;
208 }
209}
210
212 ColorComparison comp;
213 comp.x = x;
214 comp.y = y;
215 comp.palette_index = 0;
216 comp.actual_r = comp.actual_g = comp.actual_b = 0;
217 comp.expected_r = comp.expected_g = comp.expected_b = 0;
218 comp.matches = false;
219
221 return comp;
222 }
223
224 auto* surface = current_bitmap_->surface();
225 int width = current_bitmap_->width();
226 int height = current_bitmap_->height();
227
228 if (x < 0 || x >= width || y < 0 || y >= height) {
229 return comp;
230 }
231
232 if (!current_bitmap_->IsIndexed()) {
233 const auto color = current_bitmap_->ReadColor(x, y);
234 comp.palette_index = -1;
235 comp.actual_r = color.r;
236 comp.actual_g = color.g;
237 comp.actual_b = color.b;
238 return comp; // No source palette index exists after color math.
239 }
240
241 // Get palette index from bitmap data
242 const uint8_t* data = current_bitmap_->data();
243 size_t data_size = current_bitmap_->size();
244 if (!data || data_size == 0) {
245 return comp;
246 }
247
248 size_t idx = static_cast<size_t>(y * width + x);
249 if (idx >= data_size) {
250 return comp;
251 }
252
253 comp.palette_index = data[idx];
254
255 // Get expected color from the mapped dungeon render palette if available.
256 // Dungeon surfaces use the SDL/CGRAM-aligned 256-entry palette, not the raw
257 // 90-color dungeon ROM palette indices directly.
258 if (comp.palette_index < current_render_palette_.size()) {
259 const auto& c = current_render_palette_[comp.palette_index];
260 comp.expected_r = c.r;
261 comp.expected_g = c.g;
262 comp.expected_b = c.b;
263 } else if (comp.palette_index < current_palette_.size()) {
264 auto rgb = current_palette_[comp.palette_index].rgb();
265 comp.expected_r = static_cast<uint8_t>(rgb.x);
266 comp.expected_g = static_cast<uint8_t>(rgb.y);
267 comp.expected_b = static_cast<uint8_t>(rgb.z);
268 }
269
270 // Get actual color from SDL surface palette
271 SDL_Palette* palette = platform::GetSurfacePalette(surface);
272 if (palette && comp.palette_index < palette->ncolors) {
273 auto& c = palette->colors[comp.palette_index];
274 comp.actual_r = c.r;
275 comp.actual_g = c.g;
276 comp.actual_b = c.b;
277 }
278
279 // Check if they match
280 comp.matches =
281 (comp.actual_r == comp.expected_r && comp.actual_g == comp.expected_g &&
282 comp.actual_b == comp.expected_b);
283
284 return comp;
285}
286
288 const gfx::SnesPalette& palette) const {
289 uint32_t sum = 0;
290 for (size_t i = 0; i < palette.size(); i++) {
291 auto rgb = palette[i].rgb();
292 sum += static_cast<uint32_t>(rgb.x) * (i + 1);
293 sum += static_cast<uint32_t>(rgb.y) * (i + 1) * 256;
294 sum += static_cast<uint32_t>(rgb.z) * (i + 1) * 65536;
295 }
296 return sum;
297}
298
299#ifdef __EMSCRIPTEN__
300std::string PaletteDebugger::ExportToJSON() const {
301 std::ostringstream json;
302 json << "[";
303 for (size_t i = 0; i < events_.size(); i++) {
304 const auto& e = events_[i];
305 json << "{";
306 json << "\"location\":\"" << e.location << "\",";
307 json << "\"message\":\"" << e.message << "\",";
308 json << "\"level\":\""
309 << (e.level == PaletteDebugLevel::ERROR ? "error"
310 : e.level == PaletteDebugLevel::WARNING ? "warning"
311 : "info")
312 << "\",";
313 json << "\"palette_id\":" << e.palette_id << ",";
314 json << "\"color_count\":" << e.color_count;
315
316 // Include sample colors if available
317 if (!e.sample_colors.empty() && e.sample_colors.size() >= 3) {
318 json << ",\"sample_rgb\":[" << (int)e.sample_colors[0] << ","
319 << (int)e.sample_colors[1] << "," << (int)e.sample_colors[2] << "]";
320 }
321
322 json << "}";
323 if (i < events_.size() - 1)
324 json << ",";
325 }
326 json << "]";
327 return json.str();
328}
329
330std::string PaletteDebugger::ExportColorComparisonsJSON() const {
331 std::ostringstream json;
332 json << "[";
333 for (size_t i = 0; i < comparisons_.size(); i++) {
334 const auto& c = comparisons_[i];
335 json << "{";
336 json << "\"x\":" << c.x << ",\"y\":" << c.y << ",";
337 json << "\"palette_index\":" << (int)c.palette_index << ",";
338 json << "\"actual\":[" << (int)c.actual_r << "," << (int)c.actual_g << ","
339 << (int)c.actual_b << "],";
340 json << "\"expected\":[" << (int)c.expected_r << "," << (int)c.expected_g
341 << "," << (int)c.expected_b << "],";
342 json << "\"matches\":" << (c.matches ? "true" : "false");
343 json << "}";
344 if (i < comparisons_.size() - 1)
345 json << ",";
346 }
347 json << "]";
348 return json.str();
349}
350
351std::string PaletteDebugger::SamplePixelJSON(int x, int y) const {
352 auto comp = SamplePixelAt(x, y);
353 std::ostringstream json;
354 json << "{";
355 json << "\"x\":" << comp.x << ",\"y\":" << comp.y << ",";
356 json << "\"palette_index\":" << (int)comp.palette_index << ",";
357 json << "\"actual\":[" << (int)comp.actual_r << "," << (int)comp.actual_g
358 << "," << (int)comp.actual_b << "],";
359 json << "\"expected\":[" << (int)comp.expected_r << ","
360 << (int)comp.expected_g << "," << (int)comp.expected_b << "],";
361 json << "\"matches\":" << (comp.matches ? "true" : "false");
362 json << "}";
363 return json.str();
364}
365
366std::string PaletteDebugger::ExportFullStateJSON() const {
367 std::ostringstream json;
368 json << "{";
369
370 // Events timeline
371 json << "\"events\":" << ExportToJSON() << ",";
372
373 // Color comparisons
374 json << "\"comparisons\":" << ExportColorComparisonsJSON() << ",";
375
376 // Current palette data
377 json << "\"palette\":" << ExportPaletteDataJSON() << ",";
378
379 // Timeline analysis
380 json << "\"timeline\":" << ExportTimelineJSON() << ",";
381
382 // Diagnostic summary
383 json << "\"diagnostic\":\"" << GetDiagnosticSummary() << "\",";
384
385 // Hypothesis analysis
386 json << "\"hypothesis\":\"" << GetHypothesisAnalysis() << "\"";
387
388 json << "}";
389 return json.str();
390}
391
392std::string PaletteDebugger::ExportPaletteDataJSON() const {
393 std::ostringstream json;
394 json << "{";
395 json << "\"size\":" << current_palette_.size() << ",";
396 json << "\"render_size\":" << current_render_palette_.size() << ",";
397 json << "\"checksum\":" << ComputePaletteChecksum(current_palette_) << ",";
398 json << "\"colors\":[";
399
400 for (size_t i = 0; i < current_palette_.size(); i++) {
401 auto rgb = current_palette_[i].rgb();
402 json << "{\"index\":" << i << ",\"r\":" << (int)rgb.x
403 << ",\"g\":" << (int)rgb.y << ",\"b\":" << (int)rgb.z << "}";
404 if (i < current_palette_.size() - 1)
405 json << ",";
406 }
407
408 json << "],";
409 json << "\"render_colors\":[";
410
411 bool first_render = true;
412 for (size_t i = 0; i < current_render_palette_.size(); ++i) {
413 const auto& color = current_render_palette_[i];
414 if (color.a == 0) {
415 continue;
416 }
417 if (!first_render) {
418 json << ",";
419 }
420 json << "{\"index\":" << i << ",\"r\":" << static_cast<int>(color.r)
421 << ",\"g\":" << static_cast<int>(color.g)
422 << ",\"b\":" << static_cast<int>(color.b)
423 << ",\"a\":" << static_cast<int>(color.a) << "}";
424 first_render = false;
425 }
426
427 json << "]}";
428 return json.str();
429}
430
431std::string PaletteDebugger::ExportTimelineJSON() const {
432 std::ostringstream json;
433 json << "{";
434
435 // Find first and last timestamps
436 uint64_t first_ts = 0, last_ts = 0;
437 if (!events_.empty()) {
438 first_ts = events_[0].timestamp_ms;
439 last_ts = events_[events_.size() - 1].timestamp_ms;
440 }
441
442 json << "\"start_ms\":" << first_ts << ",";
443 json << "\"end_ms\":" << last_ts << ",";
444 json << "\"duration_ms\":" << (last_ts - first_ts) << ",";
445 json << "\"event_count\":" << events_.size() << ",";
446
447 // Group events by location
448 json << "\"by_location\":{";
449 std::map<std::string, int> location_counts;
450 for (const auto& e : events_) {
451 location_counts[e.location]++;
452 }
453 bool first = true;
454 for (const auto& [loc, count] : location_counts) {
455 if (!first)
456 json << ",";
457 json << "\"" << loc << "\":" << count;
458 first = false;
459 }
460 json << "},";
461
462 // Count by level
463 int info_count = 0, warn_count = 0, error_count = 0;
464 for (const auto& e : events_) {
465 if (e.level == PaletteDebugLevel::INFO)
466 info_count++;
467 else if (e.level == PaletteDebugLevel::WARNING)
468 warn_count++;
469 else
470 error_count++;
471 }
472 json << "\"info_count\":" << info_count << ",";
473 json << "\"warning_count\":" << warn_count << ",";
474 json << "\"error_count\":" << error_count;
475
476 json << "}";
477 return json.str();
478}
479
480std::string PaletteDebugger::GetDiagnosticSummary() const {
481 std::ostringstream summary;
482
483 // Count warnings and errors
484 int warnings = 0, errors = 0;
485 bool has_palette_timing_issue = false;
486 bool has_missing_palette = false;
487 bool has_color_mismatch = false;
488
489 for (const auto& e : events_) {
490 if (e.level == PaletteDebugLevel::WARNING)
491 warnings++;
492 if (e.level == PaletteDebugLevel::ERROR)
493 errors++;
494
495 // Detect specific issues
496 if (e.message.find("WITHOUT palette") != std::string::npos) {
497 has_missing_palette = true;
498 }
499 if (e.message.find("only") != std::string::npos &&
500 e.message.find("colors") != std::string::npos) {
501 has_palette_timing_issue = true;
502 }
503 }
504
505 // Check color comparisons for mismatches
506 for (const auto& c : comparisons_) {
507 if (c.palette_index >= 0 && !c.matches) {
508 has_color_mismatch = true;
509 break;
510 }
511 }
512
513 summary << "Events: " << events_.size() << ", Warnings: " << warnings
514 << ", Errors: " << errors << ". ";
515
516 if (has_missing_palette) {
517 summary << "ISSUE: Texture created without palette. ";
518 }
519 if (has_palette_timing_issue) {
520 summary << "ISSUE: Palette may not be fully loaded. ";
521 }
522 if (has_color_mismatch) {
523 summary << "ISSUE: Color mismatch detected between expected and actual. ";
524 }
525 if (!has_missing_palette && !has_palette_timing_issue &&
526 !has_color_mismatch) {
527 summary << "No obvious issues detected. ";
528 }
529
530 return summary.str();
531}
532
533std::string PaletteDebugger::GetHypothesisAnalysis() const {
534 std::ostringstream analysis;
535
536 // Analyze events for the timing hypothesis (75% confidence from plan)
537 bool texture_before_palette = false;
538 int last_palette_load_seq = -1;
539 int first_texture_create_seq = INT_MAX;
540
541 for (const auto& e : events_) {
542 if (e.message.find("Loaded palette") != std::string::npos) {
543 last_palette_load_seq =
544 std::max(last_palette_load_seq, e.sequence_number);
545 }
546 if (e.message.find("Creating texture") != std::string::npos) {
547 first_texture_create_seq =
548 std::min(first_texture_create_seq, e.sequence_number);
549 }
550 }
551
552 if (first_texture_create_seq < last_palette_load_seq) {
553 texture_before_palette = true;
554 analysis << "TIMING HYPOTHESIS CONFIRMED: Texture created (seq "
555 << first_texture_create_seq << ") before palette loaded (seq "
556 << last_palette_load_seq << "). ";
557 analysis << "FIX: Ensure palette is applied to surface BEFORE queuing "
558 "texture creation. ";
559 } else if (last_palette_load_seq >= 0 && first_texture_create_seq < INT_MAX) {
560 analysis << "Timing appears correct: palette loaded (seq "
561 << last_palette_load_seq << ") before texture created (seq "
562 << first_texture_create_seq << "). ";
563 }
564
565 // A palette size alone does not identify a bad group: eight-color source
566 // sheets, 90-color dungeon groups and 256-color CGRAM views are all valid.
567 bool wrong_palette_group = false;
568
569 // Check for surface format mismatch (40% confidence)
570 for (const auto& e : events_) {
571 if (e.message.find("NOT indexed") != std::string::npos) {
572 analysis << "FORMAT MISMATCH: Surface is not indexed, palette ignored. ";
573 break;
574 }
575 }
576
577 if (analysis.str().empty()) {
578 analysis << "No hypothesis conditions detected. Render pipeline may be "
579 "correct. Check actual colors vs expected in comparisons. ";
580 }
581
582 return analysis.str();
583}
584#endif
585
586} // namespace yaze::zelda3
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:70
const uint8_t * data() const
Definition bitmap.h:407
auto size() const
Definition bitmap.h:406
bool IsIndexed() const
Definition bitmap.h:401
int height() const
Definition bitmap.h:399
int width() const
Definition bitmap.h:398
SDL_Color ReadColor(int x, int y) const
Definition bitmap.cc:414
SDL_Surface * surface() const
Definition bitmap.h:409
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
void SetCurrentBitmap(gfx::Bitmap *bitmap)
void LogTextureCreation(const std::string &location, bool has_palette, int color_count)
uint32_t ComputePaletteChecksum(const gfx::SnesPalette &palette) const
void AddEvent(const PaletteDebugEvent &event)
std::vector< SDL_Color > current_render_palette_
void LogPaletteLoad(const std::string &location, int palette_id, const gfx::SnesPalette &palette)
static PaletteDebugger & Get()
gfx::SnesPalette current_palette_
void LogPaletteApplication(const std::string &location, int palette_id, bool success, const std::string &reason="")
void SetCurrentPalette(const gfx::SnesPalette &palette)
std::vector< PaletteDebugEvent > events_
std::vector< ColorComparison > comparisons_
void SetCurrentPresentation(gfx::Bitmap *bitmap, const gfx::SnesPalette &palette, const std::vector< SDL_Color > &render_palette)
void LogSurfaceState(const std::string &location, SDL_Surface *surface)
void ClearCurrentBitmapIf(const gfx::Bitmap *bitmap)
void SetCurrentRenderPalette(const std::vector< SDL_Color > &palette)
ColorComparison SamplePixelAt(int x, int y) const
#define LOG_DEBUG(category, format,...)
Definition log.h:133
#define LOG_ERROR(category, format,...)
Definition log.h:140
#define LOG_WARN(category, format,...)
Definition log.h:138
void LogPaletteDebugEvent(const yaze::zelda3::PaletteDebugEvent &event)
Uint32 GetSurfaceFormat(SDL_Surface *surface)
Get the pixel format of a surface as Uint32.
Definition sdl_compat.h:414
SDL_Palette * GetSurfacePalette(SDL_Surface *surface)
Get the palette attached to a surface.
Definition sdl_compat.h:398
Zelda 3 specific classes and functions.
nlohmann::json json
SDL2/SDL3 compatibility layer.
std::vector< uint8_t > sample_colors