-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmain.cpp
More file actions
2666 lines (2410 loc) · 114 KB
/
Copy pathmain.cpp
File metadata and controls
2666 lines (2410 loc) · 114 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* main.cpp - Pico bootLoader: a resident .uf2 bootloader frontend for the
* RP2350 retro-emulator family (shared pico_shared framework).
*
* Boot flow:
* 1. The RP2350 bootrom always runs this image (it owns the start of flash).
* 2. RESUME CHECK: if we got here because an emulator deliberately rebooted
* itself to flash a ROM (no-PSRAM path: watchdog_enable), jump straight
* back into the already-flashed emulator instead of showing the menu.
* A physical reset / power-cycle does NOT set that flag, so it lands here
* and shows the menu -- exactly the requested behaviour.
* 3. Otherwise: init display/SD/USB via the framework, list the emulator
* .uf2 files under <BASEDIR>/<HW_CONFIG>/ (BASEDIR defaults to /emu;
* overridable via an optional /boot.txt INI on the SD root), identify
* which one is currently in the application partition by matching
* binary_info program names, and show a picker.
*
* Picker semantics (single-slot model):
* - One flat list of every .uf2 on SD.
* - The entry whose program_name matches the in-flash image is highlighted
* as "in flash" (and selected by default).
* - Pressing B on the in-flash entry: launch it directly (no flash, VTOR
* jump). Pressing B on any other entry: show a "Flashing..." screen for
* a brief moment so the user reads it, then flash and launch.
*
* The flash map (bootloader region + app partition) lives in
* pico_shared/BootPartition.cmake and src/boot_config.h (single source).
*
* Serial output: all diagnostics go to UART (PICO_DEFAULT_UART, pins 44/45 on
* Fruit Jam, 115200-8N1). Tag every line with "[bootLoader] " so they stand out
* when the freshly-launched emulator starts speaking.
*/
#include <cstdio>
#include <cstring>
#include <strings.h>
#include "pico/stdlib.h"
#include "pico/multicore.h"
#include "pico/version.h"
#include "pico/bootrom.h"
#include "hardware/clocks.h"
#include "hardware/watchdog.h"
#include "hardware/vreg.h"
#include "FrensHelpers.h"
#include "usb_msc.h" // C++ linkage (namespace Frens); FRENS_USB_MSC gates it
#include "RomLister.h"
#include "menu.h"
#include "settings.h"
#include "gamepad.h"
#include "nespad.h"
#include "wiipad.h"
#include "tusb.h"
#include "image_convert.h" // C++ linkage; keep out of the extern "C" block
extern "C" {
#include "boot_config.h"
#include "uf2_loader.h"
#include "uf2_diag.h"
#include "uf2_format.h"
#include "app_launch.h"
#include "storage.h"
#include "program_name.h"
#include "emulators_txt.h"
#include "categories.h"
#include "uf2_crc.h"
#include "gui.h"
#include "themes.h"
#include "screensaver.h"
#include "progress_bar.h"
#include "sd_boot_ini.h"
#include <hardware/divider.h>
}
// Progress-bar colours, picked at compile time to match whichever 16-bit
// pixel format the active backend uses. Hard-coded so the SRAM-resident
// flashProgress callback never has to dereference a palette in flash.
// HSTX : RGB555 (5-5-5-1, low bit ignored)
// !HSTX : RGB444 packed as 0x0RGB (this codebase's PicoDVI encoding,
// see CC() in pico_shared/menu.cpp)
#if HSTX
#define PB_COL_BORDER 0x0000u // black
#define PB_COL_EMPTY 0x7FFFu // white (matches menu background)
#define PB_COL_FILL 0x03E0u // pure green
#else
#define PB_COL_BORDER 0x0000u // black
#define PB_COL_EMPTY 0x0FFFu // white
#define PB_COL_FILL 0x00F0u // pure green
#endif
// DrawScreen() has external linkage in pico_shared/menu.cpp but is not declared
// in menu.h. It renders the 40x30 charcell screenBuffer into the active video
// framebuffer for one frame. We drive it directly from our own picker loop.
void DrawScreen(int selectedRow, int w = 0, int h = 0, uint16_t *imagebuffer = nullptr,
int imagex = 0, int imagey = 0);
// splash() is normally provided by each emulator (it is emulator-specific art).
// The bootloader shows no splash, but menu.cpp references the symbol, so give
// the linker a harmless definition.
void splash() {}
// Defined in pico_shared/FrensHelpers.cpp but not declared in FrensHelpers.h.
// Reads the SPI flash JEDEC capacity byte; returns the chip size in bytes.
namespace Frens { uint storage_get_flash_capacity(); }
// End of usable flash for app images: build-time partition end, clamped to
// the real chip size reported by the JEDEC ID. Both the loader's runtime
// bound (set in main) and the menu's size gate (buildEmuList) derive from
// this, so the picker never lists an image the loader would refuse.
static uint32_t appFlashEnd()
{
uint32_t cap = Frens::storage_get_flash_capacity(); // cached after first call
return (cap >= FLASH_TOTAL_SIZE) ? APP_END_ADDR : (XIP_BASE + cap);
}
#define CPUFREQ_KHZ 252000
#ifndef HW_CONFIG
#define HW_CONFIG 0
#endif
#define COL_FG DEFAULT_FGCOLOR // dark text
#define COL_BG DEFAULT_BGCOLOR // light background
// Brief on-screen pause (ms) so the "Flashing..." notice registers before
// the bar starts moving. The bar itself is now live during the erase so we
// don't need a long read-time -- just enough to acknowledge the press.
#define FLASH_NOTICE_MS 500
// On picoDVI we have to stop core1 before flashing (see flashAndLaunch
// comment), so the screen goes dark for the whole flash op. Hold the
// "screen will go blank" notice on screen long enough for the user to
// actually read it.
#define PICO_DVI_FLASH_NOTICE_MS 3500
#define LOG(fmt, ...) printf("[bootLoader] " fmt "\n", ##__VA_ARGS__)
namespace {
#define PROG_NAME_MAX 32
#define IMAGE_KEY_MAX 16
#define DISPLAY_NAME_MAX 40
#define AUX_UF2_MAX 64
struct SdEmu {
char filename[ROMLISTER_MAXPATH]; // basename
char label[PROG_NAME_MAX]; // shown in the menu (program_name preferred)
char prog_name[PROG_NAME_MAX]; // binary_info match key ("" if not extractable)
char image_key[IMAGE_KEY_MAX]; // emulators.txt column 2 ("md", "nes", ...)
char display_name[DISPLAY_NAME_MAX]; // emulators.txt column 3 (human-readable)
char aux_uf2[AUX_UF2_MAX]; // emulators.txt column 4 (basename of aux .uf2; "" = none)
};
// One .uf2 found in the config dir. Filled once by scanUf2Dir() and then read
// by buildEmuList() for every category, so walking into a category costs no
// directory scan and no binary_info re-parse.
struct Uf2File {
char filename[ROMLISTER_MAXPATH]; // basename
char prog_name[PROG_NAME_MAX]; // from binary_info; never empty once stored
uint32_t ext_hi; // last flash address the image touches
bool ext_known; // false when the extent probe failed
};
char g_emuDir[80]; // "<BASEDIR>/<HW_CONFIG>"
Uf2File g_uf2[64]; // every readable .uf2 in g_emuDir
int g_uf2_count = 0;
SdEmu g_emus[32]; // the list currently on screen (one category's worth)
int g_emu_count = 0; // entries actually shown (matched in the index file)
int g_emu_seen = 0; // total .uf2 files found in g_emuDir, pre-filter
char g_flash_prog_name[PROG_NAME_MAX] = {0}; // currently-flashed image's program name
int g_flash_idx = -1; // index into g_emus, or -1 if none matches
// True when the in-flash image has the same program_name as g_emus[g_flash_idx]
// but its CRC32 differs from the SD .uf2 -- meaning the user dropped a newer
// build onto the card. Pressing A on that entry takes the flash path instead
// of the in-flash launch.
bool g_flash_drift = false;
// Paths built at boot from /boot.txt (or its defaults). Consumed everywhere
// the old EMULATORS_TXT_PATH / GUI_MODE_PATH macros used to be.
char g_index_path[144]; // "<BASEDIR>/<INDEX>"
char g_guimode_path[80]; // "<BASEDIR>/.guimode"
char g_base_dir[64]; // BASEDIR, for building category config paths
char g_categories_path[144]; // "<BASEDIR>/categories.txt"
// True when categories.txt parsed and holds at least one row. When false the
// picker is what it always was: one flat carousel driven by the INDEX file.
bool g_have_categories = false;
// Bare filename of the categories index. Not configurable in /boot.txt: a card
// either has categories or it doesn't, and INDEX already names the flat list.
#define CATEGORIES_TXT "categories.txt"
#define GUI_SLIDE_PX_PER_FRAME 20 // 320 / 20 = 16 frames ≈ 270 ms
// Vertical travel is 240 rows rather than 320 columns, so the per-frame step
// is scaled to keep both transitions the same 16 frames long -- a level
// change that ran visibly quicker than a neighbour change would read as a
// different kind of movement rather than the same gesture on another axis.
#define GUI_SLIDE_PY_PER_FRAME 15 // 240 / 15 = 16 frames
// Fallback label derived from filename when binary_info parsing fails:
// "picogenesisPlus_AdafruitFruitJam_arm_piousb.uf2" -> "picogenesisPlus"
void makeLabelFromFilename(const char *fname, char *out, size_t n)
{
char tmp[ROMLISTER_MAXPATH];
strncpy(tmp, fname, sizeof(tmp) - 1);
tmp[sizeof(tmp) - 1] = '\0';
char *dot = strrchr(tmp, '.');
if (dot && strcasecmp(dot, ".uf2") == 0) *dot = '\0';
char *us = strchr(tmp, '_');
if (us) *us = '\0';
strncpy(out, tmp, n - 1);
out[n - 1] = '\0';
}
void centerText(int row, const char *text, int fg, int bg)
{
int len = (int)strlen(text);
int x = (SCREEN_COLS - len) / 2;
if (x < 0) x = 0;
putText(x, row, text, fg, bg);
}
// Paint one menu frame. The flashed entry (g_flash_idx) gets a leading marker
// to identify it as "the one currently in flash"; the selected row is shown
// inverted. Both compose when the flashed row is also the selected row.
void drawMenu(int sel, int top, int visible)
{
ClearScreen(COL_BG);
centerText(0, "RP2350 bootloader " SWVERSION, COL_FG, COL_BG);
char hdr[SCREEN_COLS + 1];
snprintf(hdr, sizeof(hdr), "Config %d %s", HW_CONFIG, g_emuDir);
centerText(1, hdr, COL_FG, COL_BG);
char bar[SCREEN_COLS + 1];
memset(bar, ' ', SCREEN_COLS);
bar[SCREEN_COLS] = '\0';
for (int i = 0; i < visible && (top + i) < g_emu_count; i++) {
int idx = top + i;
bool seld = (idx == sel);
bool flashed = (idx == g_flash_idx);
int fg = seld ? COL_BG : COL_FG;
int bg = seld ? COL_FG : COL_BG;
int row = STARTROW + i;
putText(0, row, bar, fg, bg);
// Prefer the friendly name from emulators.txt; fall back to the
// binary_info / filename-derived label when the txt has no row for
// this prog_name.
const char *name = g_emus[idx].display_name[0] ?
g_emus[idx].display_name : g_emus[idx].label;
char line[SCREEN_COLS + 1];
// Two prefix characters: selection marker, then flash-state marker.
// ">" = cursor; "*" = currently in flash and matches SD;
// "!" = currently in flash but SD copy differs (will reflash on launch).
char flashMark = ' ';
if (flashed) flashMark = g_flash_drift ? '!' : '*';
snprintf(line, sizeof(line), "%c %c %s",
seld ? '>' : ' ',
flashMark,
name);
putText(1, row, line, fg, bg);
}
// buttonLabel1 is the label of the button that triggers Btn::A on the
// attached pad (e.g. "A" on NES, "B" on XInput, "O" on PlayStation).
char buttonLabel1[2];
char buttonLabel2[2];
getButtonLabels(buttonLabel1, buttonLabel2);
char hint[SCREEN_COLS + 1];
centerText(SCREEN_ROWS - 4, "* in flash ! SD differs (reflash)",
COL_FG, COL_BG);
centerText(SCREEN_ROWS - 3, "UP / DOWN : choose SELECT : options", COL_FG, COL_BG);
// '_' rather than spaces for the gap: putText collapses whitespace runs,
// so a space-padded string would render shorter than centerText measured
// it with strlen() and the line would sit off-centre. putText turns '_'
// back into a literal space (menu.cpp:598).
if (g_have_categories) {
snprintf(hint, sizeof(hint), "%s : start____%s : back____START : help",
buttonLabel1, buttonLabel2);
} else {
snprintf(hint, sizeof(hint), "%s : start____START : help", buttonLabel1);
}
centerText(SCREEN_ROWS - 2, hint, COL_FG, COL_BG);
}
// The category level's text menu. Same chrome as drawMenu(), minus the
// flash-state markers -- a category is not a thing that can be in flash.
void drawCategoryMenu(int sel, int top, int visible)
{
ClearScreen(COL_BG);
centerText(0, "RP2350 bootloader " SWVERSION, COL_FG, COL_BG);
char hdr[SCREEN_COLS + 1];
snprintf(hdr, sizeof(hdr), "Config %d %s", HW_CONFIG, g_emuDir);
centerText(1, hdr, COL_FG, COL_BG);
char bar[SCREEN_COLS + 1];
memset(bar, ' ', SCREEN_COLS);
bar[SCREEN_COLS] = '\0';
const int count = categories_count();
for (int i = 0; i < visible && (top + i) < count; i++) {
int idx = top + i;
bool seld = (idx == sel);
int fg = seld ? COL_BG : COL_FG;
int bg = seld ? COL_FG : COL_BG;
int row = STARTROW + i;
putText(0, row, bar, fg, bg);
char line[SCREEN_COLS + 1];
snprintf(line, sizeof(line), "%c %s",
seld ? '>' : ' ', categories_name(idx));
putText(1, row, line, fg, bg);
}
char buttonLabel1[2];
char buttonLabel2[2];
getButtonLabels(buttonLabel1, buttonLabel2);
char hint[SCREEN_COLS + 1];
centerText(SCREEN_ROWS - 3, "UP / DOWN : choose SELECT : options", COL_FG, COL_BG);
snprintf(hint, sizeof(hint), "%s : open____START : help", buttonLabel1);
centerText(SCREEN_ROWS - 2, hint, COL_FG, COL_BG);
}
void showMessage(const char *l1, const char *l2, const char *l3)
{
ClearScreen(COL_BG);
if (l1) centerText(12, l1, COL_FG, COL_BG);
if (l2) centerText(14, l2, COL_FG, COL_BG);
if (l3) centerText(16, l3, COL_FG, COL_BG);
}
// --- Graphical error-screen rendering --------------------------------------
//
// The error screen is composed in two layers per frame:
// 1. Charcell layer (via DrawScreen): title in a red bar, message body in an
// ASCII-bordered box, "Press RESET" line. Carries all the words.
// 2. Direct-framebuffer overlay (overlayErrorDecor below): diagonal yellow/
// black caution-tape bands top + bottom, a big yellow disk with a red
// "!" punched through the top stripe band. Pure pixel writes -- nothing
// goes through the charcell pipeline. Drawn AFTER DrawScreen so it sits
// on top of (and overwrites) any charcell content in those bands.
//
// Both layers are repainted every frame in the fatalErrorScreen pump loop, so
// nothing has to persist between frames.
// NesMenuPalette indices used by the charcell layer. 0x16 / 0x06 are NES reds,
// 0x30 is white. Picked to match the bright FB-direct red so the layers don't
// clash visually.
#define COL_ERR_BG 0x06
#define COL_ERR_FG 0x30
// 16-bit pixel colours for the FB-direct overlay. Two encodings depending on
// the active backend -- the same split progress_bar.cpp uses.
// HSTX : RGB555 (bit layout 0RRRRR GGGGG BBBBB)
// !HSTX : RGB444 packed as 0x0RGB (this codebase's PicoDVI encoding)
#if HSTX
#define ERRPX_YELLOW 0x7FE0u
#define ERRPX_BLACK 0x0000u
#define ERRPX_RED 0x7C00u
#else
#define ERRPX_YELLOW 0x0FF0u
#define ERRPX_BLACK 0x0000u
#define ERRPX_RED 0x0F00u
#endif
// Get the active backend's 320x240 framebuffer. Returns nullptr when there
// isn't one (PicoDVI line-stream mode without a framebuffer) -- the caller
// silently skips overlay in that case and the user still gets the charcell
// layer alone.
static uint16_t *getActiveFramebuffer()
{
#if HSTX
return (uint16_t *)hstx_getframebuffer();
#else
#if FRAMEBUFFERISPOSSIBLE
if (!Frens::isFrameBufferUsed()) return nullptr;
return Frens::framebuffer;
#else
return nullptr;
#endif
#endif
}
// Caution-tape stripe band: diagonal yellow/black stripes filling a horizontal
// strip [y0, y0+h). Stripe width fixed at 12 px which gives a clear "caution"
// look without going dizzy.
static void drawStripeBand(uint16_t *fb, int y0, int h)
{
const int W = 320;
for (int dy = 0; dy < h; dy++) {
int y = y0 + dy;
uint16_t *row = fb + y * W;
for (int x = 0; x < W; x++) {
row[x] = (((x + y) / 12) & 1) ? ERRPX_YELLOW : ERRPX_BLACK;
}
}
}
// Big yellow "!" disk centered horizontally at x=cx, vertically at y=cy.
// Sits half-embedded in the top stripe band so the icon visually breaks
// through it -- the whole point of having it there.
static void drawWarningDisk(uint16_t *fb, int cx, int cy, int r)
{
const int W = 320;
const int rSq = r * r;
const int rSqInner = (r - 3) * (r - 3);
// Yellow disk (with black outline ring carved out of the same loop).
for (int y = cy - r; y <= cy + r; y++) {
if (y < 0 || y >= 240) continue;
for (int x = cx - r; x <= cx + r; x++) {
int dx = x - cx, dy = y - cy;
int d = dx * dx + dy * dy;
if (d > rSq) continue;
fb[y * W + x] = (d >= rSqInner) ? ERRPX_BLACK : ERRPX_YELLOW;
}
}
// Red "!" inside: a 5-px-wide bar above a 5x5 dot below.
auto fillRect = [&](int x0, int y0, int x1, int y1, uint16_t col) {
for (int y = y0; y <= y1; y++) {
if (y < 0 || y >= 240) continue;
for (int x = x0; x <= x1; x++) {
if (x < 0 || x >= W) continue;
fb[y * W + x] = col;
}
}
};
fillRect(cx - 2, cy - 14, cx + 2, cy + 4, ERRPX_RED); // tall bar
fillRect(cx - 2, cy + 8, cx + 2, cy + 13, ERRPX_RED); // dot
}
// Direct-framebuffer overlay. Called after DrawScreen() each frame so it
// stays on top of the charcell pipeline.
static void overlayErrorDecor()
{
uint16_t *fb = getActiveFramebuffer();
if (!fb) return;
drawStripeBand(fb, 0, 16); // top caution-tape band
drawStripeBand(fb, 224, 16); // bottom caution-tape band
drawWarningDisk(fb, 160, 16, 22); // breaks through the top band
}
// Charcell layout. Avoids rows 0..1 and 28..29 since those get overwritten
// by the stripe overlay. Title and "Press RESET" use the same red bg as the
// FB-direct red so the layers blend rather than clash.
// Paint a full-width solid background row.
//
// putText() collapses consecutive whitespace -- a 40-space string ends up
// writing only one cell, leaving the rest of the row untouched. Use '_' so
// each cell is treated as a non-space character; putText converts it back to
// a literal space at write time (menu.cpp:598), giving us a proper solid-bg
// row with no visible characters.
void solidBar(int row, int fg, int bg)
{
char bar[SCREEN_COLS + 1];
memset(bar, '_', SCREEN_COLS);
bar[SCREEN_COLS] = '\0';
putText(0, row, bar, fg, bg);
}
void drawErrorScreen(const char *title, const char *l1, const char *l2, const char *l3)
{
ClearScreen(COL_BG);
// Title bar: solid red row with white centered title. Sits below the
// bottom edge of the warning disk (which lands around scanline 38 ~= row 4).
solidBar(5, COL_ERR_FG, COL_ERR_BG);
if (title) centerText(5, title, COL_ERR_FG, COL_ERR_BG);
// ASCII box around the three message lines (rows 10..14).
const int boxW = 36;
const int boxX = (SCREEN_COLS - boxW) / 2;
char border[SCREEN_COLS + 1];
border[0] = '+';
for (int i = 1; i < boxW - 1; i++) border[i] = '-';
border[boxW - 1] = '+';
border[boxW] = '\0';
putText(boxX, 10, border, COL_FG, COL_BG);
for (int r = 11; r <= 13; r++) {
putText(boxX, r, "|", COL_FG, COL_BG);
putText(boxX + boxW - 1, r, "|", COL_FG, COL_BG);
}
putText(boxX, 14, border, COL_FG, COL_BG);
const char *lines[3] = { l1, l2, l3 };
for (int i = 0; i < 3; i++) {
if (lines[i]) centerText(11 + i, lines[i], COL_FG, COL_BG);
}
// "Press RESET" bar: another solid red row above the bottom stripe band.
solidBar(21, COL_ERR_FG, COL_ERR_BG);
centerText(21, "Press RESET to retry", COL_ERR_FG, COL_ERR_BG);
}
// Render the error screen and never return.
//
// The screen is entirely static (no animation, no input), so on backends with
// a persistent framebuffer (HSTX, PicoDVI in framebuffer mode) we paint once
// and then just pump USB. Redrawing every frame causes visible flicker: the
// stripe rows go briefly blank between DrawScreen (which writes charcell bg
// to those rows) and overlayErrorDecor (which paints stripes on top), and
// the DMA scanout catches that gap.
//
// PicoDVI in line-stream mode has no framebuffer; every scanline is rebuilt
// from screenBuffer on the fly, so DrawScreen must run every frame. The
// overlay does nothing there (no framebuffer to write to) -- but DrawScreen
// alone never tears because the line pipeline is single-pass.
[[noreturn]] void fatalErrorScreen(const char *title,
const char *l1, const char *l2, const char *l3)
{
LOG("FATAL: %s", title ? title : "(no title)");
if (l1) LOG(" %s", l1);
if (l2) LOG(" %s", l2);
if (l3) LOG(" %s", l3);
drawErrorScreen(title, l1, l2, l3);
bool persistentFB = (getActiveFramebuffer() != nullptr);
// One-shot paint. DrawScreen pushes the charcell layer into the FB (or
// the line pipeline on line-stream); overlay then sits on top on FB-mode.
DrawScreen(-1);
if (persistentFB) overlayErrorDecor();
for (;;) {
tuh_task();
if (!persistentFB) DrawScreen(-1); // line-stream needs every frame
sleep_ms(16);
}
}
// One frame's worth of input, merged from every source the board has, in
// io::GamePadState::Button bits. Paces to 60 fps, pumps USB, blinks the
// onboard LED, and hot-plug-probes the Wii pad -- i.e. this is the whole
// per-frame housekeeping, not just the button read.
//
// Shared by the picker loop and the help screen; edge detection
// (pushed = btns & ~prev) stays with each caller since they track their own
// prevButtons. The nespad_read_start() / _finish() bracket keeps its original
// spacing: the frame-counter and LED work sits between them so the PIO shift
// register has time to clock out before we block on the result.
//
// `pace` exists only for USB drive mode. PaceFrames60fps() busy-waits for
// vsync, which is what every other screen wants, but there it would cap
// Frens::usbMscTask() at 60 calls a second and throttle the transfer to a
// crawl. That screen passes false and spends the frame on USB instead; it
// repaints only when the mounted state changes, so nothing needs the pacing.
uint32_t readPads(bool pace = true)
{
using Btn = io::GamePadState::Button;
if (pace) Frens::PaceFrames60fps(false, true);
#if NES_PIN_CLK != -1
nespad_read_start();
#endif
auto count =
#if !HSTX
dvi_->getFrameCounter();
#else
hstx_getframecounter();
#endif
auto onOff = hw_divider_s32_quotient_inlined(count, 60) & 1;
Frens::blinkLed(onOff);
#if NES_PIN_CLK != -1
nespad_read_finish(); // populates nespad_states[]
#endif
tuh_task();
#if WIIPAD_DELAYED_START and WII_PIN_SDA >= 0 and WII_PIN_SCL >= 0
// Probe once per second (onOff toggles at 60 frames) so we pick up a
// pad that was plugged in after boot. wiipad_begin() is a no-op once
// connected.
if (!wiipad_is_connected() && onOff) {
wiipad_begin();
}
#endif
uint32_t btns = io::getCurrentGamePadState(0).buttons |
io::getCurrentGamePadState(1).buttons;
// nespad_states[] and wiipad_read() use their own bit layouts (NES
// bus order / Wii nunchuk layout). Translate them into the same
// io::GamePadState::Button bits the callers check via Btn::*.
#if NES_PIN_CLK != -1 || NES_PIN_CLK_1 != -1
auto nesToBtn = [](uint8_t s) -> uint32_t {
// nespad_states is LSB-first wire order (A clocked out first lands
// in bit 0): 0x01=A, 0x02=B, 0x04=Select, 0x08=Start, 0x10=Up,
// 0x20=Down, 0x40=Left, 0x80=Right. The header comment in
// pico_shared/nespad.cpp claims the reverse and is wrong --
// infonesPlus ORs nespad_states[] straight into a bitmask with
// A=1<<0..RIGHT=1<<7, which only works under this layout.
uint32_t b = 0;
if (s & 0x01) b |= Btn::A;
if (s & 0x02) b |= Btn::B;
if (s & 0x04) b |= Btn::SELECT;
if (s & 0x08) b |= Btn::START;
if (s & 0x10) b |= Btn::UP;
if (s & 0x20) b |= Btn::DOWN;
if (s & 0x40) b |= Btn::LEFT;
if (s & 0x80) b |= Btn::RIGHT;
return b;
};
#endif
#if NES_PIN_CLK != -1
btns |= nesToBtn(nespad_states[0]);
#endif
#if NES_PIN_CLK_1 != -1
btns |= nesToBtn(nespad_states[1]);
#endif
#if WII_PIN_SDA >= 0 and WII_PIN_SCL >= 0
{
// wiipad_read() layout (see wiipad.cpp): A=1<<0, B=1<<1, SELECT=1<<2,
// START=1<<3, UP=1<<4, DOWN=1<<5, LEFT=1<<6, RIGHT=1<<7, X=1<<8, Y=1<<9.
uint16_t w = wiipad_read();
if (w & (1 << 0)) btns |= Btn::A;
if (w & (1 << 1)) btns |= Btn::B;
if (w & (1 << 2)) btns |= Btn::SELECT;
if (w & (1 << 3)) btns |= Btn::START;
if (w & (1 << 4)) btns |= Btn::UP;
if (w & (1 << 5)) btns |= Btn::DOWN;
if (w & (1 << 6)) btns |= Btn::LEFT;
if (w & (1 << 7)) btns |= Btn::RIGHT;
if (w & (1 << 8)) btns |= Btn::X;
if (w & (1 << 9)) btns |= Btn::Y;
}
#endif
return btns;
}
// --- Help screen ------------------------------------------------------------
// NesMenuPalette indices. 0x16 is a NES red for section headings; the title /
// footer bars reuse a blue-on-white pairing so they read as chrome rather than
// as an error (which owns the red bars).
#define COL_HELP_HDR 0x16
#define COL_BAR_FG 0x30
#define COL_BAR_BG 0x02
// Static rows of the help page. Two columns because putText() collapses
// whitespace runs -- a single string with padding between key and description
// would render with the gap squeezed to one space. col 1 marks a section
// heading (drawn in COL_HELP_HDR), col 3 / col 18 are the key / description
// columns. Rows needing runtime state are painted separately below.
struct HelpLine { uint8_t row, col; const char *text; };
const HelpLine HELP_BODY[] = {
{ 2, 1, "TEXT MODE" },
{ 3, 3, "UP / DOWN" }, { 3, 18, "select application" },
{ 5, 3, "SELECT" }, { 5, 18, "options menu" },
{ 6, 3, "START" }, { 6, 18, "this help screen" },
{ 8, 1, "GRAPHICAL MODE" },
{ 9, 3, "LEFT / RIGHT" }, { 9, 18, "select application" },
{ 10, 3, "UP / DOWN" }, { 10, 18, "change artwork theme" },
{ 12, 3, "SELECT" }, { 12, 18, "options menu" },
{ 13, 3, "START" }, { 13, 18, "this help screen" },
{ 15, 1, "LIST MARKERS" },
{ 16, 3, "*" }, { 16, 18, "in flash, up to date" },
{ 17, 3, "!" }, { 17, 18, "in flash, SD differs" },
{ 18, 18, "- starts by reflashing" },
{ 20, 1, "STATUS" },
{ 26, 3, "Screensaver: after 30 s idle." },
{ 27, 3, "Any button wakes it." },
};
// The same page for a card WITH categories. Rows are at a premium -- the
// static table already runs from row 2 to the footer bar at 28 -- so each mode
// section pairs SELECT with START on one line to make room for the back
// button, and the two flash markers lose their continuation line.
const HelpLine HELP_BODY_CATEGORIES[] = {
{ 2, 1, "TEXT MODE" },
{ 3, 3, "UP / DOWN" }, { 3, 18, "choose" },
{ 6, 3, "SELECT / START" }, { 6, 18, "options / help" },
{ 8, 1, "GRAPHICAL MODE" },
{ 9, 3, "LEFT / RIGHT" }, { 9, 18, "choose" },
{ 10, 3, "UP / DOWN" }, { 10, 18, "change artwork theme" },
{ 13, 3, "SELECT / START" }, { 13, 18, "options / help" },
{ 15, 1, "LIST MARKERS" },
{ 16, 3, "*" }, { 16, 18, "in flash, up to date" },
{ 17, 3, "!" }, { 17, 18, "SD differs, reflashes" },
{ 20, 1, "STATUS" },
{ 26, 3, "Screensaver: after 30 s idle." },
{ 27, 3, "Any button wakes it." },
};
// Paint the help page into the charcell layer. Pure charcell on purpose: it
// renders identically on all three backends and reuses the screenBuffer that
// is already allocated, so the screen costs no extra RAM.
void drawHelpScreen(bool graphical_mode, const char *index_file, bool cfg_save_failed)
{
ClearScreen(COL_BG);
char btn1[2], btn2[2];
getButtonLabels(btn1, btn2);
solidBar(0, COL_BAR_FG, COL_BAR_BG);
centerText(0, "HELP - RP2350 bootloader " SWVERSION, COL_BAR_FG, COL_BAR_BG);
const HelpLine *body = g_have_categories ? HELP_BODY_CATEGORIES : HELP_BODY;
const size_t body_n = g_have_categories ? (sizeof(HELP_BODY_CATEGORIES) /
sizeof(HELP_BODY_CATEGORIES[0]))
: (sizeof(HELP_BODY) / sizeof(HELP_BODY[0]));
for (size_t i = 0; i < body_n; i++) {
const HelpLine &h = body[i];
putText(h.col, h.row, h.text, h.col == 1 ? COL_HELP_HDR : COL_FG, COL_BG);
}
// The button labels follow the attached pad ("A"/"B" on NES, "B"/"A" on
// XInput, "O"/"X" on DualShock, "X"/"Z" on a keyboard), so these rows
// can't live in a static table.
for (int row : { 4, 11 }) {
putText(3, row, btn1, COL_FG, COL_BG);
putText(18, row, g_have_categories ? "open / start"
: "start selected app", COL_FG, COL_BG);
}
if (g_have_categories) {
for (int row : { 5, 12 }) {
putText(3, row, btn2, COL_FG, COL_BG);
putText(18, row, "back to categories", COL_FG, COL_BG);
}
}
char val[28];
putText(3, 21, "Mode", COL_FG, COL_BG);
putText(18, 21, graphical_mode ? "graphical" : "text", COL_FG, COL_BG);
// "3 (2 of 4)" -- the theme number (what THEME= in boot.txt holds), then
// its position among the themes that exist. Listing every present theme
// instead ran to 28 characters with all ten on the card, overflowing both
// this buffer and the 22-cell description column. Worst case here is
// "9 (10 of 10)", 12 characters. Single spaces only: putText collapses
// whitespace runs, so wider padding would not survive anyway.
{
const int active = themes_active();
int pos = 0;
for (int t = 0; t <= active && t < THEME_MAX; t++) {
if (themes_exists(t)) pos++;
}
snprintf(val, sizeof(val), "%d (%d of %d)", active, pos, themes_count());
putText(3, 22, "Theme", COL_FG, COL_BG);
putText(18, 22, val, COL_FG, COL_BG);
}
putText(3, 23, "Config", COL_FG, COL_BG);
putText(18, 23, g_emuDir, COL_FG, COL_BG);
// With categories, INDEX is not read at all -- each category names its own
// config file -- so naming it here would just mislead.
putText(3, 24, g_have_categories ? "Categories" : "Index", COL_FG, COL_BG);
putText(18, 24, g_have_categories ? CATEGORIES_TXT
: (index_file ? index_file : ""), COL_FG, COL_BG);
if (cfg_save_failed) {
putText(3, 25, "Settings not saved - SD write failed",
COL_ERR_FG, COL_ERR_BG);
}
// Own buffer: val[] is sized for the 22-cell description column, and this
// line is wider than that.
char foot[SCREEN_COLS + 1];
solidBar(28, COL_BAR_FG, COL_BAR_BG);
snprintf(foot, sizeof(foot), "Press START, %s or %s to return", btn1, btn2);
centerText(28, foot, COL_BAR_FG, COL_BAR_BG);
}
// Show the help page until the user dismisses it.
//
// Unlike fatalErrorScreen(), this redraws every frame on every backend. The
// flicker rule documented above applies to screens that alternate a charcell
// pass with an FB-direct pass -- the gap between the two is what tears. The
// help page has no FB-direct layer, so a full-screen charcell rewrite is
// idempotent and safe, exactly as the picker's own text mode already does.
void showHelpScreen(bool graphical_mode, const char *index_file, bool cfg_save_failed)
{
using Btn = io::GamePadState::Button;
drawHelpScreen(graphical_mode, index_file, cfg_save_failed);
// ~0u so the START press that opened this screen isn't immediately read
// as the press that closes it.
uint32_t prev = ~0u;
for (;;) {
uint32_t btns = readPads();
uint32_t pushed = btns & ~prev;
prev = btns;
DrawScreen(-1);
if (pushed & (Btn::START | Btn::A | Btn::B | Btn::SELECT)) break;
}
}
// Defined further down, next to the flashing helpers, but needed by the two
// screens below.
void idleFor(int ms);
// --- USB drive mode ---------------------------------------------------------
//
// Hands the raw SD card to a PC as a mass-storage device and sits here until the
// host lets go or the user presses B. Reduced port of showUsbDriveScreen() in
// pico_shared/menu.cpp, which is static and so cannot be called from here.
//
// The reference has a second path for line-buffer DVI (RP2040): core0 cannot
// serve both the SD and a scanline every 63.5us, so it parks the display and
// runs blind. We never need it -- this loader is RP2350-only and passes
// useFrameBuffer=true to Frens::initAll, so isFrameBufferUsed() is always true
// and core1 scans out of the framebuffer without core0's help. The check below
// is a guard, not a branch: if that assumption ever breaks we decline the screen
// rather than show a collapsing picture.
//
// NOTHING here may touch FatFs. usbMscBegin() unmounts the volume and the PC
// owns the filesystem until usbMscEnd() puts it back, so no sd_boot_ini_save(),
// no gui_load_image(), no themes_*, no screensaver -- which is why this screen
// is pure charcell.
#if FRENS_USB_MSC
void showUsbDriveScreen()
{
using Btn = io::GamePadState::Button;
// Long enough for a PC to notice and enumerate a new device, short enough
// that a board whose only pad is on the USB port it just gave away is never
// stuck here.
constexpr uint32_t NO_HOST_TIMEOUT_MS = 20000;
char btn1[2], btn2[2];
getButtonLabels(btn1, btn2);
char line[SCREEN_COLS + 1];
// Same pump as showHelpScreen(): draw once, then push the buffer every
// frame until a press is seen. prev = ~0u swallows the A press that got
// us here.
auto notice = [&](const char *l1, const char *l2, const char *l3) {
showMessage(l1, l2, l3);
uint32_t prev = ~0u;
for (;;) {
uint32_t btns = readPads();
uint32_t pushed = btns & ~prev;
prev = btns;
DrawScreen(-1);
if (pushed & (Btn::START | Btn::A | Btn::B | Btn::SELECT)) break;
}
};
if (!Frens::isFrameBufferUsed()) {
// Cannot happen on the configurations we build (see the note above).
LOG("USB drive mode declined: no framebuffer on this build.");
notice("USB drive mode", "Not available on this build.", nullptr);
return;
}
if (!Frens::usbMscBegin()) {
LOG("usbMscBegin() failed; staying in the picker.");
notice("Cannot read SD card", "USB drive mode unavailable.", nullptr);
return;
}
LOG("USB drive mode: device stack up.");
const uint32_t started = Frens::time_ms();
uint32_t prev = ~0u; // swallow the A press that opened this screen
int painted = -1; // last painted state; -1 = nothing painted yet
bool done = false;
while (!done) {
// 0 = waiting for a host, 1 = mounted, 2 = mounted but bus suspended.
const bool mounted = Frens::usbMscHostConnected();
const int state = !mounted ? 0 : (Frens::usbMscHostSuspended() ? 2 : 1);
// Repaint only on a state change. This screen is static otherwise, and
// every cycle not spent in DrawScreen() is a cycle spent on the
// transfer -- see the `pace` note on readPads().
if (state != painted) {
painted = state;
ClearScreen(COL_BG);
solidBar(0, COL_BAR_FG, COL_BAR_BG);
centerText(0, "USB DRIVE MODE", COL_BAR_FG, COL_BAR_BG);
if (mounted) {
centerText(8, state == 2 ? "SD card is mounted (computer asleep)."
: "SD card is mounted on your computer.",
COL_FG, COL_BG);
centerText(10, "Copy or delete files, then eject the", COL_FG, COL_BG);
centerText(11, "drive on your computer.", COL_FG, COL_BG);
} else {
centerText(8, "Connect the USB port to a computer.", COL_FG, COL_BG);
centerText(10, "Waiting for the computer...", COL_FG, COL_BG);
}
centerText(14, "Do not remove the SD card.", COL_FG, COL_BG);
#if !CFG_TUH_RPI_PIO_USB
// The reference prints "USB controllers are off until you exit."
// here, which cannot be true on these boards: the USB host runs on
// the native port, so the cable now going to the computer is the
// same one a gamepad would use and there was never one attached.
// What the user does need to know is that the way out costs a
// restart -- usbMscNeedsRebootOnExit() is unconditionally true
// here, because handing rhport 0 back leaks spinlocks.
centerText(16, "The board restarts when you leave.", COL_FG, COL_BG);
#endif
solidBar(28, COL_BAR_FG, COL_BAR_BG);
snprintf(line, sizeof(line), "Eject on the computer, or press %s", btn2);
centerText(28, line, COL_BAR_FG, COL_BAR_BG);
DrawScreen(-1);
}
// Spend the bulk of the iteration pumping the device stack. tud_task()
// is where the SCSI transfers actually happen, so this is the transfer
// rate; readPads(false) below skips the vsync wait for the same reason.
absolute_time_t until = make_timeout_time_ms(12);
while (!time_reached(until)) {
Frens::usbMscTask();
}
uint32_t btns = readPads(false);
uint32_t pushed = btns & ~prev;
prev = btns;
if (pushed & Btn::B) {
LOG("USB drive mode: cancelled by the user.");
done = true;
} else if (Frens::usbMscEverConnected()) {
// A computer had the drive; leave the moment it lets go. A bus
// suspend is not letting go, so a sleeping host does not drop us
// out mid-copy -- that is why usbMscHostConnected() ignores it.
done = !Frens::usbMscHostConnected();
if (done) LOG("USB drive mode: host ejected or unplugged.");
} else if (Frens::time_ms() - started > NO_HOST_TIMEOUT_MS) {
LOG("USB drive mode: no computer connected within %u ms.",
(unsigned)NO_HOST_TIMEOUT_MS);
done = true;
}
}
const bool wrote = Frens::usbMscMediaDirty();
Frens::usbMscEnd();
LOG("USB drive mode: ended. mediaDirty=%d needsReboot=%d",
(int)wrote, (int)Frens::usbMscNeedsRebootOnExit());
// Two independent reasons to restart:
// - the board demands it. Without PIO USB the host must take rhport 0
// back, which forces a tud_deinit() that leaks two hardware spinlocks
// TinyUSB never frees; a second visit would run the pool dry.
// - the PC wrote to the card. The .uf2 list, the in-flash CRC comparison,
// the theme scan and the cached .444/.555 conversions were all built at
// boot from what was on the card then. Rebooting re-reads all of it,
// which is exactly what we would have to do here anyway.
if (Frens::usbMscNeedsRebootOnExit() || wrote) {
showMessage("Restarting", wrote ? "The SD card was changed." : nullptr, nullptr);
DrawScreen(-1);
idleFor(1000);
// No Frens::resetWifi() here, unlike the pico_shared version: the Pico 2 W
// builds have no room for USB drive mode and compile this out entirely
// (see ENABLE_USB_MSC in CMakeLists.txt), so there is never a CYW43 to
// shut down. The loader's other reboot paths don't call it either.
// watchdog_reboot(), NOT watchdog_enable(): the latter stamps the SDK
// magic that our own resume check reads back through
// watchdog_enable_caused_reboot(), which would make the next boot jump
// straight into the application instead of showing the picker.
watchdog_reboot(0, 0, 0);
while (1) {
tight_loop_contents();
}
}
}
#endif // FRENS_USB_MSC
// --- Options menu (SELECT) --------------------------------------------------
//
// The picker's secondary screen: help, the text/graphical toggle, BOOTSEL mode
// and (where it is built in) USB drive mode. Same charcell chrome and the same
// pump as showHelpScreen(); A applies the highlighted entry, B or SELECT
// returns.
enum OptionsResult {
OPT_BACK, // nothing the caller has to act on
OPT_MODE_CHANGED, // ini->gui_graphical was flipped
};
// Entry ids rather than array indices: the USB row is compiled out on builds
// without FRENS_USB_MSC and the rows must close up behind it.
enum OptionId {
OPTID_HELP,
OPTID_MENU_MODE,
OPTID_BOOTSEL,
OPTID_USB_DRIVE,
};
#define OPT_FIRST_ROW 6 // first entry row; entries are two rows apart
#define OPT_ROW_STEP 2
#define OPT_LABEL_COL 6
#define OPT_VALUE_COL 24
void drawOptionsScreen(const OptionId *ids, int count, int sel,
const sd_boot_ini_t *ini, bool cfg_save_failed)
{