@photivra/engine 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -390,8 +390,35 @@ export function createDisplayViewExpectedSignal(input) {
390
390
  ringMaterial[q] = lastMaterial;
391
391
  return v;
392
392
  }
393
- /** A site not in the window's site cache: from the ring, a filled window without the ring, the lazy window caches, or uncached. */
393
+ // Without diffraction (#392): site values persist across windows the same way, in their own ring (a site's value does not
394
+ // depend on the window). A ring hit counts the lookups directSite would have made, so `lookups` is unchanged.
395
+ let directRows = 0, directOn = false, directRing = new Float64Array(0), directRowId = new Int32Array(0);
396
+ const directLookups = offsets.length * (scene.kind === "materials" ? 1 : nodes);
397
+ function directRead(x, y) {
398
+ const slot = y & (directRows - 1), q = slot * W + x;
399
+ if (directRowId[slot] === y) {
400
+ const v = directRing[q];
401
+ if (v === v) {
402
+ lookups += directLookups;
403
+ return v;
404
+ }
405
+ }
406
+ else {
407
+ directRowId[slot] = y;
408
+ directRing.fill(NaN, slot * W, (slot + 1) * W);
409
+ }
410
+ const v = directSite(x, y);
411
+ directRing[q] = v;
412
+ return v;
413
+ }
414
+ /** A site not in the window's site cache: from a ring, a filled window without the ring, the lazy window caches, or uncached. */
394
415
  function siteMiss(x, y, i, j, cached) {
416
+ if (directOn) {
417
+ const v = directRead(x, y);
418
+ if (cached)
419
+ siteCache[j * sw + i] = v;
420
+ return v;
421
+ }
395
422
  if (ringOn) {
396
423
  if (!cached) {
397
424
  outsideRead(i, j);
@@ -480,6 +507,7 @@ export function createDisplayViewExpectedSignal(input) {
480
507
  get lookups() { return lookups + (filled ? windowLookups() : 0); },
481
508
  get uniformMaterial() { return lastMaterial; },
482
509
  get tracksMaterial() { return ringOn; },
510
+ get cachesSites() { return sw > 0; },
483
511
  window(x0, y0, x1, y1) {
484
512
  if (filled)
485
513
  lookups += windowLookups();
@@ -497,8 +525,21 @@ export function createDisplayViewExpectedSignal(input) {
497
525
  if (siteCache.length < sw * sh)
498
526
  siteCache = new Float64Array(sw * sh);
499
527
  siteCache.fill(NaN, 0, sw * sh);
500
- if (!input.kernels)
528
+ if (!input.kernels) {
529
+ // A ring of at least twice the window's rows keeps the rows of the previous display row's windows.
530
+ if (sh > 0 && directRows < 2 * sh) {
531
+ let rows = 1;
532
+ while (rows < 2 * sh)
533
+ rows *= 2;
534
+ if (rows * W <= CACHE_LIMIT) {
535
+ directRows = rows;
536
+ directRing = new Float64Array(rows * W).fill(NaN);
537
+ directRowId = new Int32Array(rows).fill(-1);
538
+ }
539
+ }
540
+ directOn = sh > 0 && directRows >= 2 * sh;
501
541
  return;
542
+ }
502
543
  for (const group of groups) {
503
544
  group.cx0 = x0 + group.minIx - R;
504
545
  group.cy0 = y0 + group.minIy - R;
@@ -841,6 +882,8 @@ function correctionStates(key) {
841
882
  * AHD choice terms; 33 a 1 when the AHD correction applies and [34, 37) its values.
842
883
  */
843
884
  const MEMO_SIZE = 37;
885
+ /** Most real pixels a display pixel's footprint may have for the per-real-pixel memo (#392); larger footprints rarely repeat a real pixel. */
886
+ const PIXEL_MEMO_LIMIT = 1 << 16;
844
887
  /** Pilot points per axis: about one per two sites of the footprint plus its reach, 4 to 16. */
845
888
  function pilotCount(span) { return Math.max(4, Math.min(16, Math.ceil(span / 2))); }
846
889
  /** Renders the display view (docs/DISPLAY_VIEW.md). */
@@ -1063,6 +1106,13 @@ export async function renderDisplayView(setup) {
1063
1106
  let siteCount = 0, sampleCount = 0, detail = 0, uniform = 0, lastYield = Date.now();
1064
1107
  /** Recorded terms of samples whose sites all read one chart material, by material·4 + CFA phase (layout: MEMO_SIZE). */
1065
1108
  const sampleMemo = new Map();
1109
+ // Per-real-pixel memo (#392): a sample's recorded terms depend only on its real pixel (its sites' expected electrons and its
1110
+ // CFA phase), not on the display pixel or the sample, so a later sample of the same real pixel in the same display pixel
1111
+ // replays them (the same values added to the same sums in the same order, as the one-material memo does). Slots cover the
1112
+ // display pixel's footprint and are valid while their stamp equals the display pixel's generation. A replayed sample still
1113
+ // counts its sites; it is used only where every site lies in the window's site cache, where reading it again would have
1114
+ // cost no work and no scene lookup.
1115
+ let pixelStamp = new Int32Array(0), pixelRecords = new Float64Array(0), pixelGeneration = 0;
1066
1116
  // Pilot: the scene at a deterministic grid over each display pixel's footprint widened by the reach; a pixel is uniform when
1067
1117
  // every point returns the same value. Pixels next to a detailed pixel count as detailed too, so an edge that only clips a
1068
1118
  // footprint's corner (too thin for the grid) still gets the full samples. The grid is fixed, so the sample count is not
@@ -1118,6 +1168,17 @@ export async function renderDisplayView(setup) {
1118
1168
  detail++;
1119
1169
  setup.signal.window(Math.floor(vx * sx) + margin - windowReach, Math.floor(vy * sy) + margin - windowReach, Math.ceil((vx + 1) * sx) + margin + windowReach, Math.ceil((vy + 1) * sy) + margin + windowReach);
1120
1170
  const jitter = createDeterministicUniformStream(deriveNativeSiteStreamSeedUint32(seed, index, DISPLAY_VIEW_JITTER_STREAM_ID)), tracking = setup.signal.tracksMaterial;
1171
+ const mx0 = Math.floor(vx * sx) + margin, my0 = Math.floor(vy * sy) + margin;
1172
+ const mw = Math.min(frameWidth - 1, Math.ceil((vx + 1) * sx)) + margin - mx0 + 1, mh = Math.min(frameHeight - 1, Math.ceil((vy + 1) * sy)) + margin - my0 + 1;
1173
+ const pixelMemo = setup.signal.cachesSites && mw * mh <= PIXEL_MEMO_LIMIT;
1174
+ if (pixelMemo) {
1175
+ if (pixelStamp.length < mw * mh) {
1176
+ pixelStamp = new Int32Array(mw * mh);
1177
+ pixelRecords = new Float64Array(mw * mh * MEMO_SIZE);
1178
+ pixelGeneration = 0;
1179
+ }
1180
+ pixelGeneration++;
1181
+ }
1121
1182
  let s0 = 0, s1 = 0, s2 = 0, c00 = 0, c01 = 0, c02 = 0, c11 = 0, c12 = 0, c22 = 0, r0 = 0, r1 = 0, r2 = 0, pixelVariance = 0, corrected = false;
1122
1183
  for (let b = 0; b < K; b++)
1123
1184
  for (let a = 0; a < K; a++) {
@@ -1125,53 +1186,69 @@ export async function renderDisplayView(setup) {
1125
1186
  const x = Math.min(frameWidth - 1, Math.floor(fx)) + margin, y = Math.min(frameHeight - 1, Math.floor(fy)) + margin;
1126
1187
  const phase = (y & 1) * 2 + (x & 1), list = phaseList[phase];
1127
1188
  let material = -2;
1189
+ const px = x - mx0, py = y - my0;
1190
+ const pk = pixelMemo && px >= 0 && py >= 0 && px < mw && py < mh && x >= reach && y >= reach && x + reach < nativeWidth && y + reach < nativeHeight ? py * mw + px : -1;
1191
+ let memo, mo = 0;
1192
+ if (pk >= 0 && pixelStamp[pk] === pixelGeneration) {
1193
+ memo = pixelRecords;
1194
+ mo = pk * MEMO_SIZE;
1195
+ siteCount += list.length;
1196
+ }
1128
1197
  // The expected electrons of the sites the demosaic reads (sites outside the raster, at mirror borders, are read
1129
1198
  // reflect-101); where the signal tracks it, whether they all read one chart material, with the statistics below only
1130
1199
  // when the sample is not replayed; otherwise the statistics at once.
1131
- for (let li = 0; li < list.length; li++) {
1132
- const slot = list[li];
1133
- siteCount++;
1134
- let sxs = x + (slot % side) - reach, sys = y + ((slot / side) | 0) - reach;
1135
- if (sxs < 0)
1136
- sxs = -sxs;
1137
- else if (sxs >= nativeWidth)
1138
- sxs = 2 * nativeWidth - 2 - sxs;
1139
- if (sys < 0)
1140
- sys = -sys;
1141
- else if (sys >= nativeHeight)
1142
- sys = 2 * nativeHeight - 2 - sys;
1143
- const c = (sys & 1) === 0 ? ((sxs & 1) === 0 ? 0 : 1) : ((sxs & 1) === 0 ? 1 : 2);
1144
- const e = setup.signal.site(sxs, sys) + dark[c];
1145
- siteChannel[slot] = c;
1146
- if (tracking) {
1147
- siteElectrons[slot] = e;
1148
- const own = setup.signal.uniformMaterial;
1149
- material = material === -2 || material === own ? own : -1;
1150
- continue;
1151
- }
1152
- // As siteStatistics, written out here for the untracked (Speed) path.
1153
- const kc = k[c], mean = kc * e, variance = kc * kc * e + c0[c], gap = 1 - mean;
1154
- siteThird[slot] = kc * kc * kc * e;
1155
- if (gap > 0 && gap * gap > reach2 * variance) {
1156
- siteMean[slot] = mean;
1157
- siteVar[slot] = variance;
1158
- }
1159
- else {
1160
- upperClip(mean, variance, 1, clip);
1161
- siteMean[slot] = clip[0];
1162
- siteVar[slot] = clip[1];
1200
+ if (!memo)
1201
+ for (let li = 0; li < list.length; li++) {
1202
+ const slot = list[li];
1203
+ siteCount++;
1204
+ let sxs = x + (slot % side) - reach, sys = y + ((slot / side) | 0) - reach;
1205
+ if (sxs < 0)
1206
+ sxs = -sxs;
1207
+ else if (sxs >= nativeWidth)
1208
+ sxs = 2 * nativeWidth - 2 - sxs;
1209
+ if (sys < 0)
1210
+ sys = -sys;
1211
+ else if (sys >= nativeHeight)
1212
+ sys = 2 * nativeHeight - 2 - sys;
1213
+ const c = (sys & 1) === 0 ? ((sxs & 1) === 0 ? 0 : 1) : ((sxs & 1) === 0 ? 1 : 2);
1214
+ const e = setup.signal.site(sxs, sys) + dark[c];
1215
+ siteChannel[slot] = c;
1216
+ if (tracking) {
1217
+ siteElectrons[slot] = e;
1218
+ const own = setup.signal.uniformMaterial;
1219
+ material = material === -2 || material === own ? own : -1;
1220
+ continue;
1221
+ }
1222
+ // As siteStatistics, written out here for the untracked (Speed) path.
1223
+ const kc = k[c], mean = kc * e, variance = kc * kc * e + c0[c], gap = 1 - mean;
1224
+ siteThird[slot] = kc * kc * kc * e;
1225
+ if (gap > 0 && gap * gap > reach2 * variance) {
1226
+ siteMean[slot] = mean;
1227
+ siteVar[slot] = variance;
1228
+ }
1229
+ else {
1230
+ upperClip(mean, variance, 1, clip);
1231
+ siteMean[slot] = clip[0];
1232
+ siteVar[slot] = clip[1];
1233
+ }
1163
1234
  }
1164
- }
1165
1235
  if (!tracking)
1166
1236
  material = -1;
1167
1237
  // A sample whose sites all read one chart material only has the statistics of every other such sample of that material
1168
1238
  // and CFA phase: replay its recorded terms (the same values added to the same sums in the same order).
1169
- const memoKey = material >= 0 ? material * 4 + phase : -1, memo = memoKey >= 0 ? sampleMemo.get(memoKey) : undefined;
1239
+ const memoKey = !memo && material >= 0 ? material * 4 + phase : -1;
1240
+ if (memoKey >= 0) {
1241
+ memo = sampleMemo.get(memoKey);
1242
+ if (memo && pk >= 0) {
1243
+ pixelRecords.set(memo, pk * MEMO_SIZE);
1244
+ pixelStamp[pk] = pixelGeneration;
1245
+ }
1246
+ }
1170
1247
  const cs = candidateSlots[phase], cw = candidateWeights[phase], count = cs.length, share = 1 / count;
1171
1248
  if (memo) {
1172
1249
  for (let d = 0; d < count; d++) {
1173
1250
  for (let c = 0; c < 3; c++) {
1174
- const o = 3 + (d * 3 + c) * 3, a0 = memo[o], a1 = memo[o + 1], a2 = memo[o + 2], wgt = share * B[c] * memo[c];
1251
+ const o = mo + 3 + (d * 3 + c) * 3, a0 = memo[o], a1 = memo[o + 1], a2 = memo[o + 2], wgt = share * B[c] * memo[mo + c];
1175
1252
  c00 += wgt * a0 * a0;
1176
1253
  c01 += wgt * a0 * a1;
1177
1254
  c02 += wgt * a0 * a2;
@@ -1179,27 +1256,27 @@ export async function renderDisplayView(setup) {
1179
1256
  c12 += wgt * a1 * a2;
1180
1257
  c22 += wgt * a2 * a2;
1181
1258
  }
1182
- r0 += memo[21 + d * 3];
1183
- r1 += memo[22 + d * 3];
1184
- r2 += memo[23 + d * 3];
1259
+ r0 += memo[mo + 21 + d * 3];
1260
+ r1 += memo[mo + 22 + d * 3];
1261
+ r2 += memo[mo + 23 + d * 3];
1185
1262
  }
1186
- s0 += memo[27];
1187
- s1 += memo[28];
1188
- s2 += memo[29];
1263
+ s0 += memo[mo + 27];
1264
+ s1 += memo[mo + 28];
1265
+ s2 += memo[mo + 29];
1189
1266
  if (count === 2) {
1190
- r0 += memo[30];
1191
- r1 += memo[31];
1192
- r2 += memo[32];
1267
+ r0 += memo[mo + 30];
1268
+ r1 += memo[mo + 31];
1269
+ r2 += memo[mo + 32];
1193
1270
  }
1194
- if (memo[33] === 1) {
1195
- s0 += memo[34];
1196
- s1 += memo[35];
1197
- s2 += memo[36];
1271
+ if (memo[mo + 33] === 1) {
1272
+ s0 += memo[mo + 34];
1273
+ s1 += memo[mo + 35];
1274
+ s2 += memo[mo + 36];
1198
1275
  corrected = true;
1199
1276
  }
1200
1277
  continue;
1201
1278
  }
1202
- const record = memoKey >= 0 ? new Float64Array(MEMO_SIZE) : undefined;
1279
+ const record = memoKey >= 0 ? new Float64Array(MEMO_SIZE) : pk >= 0 ? pixelRecords.subarray(pk * MEMO_SIZE, (pk + 1) * MEMO_SIZE).fill(0) : undefined;
1203
1280
  if (tracking)
1204
1281
  for (let li = 0; li < list.length; li++) {
1205
1282
  const slot = list[li];
@@ -1391,8 +1468,13 @@ export async function renderDisplayView(setup) {
1391
1468
  record[36] = fix[2];
1392
1469
  }
1393
1470
  }
1394
- if (record)
1471
+ if (record && memoKey >= 0) {
1395
1472
  sampleMemo.set(memoKey, record);
1473
+ if (pk >= 0)
1474
+ pixelRecords.set(record, pk * MEMO_SIZE);
1475
+ }
1476
+ if (pk >= 0)
1477
+ pixelStamp[pk] = pixelGeneration;
1396
1478
  pixelVariance = 0;
1397
1479
  }
1398
1480
  sampleCount += Q;