Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 | 1x 1x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 25x 29x 25x 4x 4x 60x 60x 57x 57x 56x 56x 48x 52x 52x 52x 52x 52x 60x 60x 60x 60x 60x 60x 60x 60x 47x 47x 41x 41x 44x 42x 42x 42x 42x 42x 42x 10x 10x 21x 10x 10x 3x 3x 3x 3x 3x 3x 3x 3x 3x 28x 28x 28x 28x 2x 28x 28x 28x 28x 28x 28x 28x 28x 2x 2x 2x 2x 2x 2x | import * as canvasLib from "./canvas.js";
import { MaxPersistenceMs, persistenceFromMs } from "./canvas.js";
import { FakeVideo, Video } from "../video.js";
import { LineGridRows } from "../video-filters/pixel-grid.js";
import { toast } from "./toast.js";
// While running fast the state machines still run accurately, but painting is
// the most expensive part of jsbeeb, so most frames are skipped. Odd, so that
// interlaced modes (MODE 7) alternate fields across the frames that do paint.
const SpeedyFrameSkip = 9;
// More fields of decay than this leaves nothing at any setting: at the longest
// afterglow a field keeps 96%, and this many take that below a level.
const MaxDecayFields = 250;
/**
* The picture: the canvas and its filter, the video chip that paints into a
* framebuffer of our own, and the animation frame that presents it. A stalled
* display holds up the picture and not the emulation (issue #885).
*/
export class Display {
constructor({
screenCanvas,
model,
mode,
tryGl = true,
lowLatency = true,
fakeVideo = false,
frameSkip = 0,
makeCanvas = (canvasEl, filterClass) =>
tryGl
? canvasLib.bestCanvas(canvasEl, filterClass, lowLatency)
: new canvasLib.Canvas(canvasEl, lowLatency),
}) {
this.screenCanvas = screenCanvas;
this.persistence = {};
this.fieldMs = 1000 / (model.isAtom ? 60 : 50);
this.frames = 0;
this.frameSkip = frameSkip;
this.paintMsThisTick = 0;
this.presentMsMax = 0;
this.presentScheduled = false;
this.presented = true;
this.filterClass = canvasLib.getFilterForMode(mode);
// Each mode says how many pixels it wants to draw into. Set this before
// creating the context, which fixes its initial viewport.
this.sizeCanvasFor(this.filterClass);
this.canvas = makeCanvas(screenCanvas, this.filterClass);
this.reportAnyFallback(this.filterClass);
this.filterClass = this.canvas.filterClass;
// The animation frame asked for at flyback covers paints while the loop
// is stopped; while it runs, its vsync tick presents.
this.videoFb32 = new Uint32Array(this.canvas.fb32.length);
this.pendingFrame = {
minx: 0,
miny: 0,
maxx: 0,
maxy: 0,
lineBaseEven: 0,
lineBaseOdd: 0,
phaseBaseEven: 0,
phaseBaseOdd: 0,
lineGrid: new Uint8Array(LineGridRows),
fields: 1,
};
this.lastPaintedFrameCount = 0;
this.fieldsSincePresent = 0;
const display = this;
this.video = fakeVideo
? new FakeVideo()
: new Video(
model.isMaster,
this.videoFb32,
function paint(minx, miny, maxx, maxy, paintedTo) {
display.onPaint(this, minx, miny, maxx, maxy, paintedTo);
},
{ isAtom: model.isAtom },
);
this.setCrtPic();
}
/**
* While running fast the skip moves into the video chip, which skips the
* render as well as the present; the deeper of the chip's skip and any
* configured frameSkip wins, so the two never multiply together.
*/
setSpeedy(speedy) {
const skip = Math.max(SpeedyFrameSkip, this.frameSkip);
this.video.frameSkipCount = speedy ? (skip % 2 ? skip : skip + 1) : 0;
}
/**
* `paintedTo` is the row the copy stops at; the rows below it keep what the
* canvas last showed. A paint that gives one is the debugger's and is never
* skipped.
*/
onPaint(video, minx, miny, maxx, maxy, paintedTo) {
// The debugger's paint replaces the picture: its beam dot would otherwise
// outshine the black it moved off and stay, every step leaving another.
const replacesPicture = video.paintsAfresh || paintedTo !== undefined;
if (paintedTo === undefined) {
paintedTo = maxy;
if (!video.frameSkipCount) {
this.frames++;
if (this.frames < this.frameSkip) return;
this.frames = 0;
}
}
const start = performance.now();
this.canvas.fb32.set(this.videoFb32.subarray(miny * 1024, paintedTo * 1024), miny * 1024);
this.pendingFrame.lineGrid.set(video.lineGrid.subarray(miny, paintedTo), miny);
Object.assign(this.pendingFrame, {
minx,
miny,
maxx,
maxy,
lineBaseEven: video.lineBaseEven,
lineBaseOdd: video.lineBaseOdd,
phaseBaseEven: video.phaseBaseEven,
phaseBaseOdd: video.phaseBaseOdd,
});
// How many fields the phosphor has decayed over since the frame the
// canvas shows: one usually, more under a frame skip or when paints
// outrun the animation frame, none for a repaint of the same frame, and
// all of them after a restore, whose picture owes the old one nothing.
// The 6847 keeps its own count and syncs it after painting.
const frameCount = (video.video6847 ?? video).frameCount;
const elapsed = Math.max(0, frameCount - this.lastPaintedFrameCount);
this.fieldsSincePresent = replacesPicture
? MaxDecayFields
: Math.min(MaxDecayFields, this.fieldsSincePresent + elapsed);
video.paintsAfresh = false;
this.pendingFrame.fields = this.fieldsSincePresent;
this.lastPaintedFrameCount = frameCount;
this.paintMsThisTick += performance.now() - start;
this.presented = false;
if (!this.presentScheduled) {
this.presentScheduled = true;
window.requestAnimationFrame(() => {
this.presentScheduled = false;
this.present();
});
}
}
present() {
if (this.presented) return;
this.presented = true;
const start = performance.now();
const { minx, miny, maxx, maxy } = this.pendingFrame;
this.canvas.paint(minx, miny, maxx, maxy, this.pendingFrame);
this.fieldsSincePresent = 0;
this.presentMsMax = Math.max(this.presentMsMax, performance.now() - start);
}
/**
* Sets a display's afterglow time in milliseconds, by the name of its
* setting; it applies while the filter in use is the one that declares that
* setting, so a fallback gets its own amount, not the amount of the mode
* that was asked for.
*/
setPersistence(setting, afterglowMs) {
this.persistence[setting] = Number.isFinite(afterglowMs)
? persistenceFromMs(Math.min(MaxPersistenceMs, Math.max(0, afterglowMs)), this.fieldMs)
: 0;
if (setting === this.persistenceSetting()) this.applyPersistence();
}
/** The setting that governs the display in use, or nothing where the canvas cannot keep a picture. */
persistenceSetting() {
return this.canvas.canPersist ? this.filterClass.getDisplayConfig().persistence?.setting : undefined;
}
applyPersistence() {
const setting = this.persistenceSetting();
this.canvas.setPersistence(setting === undefined ? 0 : (this.persistence[setting] ?? 0));
}
/** The mode is changed from a modal, which stops the emulator, so this repaints itself. */
setMode(mode) {
const newFilterClass = canvasLib.getFilterForMode(mode);
// Everything but the filter is the same whatever the mode: the framebuffer
// texture, the vertex buffers and fb32 all carry over untouched.
canvasLib.useBestFilter(this.canvas, newFilterClass);
this.reportAnyFallback(newFilterClass);
// Follow the filter we ended up with, not the one we asked for: everything
// downstream (the monitor picture, the canvas geometry, how large a drawing
// buffer to ask for) comes from its display config.
this.filterClass = this.canvas.filterClass;
// Back to the mode's own size, undoing any scaling the last one asked for.
this.sizeCanvasFor(this.filterClass);
this.applyPersistence();
// The new mode's picture replaces the old one's, not glows through it.
this.fieldsSincePresent = MaxDecayFields;
this.video.paint();
this.setCrtPic();
}
sizeCanvasFor(filterClass) {
const displayConfig = filterClass.getDisplayConfig();
Eif (
this.screenCanvas.width === displayConfig.canvasWidth &&
this.screenCanvas.height === displayConfig.canvasHeight
)
return;
this.screenCanvas.width = displayConfig.canvasWidth;
this.screenCanvas.height = displayConfig.canvasHeight;
}
// Test which filter is actually in use, not merely whether we got WebGL: a
// filter can decline a context that works perfectly well for other modes, in
// which case we are quietly left with an unfiltered display.
reportAnyFallback(filterClass) {
if (this.canvas.filterClass === filterClass) return;
const reason = this.canvas.fallbackReason ? ` (${this.canvas.fallbackReason})` : "";
const { name } = filterClass.getDisplayConfig();
toast(`${name} is not available on this device, so the standard display is in use${reason}.`, {
title: "Display",
quietKey: "quietDisplayFallback",
});
}
/** The monitor picture around the screen follows the filter in use. */
setCrtPic() {
const config = this.filterClass.getDisplayConfig();
const monitorPic = document.getElementById("cub-monitor-pic");
monitorPic.src = config.image;
monitorPic.alt = config.imageAlt;
monitorPic.width = config.imageWidth;
monitorPic.height = config.imageHeight;
}
/** This tick's time spent copying frames out of the emulator, and start counting afresh. */
takePaintMs() {
const ms = this.paintMsThisTick;
this.paintMsThisTick = 0;
return ms;
}
/** The longest present since last asked, and start counting afresh. */
takePresentMs() {
const ms = this.presentMsMax;
this.presentMsMax = 0;
return ms;
}
}
|