Repository navigation
Board refactor - #11665
Draft
Thomas Ball (thomasjball) wants to merge 40 commits into
Draft
Board refactor#11665Thomas Ball (thomasjball) wants to merge 40 commits into
Thomas Ball (thomasjball) wants to merge 40 commits into
Conversation
Thomas Ball (thomasjball)
marked this pull request as draft
October 21, 2026 14:09
… and improve structure
…tions, and enhance environment handling
…io functions, and implement Web Audio integration
… safety, and reorganize code structure
…llel execution, and enhance tsconfig for core output
…nd core outputs, and improve type safety
…ardView for visual management, and update related components for improved state handling
…rdState, decouple view dependencies, and streamline state-to-view updates
…ig for core and sim, and enhance state management
…tures, implement audio element management, and separate DOM-dependent audio logic
…o core, enhance audio handling, and streamline state management
…ndary board frames and simulator extensions, enhance message handling and frame management
- Added `simcoordinator.ts` to manage the simulator's lifecycle, including message handling, state management, and frame creation. - Introduced `simframe.ts` to define the `SimFrame` interface and related types for frame management. - Created `simframedom.ts` for the DOM implementation of `SimFrameHost`, handling iframe creation, messaging, and appearance management. - Established a communication protocol between the simulator and its frames, allowing for message passing and state updates. - Enhanced the simulator's ability to handle multiple frames and extensions, improving overall functionality and user experience.
…r target, CLI command, and test format
…ig for testing in a Node environment
…r DOM-free simulator support
… related functionality for running tests in a DOM-free environment
…dles and update documentation
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved headless-runner correctness, declaration completeness, and view-startup lifecycle issues prevent approval.
5 open issues
1 file not reviewed
kiosk/package-lock.json: Generated file
What changed in this PR
Separates simulator state and execution from browser rendering to support headless testing, alongside the related Micro:bit and common-packages refactors.
Changes:
- Adds a DOM-free simulator core while retaining combined browser bundles and declarations.
- Separates board views, audio hosts, and iframe handling from runtime and coordination logic.
- Adds the
pxt simtestcommand, headless tests, broadcast-routing coverage, and design plans.
| File | Description |
|---|---|
| tests/simdriver-test/simdriverrunner.ts | Adds broadcast-routing and frame-lifecycle coverage. |
| tests/simcore-test/tsconfig.json | Configures headless simulator tests. |
| tests/simcore-test/simcorerunner.ts | Tests core execution and the headless runner. |
| pxtsimcore/tsconfig.json | Defines the DOM-free core build. |
| pxtsimcore/globals.d.ts | Declares minimal host-neutral globals. |
| pxtsim/visuals/breadboard.ts | Moves shared layout helpers into core. |
| pxtsim/visuals/boardhost.ts | Receives visual factories separately from state. |
| pxtsim/visuals/boardgeometry.ts | Extracts DOM-free geometry and layout helpers. |
| pxtsim/tsconfig.json | Builds browser sources against core declarations. |
| pxtsim/state/animationqueue.ts | Extracts animation scheduling into core. |
| pxtsim/sound/soundexpression.ts | Routes sound playback through the audio host. |
| pxtsim/sound/sequencer.ts | Uses host-provided playback and metronome interfaces. |
| pxtsim/sound/audiohostdom.ts | Adapts browser audio to the host interface. |
| pxtsim/sound/audiohost.ts | Defines audio contracts and silent defaults. |
| pxtsim/sound/audioContextManager.ts | Preserves public aliases for moved audio types. |
| pxtsim/simtypes.ts | Extracts shared simulator types and enums. |
| pxtsim/simlib.ts | Removes state and geometry code moved into core. |
| pxtsim/simframedom.ts | Implements browser-backed simulator frames. |
| pxtsim/simframe.ts | Defines host-neutral frame contracts. |
| pxtsim/simdriver.ts | Wraps coordination with browser-specific adapters. |
| pxtsim/simcoordinator.ts | Extracts routing, lifecycle, and debugger coordination. |
| pxtsim/runtime.ts | Separates board views and delegates host operations. |
| pxtsim/messages.ts | Extracts shared simulator message definitions. |
| pxtsim/instructions.ts | Uses view-owned visual registries. |
| pxtsim/host.ts | Defines injectable environment services. |
| pxtsim/eventbus.ts | Extracts event handling and board-location types. |
| pxtsim/embed.ts | Adds separate board-view initialization and cleanup. |
| pxtsim/domutils.ts | Isolates DOM utility functions. |
| pxtsim/domhost.ts | Registers browser environment services. |
| pxtsim/debugger.ts | Removes helpers moved into core. |
| pxtsim/debugcore.ts | Extracts runtime debugging and heap inspection. |
| pxtsim/allocator.ts | Decouples allocation results from DOM types. |
| pxtsim/.eslintrc.js | Includes both simulator compilation projects. |
| kiosk/package-lock.json | Adjusts dependency metadata, unrelated to the refactor. |
| gulpfile.js | Builds combined artifacts and wires headless tests. |
| docs/pxtsimcore-refactor-plan.md | Documents architecture, migration, and testing plans. |
| docs/microbit-sim-input-api-plan.md | Proposes typed simulator input and observation APIs. |
| cli/simtest.ts | Adds headless execution and serial-result checking. |
| cli/cli.ts | Registers and implements the simtest command. |
Review effort: Balanced
Refactor addSerial function to handle serial buffer. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: thomasjball <7671657+thomasjball@users.noreply.github.com>
…nd initialization flow
… boardRefactor
…hrough pxt simtest (#11688) * Add deterministic clock, timers, and rng * update plan docs * Run headless simulator runs on the virtual host, with a bounded virtual settle and a yield budget for busy loops * Move the deterministic virtual host into the DOM-free simulator core * update doc files * restore lost instructions * Tick the silent audio host's metronome on the host timers * Update song.ts to use shared constants --------- Co-authored-by: Eric Anderson <humanapp@users.noreply.github.com>
…les (#11690) * Move the DOM-free simulator core sources into pxtsimcore/ * Build the target core simulator bundle in buildtarget and deploy pxtsimcore.js --------- Co-authored-by: Eric Anderson <humanapp@users.noreply.github.com>
…t 2) (#11691) * Ship the core globals declaration and build the common core simulator bundle The pxtsimcore gulp task copies pxtsimcore/globals.d.ts to built/pxtsimcore-globals.d.ts, which gains btoa and atob, so target and library core configs reference it instead of hand copies; buildSimAsync's logic moves into buildSimFolderAsync, which buildCommonSimAsync shares so the common sim folder's tsconfig.core.json builds after its tsconfig.json with the same outFile check, copyCommonSim copies common-simcore.js and common-simcore.d.ts when present, and pxt serve watches bundled directories two levels deep so edits under a library's sim/core/ rebuild; host.Timers returns and accepts an opaque host.TimerHandle in place of any, and the core's timer holders are typed with it; the refactor plan and simulator instructions describe the core/ folder rule and the common-packages layout. * Document the accelerometer tilt input in the simcore plan The target layout now says a board view drives the accelerometer through visuals.AccelerometerTiltInput constructed with the board's AccelerometerState, and the open questions list asks which targets still call accelerometerState.attachEvents and must construct a tilt input instead. * Point the simulator docs at pxt-microbit's sim/core layout * Compose target simulator bundles on their core bundle buildSimFolderAsync now builds a sim folder's tsconfig.core.json before its tsconfig.json, and buildFolderAsync's prepend also concatenates each prepended x.js's sibling x.d.ts ahead of the emitted declarations when the config sets declaration, copying the composed .d.ts to built/ for a node_modules pxt-*-sim folder, so a full sim config that compiles only view files against the core .d.ts and prepends the core bundle yields sim.js = simcore.js + view and common-sim.js = common-simcore.js + view; the refactor plan's target layout and the simulator instructions state that composition rule and that a view file references only the core declarations, never a core source file. * Carry sound events, queued play and stop-all listeners on the core audio host pxsim.audiohost gains SoundEvent ("playinstructions" | "muteallchannels", the multiplayer wire strings), setSoundEventHandler and notifySoundEvent, which replace the AudioContextManager.soundEventCallback slot and its three uses, and queuePlayInstructions(when, instructions), which plays through audio().playInstructionsAsync after when ms on host.timers() unless a stopAll ran in between, counted by an onStopAll listener registered once per audio host, with AudioContextManager.queuePlayInstructions kept as a one-line delegate; the silent host now keeps its onStopAll listeners and runs them on stopAll, and notifies "playinstructions" and "muteallchannels" as the Web Audio host does, covered by three new virtual-host cases in the simcore tests and recorded in the refactor plan's seams progress. * Notify the playinstructions sound event once per play on the Web Audio host PlayInstructionsSource.playInstructionsAsync no longer notifies the sound event, leaving AudioContextManager.playInstructionsAsync as the one notifier on that path, as the silent host notifies in its own playInstructionsAsync, so a multiplayer server sends one Audio message per sound instead of two. * Build the simulator before the target libraries buildTargetAsync and rebundleAsync now run buildSimAsync before buildTargetCoreAsync, so simulatorCoverage reads the built/sim.d.ts of the same build instead of a previous one and reports missing simulator implementations on a clean build too; simshimAsync stays ahead of the libraries build, whose sims.d.ts files it generates. * Watch library simulator core folders without scanning build output pxt serve watches each bundled library's sim/core folder by listing it alongside the sim folder's subdirectories, at the existing depth of one, so edits under libs/<lib>/sim/core/ rebuild while the per-library built/ and _locales/ outputs a build rewrites stay outside the scan. * Quiet the target build's shim scan and simulator coverage output plainTscCompileDir takes a reportProgramDiagnostics flag and the simulator shim scan passes false, since it reads only the //% annotations and the bundled compiler's parse diagnostics on the sim sources are reported by the sim's own build; simulatorCoverage prints one line with the count of target functions that have no simulator implementation and lists them at debug level. --------- Co-authored-by: Eric Anderson <humanapp@users.noreply.github.com>
Thomas Ball (thomasjball)
marked this pull request as draft
October 25, 2026 18:38
* Document the board view wiring the runtime expects from targets The runtime's board view interface now documents each member with what calls it and when, the BoardHost's builtinVisuals option documents what it supplies, and BoardHost.updateState documents that the owning view calls it from its own updateView. * Rename the runtime view interface to RuntimeBoardView pxsim.BoardView collided with pxsim.visuals.BoardView, which targets use alongside it, so the runtime's interface is now pxsim.RuntimeBoardView and Runtime.boardView is typed with it; targets that extend or implement the old name switch to the new one. * Guard the runtime's no-view paths and complete the silent audio host Recording and thumbnail capture now do nothing when the runtime has no board view instead of posting undefined frames, the recorder timer fields are typed as host timer handles, the parts instructions page reads built-in part visuals only from a view that exists, and the silent audio host's createAudioElement returns an inert handle whose play resolves, with a simcore test running it in a vm context. * Route the core's remaining time and random reads through the host seams The board id fallback and the coordinator's frame ids now draw from host.rng() and the coordinator's frame cleanup timer runs on host.timers(), so no core file reads Math.random or the global timers directly; the editor's default host keeps today's behavior. * Tidy the host, audio and message declarations TimerHandle and Timers each carry their own doc, FRAME_MS is exported once from host.ts, the high-resolution clock lookup uses mozNow and falls back to Date.now instead of throwing, the spatial audio queue shares the core queuePlay helper and its stopAll generation so instrStopId is gone, Metronome.initAsync is typed as a promise, setAudioHost no longer shadows the host namespace, the multiplayer audio message reuses audiohost.SoundEvent, and the globals header says what the file declares. * Assert virtual time in the simcore tests The simcore tests assert the virtual host's time and that runs did not time out instead of bounding real elapsed time, the board cleanup check has its own case, and the toy bundle builds its instruction buffer from the shared step size and duration offset constants. * Tidy simtest, lint configuration and moved simulator frame code runHeadlessAsync clears its sandbox timers in one finally and documents its context globals, its dropped teardown errors and its rejections, its messages and timer sets are typed, the serial expectation doc matches the parser, simtest names itself in the single-test-file error, pxtsimcore lints through its own eslintrc instead of quoted parser options, the pxtsim eslintrc and the kiosk lockfile match master again, the frame host drops a duplicate icon hide and clears the dim opacity, the driver keeps one typed reference to its frame host, and the moved debugger code documents that evalGetter's getter must have no side effects. * Move the simulator plans out of the published docs * Play sound on the silent audio host in headless runs runHeadlessAsync installs the silent audio host after the virtual host, so a program that plays sound runs the same on the full bundle as with --core instead of reaching the Web Audio host in the vm; the core README and the simulator instructions say so. * State the core logging rule in the simulator instructions Core code reports through pxsim.log, pxsim.warn and pxsim.error, the pxsim.Logger a host may replace, never through console directly. --------- Co-authored-by: Eric Anderson <humanapp@users.noreply.github.com>
* Design documents for the headless test harness above pxt simtest Adds three documents beside the headless core. pxtsimcore/headless-harness-overview.md is a short tour for a first read. pxtsimcore/headless-harness.md is the complete design: contributors as one shape serving the board, modules from a simulator extension's DOM-free core, program contributors registering over the program-message channel, and media; structured declarations composed into one named surface with fixed order and collision rules; scenario files with frame-addressed inputs, windowed level and arrival expectations, a watch list and a versions pin; a canonical JSON Lines trace that is the golden and the replay oracle; three shells over one driver, each offering a run and a surface operation; worlds of several simulators stepped in lockstep; media and topology with the radio ether's model surface, hot plugging and a project staging file reachable through its own request family; recording and replay at the host seams; conformance checks; worked examples throughout; and a Status section recording which parts exist. pxtsimcore/headless-harness-plan.md sequences the rest into nine phases with testable landing criteria and the working agreements for the agents that build it. The README and simulator.instructions.md point at the three files. Documentation only. * Name the command-line shells a project author runs tests with pxt simtest runs in a target checkout over the bundles the checkout builds and in a project folder over the bundles the installed target package ships; mkc test is the same command for a project author with no checkout, fetching the two headless bundles from the target's release beside the compiler it already fetches, compiling the project and running every scenario it holds; both are front ends over the Node caller, and a target release ships pxtsimcore.js and its simcore.js beside pxtworker.js. The Status section records that simtest runs only in a checkout today, that no release ships the bundles and that the MakeCode CLI has no test command. The plan adds pxt-mkc to its repositories and lands the two commands and the release packaging in phase 4, whose landing test requires byte-identical traces from every shell. The overview names both commands. * Remove harness line from instruction file * Let extensions ship media, with contracts, declared quantities and driven kinds A simulator extension's core script defines a medium with defineMedium(kind, factory) as it defines a module, and a staging's media entry names the extension as its module; the platform keeps the media a target's cores speak to, the radio ether, the jacdac bus, the display shield and a spatial world of positions alone, and a richer world with obstacles, surfaces, sources or echo is an extension's medium whose layers are its own structure. Media propagate in the order the staging lists them and feed one another only through declared kinds, a world applying per-link strength through the ether's strength kind and sensor levels through each board's kinds, read on the next frame; propagate(apply) is the seam, delivering to a board under a quantity its attachment map names or to a later medium under one of its kinds. A board's attachment carries what a medium reads and nothing else, a radio profile the target declares for the ether and a quantity-to-kind map for a sensing world. The seam between a target's core and a medium is a named contract, radio, jacdac or shield, implemented once by the platform and implementable by an extension's medium, the radio contract fixing the link, strength and drop names every implementation declares. A medium declares the quantities it applies and the params its staging accepts, maps and params are validated against them, and a board kind a staged map targets is marked driven by that medium in the surface and refused when a scenario scripts it. A lamps world is the worked example, core script and staging. Core scripts are plain JavaScript in the simulator context; the harness library and program contributors compile under the static subset. Plan phases 5, 6 and 7 carry the work with landing tests; the overview names extension media. * Add the STS form of a scenario Any file the project compiles may declare scenarios through `harness.scenario(name, fields, declare)`, a test file or a program file, the handle taking inputs, expectations and a watch list as values; the library sends the declaration over the harness channel, the driver takes every declared scenario at the first frame boundary and runs each as if it were the file beside the declaring file, a declaration after that boundary is refused. * Name the headless seam the program-message channel rides Phase 3's driver side runs over the runtime's existing hooks, Runtime.messagePosted collecting what the program sends and board.receiveMessage delivering what the driver posts, both DOM-free and the same seam the live simulator puts a window behind, so the program-message channel needs no abstraction to work headless. * Save and restore a world's state at a frame boundary Contributors gain optional saveState and restoreState, plain JSON in a shape of their own carrying their own format version, and a save is one JSON value serialized canonically: the driver's form (frame index, pending inputs, the position of every random stream it handed out, the surface digest), every contributor's state in composed order, each board's runtime saved form with the compiled program it ran, and the versions record. A restore runs the embedded program on the restoring runtime, so a save made under an older target resumes under a newer one; every saved form restores every version it has ever written, release versions are recorded and never refused on, and a restore is refused only by a layer that cannot read a format version or by a surface digest that does not match the staging; a contributor without saveState makes its world unsaveable, refused by name. A save is the driver's operation, never the program's: every shell offers save as a third operation beside run and surface, a scenario asks for saves at frames through a saves field and starts from one through a from field that names a save or carries it whole, the static TypeScript handle declares save frames with t.save, and a tab asks through the coordinator. Serialization and storage are separate layers throughout: the driver produces and consumes serialized state and opens no file; the command-line shells keep saves, traces and bulk values beside the scenario and resolve from paths, and the worker shell passes a save as a message, so browser-stored saves load back in. The ether's random stream is handed to it by the driver, as any medium's is. Two saves at one frame are byte-identical and a restored run continues to the same trace, which conformance checks together with a corpus of saves from each released format. * Reconcile the harness documents after the media, scenario and save additions A fresh read of the overview, the spec and the plan to fix inconsistencies and stale facts. * Add anchored windows and cap the scenario's expectation vocabulary An expectation's window may be given relative to an anchor instead of to frame 0: an input at the frame it is due, or an event at its first arrival of that kind, with that value when one is given and from a floor frame onward when the anchor carries from. The window begins at the anchor's frame and runs through within frames after it inclusive, within 0 being the point form, or to the run's end when unbounded; each form keeps its meaning over the resolved window, the anchoring event does not count toward an arrival of its own kind and value, a window the run cuts short is unsatisfied as an absolute one is, an anchor that never occurs leaves every form unsatisfied, negated included, which is the opposite of a logic's vacuous truth and is chosen, and an input anchor naming no scripted input or a negative within is refused before the run. Anchors are one input or one event, never another expectation and never nested; they resolve to absolute frames from the trace, which the report shows beside the verdict, so the evaluator, the trace and the golden are those of absolute windows, and absolute windows remain for recordings and tests of timing. The vocabulary ends there: absolute windows, anchored windows and the four forms over them, with no comparison, composition or conditional; a richer check is TypeScript over the trace through the trace reader, which pxt ships as a library and pxt simtest runs beside a scenario, leaving the run and the trace unchanged. The counter scenario is shown a second time with anchored windows; the static TypeScript handle takes a window as from and to or as an anchor object; plan phase 1 exports the reader and tests the anchor rules, and phase 2 runs checks beside scenarios. --------- Co-authored-by: Eric Anderson <humanapp@users.noreply.github.com>
Co-authored-by: Eric Anderson <humanapp@users.noreply.github.com>
* Pass btoa and atob into the headless sandboxes cli/simtest.ts and tests/simcore-test/simcorerunner.ts provide btoa and atob in the vm context beside console, performance, the text codecs, URL and the timer functions, matching the shared globals that pxtsimcore/globals.d.ts declares, so a simulator library that encodes buffers through them, such as settings, runs headless. * Run pxt simtest on the DOM-free bundles when the target ships them pxt simtest runs pxtsimcore.js with built/simcore.js when the target has built simcore.js and pxtsim.js with built/sim.js otherwise, --core requires the DOM-free pair and stops when the target has not built one, --sim names the target bundle explicitly, and the pxtsimcore README states that a target shipping simcore.js is never loaded headless through sim.js, whose view code may use the DOM at load. * Record the Arcade simulator on the DOM-free split The pxtsimcore README's open items no longer list the browser-events input state or pxt-arcade's simulator, the tilt-input wiring sentence stands in "Board state and view" as a mechanism of its own, the harness Status names pxt-arcade's initHeadlessArcadeBoard and simcore.js beside pxt-microbit's, and phase 9 of the harness plan carries the Arcade screen channel with no split condition. * Describe how a target ports to the DOM-free split * State the live topology a project without staging.json has The live simulator's coordinator links every extension frame to every other when the project has no staging.json, so an extension that stages nothing receives every channel it subscribes to, and staging narrows the topology only where a project or a run declares one. * Let a board declare the frame period its scenarios run at A board contributor may declare framePeriod, the period in milliseconds its engine steps on, which is the nominal frame period of a scenario that states none and FRAME_MS when the board declares none; a world of several boards takes the scenario's period, else the smallest period any of its boards declares, every board's engine stepping on its own timers inside it. --------- Co-authored-by: Eric Anderson <humanapp@users.noreply.github.com>
The uploaded markdown (snapshot-and-restore-proposal.md) outlines the architectural blueprint for a deterministic runtime snapshot and restore mechanism within the PXT MakeCode simulator.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

The PR here decouples the simulator runtime responsible for running generated JavaScript from the browser DOM. We test first in the context of pxt-microbit. Once done, we will make necessary mods to pxt-arcade. With related PRs:
This is a breaking change that requires a bump in major version.
See pxtsimcore/README.md for details, reproduced here
The DOM-free simulator core
The simulator is two TypeScript projects that share the
pxsimnamespace.pxtsimcore/holdseverything that runs without a browser: the VM, board state, sound synthesis, the host seams and
the editor-side frame policy.
pxtsim/holds the code that needs the DOM: the embed, SVG views,Web Audio and iframes. Consumers keep loading one composed
built/pxtsim.js.Layout
pxtsimcore/tsconfig.jsoncompilespxtsimcore/**/*.tswithlib: ["es2017", "ES2018.Promise"],types: []and nodom, tobuilt/pxtsimcore.jsandbuilt/pxtsimcore.d.ts. That theproject compiles without
domis the check that core is DOM-free.runtime.ts: the VM,Runtime,BaseBoard,CoreBoardandRuntimeBoardView.messages.ts(the simulator message interfaces),eventbus.ts(EventBusand the boardlocation types),
simtypes.ts(SimulatorState,SimulatorRunOptionsand friends).host.tsandvirtualhost.ts: the host seams and the virtual host.allocator.ts,debugcore.ts,debugProtocol.ts,langsupport.ts,libgeneric.ts,localization.ts,logger.ts,utils.ts.simcoordinator.tsandsimframe.ts: the editor-side frame policy.sound/: synthesis and sequencing (soundSynthesizer*.ts,soundEmojiSynthesizer.ts,soundexpression.ts,song.ts,sequencer.ts,musicalProgressions.ts), theplay-instructions format (
instructions.ts) and the audio seam (audiohost.ts).state/:AnimationQueueand the genericButtonPairState,CompassState,FileSystemStateandLightSensorState.visuals/boardgeometry.ts: coordinates, wire colors and breadboard geometry, whichallocator.tsneeds.pxtsim/tsconfig.jsoncompilespxtsim/**/*.tswith thedomlibs, againstbuilt/pxtsimcore.d.ts, tobuilt/pxtsimdom.jsandbuilt/pxtsimdom.d.ts:embed.ts,domhost.ts,domutils.ts(thepxsim.Uelement helpers),debugger.ts,instructions.ts,accessibility.ts,simlib.ts,svg.ts,simdriver.ts,simframedom.ts,sound/(the WebAudio implementation,
audiohostdom.ts, the metronome worker and the worklet) andvisuals/(
BoardHost, the breadboard, generic boards and parts, wiring).config lists sources.
pxtsimtask builds core, then the DOM project, then composesbuilt/pxtsim.jsaspxtsimcore.jsfollowed bypxtsimdom.js, andbuilt/pxtsim.d.tsthe same way with the DOMdeclarations' reference to
pxtsimcore.d.tsremoved. Itspxtsimcorestep also copiespxtsimcore/globals.d.tstobuilt/pxtsimcore-globals.d.ts.built/*.js,built/**/*.d.ts). A target'score config compiles against
pxtsimcore.d.tsfollowed bypxtsimcore-globals.d.ts.gulp lintlints both directories;pxtsimcore/.eslintrc.jspoints the parser at the coreconfig.
The boundary
pxtsimcore/globals.d.tsand thelocaltypings/files it references (pxtparts.d.ts,pxtarget.d.ts,pxtmusic.d.ts,vscode-debug-protocol.d.ts). It never names anything inpxtsim/; DOM code reaches corethrough
built/pxtsimcore.d.ts.globals.d.tsdeclares what browsers, workers and Node share: the timer functions,console,performance,btoa/atob,TextEncoder/TextDecoder,URL/URLSearchParams, theImageDatafields core reads, andrequestAnimationFrame,cancelAnimationFrame,setImmediateandMessageChanneltyped as possibly undefined, so core checks before calling.pxsim.host, and audio throughpxsim.audiohost. The serial message'stime:field stays onDate.nowas wall-clocktelemetry.
file's members while the script loads pins that file ahead of it with
/// <reference path>.In core the one load-time read is
libgeneric.ts, whoseRefCollectionextendsRefObjectfrom
langsupport.ts. Inpxtsim/the Web Audio sources pinsound/audioSource.ts, theirbase class, and
simdriver.tspinssimframedom.ts.Host seams
pxsim.host.Environmentis what the runtime knows about its page:postToParent,locationHref,isPxtElectron,isIpcRendererandframeId. The defaults are inert;pxtsim/domhost.tsinstalls the browser environment at load time throughsetEnvironment.pxsim.host.Clock(now,perfNowUs),Timers(setTimeout,clearTimeout,setInterval,clearInterval,requestFrame,cancelFrame) andRandomSource(next,seed) are readthrough
host.clock(),host.timers()andhost.rng(). Every timer and frame call returns aTimerHandle, an opaque value passed back to the matching cancel call.Date.now, a performance-based microsecond clock, the global timers looked upat call time,
requestAnimationFrameor aFRAME_MS(16 ms) timeout, andMath.randomwith aseedthat does nothing.setEnvironment,setClock,setTimersandsetRngoverride somemembers and fill the rest from the defaults.
pxsim.audiohost.AudioHostis the audio seam: tones, sample and PCM playback, MIDI, audioelements for recorded audio, muting,
stopAll, spatial players andcreateMetronome.audiohost.audio()returns the current host andsetAudioHostoverrides members over thesilent defaults.
pxtsim/sound/audiohostdom.tsregisters the Web Audio host, which forwards toAudioContextManager.playInstructionsAsyncresolves once the buffer's duration (music.instructionsDurationMs)has passed on the host clock, or early when
isCancelledreturns true, polled on frame-sizedticks;
createMetronomereturns anaudiohost.TimerMetronome, which ticks onhost.timers();stopAllcalls itsonStopAlllisteners in registration order;createAudioElementreturns an inert element whoseplayresolves at once;isMutedis true,isAudioElementActiveis false, sample and PCM playback resolve at once,and everything else does nothing.
audiohost.SoundEventis"playinstructions" | "muteallchannels".audiohost.setSoundEventHandlersets the single observer andaudiohost.notifySoundEventcalls it. Both hosts notify: playing instructions with the buffer,
stopAllwith none.audiohost.queuePlay(when, play)callsplayafterwhenms onhost.timers()unless a
stopAllran in between, andaudiohost.queuePlayInstructionsqueues aplayInstructionsAsyncthat way.AudioContextManager.queuePlayInstructionsdelegates to it.The virtual host
pxsim.host.createVirtualHost(options)returns a deterministic host: a clock that moves onlywhen driven, timers and
FRAME_MSframes on one queue ordered by virtual time, and a seededrandom source.
use()makes it the host the runtime reads.advance(ms)moves virtual time forward, firing everything due on the way in time order andyielding to the event loop around each callback.
runUntil(done)fires queued callbacks one ata time until
done()is true, and keeps yielding while the queue is empty so real timers canmake it true.
stop()ends both for good.now()reads elapsed virtual milliseconds; the clockitself reads from a nonzero epoch, since the runtime treats a performance time of 0 as unset.
yieldToEventLoopmay be injected; the default issetImmediate, else aMessageChannelmessage, else
setTimeout(fn, 0).pxsim.host.createSeededRandom(seed), a mulberry32 generator,started from
options.seedorpxsim.host.DEFAULT_SEED. One seed always yields one sequence;a program that calls
seedAddRandomreseeds it.maybeYieldyields after 20 ms of host time, or afterYIELD_BUDGET(100000) calls on one unchanged clock reading. On the virtual clock a busy looptherefore still yields, so a headless run times out instead of hanging Node.
Board state and view
runtime.boardis the board state (BaseBoard, usually aCoreBoardsubclass) andruntime.boardViewis its optional view (RuntimeBoardView). Theboard()andboardView()accessors are defined by each target:
board()for shims and state code,boardView()forview code only. A board state constructs and runs with no view.
BaseBoardholds the id, theEventBus, the run options, stored state, serial buffering andmessage listeners.
CoreBoardaddsbuiltinParts, the part states keyed by simulationbehavior, and stops sound when killed.
RuntimeBoardViewmembers and their callers:builtinVisuals(part view constructors by simulation behavior) andbuiltinPartVisuals(part image constructors by built-in part name): read by the parts instructions page in
pxtsim/instructions.ts.initAsync(msg): called by the embed for each run once the board'sinitAsynchasresolved, while that runtime is still current and alive; the program starts when it resolves.
updateView(): called byRuntime.updateDisplay, which a target calls to redraw and whichthe scheduler calls when updates queued with
queueDisplayUpdateare flushed.addUpdateSubscriber(fn): registersfnto run on everyupdateView; view code such asthe board SVG subscribes here.
screenshotAsync(width?): called for each screenshot request, for each frame whilerecording, and for thumbnail frames after a user interaction.
kill(): called by the embed when it stops a run, after the runtime and the board.nothing.
pxsim.initCurrentRuntime, which setsruntime.boardandruntime.boardView; a view class implementingRuntimeBoardView; and, torun headless,
pxsim.initHeadlessRuntime, which sets the board with no view.BoardHostwiring: the view'sinitAsyncbuilds avisuals.BoardHostfromvisuals.mkBoardView(...)andvisuals.BoardHostOpts, passing the board state asstateandits own
builtinVisuals.BoardHost.addPartbuilds each part that declares asimulationBehaviorfrom the matchingbuiltinVisualsentry and initializes it with thematching
builtinPartsstate.Simulator frames
SimulatorDriverruns in the editor, not in the simulator frame. Its policy is DOM-free and livesin core; only the iframe handling needs the DOM.
pxtsimcore/simframe.tsdefinesSimFrame, an opaque handle to one frame (id,messageChannel,runId,runtimeId,permanent,loading,origin,aspectRatio,attached,matchesSource,focus), andSimFrameHost, which the environment holding theframes implements: list, create, remove, hide, reveal, post to, style, size and freeze frames.
pxtsimcore/simcoordinator.tsdefinesSimEnvironment(origin, URL, parent window, dependenteditor liveness, configured simulator URL, message listener) and
SimulatorCoordinator, whichowns the policy:
SimulatorStatemachine andrun,stop,restart,suspend,muteandpreload;none in single-simulator mode), simulator extension and legacy message simulator frames, and
restarting a frame whose run is stale;
boards skipped, messages deferred until a frame has loaded, and at most one recorder;
pxtsim/simframedom.tshasDomSimFrameHost, the iframe-backedSimFrameHost; a frame's statelives in the iframe's dataset, and loaning a frame is DOM-only.
pxtsim/simdriver.tshasSimulatorDriver extends SimulatorCoordinator, which builds aDomSimFrameHostand a DOMSimEnvironmentand addsloanSimulator,unloanSimulator,isLoanedSimulatorand theelement callbacks in
SimulatorDriverOptions; the rest of its public API is thecoordinator's.
pxsim.host.tests/simdriver-testdrivesSimulatorDriverthrough a fake DOM.Target core bundles
sim/tsconfig.core.jsonwithoutFile../built/simcore.js. Its files are the target'ssim/core/folder (and any library corefiles it lists), compiled against
pxtsimcore.d.tsandpxtsimcore-globals.d.tswith nodomlib.sim/tsconfig.jsoncompiles only the view files, againstpxtsim.d.tsand the core bundle's.d.ts, and lists../built/simcore.jsin a top-levelprepend.prependis a pxt key, not aTypeScript one, and only
tsconfig.jsonmay carry it.pxt buildtarget(andpxt rebundle) compile the sim folder:tsconfig.core.jsonfirst, tobuilt/simcore.js, thentsconfig.json, tobuilt/sim.js. After a build, eachprependentry is concatenated ahead of the output, and when the config emits declarations the prepends'
.d.tsfiles are concatenated ahead of the output's.d.ts. Sosim.jsissimcore.jsfollowed by the view code. An
outFileother than the required path is a build error.node_modules/pxt-<target>-simmay use./built/simcore.jsand./built/sim.js, and the outputs are copied into the target'sbuilt/.libs/<lib>/sim/core/and its viewfiles in
libs/<lib>/sim/. Itssim/tsconfig.core.jsoncompiles../libs/*/sim/core/*.tstobuilt/common-simcore.js, and itssim/tsconfig.jsoncompiles../libs/*/sim/*.tsandprepends
built/common-simcore.js, tobuilt/common-sim.js. A target'spxt buildtargetbuilds both when
common-sim.jsis missing,pxt servebuilds both on every build, and bothcopy both pairs into the target's
built/.pxtsim.jsand the target'ssim.js(the browser simulator), orpxtsimcore.jsandsimcore.js(headless, DOM-free); never both pairs, sincepxtsim.jsalready contains core.
Running programs headless
pxt simtest [file|dir ...]compiles each test program on top of a base project and runs it ina Node
vmcontext with no browser globals (cli/simtest.ts,runHeadlessAsync).--base <dir>is the project each test compiles on top of (default: the currentdirectory); it must have exactly one
.tsfile, which the test replaces.--sim <file>is thetarget bundle (default
built/sim.js, orbuilt/simcore.jswith--core).--coreruns onpxtsimcore.jsandbuilt/simcore.jsinstead ofpxtsim.jsandbuilt/sim.js, and stopswith a message when the target has no core bundle.
--timeout <ms>bounds each run in real time(default 5000).
--settle <ms>is thevirtual time a program runs after its top-level code finishes (default 300).
.tsfile in it, in name order.Runtime,sets
pxsim.initCurrentRuntimetopxsim.initHeadlessRuntime, steps the host until the top-levelcode finishes, advances it by
--settle, and then ends whatever is still scheduled. A runnever waits for the program to go idle, so a
foreverprogram passes. The timeout watchdogstays on real time.
// @serial: textis one expected serial line, in order.it has
@seriallines, writes exactly those lines.simtestruns every test, printsokorFAILwith the reason for each and a count, and exits 1 when any failed.gulp testsimcore(tests/simcore-test) loadsbuilt/pxtsimcore.jsalone into such acontext, runs programs through
runHeadlessAsyncon the TypeScript fixturetests/simcore-test/fixtures/toybundle.ts, and drives a virtual host directly.Open items
libs/browser-events/sim/: keyboard andmouse) is view code; it has no DOM-free state in
sim/core/.libs/mqtt/sim/net.ts,libs/wifi---esp32/sim/wifisockets.ts,libs/text-to-speech/sim/tts.ts) are view code only.sim/core/instead of using pxt-common-packages' versions.debugProtocol.tstimes out pending requests on the globalsetTimeout, not onhost.timers().SimFrameHost; its tests go throughSimulatorDriverand a fake DOM.board methods that only an arcade board defines, through an
anycast:updateStats(screenstats, view-only) and
handleKeyEvent(multiplayer key input, state). A board view that tiltsthe accelerometer constructs
visuals.AccelerometerTiltInputwith the board'sAccelerometerStateand callsattachwith the board element; pxt-arcade's simulator is acaller.