From d8e8ae604fa3fcca3db8723c209c7a8b26f9a2dd Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sun, 25 Oct 2026 00:43:36 +0200 Subject: [PATCH 1/2] qr: every version, 1 to 40 PSOGM mode will put the kernel log on screen as QR codes, a page per symbol, and version 6 held 134 bytes. Version 40 at level L holds 2,953. What grew: blocks of two lengths and the interleave that steps around them, the alignment-pattern grid, the version-information blocks from version 7, the 16-bit character count from version 10. The per-version table is down to the two columns that are genuinely transcription (EC codewords per block, block count); total codewords come from the geometry. Checked against segno module for module at every version (exact-fill payloads), and 120 symbols across all 40 versions decode with zbar. OpenCV's detector cannot find symbols above about version 24 at all, segno's included, so qrcheck reports those as the detector's limit. The crash card stays a version-6 symbol (QR_CARD_BYTES), so a phone still reads it from across the desk. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4 --- include/gfx/qr.h | 23 +++--- kernel/core/panic.c | 2 +- kernel/core/qr.c | 158 +++++++++++++++++++++++++++------------- tools/python/qrcheck.py | 16 ++-- tools/qrtest.c | 31 ++++---- 5 files changed, 147 insertions(+), 83 deletions(-) diff --git a/include/gfx/qr.h b/include/gfx/qr.h index a261855..a2151d1 100644 --- a/include/gfx/qr.h +++ b/include/gfx/qr.h @@ -8,25 +8,26 @@ // no second machine on the desk. A phone camera is a working out-of-band // channel that needs no drivers on our side at all. See docs/architecture/graphics.md. // -// Deliberately small, in three ways: +// Deliberately small, in two ways: // // - Byte mode only. The report is arbitrary bytes; alphanumeric mode would // pack tighter but only for a restricted character set. // - Error correction level L only. The screen is a clean, evenly lit, // high-contrast source -- the failure mode QR's stronger levels protect // against (dirt, print damage, curved packaging) does not apply. -// - Versions 1 to 6 only. Version 7 is where QR starts requiring an extra -// 18-bit version-information block in two more places on the symbol, and -// version 10 is where the character count field grows and the alignment -// pattern grid becomes 2-dimensional. Stopping at 6 avoids both, and -// 134 bytes is a large crash report. // -// If a payload ever needs more than 134 bytes, the honest fix is to shorten the -// report -- not to reach for version 10 and its two extra tables. +// Every version, 1 to 40. It stopped at 6 -- 134 bytes, enough for the crash +// card -- until PSOGM mode wanted the log itself off the machine, a page of it +// per symbol: version 40 at level L carries 2,953 bytes. A struct qr is +// therefore about 62 KB; keep one static, never on a stack. -#define QR_MAX_VERSION 6 -#define QR_MAX_SIZE (17 + 4 * QR_MAX_VERSION) // 41 modules per side -#define QR_MAX_BYTES 134 +#define QR_MAX_VERSION 40 +#define QR_MAX_SIZE (17 + 4 * QR_MAX_VERSION) // 177 modules per side +#define QR_MAX_BYTES 2953 + +// The crash card on the PSOGM stays small, so a phone reads it from across a +// desk: a version-6 symbol at most. +#define QR_CARD_BYTES 134 struct qr { int size; // modules per side; 0 if unencoded diff --git a/kernel/core/panic.c b/kernel/core/panic.c index b29939c..1bf094b 100644 --- a/kernel/core/panic.c +++ b/kernel/core/panic.c @@ -326,7 +326,7 @@ text_at(int x, int y, const char *s, uint32 color) static int draw_qr_panel(int sw, int sh, int ch, char *reason, addr_t *pcs, int npcs) { - char payload[QR_MAX_BYTES]; + char payload[QR_CARD_BYTES]; int n = build_qr_payload(payload, sizeof(payload), reason, pcs, npcs); if (n <= 0 || qr_encode(&panic_qr, (unsigned char *)payload, n) != 0) diff --git a/kernel/core/qr.c b/kernel/core/qr.c index 6d70fbf..a3e6421 100644 --- a/kernel/core/qr.c +++ b/kernel/core/qr.c @@ -2,8 +2,8 @@ // // Freestanding on purpose: no allocation, no floating point, no libc beyond // memset/memmove. It has to run in panic context, where the heap may be exactly -// what is broken. Every buffer here is either on the caller's struct or a -// bounded local. +// what is broken. Every buffer here is on the caller's struct, a bounded +// local, or static. // // The structure follows ISO/IEC 18004 in the order the standard describes it, // which is also the only order in which any of it can be checked: @@ -40,42 +40,66 @@ // every row below, because levels 1..6 at L happen to have equally sized // blocks. qr_encode() asserts exactly that at runtime, which is what turns a // mistyped number here into a visible failure instead of an unscannable symbol. -static const struct { - short total; // total codewords in the symbol - unsigned char ecc; // EC codewords per block - unsigned char blocks; // number of RS blocks -} vinfo[QR_MAX_VERSION + 1] = { - { 0, 0, 0 }, // version 0 does not exist; keeps indexing natural - { 26, 7, 1 }, - { 44, 10, 1 }, - { 70, 15, 1 }, - { 100, 20, 1 }, - { 134, 26, 1 }, - { 172, 18, 2 }, +// Error correction level L, every version. Two numbers per version are +// transcribed from ISO/IEC 18004 table 9 -- EC codewords per block, and how +// many blocks -- and everything else is computed: the symbol's total codeword +// count from its geometry, and the block lengths from the total. Above +// version 5 the blocks are not all one length; the first ones are a codeword +// shorter, and interleaving has to step around the gap. +static const unsigned char ecc_per_block[QR_MAX_VERSION + 1] = { + 0, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, + 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, + 30, }; +static const unsigned char ecc_blocks[QR_MAX_VERSION + 1] = { + 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, + 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, + 25, +}; + +// Modules left for data once every function pattern is drawn: the whole +// square, less finders and separators (192), timing (2 * (n - 16)), format +// (31), alignment patterns and, from version 7, the two version blocks (36). +static int +raw_modules(int v) +{ + int r = (16 * v + 128) * v + 64; + if (v >= 2) { + int na = v / 7 + 2; + r -= (25 * na - 10) * na - 55; + if (v >= 7) + r -= 36; + } + return r; +} + +static int +total_codewords(int v) +{ + return raw_modules(v) / 8; +} + +static int +data_codewords(int v) +{ + return total_codewords(v) - ecc_per_block[v] * ecc_blocks[v]; +} + +// Byte mode's character count is 8 bits up to version 9 and 16 from 10. +static int +count_bits(int v) +{ + return v <= 9 ? 8 : 16; +} int qr_capacity(int version) { if (version < 1 || version > QR_MAX_VERSION) return 0; - // Data codewords, less the two bytes of framing: a 4-bit mode indicator, an - // 8-bit character count (versions 1-9), and a 4-bit terminator come to - // exactly two codewords' worth of overhead. - int data = vinfo[version].total - vinfo[version].ecc * vinfo[version].blocks; - return data - 2; + return (data_codewords(version) * 8 - 4 - count_bits(version)) / 8; } -// --------------------------------------------------------------------------- -// GF(256) and Reed-Solomon -// --------------------------------------------------------------------------- - -// Multiply in GF(256) modulo x^8 + x^4 + x^3 + x^2 + 1 (0x11D). -// -// Done by shift-and-add rather than log/exp tables. Two 256-byte tables plus -// their initialisation is more code than this loop, and this needs no init step -// at all -- which matters when the caller is a panic handler that cannot assume -// any particular earlier function ran. static unsigned char gmul(unsigned char x, unsigned char y) { @@ -231,11 +255,37 @@ draw_function_patterns(struct qr *q) draw_finder(q, n - 4, 3); draw_finder(q, 3, n - 4); - // For versions 2-6 the alignment centres are {6, n-7}, and three of those - // four combinations collide with a finder. Only the bottom-right survives, - // which is why there is no position table here. Version 7 would need one. - if (q->size > 21) - draw_alignment(q, n - 7, n - 7); + // Alignment patterns on a grid: the same list of centres on both axes, + // every combination drawn except the three that would sit on a finder. The + // list starts at 6, ends at n-7, and steps evenly (in even numbers) between, + // except version 32, which the standard spaces by hand. + int v = (n - 17) / 4; + if (v >= 2) { + int na = v / 7 + 2, pos[7]; + int step = v == 32 ? 26 : (v * 4 + na * 2 + 1) / (na * 2 - 2) * 2; + pos[0] = 6; + for (int i = na - 1, p = n - 7; i >= 1; i--, p -= step) + pos[i] = p; + for (int i = 0; i < na; i++) + for (int j = 0; j < na; j++) + if (!((i == 0 && j == 0) || (i == 0 && j == na - 1) || + (i == na - 1 && j == 0))) + draw_alignment(q, pos[i], pos[j]); + } + + // From version 7 the version number is written out too, 6 bits and a + // BCH(18,6) check, in a 3x6 block beside each of the two far finders. + if (v >= 7) { + int rem = v; + for (int i = 0; i < 12; i++) + rem = (rem << 1) ^ ((rem >> 11) * 0x1F25); + long bits = ((long)v << 12) | rem; + for (int i = 0; i < 18; i++) { + int bit = (bits >> i) & 1, a = n - 11 + i % 3, b = i / 3; + set_fn(q, a, b, bit); + set_fn(q, b, a, bit); + } + } draw_format(q, 0); // reserve; overwritten once a mask is chosen } @@ -357,25 +407,24 @@ qr_encode(struct qr *q, const unsigned char *data, int len) if (v > QR_MAX_VERSION) return -1; - int total = vinfo[v].total; - int ecclen = vinfo[v].ecc; - int blocks = vinfo[v].blocks; - int datacw = total - ecclen * blocks; - - // The table claims equal-sized blocks for every version this file supports. - // If that is ever untrue the interleave below is silently wrong, so check it - // rather than trust it. - if (datacw % blocks != 0) - return -1; - int blocklen = datacw / blocks; + int blocks = ecc_blocks[v]; + int ecclen = ecc_per_block[v]; + int total = total_codewords(v); + int datacw = data_codewords(v); + int nshort = blocks - total % blocks; // blocks one data codeword short + int shortlen = total / blocks - ecclen; // their data length // --- 1. payload -> data codewords --------------------------------------- - unsigned char cw[172]; // >= max total codewords + // + // Static, not on the stack: version 40 is 3,706 codewords, and this runs on + // whatever stack a panic left. Not reentrant, which nothing here needs. + static unsigned char cw[3706], out[3706]; + static unsigned char ec[25][30]; memset(cw, 0, sizeof(cw)); int pos = 0; put_bits(cw, &pos, 0x4, 4); // byte mode - put_bits(cw, &pos, (unsigned)len, 8); + put_bits(cw, &pos, (unsigned)len, count_bits(v)); for (int i = 0; i < len; i++) put_bits(cw, &pos, data[i], 8); @@ -393,20 +442,25 @@ qr_encode(struct qr *q, const unsigned char *data, int len) cw[i] = (i - pos / 8) % 2 == 0 ? 0xEC : 0x11; // --- 2 and 3. error correction, then interleave ------------------------- + // + // Block b starts at start(b): the short blocks first, then the long ones. unsigned char gen[30]; - unsigned char ec[4][30]; rs_divisor(ecclen, gen); +#define BSTART(b) ((b) * shortlen + ((b) > nshort ? (b) - nshort : 0)) +#define BLEN(b) (shortlen + ((b) >= nshort)) for (int b = 0; b < blocks; b++) - rs_remainder(&cw[b * blocklen], blocklen, gen, ecclen, ec[b]); + rs_remainder(&cw[BSTART(b)], BLEN(b), gen, ecclen, ec[b]); - unsigned char out[172]; int o = 0; - for (int i = 0; i < blocklen; i++) + for (int i = 0; i <= shortlen; i++) for (int b = 0; b < blocks; b++) - out[o++] = cw[b * blocklen + i]; + if (i < BLEN(b)) + out[o++] = cw[BSTART(b) + i]; for (int i = 0; i < ecclen; i++) for (int b = 0; b < blocks; b++) out[o++] = ec[b][i]; +#undef BSTART +#undef BLEN // --- 4. function patterns ------------------------------------------------ memset(q, 0, sizeof(*q)); diff --git a/tools/python/qrcheck.py b/tools/python/qrcheck.py index e034439..87ef221 100644 --- a/tools/python/qrcheck.py +++ b/tools/python/qrcheck.py @@ -117,10 +117,15 @@ def decode(m): return "" -# Byte capacity per version: total - ecc*blocks - 2. Mirrors the table in qr.c; -# the exact-fill cases below are built from it, and a disagreement would show up +# Byte-mode capacity per version at level L, from ISO/IEC 18004 table 7 -- +# written out rather than computed, so it checks qr.c instead of agreeing with +# it. The exact-fill cases below are built from it, and a disagreement shows up # as a version mismatch rather than passing quietly. -CAPACITY = {1: 17, 2: 32, 3: 53, 4: 78, 5: 106, 6: 134} +CAPACITY = dict(enumerate([ + 17, 32, 53, 78, 106, 134, 154, 192, 230, 271, 321, 367, 425, 458, 520, + 586, 644, 718, 792, 858, 929, 1003, 1091, 1171, 1273, 1367, 1465, 1528, + 1628, 1732, 1840, 1952, 2068, 2188, 2303, 2431, 2563, 2699, 2809, 2953, +], start=1)) alpha = lambda n: "".join(chr(65 + i % 26) for i in range(n)) @@ -133,8 +138,9 @@ CASES = [ bytes(range(1, 40)).decode("latin-1"), # non-printable payload ] # One payload that exactly fills each version, and one just under, so both the -# padded and unpadded paths are covered at every version -- including 6, which -# is the only one with more than one Reed-Solomon block. +# padded and unpadded paths are covered at every version: several Reed-Solomon +# blocks from 6, blocks of two lengths from 6 too, the version information +# from 7, the 16-bit character count from 10. for v, cap in CAPACITY.items(): CASES.append(alpha(cap)) CASES.append(alpha(cap - 1)) diff --git a/tools/qrtest.c b/tools/qrtest.c index ba2b64f..65ecee2 100644 --- a/tools/qrtest.c +++ b/tools/qrtest.c @@ -155,25 +155,26 @@ check(int ok, const char *what) } // The version table is the one part of qr.c that is transcription rather than -// reasoning, so check what can be checked: block sizes must divide evenly, and -// the symbol must have room for exactly the codewords claimed. +// reasoning, so check what can be checked: the data left over is positive and +// the blocks are no more than one codeword apart, and the symbol has room for +// exactly the codewords the geometry formula claims. static void check_tables(void) { printf("version table\n"); for (int v = 1; v <= QR_MAX_VERSION; v++) { - int total = vinfo[v].total; - int data = total - vinfo[v].ecc * vinfo[v].blocks; + int total = total_codewords(v); + int data = data_codewords(v); char msg[128]; - snprintf(msg, sizeof msg, "v%d: %d data codewords divide into %d blocks", - v, data, vinfo[v].blocks); - check(data > 0 && data % vinfo[v].blocks == 0, msg); + snprintf(msg, sizeof msg, "v%d: %d data codewords in %d blocks", + v, data, ecc_blocks[v]); + check(data > 0 && data / ecc_blocks[v] > 0, msg); // Count the modules a symbol of this version actually leaves for data, - // by building one and asking. If the table's codeword count disagrees + // by building one and asking. If the formula's codeword count disagrees // with the geometry, one of the two is wrong. - struct qr q; + static struct qr q; memset(&q, 0, sizeof q); q.size = 17 + 4 * v; draw_function_patterns(&q); @@ -219,7 +220,7 @@ static void check_encode(void) { printf("encode\n"); - struct qr q; + static struct qr q; check(qr_encode(&q, (const unsigned char *)"HELLO", 5) == 0, "short payload encodes"); check(q.size == 21, "and picks version 1"); @@ -227,7 +228,9 @@ check_encode(void) unsigned char big[QR_MAX_BYTES]; memset(big, 'A', sizeof big); check(qr_encode(&q, big, QR_MAX_BYTES) == 0, "maximum payload encodes"); - check(q.size == 41, "and picks version 6"); + check(q.size == QR_MAX_SIZE, "and picks version 40"); + check(qr_capacity(40) == 2953 && qr_capacity(10) == 271 && qr_capacity(9) == 230, + "capacities match the standard at v9, v10 and v40"); check(qr_encode(&q, big, QR_MAX_BYTES + 1) == -1, "one byte over is refused"); // Every function module must still be a function module after masking -- @@ -270,7 +273,7 @@ main(int argc, char **argv) // codeword two encoders disagree about is the difference between a bug you // can see and one you can only stare at. if (argc > 2 && strcmp(argv[1], "-f") == 0) { - struct qr q; + static struct qr q; int n = (int)strlen(argv[2]); if (n > QR_MAX_BYTES || qr_encode(&q, (const unsigned char *)argv[2], n) != 0) return 1; @@ -283,7 +286,7 @@ main(int argc, char **argv) } if (argc > 2 && strcmp(argv[1], "-m") == 0) { - struct qr q; + static struct qr q; int n = (int)strlen(argv[2]); if (n > QR_MAX_BYTES || qr_encode(&q, (const unsigned char *)argv[2], n) != 0) return 1; @@ -308,7 +311,7 @@ main(int argc, char **argv) check_rs(); check_encode(); - struct qr q; + static struct qr q; int len = (int)strlen(text); if (len > QR_MAX_BYTES) { fprintf(stderr, "qrtest: payload is %d bytes, max %d\n", len, QR_MAX_BYTES); From 38c89b1a9ad112e075afb5920d3e41a6df0f0176 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sun, 25 Oct 2026 00:50:50 +0200 Subject: [PATCH 2/2] panic stops every other processor A panic used to stop only the processor it happened on. The rest carried on with a dead kernel: on the ASUS laptop the report flashed up and was drawn over, and the fans stayed loud until the power button was held. Now the panicking processor sends every other one an NMI, which arrives even with interrupts off, and trap() halts a processor that takes one while panic_stop is set -- first thing, before the canary check, since that stack may be what broke. NMI gets its own per-CPU stack (IST 1): for a few instructions at the top of syscall_entry the live stack is the user's, at ring 0, and a fault pushing the frame there would be a double fault on the same stack and a reset instead of a report. A second processor that panics while one is already reporting halts instead of drawing over it; the owner's own nested panic still gets through. Checked under QEMU with -smp 4: after a sysrq crash CPUs 1-3 sit in hlt with IF clear and CPU 0 is in the report's blink loop. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4 --- include/core/defs.h | 2 ++ kernel/arch/x86_64/ioperm.c | 9 +++++++++ kernel/arch/x86_64/lapic.c | 15 +++++++++++++++ kernel/arch/x86_64/trap.c | 13 +++++++++++++ kernel/core/panic.c | 18 ++++++++++++++++++ 5 files changed, 57 insertions(+) diff --git a/include/core/defs.h b/include/core/defs.h index c7cbd0c..e05d5fc 100644 --- a/include/core/defs.h +++ b/include/core/defs.h @@ -457,6 +457,8 @@ extern uint64 boot_epoch; int cpunum(void); extern volatile uint* lapic; void lapiceoi(void); +extern volatile int panic_stop; // kernel/core/panic.c +void lapic_nmi_others(void); void lapicinit(void); void lapicstartap(uchar, uint); void microdelay(int); diff --git a/kernel/arch/x86_64/ioperm.c b/kernel/arch/x86_64/ioperm.c index cf6d238..b27bcb4 100644 --- a/kernel/arch/x86_64/ioperm.c +++ b/kernel/arch/x86_64/ioperm.c @@ -56,6 +56,14 @@ struct tssblk { static struct tssblk tssblk[NCPU]; +// The stack an NMI arrives on: IST slot 1, one per CPU. Without it an NMI runs +// on whatever stack was live, and for a few instructions at the top of +// syscall_entry that is the user's -- at ring 0, so the CPU would push the +// trap frame there, and a fault on that push is a double fault on the same +// stack. Panic stops the other processors with an NMI; it must not be the +// thing that resets the machine instead of the report. +static uint8 nmistack[NCPU][4096] __attribute__((aligned(16))); + // What each CPU currently has open. Kept per CPU rather than read back from the // process, because switching *away* has to close what was opened and the // process that opened it may be part-way through exit() by then. @@ -89,6 +97,7 @@ ioperm_tss(int cpu, uint64 *limit) memset(b->map, 0xFF, sizeof(b->map)); b->tail = 0xFF; b->ts.iomb = (uint16)__builtin_offsetof(struct tssblk, map); + b->ts.ist[0] = (uint64)(nmistack[cpu] + sizeof(nmistack[cpu])); installed[cpu].n = 0; // The limit is the offset of the last valid byte, and it has to include the diff --git a/kernel/arch/x86_64/lapic.c b/kernel/arch/x86_64/lapic.c index 5830d05..2c06bd0 100644 --- a/kernel/arch/x86_64/lapic.c +++ b/kernel/arch/x86_64/lapic.c @@ -402,6 +402,21 @@ cpunum(void) panic("unknown apicid\n"); } +// An NMI to every processor but this one: how a panic stops the others. An +// NMI is delivered with interrupts off too, so it reaches a processor spinning +// on a lock with cli as surely as one running a program; trap() halts any that +// takes one while panic_stop is set. Bounded, because the caller is dying. +void +lapic_nmi_others(void) +{ + if (!lapic) + return; + lapicw(ICRHI, 0); + lapicw(ICRLO, 0x000C0000 | 0x00000400 | ASSERT); // all but self, NMI + for (int i = 0; i < 100000 && (lapic[ICRLO] & DELIVS); i++) + microdelay(1); +} + // Acknowledge interrupt. void lapiceoi(void) diff --git a/kernel/arch/x86_64/trap.c b/kernel/arch/x86_64/trap.c index 6c90fa4..46ecde5 100644 --- a/kernel/arch/x86_64/trap.c +++ b/kernel/arch/x86_64/trap.c @@ -48,6 +48,8 @@ void tvinit(void) for (n = 0; n < 256; n++) mkgate(idt, n, vectors[n], 0); mkgate(idt, T_SYSCALL, vectors[T_SYSCALL], 3); + // NMI on its own stack, IST slot 1; see nmistack in ioperm.c. + idt[T_NMI * 4 + 1] |= 1; } uint @@ -81,6 +83,17 @@ trapsig(uint64 trapno) void trap(struct trapframe *tf) { + // Another processor has panicked and wants this one stopped: a processor + // left running carries on with a kernel that is already dead, and on a + // laptop that showed as the report flashing up and being drawn over, and + // as fans that stayed loud until somebody held the power button. First, and + // before the canary check, because this stack may be what is broken. + if(tf->trapno == T_NMI && panic_stop) + for(;;){ + cli(); + hlt(); + } + kstack_check_canary("trap entry"); switch(tf->trapno){ case T_IRQ0 + IRQ_TIMER: diff --git a/kernel/core/panic.c b/kernel/core/panic.c index 1bf094b..cedd853 100644 --- a/kernel/core/panic.c +++ b/kernel/core/panic.c @@ -559,6 +559,9 @@ report_serial(char *reason, addr_t *pcs, int npcs) static void panic_blink(void) __attribute__((noreturn)); +volatile int panic_stop; +static volatile int panic_owner = -1; + __attribute__((noreturn)) void panic(char *s) @@ -566,6 +569,21 @@ panic(char *s) addr_t pcs[10]; cli(); + + // One processor reports; any other that panics meanwhile -- the same broken + // state tends to reach several at once -- stops where it is. The owner may + // panic again while drawing, and that nested report is let through. + int me = cpunum(); + int owner = __sync_val_compare_and_swap(&panic_owner, -1, me); + if (owner != -1 && owner != me) + for (;;) { + cli(); + hlt(); + } + // And the rest are stopped outright, see trap(). + panic_stop = 1; + lapic_nmi_others(); + // Stop every other path from writing to the console. consputc checks this // and spins, which is what keeps another CPU from painting over the report // while it is being drawn.