@@ -192,6 +192,116 @@ int emulator_compare_bmp(const char* file_a, const char* file_b) {
192192 return diff_count;
193193}
194194
195+ // ---------------------------------------------------------------------------
196+ // 3-panel diff visualizer: [reference | output | diff-highlighted]
197+ // Matching pixels in the diff panel are dimmed to 30% brightness so regressions
198+ // jump out immediately. Differing pixels are shown as magenta (255, 0, 255).
199+ // The combined image is (SCREEN_WIDTH * 3 + 4) wide with 2-px white separators.
200+ // ---------------------------------------------------------------------------
201+ bool emulator_save_diff_bmp (const char * ref_path, const char * out_path,
202+ const char * diff_path) {
203+ const int W = SCREEN_WIDTH;
204+ const int H = SCREEN_HEIGHT;
205+ const int PANEL_BYTES = W * H * 3 ;
206+
207+ // Heap-allocate pixel buffers (bottom-up BMP rows, BGR order)
208+ uint8_t * ref_px = static_cast <uint8_t *>(malloc (PANEL_BYTES));
209+ uint8_t * out_px = static_cast <uint8_t *>(malloc (PANEL_BYTES));
210+ if (!ref_px || !out_px) { free (ref_px); free (out_px); return false ; }
211+
212+ auto read_pixels = [&](const char * path, uint8_t * dst) -> bool {
213+ FILE* f = fopen (path, " rb" );
214+ if (!f) return false ;
215+ fseek (f, 54 , SEEK_SET); // skip BMP header
216+ bool ok = (fread (dst, 1 , PANEL_BYTES, f) == (size_t )PANEL_BYTES);
217+ fclose (f);
218+ return ok;
219+ };
220+
221+ if (!read_pixels (ref_path, ref_px) || !read_pixels (out_path, out_px)) {
222+ free (ref_px); free (out_px); return false ;
223+ }
224+
225+ // Combined image layout: ref | 2px sep | output | 2px sep | diff
226+ const int SEP = 2 ;
227+ const int TOTAL_W = W * 3 + SEP * 2 ;
228+ int row_bytes = TOTAL_W * 3 ;
229+ int padding = (4 - (row_bytes % 4 )) % 4 ;
230+ int padded = row_bytes + padding;
231+ int pix_size = padded * H;
232+ int file_size = 54 + pix_size;
233+
234+ FILE* f = fopen (diff_path, " wb" );
235+ if (!f) { free (ref_px); free (out_px); return false ; }
236+
237+ // BMP file header
238+ uint8_t fhdr[14 ] = {
239+ ' B' , ' M' ,
240+ (uint8_t )file_size, (uint8_t )(file_size >> 8 ),
241+ (uint8_t )(file_size >> 16 ),(uint8_t )(file_size >> 24 ),
242+ 0 , 0 , 0 , 0 , 54 , 0 , 0 , 0
243+ };
244+ fwrite (fhdr, 1 , 14 , f);
245+
246+ // BMP info header
247+ uint8_t ihdr[40 ] = {};
248+ ihdr[0 ] = 40 ;
249+ ihdr[4 ] = (uint8_t )TOTAL_W; ihdr[5 ] = (uint8_t )(TOTAL_W >> 8 );
250+ ihdr[8 ] = (uint8_t )H; ihdr[9 ] = (uint8_t )(H >> 8 );
251+ ihdr[12 ] = 1 ; // color planes
252+ ihdr[14 ] = 24 ; // bits per pixel
253+ fwrite (ihdr, 1 , 40 , f);
254+
255+ // Rows are written bottom-up (matches how emulator_save_bmp stores them)
256+ uint8_t pad_bytes[3 ] = {0 , 0 , 0 };
257+ for (int row = 0 ; row < H; row++) {
258+ // Source row index inside the bottom-up pixel arrays
259+ int src_row = row; // same order: both arrays are bottom-up
260+
261+ for (int panel = 0 ; panel < 3 ; panel++) {
262+ // Write 2-px white separator before panels 1 and 2
263+ if (panel > 0 ) {
264+ for (int s = 0 ; s < SEP; s++) {
265+ uint8_t white[3 ] = {255 , 255 , 255 };
266+ fwrite (white, 1 , 3 , f);
267+ }
268+ }
269+
270+ for (int col = 0 ; col < W; col++) {
271+ int idx = (src_row * W + col) * 3 ;
272+ uint8_t r_b = ref_px[idx], r_g = ref_px[idx+1 ], r_r = ref_px[idx+2 ];
273+ uint8_t o_b = out_px[idx], o_g = out_px[idx+1 ], o_r = out_px[idx+2 ];
274+ bool differs = (r_b != o_b || r_g != o_g || r_r != o_r);
275+
276+ uint8_t px[3 ];
277+ if (panel == 0 ) {
278+ // Left: reference as-is
279+ px[0 ] = r_b; px[1 ] = r_g; px[2 ] = r_r;
280+ } else if (panel == 1 ) {
281+ // Middle: output as-is
282+ px[0 ] = o_b; px[1 ] = o_g; px[2 ] = o_r;
283+ } else {
284+ // Right: diff — magenta where different, dimmed output where same
285+ if (differs) {
286+ px[0 ] = 255 ; px[1 ] = 0 ; px[2 ] = 255 ; // magenta (BGR)
287+ } else {
288+ px[0 ] = (uint8_t )(o_b * 30 / 100 );
289+ px[1 ] = (uint8_t )(o_g * 30 / 100 );
290+ px[2 ] = (uint8_t )(o_r * 30 / 100 );
291+ }
292+ }
293+ fwrite (px, 1 , 3 , f);
294+ }
295+ }
296+ if (padding > 0 ) fwrite (pad_bytes, 1 , padding, f);
297+ }
298+
299+ fclose (f);
300+ free (ref_px);
301+ free (out_px);
302+ return true ;
303+ }
304+
195305void emulator_teardown () {
196306 // Delete the screen (which deletes all child widgets)
197307 // Don't call lv_deinit - keep LVGL alive across tests
0 commit comments