+ final TriangleBvh.Entry hitEntry = hit.entry;
+ if (hitEntry.lightmap != null) {
+ final Lightmap lm = hitEntry.lightmap;
+ final float[] prevR = lm.deltaPrevR;
+ if (prevR == null)
+ return; // raced with a bake restart: no gather source yet
+ final int hitTexel = lm.texelAt(hit.pointX, hit.pointY, hit.pointZ);
+ eR = prevR[hitTexel];
+ eG = lm.deltaPrevG[hitTexel];
+ eB = lm.deltaPrevB[hitTexel];
+ } else {
+ final GiState hitState = states.get(stateKey(hitEntry));
+ if (hitState == null)
+ return;
+ eR = hitState.deltaPrevR;
+ eG = hitState.deltaPrevG;
+ eB = hitState.deltaPrevB;
+ }
+ }
+
+ final Color hitColor = colorOf(hit.entry);
+ out[0] = BOUNCE_GAIN * hitColor.r * eR / 255.0;
+ out[1] = BOUNCE_GAIN * hitColor.g * eG / 255.0;
+ out[2] = BOUNCE_GAIN * hitColor.b * eB / 255.0;
+ }
+
+ // ------------------------------------------------------------------
+ // Generation barrier
+ // ------------------------------------------------------------------
+
+ /**
+ * Rolls the scene over to the next generation once the queue is
+ * drained and no sampling is in flight: folds every entry's finished
+ * delta into its accumulated total, publishes the delta as the next
+ * generation's gather source, then either advances the generation
+ * counter or — on the generation cap / a calm delta — completes the
+ * bake with a final composite pass and frees all per-texel arrays.
+ */
+ private void advanceGeneration(final Snapshot snap) {
+ synchronized (genLock) {
+ if (snap.baked)
+ return; // teardown already done by another worker
+ if (snap.cursor.get() < snap.queue.length || snap.inFlight.get() > 0)
+ return;
+ if (snap != snapshot)
+ return; // a rebuild replaced the snapshot while we waited
+
+ final int finished = snap.generation;
+ double energy = 0;
+ long units = 0;
+ if (finished >= 2) {
+ for (final Lightmap lightmap : snap.lightmaps) {
+ energy += lightmap.rollover();
+ units += lightmap.validTexels.length;
+ }
+ for (final GiState state : states.values()) {
+ synchronized (state) {
+ energy += Math.max(Math.abs(state.deltaCurR),
+ Math.max(Math.abs(state.deltaCurG), Math.abs(state.deltaCurB)));
+ units++;
+ state.indirectR += state.deltaCurR;
+ state.indirectG += state.deltaCurG;
+ state.indirectB += state.deltaCurB;
+ state.deltaPrevR = state.deltaCurR;
+ state.deltaPrevG = state.deltaCurG;
+ state.deltaPrevB = state.deltaCurB;
+ state.deltaCurR = 0;
+ state.deltaCurG = 0;
+ state.deltaCurB = 0;
+ }
+ }
+ // Mesh blocks paint from their giLight array: publish
+ // direct + accumulated indirect at the barrier.
+ for (final TriangleBvh.Entry entry : snap.entries) {
+ if (entry.block == null)
+ continue;
+ final GiState state = states.get(entry);
+ if (state != null)
+ entry.block.setGiLight(entry.blockTri,
+ state.directR + state.indirectR,
+ state.directG + state.indirectG,
+ state.directB + state.indirectB);
+ }
+ }
+ final double meanDelta = units == 0 ? 0 : energy / units;
+
+ if (finished >= MAX_GENERATIONS || (finished >= 2 && meanDelta < DELTA_THRESHOLD)) {
+ // Final composite with no sampling in flight, then free
+ // all per-texel state. Holding the composite single-flight
+ // keeps a concurrent timed updateComposites from racing
+ // the last writes; baked=true makes later attempts skip.
+ while (!compositeUpdateInFlight.compareAndSet(false, true))
+ Thread.yield();
+ try {
+ for (final Lightmap lightmap : snap.lightmaps) {
+ compositeLightmap(snap, lightmap);
+ lightmap.freeArrays();
+ }
+ snap.baked = true;
+ } finally {
+ compositeUpdateInFlight.set(false);
+ }
+ if (DEBUG)
+ System.out.println("[GI] bake done after generation " + finished
+ + ", mean delta " + String.format("%.3f", meanDelta)
+ + " — texel arrays freed");
+ } else {
+ snap.generation = finished + 1;
+ snap.cursor.set(0);
+ if (DEBUG)
+ System.out.println("[GI] generation " + finished + " done, mean delta "
+ + String.format("%.3f", meanDelta)
+ + " -> generation " + (finished + 1));
+ }