Fix black spots in lowres layer (syncronization issue)

This commit is contained in:
Lukas Rieger (Blue) 2022-08-15 23:43:06 +02:00
parent 0a4d85982c
commit 5448850eca
No known key found for this signature in database
GPG Key ID: 2D09EC5ED2687FF2
3 changed files with 194 additions and 185 deletions

View File

@ -0,0 +1,158 @@
package de.bluecolored.bluemap.core.map.lowres;
import com.flowpowered.math.vector.Vector2i;
import com.github.benmanes.caffeine.cache.*;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.util.Vector2iCache;
import de.bluecolored.bluemap.core.util.math.Color;
import de.bluecolored.bluemap.core.world.Grid;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.TimeUnit;
public class LowresLayer {
private static final Vector2iCache VECTOR_2_I_CACHE = new Vector2iCache();
private final Storage.MapStorage mapStorage;
private final Grid tileGrid;
private final int lodFactor;
private final int lod;
private final LoadingCache<Vector2i, LowresTile> tileCache;
@Nullable private final LowresLayer nextLayer;
public LowresLayer(
Storage.MapStorage mapStorage, Grid tileGrid, int lodCount, int lodFactor,
int lod, @Nullable LowresLayer nextLayer
) {
this.mapStorage = mapStorage;
this.tileGrid = tileGrid;
this.lodFactor = lodFactor;
this.lod = lod;
this.nextLayer = nextLayer;
// this extra cache makes sure that a tile instance is reused as long as it is still referenced somewhere ..
// so always only one instance of the same lowres-tile exists
LoadingCache<Vector2i, LowresTile> tileWeakInstanceCache = Caffeine.newBuilder()
.executor(BlueMap.THREAD_POOL)
.weakValues()
.build(this::createTile);
this.tileCache = Caffeine.newBuilder()
.executor(BlueMap.THREAD_POOL)
.scheduler(Scheduler.systemScheduler())
.expireAfterAccess(10, TimeUnit.SECONDS)
.expireAfterWrite(5, TimeUnit.MINUTES)
.removalListener(this::saveTile)
.build(tileWeakInstanceCache::get);
}
public void save() {
Logger.global.logInfo("Saving lowres layer " + lod);
tileCache.invalidateAll();
tileCache.cleanUp();
}
private LowresTile createTile(Vector2i tilePos) {
try (InputStream in = mapStorage.read(lod, tilePos).orElse(null)) {
if (in == null)
return new LowresTile(tileGrid.getGridSize());
return new LowresTile(tileGrid.getGridSize(), in);
} catch (IOException e) {
Logger.global.logError("Failed to load tile " + tilePos + " (lod: " + lod + ")", e);
return null;
}
}
private void saveTile(Vector2i tilePos, @Nullable LowresTile tile, RemovalCause removalCause) {
if (tile == null) return;
// save the tile
try (OutputStream out = mapStorage.write(lod, tilePos)) {
tile.save(out);
} catch (IOException e) {
Logger.global.logError("Failed to save tile " + tilePos + " (lod: " + lod + ")", e);
}
// write to next LOD (prepare for the most confusing grid-math you will ever see)
if (this.nextLayer == null) return;
Color averageColor = new Color();
int averageHeight, averageBlockLight;
int count;
Color color = new Color();
int nextLodTileX = Math.floorDiv(tilePos.getX(), lodFactor);
int nextLodTileY = Math.floorDiv(tilePos.getY(), lodFactor);
Vector2i groupCount = new Vector2i(
Math.floorDiv(tileGrid.getGridSize().getX(), lodFactor),
Math.floorDiv(tileGrid.getGridSize().getY(), lodFactor)
);
for (int gX = 0; gX < groupCount.getX(); gX++) {
for (int gY = 0; gY < groupCount.getY(); gY++) {
averageColor.set(0, 0, 0, 0, true);
averageHeight = 0;
averageBlockLight = 0;
count = 0;
for (int x = 0; x < lodFactor; x++) {
for (int y = 0; y < lodFactor; y++) {
count++;
averageColor.add(tile.getColor(gX * lodFactor + x, gY * lodFactor + y, color).premultiplied());
averageHeight += tile.getHeight(gX * lodFactor + x, gY * lodFactor + y);
averageBlockLight += tile.getBlockLight(gX * lodFactor + x, gY * lodFactor + y);
}
}
averageColor.div(count);
averageHeight /= count;
averageBlockLight /= count;
this.nextLayer.set(
nextLodTileX,
nextLodTileY,
Math.floorMod(tilePos.getX(), lodFactor) * groupCount.getX() + gX,
Math.floorMod(tilePos.getY(), lodFactor) * groupCount.getY() + gY,
averageColor,
averageHeight,
averageBlockLight
);
}
}
}
private LowresTile getTile(int x, int z) {
return tileCache.get(VECTOR_2_I_CACHE.get(x, z));
}
void set(int cellX, int cellZ, int pixelX, int pixelZ, Color color, int height, int blockLight) {
getTile(cellX, cellZ)
.set(pixelX, pixelZ, color, height, blockLight);
// for seamless edges
if (pixelX == 0) {
getTile(cellX - 1, cellZ)
.set(tileGrid.getGridSize().getX(), pixelZ, color, height, blockLight);
}
if (pixelZ == 0) {
getTile(cellX, cellZ - 1)
.set(pixelX, tileGrid.getGridSize().getY(), color, height, blockLight);
}
if (pixelX == 0 && pixelZ == 0) {
getTile(cellX - 1, cellZ - 1)
.set(tileGrid.getGridSize().getX(), tileGrid.getGridSize().getY(), color, height, blockLight);
}
}
}

View File

@ -8,16 +8,17 @@ import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class LowresTile {
public static final int HEIGHT_UNDEFINED = Integer.MIN_VALUE;
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final BufferedImage texture;
private final Vector2i size;
private volatile boolean closed = false;
public LowresTile(Vector2i tileSize) {
this.size = tileSize.add(1, 1); // add 1 for seamless edges
this.texture = new BufferedImage(this.size.getX(), this.size.getY() * 2, BufferedImage.TYPE_INT_ARGB);
@ -32,14 +33,18 @@ public class LowresTile {
}
}
public void set(int x, int z, Color color, int height, int blockLight) throws TileClosedException {
if (closed) throw new TileClosedException();
texture.setRGB(x, z, color.straight().getInt());
texture.setRGB(x, size.getY() + z,
(height & 0x0000FFFF) |
((blockLight << 16) & 0x00FF0000) |
0xFF000000
);
public void set(int x, int z, Color color, int height, int blockLight) {
lock.readLock().lock();
try {
texture.setRGB(x, z, color.straight().getInt());
texture.setRGB(x, size.getY() + z,
(height & 0x0000FFFF) |
((blockLight << 16) & 0x00FF0000) |
0xFF000000
);
} finally {
lock.readLock().unlock();
}
}
public Color getColor(int x, int z, Color target) {
@ -58,16 +63,11 @@ public class LowresTile {
}
public void save(OutputStream out) throws IOException {
ImageIO.write(texture, "png", out);
}
public void close() {
closed = true;
}
public static class TileClosedException extends Exception {
public TileClosedException() {
super("Tile is closed");
lock.writeLock().lock();
try {
ImageIO.write(texture, "png", out);
} finally {
lock.writeLock().unlock();
}
}

View File

@ -1,193 +1,35 @@
package de.bluecolored.bluemap.core.map.lowres;
import com.flowpowered.math.vector.Vector2i;
import com.github.benmanes.caffeine.cache.*;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.TileMetaConsumer;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.util.Vector2iCache;
import de.bluecolored.bluemap.core.util.math.Color;
import de.bluecolored.bluemap.core.world.Grid;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class LowresTileManager implements TileMetaConsumer {
private final Storage.MapStorage mapStorage;
private final Grid tileGrid;
private final int lodFactor, lodCount;
private final Vector2iCache vector2iCache;
private final List<LoadingCache<Vector2i, LowresTile>> tileCaches;
private final LowresLayer[] layers;
public LowresTileManager(Storage.MapStorage mapStorage, Grid tileGrid, int lodCount, int lodFactor) {
this.mapStorage = mapStorage;
this.tileGrid = tileGrid;
this.lodFactor = lodFactor;
this.lodCount = lodCount;
this.vector2iCache = new Vector2iCache();
List<LoadingCache<Vector2i, LowresTile>> tileCacheList = new ArrayList<>();
for (int i = 0; i < lodCount; i++) {
int lod = i + 1;
tileCacheList.add(Caffeine.newBuilder()
.executor(BlueMap.THREAD_POOL)
.scheduler(Scheduler.systemScheduler())
.expireAfterAccess(10, TimeUnit.SECONDS)
.expireAfterWrite(5, TimeUnit.MINUTES)
.writer(new CacheWriter<Vector2i, LowresTile>() {
@Override
public void write(@NonNull Vector2i key, @NonNull LowresTile value) {}
@Override
public void delete(@NonNull Vector2i key, @Nullable LowresTile value, @NonNull RemovalCause cause) {
if (value != null)
saveTile(key, lod, value, cause);
}
})
.build(key -> loadTile(key, lod))
);
}
this.tileCaches = Collections.unmodifiableList(tileCacheList);
}
private LowresTile loadTile(Vector2i tilePos, int lod) {
try (InputStream in = mapStorage.read(lod, tilePos).orElse(null)) {
if (in == null)
return new LowresTile(tileGrid.getGridSize());
return new LowresTile(tileGrid.getGridSize(), in);
} catch (IOException e) {
Logger.global.logError("Failed to load tile " + tilePos + " (lod: " + lod + ")", e);
return null;
}
}
@SuppressWarnings("unused")
private void saveTile(Vector2i tilePos, int lod, LowresTile tile, RemovalCause removalCause) {
// close the tile so it can't be edited anymore
tile.close();
// save the tile
try (OutputStream out = mapStorage.write(lod, tilePos)) {
tile.save(out);
} catch (IOException e) {
Logger.global.logError("Failed to save tile " + tilePos + " (lod: " + lod + ")", e);
}
if (lod >= lodCount) return;
// write to next LOD (prepare for the most confusing grid-math you will ever see)
Color averageColor = new Color();
int averageHeight, averageBlockLight;
int count;
Color color = new Color();
int nextLodTileX = Math.floorDiv(tilePos.getX(), lodFactor);
int nextLodTileY = Math.floorDiv(tilePos.getY(), lodFactor);
Vector2i groupCount = new Vector2i(
Math.floorDiv(tileGrid.getGridSize().getX(), lodFactor),
Math.floorDiv(tileGrid.getGridSize().getY(), lodFactor)
);
for (int gX = 0; gX < groupCount.getX(); gX++) {
for (int gY = 0; gY < groupCount.getY(); gY++) {
averageColor.set(0, 0, 0, 0, true);
averageHeight = 0;
averageBlockLight = 0;
count = 0;
for (int x = 0; x < lodFactor; x++) {
for (int y = 0; y < lodFactor; y++) {
count++;
averageColor.add(tile.getColor(gX * lodFactor + x, gY * lodFactor + y, color).premultiplied());
averageHeight += tile.getHeight(gX * lodFactor + x, gY * lodFactor + y);
averageBlockLight += tile.getBlockLight(gX * lodFactor + x, gY * lodFactor + y);
}
}
averageColor.div(count);
averageHeight /= count;
averageBlockLight /= count;
set(
nextLodTileX,
nextLodTileY,
lod + 1,
Math.floorMod(tilePos.getX(), lodFactor) * groupCount.getX() + gX,
Math.floorMod(tilePos.getY(), lodFactor) * groupCount.getY() + gY,
averageColor,
averageHeight,
averageBlockLight
);
}
this.layers = new LowresLayer[lodCount];
for (int i = lodCount - 1; i >= 0; i--) {
this.layers[i] = new LowresLayer(mapStorage, tileGrid, lodCount, lodFactor, i + 1,
(i == lodCount - 1) ? null : layers[i + 1]);
}
}
public synchronized void save() {
for (LoadingCache<Vector2i, LowresTile> cache : this.tileCaches) {
cache.invalidateAll();
cache.cleanUp();
for (LowresLayer layer : this.layers) {
layer.save();
}
}
public LowresTile getTile(int x, int z, int lod) {
return tileCaches.get(lod - 1).get(vector2iCache.get(x, z));
}
@Override
public void set(int x, int z, Color color, int height, int blockLight) {
int cellX = tileGrid.getCellX(x);
int cellZ = tileGrid.getCellY(z);
int localX = tileGrid.getLocalX(x);
int localZ = tileGrid.getLocalY(z);
set(cellX, cellZ, 1, localX, localZ, color, height, blockLight);
}
private void set(int cellX, int cellZ, int lod, int pixelX, int pixelZ, Color color, int height, int blockLight) {
int tries = 0;
LowresTile.TileClosedException closedException;
do {
tries ++;
closedException = null;
try {
getTile(cellX, cellZ, lod)
.set(pixelX, pixelZ, color, height, blockLight);
// for seamless edges
if (pixelX == 0) {
getTile(cellX - 1, cellZ, lod)
.set(tileGrid.getGridSize().getX(), pixelZ, color, height, blockLight);
}
if (pixelZ == 0) {
getTile(cellX, cellZ - 1, lod)
.set(pixelX, tileGrid.getGridSize().getY(), color, height, blockLight);
}
if (pixelX == 0 && pixelZ == 0) {
getTile(cellX - 1, cellZ - 1, lod)
.set(tileGrid.getGridSize().getX(), tileGrid.getGridSize().getY(), color, height, blockLight);
}
} catch (LowresTile.TileClosedException ex) {
closedException = ex;
}
} while (closedException != null && tries < 10);
}
public Grid getTileGrid() {
return tileGrid;
}
@ -200,4 +42,13 @@ public class LowresTileManager implements TileMetaConsumer {
return lodFactor;
}
@Override
public void set(int x, int z, Color color, int height, int blockLight) {
int cellX = tileGrid.getCellX(x);
int cellZ = tileGrid.getCellY(z);
int localX = tileGrid.getLocalX(x);
int localZ = tileGrid.getLocalY(z);
layers[0].set(cellX, cellZ, localX, localZ, color, height, blockLight);
}
}