Skip to content

QR-code rendering: BGE.QrCode - #174

Merged
markwpearce merged 5 commits into
mainfrom
qr-code-discovery-168
Aug 30, 2026
Merged

QR-code rendering: BGE.QrCode#174
markwpearce merged 5 commits into
mainfrom
qr-code-discovery-168

Conversation

@markwpearce

Copy link
Copy Markdown
Owner

Closes #168.

What

BGE.QrCode - a plain-BrighterScript QR code generator/renderer, no SceneGraph. Ports Project Nayuki's QR Code generator via the paramount-engineering BrightScript port (both MIT, attribution in LICENSE-THIRD-PARTY.md) into real BrighterScript classes/functions.

BGE.QrCode.draw(renderer, x, y, code, width)

One call: encodes code and rasterizes it onto renderer's draw surface as a width x width square, black-on-white by default (darkRgba/lightRgba override). qrEncodeText/qrEncodeBinary/qrEncodeSegments expose the encoder directly (BGE.QrCode.QrCode.getModule(x, y)) if you want to draw a code some other way.

Screenshot (real hardware, via rokubot)

QR code next to the controller connection URL

examples/controller's MainRoom now draws the connection URL as a QR code (onDrawEnd) alongside the existing text label.

Why not Renderer.drawQRCode()

Ran into and confirmed (bisected on a fresh clone) a real toolchain limitation: in this project's exact brighterscript/@rokucommunity/bslint/rooibos-roku version combo, a pre-existing file can't reference a symbol from a brand-new file - tested and ruled out namespace/class name collision and custom-class-type coupling as the cause per teammate suggestions, it's specifically the existing→new direction, in general. The reverse (new file calling an existing class's public method) works fine, so BGE.QrCode.draw() takes the Renderer instance and calls two trivial new (same-file-only) public methods on it - getDraw2d()/incrementDrawCalls() - rather than Renderer gaining a drawQRCode() that reaches into the new files.

Correctness verification

Beyond the 15 new Rooibos tests (structural invariants - version growth, determinism, forced mask, ECC boosting, finder-pattern placement, draw-call counting): extracted the actual module grid via a throwaway test print, reconstructed a clean PNG from it, and decoded it with zbarimg - exact round-trip match on the encoded URL. The on-device screenshot above is too JPEG-compressed for a scanner to read at that size (confirmed unrelated to the encoder itself).

Testing

npm run check: 850 passed, lint clean. examples/controller validates clean and confirmed running on real hardware.

🤖 Generated with Claude Code

- Ports Project Nayuki's QR Code generator (via the paramount-engineering
  BrightScript port, both MIT) to plain BrighterScript classes/functions
  under src/source/utils/qrcode/ - versions 1-40, all 4 ECC levels, no
  SceneGraph. Attribution in LICENSE-THIRD-PARTY.md.
- Public entry point: BGE.QrCode.draw(renderer, x, y, code, width,
  darkRgba=Black, lightRgba=White) as boolean - encodes and rasterizes
  in one call, matching Renderer.draw*'s bounds-check/draw-call-count
  conventions.
- BGE.QrCode.qrEncodeText/qrEncodeBinary/qrEncodeSegments expose the
  encoder directly (returns a BGE.QrCode.QrCode with getModule(x,y)) for
  anyone who wants to draw a code some other way.
- Not a Renderer.drawQRCode() method: a confirmed toolchain limitation in
  this project's exact brighterscript/bslint/rooibos-roku version combo
  means a pre-existing file can't reference a symbol from a brand-new
  file (reproduced on a fresh clone; bisected away namespace/class-name
  collision and custom-type coupling as causes - see project dev notes).
  Renderer.bs only gained two trivial, itself-unaffected additions
  (getDraw2d(), incrementDrawCalls()) so BGE.QrCode.draw() can call into
  it - the reverse direction, which works fine.
- examples/controller's MainRoom now draws the connection URL as a QR
  code (onDrawEnd) alongside the existing text. New docs/qr-codes.md
  guide; docs/controller-input.md links to it.
- Verified correctness independently of the engine: extracted the actual
  module grid via a throwaway test, reconstructed a clean PNG, and
  decoded it with zbarimg - exact round-trip match on the encoded URL.
  Confirmed rendering on real hardware via rokubot (screenshot in
  docs/images/); the on-device JPEG screenshot itself is too compressed
  for a scanner to read at that size, unrelated to the encoder.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
markwpearce and others added 4 commits August 30, 2026 15:43
…e off-by-one, uncaught throw, per-frame re-encoding

- qrSpliceArray had two off-by-one bugs (inherited from the upstream
  reference port) comparing against arr.Count()-1 where arr.Count() was
  needed: the clamp/negative-index handling, and the relocate-loop
  condition that stopped one element short of the end. The latter
  silently misordered getAlignmentPatternPositions() for QR version 7+
  (the first version needing 2+ insertions), placing an alignment
  pattern on top of the bottom-left finder pattern instead of its
  correct position - a real, confirmed correctness bug (regression test
  added, verified failing before the fix).
- qrSliceArray had the same class of off-by-one resolving a negative
  start/finish (dormant - no current caller passes one, but it's public
  documented API).
- BGE.QrCode.draw() now catches the uncaught throw qrEncodeSegments()
  raises when text doesn't fit any QR version, returning false instead
  of crashing the render loop - matching every other failure path in
  draw() already returning false rather than throwing.
- examples/controller no longer re-encodes the (unchanging) connection
  URL every frame - encodes once in onCreate, caches the QrCode object,
  and only re-runs the per-module blit (qrDrawModules) in onDrawEnd.
  Also derives the info label's position from the QR width instead of
  an unrelated magic number.
- Added missing doc comments on newQrMode/qrFloor/qrCeil/qrMin/qrMax.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Already credited in LICENSE-THIRD-PARTY.md and QrConstants.bs's file
header; adding it to docs/qr-codes.md too since that's the page a
consumer actually reads. Also fixes the guide's 'drawing one every
frame' sample to show the cache-once pattern (encode in onCreate,
re-blit via qrDrawModules per frame) instead of the naive re-encode
that examples/controller no longer does either.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
# Conflicts:
#	examples/controller/src/source/Rooms/MainRoom.bs
The test suite has grown enough (860+ specs, after merging the
controller-protocol-v2 work and adding this PR's QR-code suite) that a
CI runner can take noticeably longer than a local dev machine to
finish - confirmed by two consecutive CI failures that each hung at a
different point in the run (varying by normal runner-speed jitter, not
a deterministic hang), both comfortably under the new budget locally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@markwpearce
markwpearce merged commit 9c4ffde into main Aug 30, 2026
3 checks passed
@markwpearce
markwpearce deleted the qr-code-discovery-168 branch August 30, 2026 19:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

QR-code discovery for controller input

1 participant