From 88f1cac4fa530fc30fd6ef01970af11c7a959b8e Mon Sep 17 00:00:00 2001 From: cirakg Date: Wed, 21 Oct 2026 03:55:01 +0200 Subject: fix(codes): bound every 2D encoding step under the watchdog; draw large codes from runs M6's step sizes were estimates, only exercised with small codes. With the largest inputs the app accepts, every 2D encoder tripped the watchdog on the Forerunner 255S (and the biggest steps were far over the 768 KB watches' limit), so a large code added from the phone would have stopped the app during settings processing at every start. - QR: resumable segmentation, Reed-Solomon, interleaving, function patterns and final mask; mask scoring on rows and columns packed 16 modules per word (same penalties, checked against the per-module rules). - Reed-Solomon: resumable field tables and generator, encoder shifting with native slice. - Data Matrix, Aztec, PDF417: resumable encodation, stuffing, compaction, placement and build; Data Matrix's grid as a byte array (90 KB -> 35 KB); PDF417 refuses text that can't fit the screen before compacting. - Cache format 2 with 16-bit sizes (wide PDF417 was cached wrong); packing in steps; codes drawn as runs, and from a bitmap painted in steps when a frame can't draw all runs. - The job runner does two steps per tick on 768 KB watches; gen_products --check verifies each product's watchdogCount fits that. - CodeView renews full brightness (AMOLED) or the backlight (MIP) for up to a minute from its single ticker; a tap restarts the minute. Co-Authored-By: Claude Opus 5.5 --- source/codes/Codes.mc | 279 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 243 insertions(+), 36 deletions(-) (limited to 'source/codes/Codes.mc') diff --git a/source/codes/Codes.mc b/source/codes/Codes.mc index f157b37..247acfa 100644 --- a/source/codes/Codes.mc +++ b/source/codes/Codes.mc @@ -76,55 +76,262 @@ module Codes { return height * rowHeight; } - // Compact form for the encrypted cache: width, height, quiet zone, - // row height (one byte each), then the modules packed 8 per byte - // (eight per loop round: this runs in one event, under the watchdog). + // The cache form (Packed), in one go: tests and small codes. Jobs + // use Packer. function toBytes() as ByteArray { - var m = modules; + var packer = new Packer(self); + Jobs.runToCompletion(packer, 1); + return packer.result; + } + + // A matrix from its cache form, unpacked in one go (tests); the app + // draws from Packed through RunFinder instead. + static function fromBytes(data as ByteArray) as Matrix? { + var packed = Packed.parse(data); + if (packed == null) { + return null; + } + var n = packed.width * packed.height; + var bits = new [n]b; + for (var i = 0; i < n; i++) { + bits[i] = packed.moduleAt(i); + } + var matrix = new Matrix(packed.width, packed.height, packed.quiet, bits); + matrix.rowHeight = packed.rowHeight; + return matrix; + } + } + + // A matrix packed 8 modules per byte, as cached (sealed) in the vault: + // FORMAT (2) | width (2 bytes) | height (2 bytes) | quiet | rowHeight + // | modules, row-major, most significant bit first + // Version 0.7 cached one byte of width, too few for wide PDF417 + // symbols; such a cache doesn't parse, and the code is encoded again. + class Packed { + static const FORMAT = 2; + static const HEADER = 7; + + var width as Number; + var height as Number; + var quiet as Number; + var rowHeight as Number; + var bytes as ByteArray; + + function initialize(data as ByteArray) { + bytes = data; + width = (data[1] << 8) | data[2]; + height = (data[3] << 8) | data[4]; + quiet = data[5]; + rowHeight = data[6]; + } + + // The cache bytes as a Packed, or null when they are not format 2 or + // the size doesn't match. + static function parse(data as ByteArray?) as Packed? { + if (data == null || data.size() < HEADER || data[0] != FORMAT) { + return null; + } + var p = new Packed(data); + var n = p.width * p.height; + if (p.width == 0 || p.height == 0 || p.rowHeight == 0 || data.size() != HEADER + (n + 7) / 8) { + return null; + } + return p; + } + + function drawnHeight() as Number { + return height * rowHeight; + } + + // Module i (row-major), 1 = dark. + function moduleAt(i as Number) as Number { + return (bytes[HEADER + (i >> 3)] >> (7 - (i & 7))) & 1; + } + } + + // Matrix -> Packed bytes, BYTES_PER_STEP output bytes (8 modules each) + // per step. + class Packer extends Jobs.Job { + static const BYTES_PER_STEP = 300; + + var result as ByteArray = []b; + + private var _matrix as Matrix; + private var _i as Number = 0; + + function initialize(matrix as Matrix) { + Job.initialize(); + _matrix = matrix; + var w = matrix.width; + var h = matrix.height; + result = new [Packed.HEADER + (w * h + 7) / 8]b; + result[0] = Packed.FORMAT; + result[1] = (w >> 8) & 0xFF; + result[2] = w & 0xFF; + result[3] = (h >> 8) & 0xFF; + result[4] = h & 0xFF; + result[5] = matrix.quiet; + result[6] = matrix.rowHeight; + } + + function step(budget as Number) as Boolean { + var m = _matrix.modules; var n = m.size(); var full = n / 8; - var out = new [4 + (n + 7) / 8]b; - out[0] = width; - out[1] = height; - out[2] = quiet; - out[3] = rowHeight; - for (var i = 0; i < full; i++) { + var out = result; + var stop = _i + BYTES_PER_STEP < full ? _i + BYTES_PER_STEP : full; + for (var i = _i; i < stop; i++) { var k = i * 8; - out[4 + i] = (m[k] << 7) | (m[k + 1] << 6) | (m[k + 2] << 5) | (m[k + 3] << 4) + out[Packed.HEADER + i] = (m[k] << 7) | (m[k + 1] << 6) | (m[k + 2] << 5) | (m[k + 3] << 4) | (m[k + 4] << 3) | (m[k + 5] << 2) | (m[k + 6] << 1) | m[k + 7]; } + _i = stop; + if (_i < full) { + return false; + } for (var k = full * 8; k < n; k++) { - out[4 + full] = out[4 + full] | (m[k] << (7 - k % 8)); + out[Packed.HEADER + full] = out[Packed.HEADER + full] | (m[k] << (7 - k % 8)); } - return out; + return true; } + } - static function fromBytes(data as ByteArray) as Matrix? { - if (data.size() < 4) { - return null; + // A code as the dark runs of each stored row, ready to draw with one + // fillRectangle per run (drawing module by module in onUpdate would be + // too much for the watchdog with large codes). Each run is + // (row << 20) | (first column << 10) | length. + class Runs { + var width as Number; + var height as Number; + var quiet as Number; + var rowHeight as Number; + var runs as Array = [] as Array; + + function initialize(packed as Packed) { + width = packed.width; + height = packed.height; + quiet = packed.quiet; + rowHeight = packed.rowHeight; + } + + function drawnHeight() as Number { + return height * rowHeight; + } + } + + // Packed -> Runs, about MODULES_PER_STEP modules (whole rows) per step; + // all-light and all-dark bytes are taken eight modules at a time. + class RunFinder extends Jobs.Job { + static const MODULES_PER_STEP = 800; + + var result as Runs; + + private var _packed as Packed; + private var _row as Number = 0; + + function initialize(packed as Packed) { + Job.initialize(); + _packed = packed; + result = new Runs(packed); + } + + function step(budget as Number) as Boolean { + var p = _packed; + var w = p.width; + var bytes = p.bytes; + var runs = result.runs; + var rows = MODULES_PER_STEP / w > 0 ? MODULES_PER_STEP / w : 1; + var last = _row + rows < p.height ? _row + rows : p.height; + for (var y = _row; y < last; y++) { + var bit = y * w; + var end = bit + w; + while (bit < end) { + var b = bytes[Packed.HEADER + (bit >> 3)]; + if ((bit & 7) == 0 && bit + 8 <= end && b == 0) { + bit += 8; + continue; + } + if (((b >> (7 - (bit & 7))) & 1) == 0) { + bit++; + continue; + } + var start = bit; + while (bit < end) { + var c = bytes[Packed.HEADER + (bit >> 3)]; + if ((bit & 7) == 0 && bit + 8 <= end && c == 0xFF) { + bit += 8; + } else if (((c >> (7 - (bit & 7))) & 1) == 1) { + bit++; + } else { + break; + } + } + runs.add((y << 20) | ((start - y * w) << 10) | (bit - start)); + } } - var w = data[0]; - var h = data[1]; - var n = w * h; - if (w == 0 || h == 0 || data[3] == 0 || data.size() != 4 + (n + 7) / 8) { - return null; + _row = last; + return _row >= p.height; + } + } + + // A 2D code from the cache or a fresh encoding to Runs: encode (unless + // cached), pack (`cache` then holds the bytes to store), find the runs. + class DisplayJob extends Jobs.Job { + var error as String? = null; + var cache as ByteArray? = null; + var runs as Runs? = null; + + private var _encoder as MatrixJob?; + private var _packer as Packer? = null; + private var _finder as RunFinder? = null; + + // Either an encoder (fresh) or the cached bytes. + function initialize(encoder as MatrixJob?, packed as Packed?) { + Job.initialize(); + _encoder = encoder; + if (packed != null) { + _finder = new RunFinder(packed); } - var bits = new [(n + 7) / 8 * 8]b; - for (var i = 0; i < (n + 7) / 8; i++) { - var v = data[4 + i]; - var k = i * 8; - bits[k] = (v >> 7) & 1; - bits[k + 1] = (v >> 6) & 1; - bits[k + 2] = (v >> 5) & 1; - bits[k + 3] = (v >> 4) & 1; - bits[k + 4] = (v >> 3) & 1; - bits[k + 5] = (v >> 2) & 1; - bits[k + 6] = (v >> 1) & 1; - bits[k + 7] = v & 1; + } + + // Progress 0-100: encoding is most of it. + function percent() as Number { + var e = _encoder; + return e != null ? e.percent() * 9 / 10 : 90; + } + + function step(budget as Number) as Boolean { + var finder = _finder; + if (finder != null) { + if (!finder.step(budget)) { + return false; + } + runs = finder.result; + _finder = null; + return true; } - var matrix = new Matrix(w, h, data[2], bits.size() == n ? bits : bits.slice(0, n)); - matrix.rowHeight = data[3]; - return matrix; + var packer = _packer; + if (packer != null) { + if (packer.step(budget)) { + cache = packer.result; + _packer = null; + _finder = new RunFinder(new Packed(packer.result)); + } + return false; + } + var encoder = _encoder as MatrixJob; + if (!encoder.step(budget)) { + return false; + } + var matrix = encoder.matrix(); + var err = encoder.error; + if (err != null || matrix == null) { + error = err != null ? err : "could not encode"; + return true; + } + _packer = new Packer(matrix); + _encoder = null; + return false; } } -- cgit v1.2.3