11/*
2- * MemForge2 v0.4.22 — UEFI memory tester written from scratch.
2+ * MemForge2 v0.4.23 — UEFI memory tester written from scratch.
33 *
44 * Latest release: https://github.com/Paradoxdov/memforge/releases
55 * For per-version changes see git log / GitHub Releases page.
@@ -834,7 +834,7 @@ static void init_splash(CHAR16 *stage) {
834834 cls();
835835 UINTN cy = g_h / 2;
836836 /* Title — large centered line. */
837- CHAR16 *title = L"MEMFORGE v0.4.22 ";
837+ CHAR16 *title = L"MEMFORGE v0.4.23 ";
838838 UINTN tx = (g_w - StrLen(title) * g_char_w) / 2;
839839 gfx_draw_str_color(tx, cy - g_char_h * 2, title, COL_ACCENT_HI);
840840 /* Stage indicator — what we're doing right now. */
@@ -938,7 +938,7 @@ static UINTN g_card_cols = 1;
938938 compute_layout(). */
939939static int g_show_cards = 1;
940940
941- /* v0.4.22 — focused cards layout for small screens (g_h < 900).
941+ /* v0.4.23 — focused cards layout for small screens (g_h < 900).
942942 Instead of one full-width row per test (14 rows × ~40 px = 560 px,
943943 which on a 1024×768 screen eats 70% of vertical space and clips the
944944 core panel + footer), we draw:
@@ -1008,7 +1008,7 @@ static void compute_layout(UINTN n_tests) {
10081008 g_card_w = g_inner;
10091009 g_card_row_h = g_compact ? g_char_h : (g_char_h + 16);
10101010
1011- /* v0.4.22 — focused layout on small screens.
1011+ /* v0.4.23 — focused layout on small screens.
10121012 On g_h<900 the per-test card list eats 60-70% of vertical space
10131013 and clips the core panel / footer (YgrecK field report on 1024×768
10141014 Radeon HD 4350). Replace with: 1-row strip of all test dots +
@@ -1222,9 +1222,9 @@ static void render_header(UINT64 elapsed_ms, UINTN done, UINTN total) {
12221222 UINTN cols = g_text_cols;
12231223 if (cols >= 110) {
12241224 SPrint(buf, sizeof(buf),
1225- T(L" MEMFORGE v0.4.22 | %ld.%ld ГБ RAM | %s "
1225+ T(L" MEMFORGE v0.4.23 | %ld.%ld ГБ RAM | %s "
12261226 L"| %s | прошло %02d:%02d | осталось ~%02d:%02d | Тесты %d/%d",
1227- L" MEMFORGE v0.4.22 | %ld.%ld GB RAM | %s "
1227+ L" MEMFORGE v0.4.23 | %ld.%ld GB RAM | %s "
12281228 L"| %s | elapsed %02d:%02d | ETA ~%02d:%02d | Tests %d/%d"),
12291229 ram_gb_x10 / 10, ram_gb_x10 % 10,
12301230 pass_tag,
@@ -1234,25 +1234,25 @@ static void render_header(UINT64 elapsed_ms, UINTN done, UINTN total) {
12341234 (UINT32)done, (UINT32)total);
12351235 } else if (cols >= 90) {
12361236 SPrint(buf, sizeof(buf),
1237- T(L" MEMFORGE v0.4.22 | %ld.%ld ГБ RAM | %s | %s | прошло %02d:%02d | осталось ~%02d:%02d",
1238- L" MEMFORGE v0.4.22 | %ld.%ld GB RAM | %s | %s | elapsed %02d:%02d | ETA ~%02d:%02d"),
1237+ T(L" MEMFORGE v0.4.23 | %ld.%ld ГБ RAM | %s | %s | прошло %02d:%02d | осталось ~%02d:%02d",
1238+ L" MEMFORGE v0.4.23 | %ld.%ld GB RAM | %s | %s | elapsed %02d:%02d | ETA ~%02d:%02d"),
12391239 ram_gb_x10 / 10, ram_gb_x10 % 10,
12401240 pass_tag,
12411241 err_tag,
12421242 secs / 60, secs % 60,
12431243 eta_secs / 60, eta_secs % 60);
12441244 } else if (cols >= 70) {
12451245 SPrint(buf, sizeof(buf),
1246- T(L" MEMFORGE v0.4.22 | %ld.%ld ГБ RAM | %s | %s | прошло %02d:%02d",
1247- L" MEMFORGE v0.4.22 | %ld.%ld GB RAM | %s | %s | elapsed %02d:%02d"),
1246+ T(L" MEMFORGE v0.4.23 | %ld.%ld ГБ RAM | %s | %s | прошло %02d:%02d",
1247+ L" MEMFORGE v0.4.23 | %ld.%ld GB RAM | %s | %s | elapsed %02d:%02d"),
12481248 ram_gb_x10 / 10, ram_gb_x10 % 10,
12491249 pass_tag,
12501250 err_tag,
12511251 secs / 60, secs % 60);
12521252 } else {
12531253 SPrint(buf, sizeof(buf),
1254- T(L" MEMFORGE v0.4.22 | %s | %s | прошло %02d:%02d",
1255- L" MEMFORGE v0.4.22 | %s | %s | elapsed %02d:%02d"),
1254+ T(L" MEMFORGE v0.4.23 | %s | %s | прошло %02d:%02d",
1255+ L" MEMFORGE v0.4.23 | %s | %s | elapsed %02d:%02d"),
12561256 pass_tag,
12571257 err_tag,
12581258 secs / 60, secs % 60);
@@ -1778,7 +1778,7 @@ static int dominant_dimm_idx(void) {
17781778 return best;
17791779}
17801780
1781- /* v0.4.22 — detect dual-channel interleave ambiguity.
1781+ /* v0.4.23 — detect dual-channel interleave ambiguity.
17821782 On consumer desktops with dual/quad-channel memory, the iMC interleaves
17831783 addresses between channels at 64-byte (cache-line) granularity. A
17841784 SINGLE bad chip on one stick produces errors that, when mapped through
@@ -1787,7 +1787,7 @@ static int dominant_dimm_idx(void) {
17871787
17881788 Field report from a Habr user (Netac DDR4 kit): same stuck bit
17891789 D[53] was reported 24 times, distributed as A2 (8) + B2 (11) + ? (5).
1790- Pre-v0.4.22 verdict confidently said "REPLACE: DDR4-B2 (HIGH)" — but
1790+ Pre-v0.4.23 verdict confidently said "REPLACE: DDR4-B2 (HIGH)" — but
17911791 physically it's likely ONE bad chip on one of A2/B2, NOT both.
17921792
17931793 This helper returns the list of DIMM indices that each hold >=25% of
@@ -1833,7 +1833,7 @@ static UINTN distributed_dimm_indices(int *out_idx, UINTN cap) {
18331833 return n;
18341834}
18351835
1836- /* v0.4.22 — Approach D: detect whether SMBIOS Type 20 reports REAL
1836+ /* v0.4.23 — Approach D: detect whether SMBIOS Type 20 reports REAL
18371837 cache-line interleave (overlapping address ranges across DIMMs) or
18381838 BLOCK mapping (disjoint ranges, each DIMM owns its own physical
18391839 region). PassMark forum & KIT paper both confirm that even though
@@ -1877,7 +1877,7 @@ static UINT8 type20_max_interleave_depth(void) {
18771877 return m;
18781878}
18791879
1880- /* v0.4.22 — Approach A: bit-6 polarity analysis of error addresses.
1880+ /* v0.4.23 — Approach A: bit-6 polarity analysis of error addresses.
18811881 On most Intel/AMD consumer dual-channel desktops with DDR4/DDR5, the
18821882 iMC's channel selector is physical address bit 6 (alternating 64-byte
18831883 cache lines between channels). If all error records share the same
@@ -4845,15 +4845,15 @@ static void amd_thermal_probe(void) {
48454845}
48464846
48474847static UINT32 amd_thermal_sample(void) {
4848- /* v0.4.22 — correct decode per Linux k10temp / FreeBSD amdtemp.c:
4848+ /* v0.4.23 — correct decode per Linux k10temp / FreeBSD amdtemp.c:
48494849 SMN 0x59800 (SMU_THM_TCON_CUR_TMP)
48504850 bits [31:21] raw temperature value (11 bits, mask 0x7FF)
48514851 bit 19 TempRangeSel — when SET, scale is -49°C..+206°C
48524852 (subtract 49°C from the raw decode); when CLEAR
48534853 scale is 0..225°C (no offset).
48544854 temp_c = (raw * 0.125) - (range_sel ? 49 : 0)
48554855
4856- Pre-v0.4.22 code was missing both the 0x7FF mask AND the bit-19
4856+ Pre-v0.4.23 code was missing both the 0x7FF mask AND the bit-19
48574857 range adjustment, which inflated readings by ~49°C on Ryzen SKUs
48584858 that report on the -49..206 scale (most Renoir/Cezanne/Zen3+
48594859 desktop parts). Field report on Ryzen 5 4500 showed Tctl=93°C at
@@ -6473,7 +6473,7 @@ static test_def_t g_tests[] = {
64736473};
64746474#define N_TESTS (sizeof(g_tests) / sizeof(g_tests[0]))
64756475
6476- /* v0.4.22 — map a kernel enum (KER_*) to its position in g_tests[].
6476+ /* v0.4.23 — map a kernel enum (KER_*) to its position in g_tests[].
64776477 CRITICAL: do NOT index g_tests[] directly by a kernel_id_t value.
64786478 The enum values do not match array positions (e.g., KER_AVX2_SUSTAINED
64796479 = 12 maps to position 0 in g_tests because AVX2 Sustained is the
@@ -6625,7 +6625,7 @@ typedef struct {
66256625} card_info_t;
66266626static card_info_t g_cards[N_TESTS];
66276627
6628- /* v0.4.22 — Forward decls for focused-mode helpers (defined below
6628+ /* v0.4.23 — Forward decls for focused-mode helpers (defined below
66296629 card_paint so they can share the same color-lookup logic). */
66306630static void card_paint_full(UINTN i);
66316631static void card_strip_paint(UINTN i);
@@ -6739,7 +6739,7 @@ static void card_paint_full(UINTN i) {
67396739 }
67406740}
67416741
6742- /* ---------- Focused-mode card painters (v0.4.22 ) ---------- */
6742+ /* ---------- Focused-mode card painters (v0.4.23 ) ---------- */
67436743
67446744/* Paint the small status dot for test i in the top strip. The strip is
67456745 one row tall and shows N evenly-spaced dots, one per test. The dot
@@ -6822,7 +6822,7 @@ static void card_focused_paint(UINTN i) {
68226822 blt_fill(ix, row3_y, iw, row_h, COL_PANEL);
68236823
68246824 /* Row 1: test name (left) + short description in dim color + index counter (right).
6825- v0.4.22 — description lets non-expert user know what the test
6825+ v0.4.23 — description lets non-expert user know what the test
68266826 actually checks (TRRespass / March-C- / Butterfly etc. are jargon). */
68276827 say_at_px(ix + 4, row1_y, g_tests[i].name);
68286828 UINTN name_chars = StrLen(g_tests[i].name);
@@ -7107,8 +7107,11 @@ static void core_cols_compute(core_cols_t *c) {
71077107 if (slack >= 9 * cw + pad) { w_freq = 9 * cw; slack -= w_freq + pad; }
71087108 /* Priority 4: Per-core MB/s — 6 chars */
71097109 if (slack >= 6 * cw + pad) { w_mbs = 6 * cw; slack -= w_mbs + pad; }
7110- /* Priority 5: Address offset (debug-ish) — 9 chars ("@1234 МБ") */
7111- if (slack >= 9 * cw + pad) { w_addr = 9 * cw; slack -= w_addr + pad; }
7110+ /* v0.4.23 — "Смещ" (buffer-offset for this core's slice) column dropped
7111+ from the main test screen. It was a developer-debug field that nobody
7112+ in the field could interpret; removing it frees ~9 chars to widen the
7113+ activity bar. The offset is still in the log and the JSON. */
7114+ (void)w_addr;
71127115 /* Remaining slack goes into the activity bar so it visually fills the
71137116 row. Cap at 16 cw so the bar doesn't look obnoxious on wide screens. */
71147117 if (slack > 0) {
@@ -7470,8 +7473,8 @@ static void drain_conin(void) {
74707473 }
74717474}
74727475
7473- /* v0.4.22 — countdown UX rework.
7474- Pre-v0.4.22 : ESC meant "skip the wait and start the test now" — which
7476+ /* v0.4.23 — countdown UX rework.
7477+ Pre-v0.4.23 : ESC meant "skip the wait and start the test now" — which
74757478 completely contradicts the universal "ESC = cancel" convention. Users
74767479 pressed ESC expecting "I don't want this test" and instead launched it.
74777480
@@ -8110,7 +8113,7 @@ static void render_simple_verdict(UINT64 total_ms) {
81108113 UINTN dist_n = distributed_dimm_indices(dist_idx, MAX_DIMMS);
81118114 int is_distributed = (dist_n >= 2);
81128115
8113- /* v0.4.22 — Approach D + A: classify WHY errors are distributed.
8116+ /* v0.4.23 — Approach D + A: classify WHY errors are distributed.
81148117 type20_overlap = 1 → ranges overlap (real cache-line interleave)
81158118 → "ONE chip behind two labels"
81168119 type20_overlap = 0, depth ≤ 1 → block mode (disjoint ranges,
@@ -8396,8 +8399,8 @@ static void render_summary(UINT64 total_ms) {
83968399 UINTN hrow = (g_hdr_h / 2 - g_char_h / 2) / g_char_h;
83978400 CHAR16 buf[200];
83988401 SPrint(buf, sizeof(buf),
8399- T(L" MEMFORGE v0.4.22 ИТОГИ | %d сек | Ядра %d/%d",
8400- L" MEMFORGE v0.4.22 SUMMARY | %d sec | Cores %d/%d"),
8402+ T(L" MEMFORGE v0.4.23 ИТОГИ | %d сек | Ядра %d/%d",
8403+ L" MEMFORGE v0.4.23 SUMMARY | %d sec | Cores %d/%d"),
84018404 (UINT32)(total_ms / 1000),
84028405 (UINT32)g_n_enabled, (UINT32)g_n_cores);
84038406 say_at_rc(0, hrow, buf);
@@ -8479,7 +8482,7 @@ static void render_summary(UINT64 total_ms) {
84798482 CHAR16 chip[64] = L"";
84808483 if (didx >= 0)
84818484 chip_label_for_bit((UINT32)didx, bp, chip, 64);
8482- /* v0.4.22 — use SMBIOS Type 17 locator string ("DDR4-B2")
8485+ /* v0.4.23 — use SMBIOS Type 17 locator string ("DDR4-B2")
84838486 instead of array-index-based "DIMM%d" which had nothing
84848487 to do with the physical slot label the user sees. */
84858488 CHAR8 *loc = (didx >= 0 && g_dimms[didx].locator[0])
@@ -10190,7 +10193,7 @@ EFI_STATUS efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable) {
1019010193 }
1019110194 }
1019210195
10193- log_line(L"=== MemForge2 v0.4.22 init ===");
10196+ log_line(L"=== MemForge2 v0.4.23 init ===");
1019410197 log_line(L"[WATCHDOG] UEFI 5-min watchdog disabled at app entry");
1019510198 /* Show splash IMMEDIATELY so the user sees the program is alive while
1019610199 INI parsing, SMBus probes and SMBIOS walk happen. Without this, the
@@ -10235,7 +10238,7 @@ EFI_STATUS efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable) {
1023510238 if (uefi_call_wrapper(g_gop->QueryMode, 4,
1023610239 g_gop, m, &info_sz, &info) != EFI_SUCCESS)
1023710240 continue;
10238- /* v0.4.22 — also log PixelFormat and PixelsPerScanLine
10241+ /* v0.4.23 — also log PixelFormat and PixelsPerScanLine
1023910242 so we can see if a card (e.g. old Radeon HD 4350) only
1024010243 offers BltOnly modes (PixelFormat=3) that prevent
1024110244 direct-fb rendering. */
@@ -10250,7 +10253,7 @@ EFI_STATUS efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable) {
1025010253 log_line(L"[GFX] NO GOP PROTOCOL FOUND — firmware has no UEFI graphics. "
1025110254 L"Falling back to 800x600 default. UI will not render correctly.");
1025210255 }
10253- /* v0.4.22 — MP Services Protocol diagnostic. Without this log it
10256+ /* v0.4.23 — MP Services Protocol diagnostic. Without this log it
1025410257 was impossible to tell from a field report whether multi-core
1025510258 dispatch failed (LocateProtocol error / GetNumberOfProcessors
1025610259 returned 1) or the test was simply running on a single-core
@@ -10852,7 +10855,7 @@ EFI_STATUS efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable) {
1085210855 g_cards[i].errors = 0;
1085310856 card_paint(i);
1085410857
10855- /* v0.4.22 — countdown returns 0=start, 1=skip this test, 2=abort run */
10858+ /* v0.4.23 — countdown returns 0=start, 1=skip this test, 2=abort run */
1085610859 int cd_rc = countdown(2, i);
1085710860 if (cd_rc == 2) break; /* Q → abort whole run */
1085810861 if (cd_rc == 1) { /* ESC → skip this test */
@@ -10869,6 +10872,20 @@ EFI_STATUS efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable) {
1086910872 done_tests++;
1087010873 continue;
1087110874 }
10875+ /* v0.4.23 — clear the countdown footer once the test starts.
10876+ The old "[N/14] Test starts in 2 sec ..." line would linger
10877+ throughout the test run, taking up screen space without
10878+ serving any purpose during the test itself. Replace with a
10879+ short hint about how to abort. */
10880+ {
10881+ UINTN frow = g_foot_y / g_char_h;
10882+ if (frow < g_text_rows) {
10883+ clear_row(frow);
10884+ say_at_rc(0, frow,
10885+ T(L" [Q] = прервать прогон · логи пишутся в memforge2.log на USB",
10886+ L" [Q] = abort run · logs are written to memforge2.log on USB"));
10887+ }
10888+ }
1087210889
1087310890 SPrint(lb, sizeof(lb), L"[STEP 5.%d.B] Calling run_test_mc(%s)", (UINT32)i, g_tests[i].name);
1087410891 log_line(lb);
@@ -10881,8 +10898,8 @@ EFI_STATUS efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable) {
1088110898 per-test results to survive that. Cheap (1× per test, not
1088210899 1× per log line). */
1088310900 flush_log_now();
10884- /* v0.4.22 — ACCUMULATE across marathon passes, do not OVERWRITE.
10885- Pre-v0.4.22 the line was `g_summary[i] = r;` which kept only
10901+ /* v0.4.23 — ACCUMULATE across marathon passes, do not OVERWRITE.
10902+ Pre-v0.4.23 the line was `g_summary[i] = r;` which kept only
1088610903 the LAST pass's per-test result. On a 16-hour marathon with
1088710904 an intermittent error rate of 1 per pass, that meant the
1088810905 final summary table showed "errors: 0" because the most
0 commit comments