diff --git a/README.md b/README.md index 7d5c5cd..d4e27ef 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,12 @@ CUDA app that uses various clever tricks to produce mushroom islands of adequate - Change GPU target for PTX compilation - `ARCH=native` is default for GPUs 30 series or older - `ARCH=sm_89` is default for GPUs 40 series or newer + - CPU_ARCH + - Optional CPU architecture tuning for the GCC/Clang build path. The default + remains portable; use `CPU_ARCH=native` when the binary will run only on + the machine that builds it. + - Example usage + `make -B CPU_ARCH=native` - PRINT_INTERVAL - Change how often the program prints benchmarking info - Default is 256, meaning every 256 iterations of the full GPU pipeline, it will print the table with stats. @@ -60,3 +66,4 @@ CUDA app that uses various clever tricks to produce mushroom islands of adequate - Example usage `make -B LARGE_BIOMES=1` for large biomes `make -B LARGE_BIOMES=0` for small biomes (default) + diff --git a/makefile b/makefile index 15afc77..32393b9 100644 --- a/makefile +++ b/makefile @@ -14,6 +14,7 @@ else endif PRINT_INTERVAL ?= 256 +CPU_ARCH ?= # Auto-detect GPU architecture: # - RTX 40xx/50xx series: sm_89 is faster than native sm_120 # - Everything else: use native @@ -31,6 +32,9 @@ endif $(info Using ARCH = $(ARCH)) override CFLAGS += -O3 +ifneq ($(strip $(CPU_ARCH)),) + override CFLAGS += -march=$(CPU_ARCH) +endif override CXXFLAGS += -O3 -std=c++20 -I asio/asio/include -DOMISSION_LARGE_BIOMES=$(LARGE_BIOMES) -DOMISSION_UNBOUND=$(UNBOUND) -DPRINT_INTERVAL=$(PRINT_INTERVAL) override NVCC_FLAGS += $(CXXFLAGS) --expt-relaxed-constexpr --default-stream per-thread -arch=$(ARCH) -use_fast_math @@ -105,3 +109,4 @@ server.o: src/server.cpp src/server.h src/common.h main: $(MAIN_DEP) $(MAIN_CXX) $(MAIN_SRC) -o $@-$(BIN_SUFFIX) $(MAIN_CXXFLAGS) endif + diff --git a/src/cubiomes.c b/src/cubiomes.c index e01c46f..74b50a4 100644 --- a/src/cubiomes.c +++ b/src/cubiomes.c @@ -6,12 +6,55 @@ #include #include +typedef struct FloodEntry { + int i; + int j; + int d; +} FloodEntry; + struct Cubiomes { Generator g; + uint8_t *visited; + size_t visited_capacity; + uint32_t *candidates; + size_t candidates_capacity; + FloodEntry *queue; + size_t queue_capacity; }; +// getSpline is implemented in the bundled cubiomes biomenoise module, but is +// not part of its public header. +float getSpline(const Spline *sp, const float *vals); + +static void ensure_capacity(void **buffer, size_t *capacity, size_t needed, + size_t element_size, const char *name) { + if (*capacity >= needed) + return; + + size_t new_capacity = *capacity ? *capacity : 4096; + while (new_capacity < needed) { + if (new_capacity > SIZE_MAX / 2) { + new_capacity = needed; + break; + } + new_capacity *= 2; + } + if (new_capacity > SIZE_MAX / element_size) { + fprintf(stderr, "%s is too large\n", name); + abort(); + } + + void *resized = realloc(*buffer, new_capacity * element_size); + if (resized == NULL) { + fprintf(stderr, "could not allocate %s\n", name); + abort(); + } + *buffer = resized; + *capacity = new_capacity; +} + Cubiomes *cubiomes_create(int large_biomes) { - Cubiomes *cubiomes = malloc(sizeof(Cubiomes)); + Cubiomes *cubiomes = calloc(1, sizeof(Cubiomes)); if (cubiomes == NULL) { fprintf(stderr, "cubiomes_create failed\n"); abort(); @@ -21,6 +64,9 @@ Cubiomes *cubiomes_create(int large_biomes) { } void cubiomes_free(Cubiomes *cubiomes) { + free(cubiomes->visited); + free(cubiomes->candidates); + free(cubiomes->queue); free(cubiomes); } @@ -28,13 +74,81 @@ void cubiomes_apply_seed(Cubiomes *cubiomes, uint64_t seed) { applySeed(&cubiomes->g, DIM_OVERWORLD, seed); } +static int has_mushroom_continentalness(Generator *g, int scale, int x, int z) { + int qx = x; + int qz = z; + double px; + double pz; + + if (scale > 4) { + int qscale = scale / 4; + int mid = qscale / 2; + qx = x * qscale + mid; + qz = z * qscale + mid; + px = qx; + pz = qz; + } else { + px = qx + sampleDoublePerlin(&g->bn.climate[NP_SHIFT], qx, 0, qz) * 4.0; + pz = qz + sampleDoublePerlin(&g->bn.climate[NP_SHIFT], qz, qx, 0) * 4.0; + } + + float c = sampleDoublePerlin(&g->bn.climate[NP_CONTINENTALNESS], px, 0, pz); + return (int64_t)(10000.0F * c) <= -10500; +} + static int eval(Generator *g, int scale, int x, int y, int z, void *data) { - double px = x, pz = z; - px += sampleDoublePerlin(&g->bn.climate[NP_SHIFT], x, 0, z) * 4.0; - pz += sampleDoublePerlin(&g->bn.climate[NP_SHIFT], x, z, 0) * 4.0; + (void)y; + (void)data; + return has_mushroom_continentalness(g, scale, x, z); +} + +static int is_mushroom_fields(Generator *g, int scale, int x, int y, int z) { + uint64_t dat = 0; + uint64_t *p_dat = NULL; + double px; + double pz; + + if (scale > 4) { + int qscale = scale / 4; + int mid = qscale / 2; + x = x * qscale + mid; + z = z * qscale + mid; + px = x; + pz = z; + p_dat = &dat; + } else { + px = x + sampleDoublePerlin(&g->bn.climate[NP_SHIFT], x, 0, z) * 4.0; + pz = z + sampleDoublePerlin(&g->bn.climate[NP_SHIFT], z, x, 0) * 4.0; + } + + float c = sampleDoublePerlin(&g->bn.climate[NP_CONTINENTALNESS], px, 0, pz); + int64_t c_quantized = (int64_t)(10000.0F * c); + // Mushroom fields have no parameter point above this continentalness. + // Rejecting here avoids sampling the other climate fields for most cells. + if (c_quantized > -10500) + return 0; - double c = sampleDoublePerlin(&g->bn.climate[NP_CONTINENTALNESS], px, 0, pz); - return c < -1.05f; + // This mirrors the remainder of sampleBiomeNoise(), without resampling + // shifts and continentalness or allocating getBiomeAt()'s one-cell cache. + float e = sampleDoublePerlin(&g->bn.climate[NP_EROSION], px, 0, pz); + float w = sampleDoublePerlin(&g->bn.climate[NP_WEIRDNESS], px, 0, pz); + float np_param[] = { + c, e, -3.0F * (fabsf(fabsf(w) - 0.6666667F) - 0.33333334F), w, + }; + double off = getSpline(g->bn.sp, np_param) + 0.015F; + float d = 1.0 - (y * 4) / 128.0 - 83.0 / 160.0 + off; + float t = sampleDoublePerlin(&g->bn.climate[NP_TEMPERATURE], px, 0, pz); + float h = sampleDoublePerlin(&g->bn.climate[NP_HUMIDITY], px, 0, pz); + + int64_t np[] = { + (int64_t)(10000.0F * t), + (int64_t)(10000.0F * h), + c_quantized, + (int64_t)(10000.0F * e), + (int64_t)(10000.0F * d), + (int64_t)(10000.0F * w), + }; + return climateToBiome(g->mc, (const uint64_t *)np, p_dat) == mushroom_fields; } static Range make_range(int32_t x, int32_t z, int32_t range, int32_t scale) { @@ -51,45 +165,49 @@ static Range make_range(int32_t x, int32_t z, int32_t range, int32_t scale) { struct locate_info_t { + Cubiomes *cubiomes; Generator *g; - int *ids; Range r; - int match, tol; + int tol; volatile char *stop; }; +static int is_visited(const Cubiomes *cubiomes, size_t index) +{ + return (cubiomes->visited[index >> 3] >> (index & 7)) & 1; +} + +static void mark_visited(Cubiomes *cubiomes, size_t index) +{ + cubiomes->visited[index >> 3] |= (uint8_t)(1u << (index & 7)); +} + static int floodFillGen(struct locate_info_t *info, int i, int j, Pos *p) { - typedef struct { int i, j, d; } entry_t; - entry_t *queue = (entry_t*) malloc(info->r.sx*info->r.sz * sizeof(*queue)); - int qn = 1; - queue->i = i; - queue->j = j; - queue->d = 0; + Cubiomes *cubiomes = info->cubiomes; + ensure_capacity((void **)&cubiomes->queue, &cubiomes->queue_capacity, + 1, sizeof(*cubiomes->queue), "flood-fill queue"); + size_t qn = 1; + cubiomes->queue[0] = (FloodEntry){i, j, 0}; int64_t sumx = 0; int64_t sumz = 0; int n = 0; - while (--qn >= 0) + while (qn != 0) { + FloodEntry entry = cubiomes->queue[--qn]; if (info->stop && *info->stop) - { - free(queue); return 0; - } - int d = queue[qn].d; - i = queue[qn].i; - j = queue[qn].j; - int k = j * info->r.sx + i; - int id = info->ids[k]; - if (id == INT_MAX) + int d = entry.d; + i = entry.i; + j = entry.j; + size_t index = (size_t)j * info->r.sx + i; + if (is_visited(cubiomes, index)) continue; - info->ids[k] = INT_MAX; + mark_visited(cubiomes, index); int x = info->r.x + i; int z = info->r.z + j; - if (info->g->mc >= MC_1_18) - id = getBiomeAt(info->g, info->r.scale, x, info->r.y, z); - if (id == info->match) + if (is_mushroom_fields(info->g, info->r.scale, x, info->r.y, z)) { sumx += x; sumz += z; @@ -101,18 +219,19 @@ int floodFillGen(struct locate_info_t *info, int i, int j, Pos *p) if (++d >= info->tol) continue; } - entry_t next[] = { {i,j-1,d}, {i,j+1,d}, {i-1,j,d}, {i+1,j,d} }; - for (k = 0; k < 4; k++) + FloodEntry next[] = { {i,j-1,d}, {i,j+1,d}, {i-1,j,d}, {i+1,j,d} }; + ensure_capacity((void **)&cubiomes->queue, &cubiomes->queue_capacity, + qn + 4, sizeof(*cubiomes->queue), "flood-fill queue"); + for (int k = 0; k < 4; k++) { i = next[k].i; j = next[k].j; if (i < 0 || i >= info->r.sx || j < 0 || j >= info->r.sz) continue; - if (info->ids[j * info->r.sx + i] == INT_MAX) + if (is_visited(cubiomes, (size_t)j * info->r.sx + i)) continue; - queue[qn++] = next[k]; + cubiomes->queue[qn++] = next[k]; } } - free(queue); if (n) { p->x = (int) round((sumx / (double)n + 0.5) * info->r.scale); @@ -122,140 +241,97 @@ int floodFillGen(struct locate_info_t *info, int i, int j, Pos *p) } static -int getBiomeCentersOpt(Pos *pos, int *siz, int nmax, Generator *g, Range r, +int getBiomeCentersOpt(Pos *pos, int *siz, int nmax, Cubiomes *cubiomes, Range r, int match, int minsiz, int tol, int step, volatile char *stop) { + Generator *g = &cubiomes->g; if (minsiz <= 0) minsiz = 1; int i, j, k, n = 0; - int *ids = (int*) malloc(r.sx*r.sz * sizeof(int)); - memset(ids, -1, r.sx*r.sz * sizeof(int)); + size_t cell_count = (size_t)r.sx * r.sz; + if (cell_count > UINT32_MAX) { + fprintf(stderr, "biome-center range is too large\n"); + abort(); + } + size_t visited_size = (cell_count + 7) / 8; + ensure_capacity((void **)&cubiomes->visited, &cubiomes->visited_capacity, + visited_size, sizeof(*cubiomes->visited), "visited bitmap"); + memset(cubiomes->visited, 0, visited_size); if (tol <= 0) tol = 1; if (step <= 0) step = 1; struct locate_info_t info; + info.cubiomes = cubiomes; info.g = g; - info.ids = ids; info.r = r; info.stop = stop; - info.match = match; info.tol = tol; - if (g->mc >= MC_1_18) - { - const int *lim = getBiomeParaLimits(g->mc, match); - - int para[] = { - NP_TEMPERATURE, - NP_HUMIDITY, - NP_EROSION, - NP_CONTINENTALNESS, - NP_WEIRDNESS, - }; - int npara = sizeof(para) / sizeof(para[0]); - if (step == 1) - step = 1 + floor(sqrt(minsiz) * 0.5); - - for (j = 0; j < r.sz; j += step) - { - for (i = 0; i < r.sx; i += step) - { - if (stop && *stop) - break; - for (k = 0; k < npara; k++) - { - const int *plim = lim + 2*para[k]; - if (plim[0] == INT_MIN && plim[1] == INT_MAX) - continue; - DoublePerlinNoise *dpn = &g->bn.climate[para[k]]; - double px = (r.x+i) * r.scale / 4.0; - double pz = (r.z+j) * r.scale / 4.0; - int p = 10000 * sampleDoublePerlin(dpn, px, 0, pz); - if (p < plim[0] || p > plim[1]) - { - ids[j*r.sx + i] = -2; - break; - } - } - } - } - match = -1; // id entries that are still -1 are our candidates - } - else // 1.17- - { - int ts = 32 / r.scale; - if (r.sx + r.sz < 32) - ts = 8; - - int tx = (int) floor(r.x / (double)ts); - int tz = (int) floor(r.z / (double)ts); - int tw = (int) ceil((r.x+r.sx) / (double)ts) - tx; - int th = (int) ceil((r.z+r.sz) / (double)ts) - tz; - int ti, tj; - - BiomeFilter bf; - setupBiomeFilter(&bf, g->mc, 0, &match, 1, 0, 0, 0, 0); - //applySeed(g, 0, g->seed); + const int *lim = getBiomeParaLimits(g->mc, match); + size_t candidates_len = 0; - Range tr = { r.scale, 0, 0, ts, ts, 0, 1 }; - int *cache = allocCache(g, r); + int para[] = { + NP_TEMPERATURE, + NP_HUMIDITY, + NP_EROSION, + NP_CONTINENTALNESS, + NP_WEIRDNESS, + }; + int npara = sizeof(para) / sizeof(para[0]); + if (step == 1) + step = 1 + floor(sqrt(minsiz) * 0.5); - for (tj = 0; tj < th; tj++) + for (j = 0; j < r.sz; j += step) + { + for (i = 0; i < r.sx; i += step) { - for (ti = 0; ti < tw; ti++) + if (stop && *stop) + break; + int candidate = 1; + for (k = 0; k < npara; k++) { - if (stop && *stop) - break; - tr.x = (tx+ti) * ts; - tr.z = (tz+tj) * ts; - if (checkForBiomes(g, cache, tr, DIM_OVERWORLD, g->seed, - &bf, stop) != 1) - { + const int *plim = lim + 2*para[k]; + if (plim[0] == INT_MIN && plim[1] == INT_MAX) continue; - } - for (j = 0; j < ts; j++) + DoublePerlinNoise *dpn = &g->bn.climate[para[k]]; + double px = (r.x+i) * r.scale / 4.0; + double pz = (r.z+j) * r.scale / 4.0; + int p = 10000 * sampleDoublePerlin(dpn, px, 0, pz); + if (p < plim[0] || p > plim[1]) { - int jj = tr.z + j - r.z; - if (jj < 0 || jj >= r.sz) - continue; - for (i = 0; i < ts; i++) - { - int ii = tr.x + i - r.x; - if (ii < 0 || ii >= r.sx) - continue; - ids[jj*r.sx + ii] = cache[j*tr.sx + i]; - } + candidate = 0; + break; } } + if (candidate) { + ensure_capacity((void **)&cubiomes->candidates, + &cubiomes->candidates_capacity, + candidates_len + 1, sizeof(*cubiomes->candidates), + "biome-center candidates"); + cubiomes->candidates[candidates_len++] = (uint32_t)((size_t)j * r.sx + i); + } } - free(cache); } - // applySeed(g, DIM_OVERWORLD, g->seed); - for (j = 0; j < r.sz; j += step) + for (size_t candidate_index = 0; candidate_index < candidates_len; candidate_index++) { - for (i = 0; i < r.sx; i += step) + size_t index = cubiomes->candidates[candidate_index]; + if (is_visited(cubiomes, index)) + continue; + i = (int)(index % r.sx); + j = (int)(index / r.sx); + Pos center; + int area = floodFillGen(&info, i, j, ¢er); + if (area >= minsiz) { - if (stop && *stop) + pos[n] = center; + if (siz) siz[n] = area; + if (++n >= nmax) break; - if (ids[j*r.sx + i] != match) - continue; - Pos center; - int area = floodFillGen(&info, i, j, ¢er); - if (area >= minsiz) - { - pos[n] = center; - if (siz) siz[n] = area; - if (++n >= nmax) - goto L_end; - } } } -L_end: - free(ids); - return n; } @@ -271,7 +347,7 @@ int cubiomes_test_biome_centers(Cubiomes *cubiomes, int32_t x, int32_t z, int32_ int siz; Range r = make_range(x, z, range, scale); int minsiz = min_area / (scale * scale); - int n = getBiomeCentersOpt(&pos, &siz, 1, &cubiomes->g, r, mushroom_fields, minsiz, tol, 0, NULL); + int n = getBiomeCentersOpt(&pos, &siz, 1, cubiomes, r, mushroom_fields, minsiz, tol, 0, NULL); if (n == 1) { if (out) { *out = (PosArea){ @@ -284,3 +360,4 @@ int cubiomes_test_biome_centers(Cubiomes *cubiomes, int32_t x, int32_t z, int32_ } return 0; } +