From 8a8fd9a185bcfb55ac4f8dc9a9ecd66216db39d7 Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Sun, 15 Mar 2026 23:21:16 -0500 Subject: [PATCH 01/39] POC: parallelize r.proj via RAM-resident buffer, 2.5x speedup on 8-core Apple M-series --- raster/r.proj/main.c | 119 +++++++++++++++++++++++++++++-------------- 1 file changed, 80 insertions(+), 39 deletions(-) diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index 59702cdfc92..2d57c1b52d0 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -66,6 +66,10 @@ #include #include "r.proj.h" +#define PACKAGE "grassmods" + +#include + /* modify this table to add new methods */ struct menu menu[] = { {p_nearest, "nearest", "nearest neighbor"}, @@ -80,6 +84,28 @@ struct menu menu[] = { static char *make_ipol_list(void); static char *make_ipol_desc(void); + +/* Custom Interpolation for RAM Bypass - Lock-Free */ +void interpolate_ram(void *full_map, void *obufptr, int cell_type, + double col_idx, double row_idx, struct Cell_head *incellhd) +{ + int c = (int)floor(col_idx); + int r = (int)floor(row_idx); + int cell_size = Rast_cell_size(cell_type); + + /* Boundary check */ + if (r < 0 || r >= incellhd->rows || c < 0 || c >= incellhd->cols) { + Rast_set_null_value(obufptr, 1, cell_type); + return; + } + + /* Direct memory access - Thread Safe for Reads */ + unsigned char *src = (unsigned char *)full_map + + (((size_t)r * incellhd->cols + c) * cell_size); + memcpy(obufptr, src, cell_size); +} + + int main(int argc, char **argv) { char *mapname, /* ptr to name of output layer */ @@ -671,7 +697,6 @@ int main(int argc, char **argv) cell_type = Rast_get_map_type(fdi); ibuffer = readcell(fdi, memory->answer); Rast_close(fdi); - /* And switch back to original location */ G_switch_env(); Rast_set_output_window(&outcellhd); @@ -702,58 +727,73 @@ int main(int argc, char **argv) xcoord2 = outcellhd.west + (outcellhd.ew_res / 2); ycoord2 = outcellhd.north - (outcellhd.ns_res / 2); - G_important_message(_("Projecting...")); - for (row = 0; row < outcellhd.rows; row++) { - /* obufptr = obuffer */; - G_percent(row, outcellhd.rows - 1, 2); - -#if 0 - /* parallelization does not always work, - * segfaults in the interpolation functions - * can happen */ -#pragma omp parallel for schedule(static) -#endif + /* --- RAM BYPASS START --- */ + /* 1. Allocate a flat buffer for the entire input map in RAM */ + size_t total_cells = (size_t)incellhd.rows * incellhd.cols; - for (col = 0; col < outcellhd.cols; col++) { - void *obufptr = - (void *)((const unsigned char *)obuffer + col * cell_size); + /* Simple memory safety check */ + double memory_mb = (double)(total_cells * cell_size) / (1024.0 * 1024.0); + G_debug(1, "Bypass buffer requires %.2f MB of RAM", memory_mb); - double xcoord1 = xcoord2 + (col)*outcellhd.ew_res; - double ycoord1 = ycoord2; + if (memory_mb > 4000) { + G_warning(_("Input map requires %.2f MB of RAM for parallel processing. " + "If this causes a crash, reduce the region size with g.region."), + memory_mb); + } + /* Proceed with allocation */ + void *full_map_array = G_malloc(total_cells * cell_size); + + if (!full_map_array) + G_fatal_error("Insufficient RAM for bypass. Try a smaller region."); + + G_important_message(_("Loading map into RAM buffer for parallel bypass...")); + + /* 2. Sequential load (Single-threaded, library-safe) */ + /* ibuffer is already loaded by readcell, but we move it to a flat array + so we can access it lock-free without the readcell tile cache logic */ + for (int r = 0; r < incellhd.rows; r++) { + void *row_ptr = (void *)((unsigned char *)full_map_array + ((size_t)r * incellhd.cols * cell_size)); + /* We use the already-loaded cache here, but single-threaded */ + interpolate(ibuffer, row_ptr, cell_type, 0.0, (double)r, &incellhd); + G_percent(r, incellhd.rows - 1, 5); + } - /* project coordinates in output matrix to */ - /* coordinates in input matrix */ - if (GPJ_transform(&oproj, &iproj, &tproj, PJ_FWD, &xcoord1, - &ycoord1, NULL) < 0) { - G_fatal_error(_("Error in %s"), "GPJ_transform()"); - Rast_set_null_value(obufptr, 1, cell_type); - } - else { - /* convert to row/column indices of input matrix */ + G_important_message(_("Projecting (Lock-Free Parallel)...")); + + #pragma omp parallel for private(row, col) schedule(dynamic) + for (row = 0; row < outcellhd.rows; row++) { + void *local_obuffer = Rast_allocate_output_buf(cell_type); + double local_y = outcellhd.north - (outcellhd.ns_res / 2) - (row * outcellhd.ns_res); + double local_x_start = outcellhd.west + (outcellhd.ew_res / 2); - /* column index in input matrix */ - double col_idx = (xcoord1 - incellhd.west) / incellhd.ew_res; + for (col = 0; col < outcellhd.cols; col++) { + void *obufptr = (void *)((unsigned char *)local_obuffer + (size_t)col * cell_size); + double x1 = local_x_start + (col * outcellhd.ew_res); + double y1 = local_y; - /* row index in input matrix */ - double row_idx = (incellhd.north - ycoord1) / incellhd.ns_res; + if (GPJ_transform(&oproj, &iproj, &tproj, PJ_FWD, &x1, &y1, NULL) < 0) { + Rast_set_null_value(obufptr, 1, cell_type); + } else { + double c_idx = (x1 - incellhd.west) / incellhd.ew_res; + double r_idx = (incellhd.north - y1) / incellhd.ns_res; - /* and resample data point */ - interpolate(ibuffer, obufptr, cell_type, col_idx, row_idx, - &incellhd); + /* CALL OUR LOCK-FREE RAM INTERPOLATOR */ + interpolate_ram(full_map_array, obufptr, cell_type, c_idx, r_idx, &incellhd); } - - /* obufptr = G_incr_void_ptr(obufptr, cell_size); */ } - Rast_put_row(fdo, obuffer, cell_type); - - xcoord2 = outcellhd.west + (outcellhd.ew_res / 2); - ycoord2 -= outcellhd.ns_res; + #pragma omp critical + { + Rast_put_row(fdo, local_obuffer, cell_type); + } + G_free(local_obuffer); } + /*RAM BYPASS END*/ Rast_close(fdo); release_cache(ibuffer); + G_free(full_map_array); /* Clean up our bypass buffer */ if (have_colors > 0) { Rast_write_colors(mapname, G_mapset(), &colr); @@ -811,3 +851,4 @@ char *make_ipol_desc(void) return buf; } + From 36a578978b599c93b8d644a88d30eed515b17a7c Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Mon, 16 Mar 2026 17:27:58 -0500 Subject: [PATCH 02/39] r.proj: use memory option to limit RAM buffer per community feedback --- raster/r.proj/main.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index 2d57c1b52d0..40a3f8be2c2 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -736,10 +736,13 @@ int main(int argc, char **argv) double memory_mb = (double)(total_cells * cell_size) / (1024.0 * 1024.0); G_debug(1, "Bypass buffer requires %.2f MB of RAM", memory_mb); - if (memory_mb > 4000) { - G_warning(_("Input map requires %.2f MB of RAM for parallel processing. " - "If this causes a crash, reduce the region size with g.region."), - memory_mb); + double user_limit_mb = atof(memory->answer); + if (memory_mb > user_limit_mb) { + G_warning(_("Input map requires %.2f MB of RAM for parallel processing, " + "which exceeds the current memory limit (%.0f MB)."), + memory_mb, user_limit_mb); + G_important_message(_("The process may crash if system RAM is insufficient. " + "Increase the 'memory' option or use g.region to reduce the area.")); } /* Proceed with allocation */ void *full_map_array = G_malloc(total_cells * cell_size); From 4ddf520b2aac22896886f5bf128532570ad88fad Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Wed, 1 Jul 2026 21:26:58 -0700 Subject: [PATCH 03/39] r.proj: replace full-map RAM buffer with memory-bounded banding Replace the whole-map RAM buffer (Path A) with a two-level band loop modeled on r.neighbors, adapted for r.proj's CRS-dependent input access: each output band's input footprint is found by back-projecting the band's edges (dense edge walk), so the loaded input strip is sized per band rather than by a fixed neighborhood stencil. Band height adapts to the memory option; if a single output row's footprint exceeds the cap (oblique or large-halo transforms) the module bails, since that case needs the tile cache path, which is not implemented here. Per-thread PROJ contexts (one PJ clone per thread) are retained. Input strips are loaded serially per band because a single fd read path is not thread-safe; each band's output is written in order. Bit-exact against the serial output at 1/2/4/8 threads, for both column-varying and row-varying inputs, with multiple bands exercised. On the test case (105.3M output cells, EPSG:4326 to EPSG:3857, nearest, memory=50) peak RSS was 130 MB versus 763 MB for the whole-map buffer. Developed with assistance from Claude (Anthropic). --- raster/r.proj/main.c | 331 ++++++++++++++++++++++++++++++------------- 1 file changed, 233 insertions(+), 98 deletions(-) diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index 40a3f8be2c2..7afc15e60e8 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -84,27 +84,105 @@ struct menu menu[] = { static char *make_ipol_list(void); static char *make_ipol_desc(void); - -/* Custom Interpolation for RAM Bypass - Lock-Free */ -void interpolate_ram(void *full_map, void *obufptr, int cell_type, - double col_idx, double row_idx, struct Cell_head *incellhd) +/* Nearest read from an in-RAM input STRIP holding input rows [imin, imax]. + * col_idx/row_idx are full-map input indices; the strip is addressed relative + * to imin. A sample inside the full input map but outside the loaded strip + * means the band footprint was under-sized: this is the stop-on-divergence + * trip (must never fire if band_input_row_span is correct). Lock-free: reads + * only, disjoint output slots per thread. */ +static void interpolate_strip(void *strip, void *obufptr, int cell_type, + double col_idx, double row_idx, + struct Cell_head *incellhd, int imin, int imax) { int c = (int)floor(col_idx); int r = (int)floor(row_idx); int cell_size = Rast_cell_size(cell_type); - /* Boundary check */ + /* Outside the full input map: legitimate NULL (same as p_nearest). */ if (r < 0 || r >= incellhd->rows || c < 0 || c >= incellhd->cols) { Rast_set_null_value(obufptr, 1, cell_type); return; } - /* Direct memory access - Thread Safe for Reads */ - unsigned char *src = (unsigned char *)full_map + - (((size_t)r * incellhd->cols + c) * cell_size); + /* Inside the map but outside the loaded strip: the span check under-sized + * the strip. This is a correctness failure, not a NULL. */ + if (r < imin || r > imax) + G_fatal_error(_("Band strip under-sized: input row %d outside loaded " + "range [%d, %d] at column %d"), + r, imin, imax, c); + + unsigned char *src = + (unsigned char *)strip + + (((size_t)(r - imin) * incellhd->cols + c) * cell_size); memcpy(obufptr, src, cell_size); } +/* Dense edge-walk of an output band's rectangle [obr0, obr1) projected into + * input space; returns the min/max INPUT ROW touched, plus a 2-cell margin, + * clamped to the input map. Samples the band's top and bottom rows across all + * columns and its left and right columns across all band rows (bordwalk-style), + * so a curved transform's interior-edge extremum is caught -- corner-only + * sampling can under-size the strip. Called serially, before the parallel + * region, so the shared tproj is safe here. Returns imax < imin for a band + * that projects entirely outside the input. */ +static void band_input_row_span(const struct Cell_head *ohd, + const struct Cell_head *ihd, + const struct pj_info *oproj, + const struct pj_info *iproj, + const struct pj_info *tproj, int obr0, int obr1, + int *imin, int *imax) +{ + double rmin = 1e300, rmax = -1e300; + int e, r, c; + + /* top edge (row obr0) and bottom edge (row obr1-1), all columns */ + for (e = 0; e < 2; e++) { + int orow = (e == 0) ? obr0 : (obr1 - 1); + double y = ohd->north - (orow + 0.5) * ohd->ns_res; + for (c = 0; c < ohd->cols; c++) { + double x = ohd->west + (c + 0.5) * ohd->ew_res; + double xx = x, yy = y; + if (GPJ_transform(oproj, iproj, tproj, PJ_FWD, &xx, &yy, NULL) < 0) + continue; + double ri = (ihd->north - yy) / ihd->ns_res; + if (ri < rmin) + rmin = ri; + if (ri > rmax) + rmax = ri; + } + } + /* left edge (col 0) and right edge (col cols-1), all band rows */ + for (e = 0; e < 2; e++) { + int ocol = (e == 0) ? 0 : (ohd->cols - 1); + double x = ohd->west + (ocol + 0.5) * ohd->ew_res; + for (r = obr0; r < obr1; r++) { + double y = ohd->north - (r + 0.5) * ohd->ns_res; + double xx = x, yy = y; + if (GPJ_transform(oproj, iproj, tproj, PJ_FWD, &xx, &yy, NULL) < 0) + continue; + double ri = (ihd->north - yy) / ihd->ns_res; + if (ri < rmin) + rmin = ri; + if (ri > rmax) + rmax = ri; + } + } + + if (rmax < rmin) { /* band projects entirely outside the input */ + *imin = 0; + *imax = -1; + return; + } + + int lo = (int)floor(rmin) - 2; /* 2-cell margin for interp stencils */ + int hi = (int)floor(rmax) + 2; + if (lo < 0) + lo = 0; + if (hi > ihd->rows - 1) + hi = ihd->rows - 1; + *imin = lo; + *imax = hi; +} int main(int argc, char **argv) { @@ -124,14 +202,7 @@ int main(int argc, char **argv) overwrite, /* Overwrite */ curr_proj; /* output projection (see gis.h) */ - void *obuffer; /* buffer that holds one output row */ - - struct cache *ibuffer; /* buffer that holds the input map */ - func interpolate; /* interpolation routine */ - - double xcoord2, /* temporary x coordinates */ - ycoord2, /* temporary y coordinates */ - onorth, osouth, /* save original border coords */ + double onorth, osouth, /* save original border coords */ oeast, owest, inorth, isouth, ieast, iwest; char north_str[30], south_str[30], east_str[30], west_str[30]; @@ -302,7 +373,6 @@ int main(int argc, char **argv) if (!ipolname) G_fatal_error(_("<%s=%s> unknown %s"), interpol->key, interpol->answer, interpol->key); - interpolate = menu[method].method; mapname = outmap->answer ? outmap->answer : inmap->answer; if (mapname && !list->answer && !overwrite && !print_bounds->answer && @@ -690,18 +760,23 @@ int main(int argc, char **argv) G_message(_("NS-res: %f"), outcellhd.ns_res); G_message(" "); - /* open and read the relevant parts of the input map and close it */ + /* Open the input map (input location env). Banding loads only per-band + * input strips, not the whole map, so fdi stays open across the band loop. + */ G_switch_env(); Rast_set_input_window(&incellhd); fdi = Rast_open_old(inmap->answer, setname); cell_type = Rast_get_map_type(fdi); - ibuffer = readcell(fdi, memory->answer); - Rast_close(fdi); - /* And switch back to original location */ + if (strcmp(interpol->answer, "nearest") != 0) + cell_type = FCELL_TYPE; + cell_size = Rast_cell_size(cell_type); + + /* Back to the output location: set output window, init transform, open + * output map. Both fds now stay open; rd_window/wr_window are set and + * survive env switches, so reads/writes use the right windows throughout. + */ G_switch_env(); Rast_set_output_window(&outcellhd); - - /* reproject from output to input */ G_unset_window(); G_set_window(&outcellhd); tproj.def = NULL; @@ -712,91 +787,152 @@ int main(int argc, char **argv) if (GPJ_init_transform(&oproj, &iproj, &tproj) < 0) G_fatal_error(_("Unable to initialize coordinate transformation")); - if (strcmp(interpol->answer, "nearest") == 0) { + if (strcmp(interpol->answer, "nearest") == 0) fdo = Rast_open_new(mapname, cell_type); - obuffer = (CELL *)Rast_allocate_output_buf(cell_type); - } - else { + else fdo = Rast_open_fp_new(mapname); - cell_type = FCELL_TYPE; - obuffer = (FCELL *)Rast_allocate_output_buf(cell_type); - } - - cell_size = Rast_cell_size(cell_type); - xcoord2 = outcellhd.west + (outcellhd.ew_res / 2); - ycoord2 = outcellhd.north - (outcellhd.ns_res / 2); - - - /* --- RAM BYPASS START --- */ - /* 1. Allocate a flat buffer for the entire input map in RAM */ - size_t total_cells = (size_t)incellhd.rows * incellhd.cols; - - /* Simple memory safety check */ - double memory_mb = (double)(total_cells * cell_size) / (1024.0 * 1024.0); - G_debug(1, "Bypass buffer requires %.2f MB of RAM", memory_mb); + /* Banding (r.neighbors two-level structure): outer serial band loop -> + * serial strip load -> parallel compute into a per-band buffer -> serial + * in-order per-band write -> next band. Bounds peak memory by the cap + * instead of the whole input map (Path A). */ + double cap_mb = atof(memory->answer); + size_t cap_bytes = (size_t)(cap_mb * 1024.0 * 1024.0); + double t_size = 0.0, t_fill = 0.0, t_compute = 0.0, t_write = 0.0; + int n_bands = 0; + + G_important_message(_("Projecting (banded, per-thread PROJ context)...")); + + int obr0 = 0; + while (obr0 < outcellhd.rows) { + /* Band height: start from all remaining rows, halve until the input + * strip plus the band's output buffer fit the cap. band_input_row_span + * is RE-RUN for every candidate height -- the previous band's span is + * never reused. */ + double ts = omp_get_wtime(); + int band_orows = outcellhd.rows - obr0; + int imin = 0, imax = -1; + for (;;) { + band_input_row_span(&outcellhd, &incellhd, &oproj, &iproj, &tproj, + obr0, obr0 + band_orows, &imin, &imax); + int strip_rows = imax - imin + 1; + size_t strip_bytes = + strip_rows > 0 ? (size_t)strip_rows * incellhd.cols * cell_size + : 0; + size_t out_bytes = (size_t)band_orows * outcellhd.cols * cell_size; + if (strip_bytes + out_bytes <= cap_bytes) + break; + if (band_orows == 1) + G_fatal_error( + _("A single output row needs %.1f MB (input footprint %d " + "rows), exceeding the memory cap (%.1f MB). This " + "large-halo/oblique case needs the tile-cache path, " + "which is not implemented."), + (double)(strip_bytes + out_bytes) / (1024.0 * 1024.0), + strip_rows, cap_mb); + band_orows = (band_orows + 1) / 2; /* halve (round up), re-sample */ + } + t_size += omp_get_wtime() - ts; + + int obr1 = obr0 + band_orows; + int strip_rows = imax - imin + 1; + n_bands++; + + /* Serial strip load (single fd -> get_row not thread-safe). Reads in + * the INPUT env (matching the serial code's invariant), then back to + * OUTPUT for compute+write. EMPTY BAND: strip_rows <= 0 means the band + * projects entirely outside the input -> no malloc, no read; its cells + * become NULL via interpolate_strip's out-of-map path. */ + void *strip = NULL; + if (strip_rows > 0) { + strip = G_malloc((size_t)strip_rows * incellhd.cols * cell_size); + double t0 = omp_get_wtime(); + G_switch_env(); /* -> input */ + for (int r = imin; r <= imax; r++) + Rast_get_row(fdi, + (unsigned char *)strip + + (size_t)(r - imin) * incellhd.cols * cell_size, + r, cell_type); + G_switch_env(); /* -> output */ + t_fill += omp_get_wtime() - t0; + } - double user_limit_mb = atof(memory->answer); - if (memory_mb > user_limit_mb) { - G_warning(_("Input map requires %.2f MB of RAM for parallel processing, " - "which exceeds the current memory limit (%.0f MB)."), - memory_mb, user_limit_mb); - G_important_message(_("The process may crash if system RAM is insufficient. " - "Increase the 'memory' option or use g.region to reduce the area.")); - } - /* Proceed with allocation */ - void *full_map_array = G_malloc(total_cells * cell_size); - - if (!full_map_array) - G_fatal_error("Insufficient RAM for bypass. Try a smaller region."); - - G_important_message(_("Loading map into RAM buffer for parallel bypass...")); - - /* 2. Sequential load (Single-threaded, library-safe) */ - /* ibuffer is already loaded by readcell, but we move it to a flat array - so we can access it lock-free without the readcell tile cache logic */ - for (int r = 0; r < incellhd.rows; r++) { - void *row_ptr = (void *)((unsigned char *)full_map_array + ((size_t)r * incellhd.cols * cell_size)); - /* We use the already-loaded cache here, but single-threaded */ - interpolate(ibuffer, row_ptr, cell_type, 0.0, (double)r, &incellhd); - G_percent(r, incellhd.rows - 1, 5); - } + /* Per-band output buffer, lock-free disjoint row slots (band-relative + * index), mirroring r.neighbors' outputs[i].buf. */ + void *band_out = + G_malloc((size_t)band_orows * outcellhd.cols * cell_size); - G_important_message(_("Projecting (Lock-Free Parallel)...")); - - #pragma omp parallel for private(row, col) schedule(dynamic) - for (row = 0; row < outcellhd.rows; row++) { - void *local_obuffer = Rast_allocate_output_buf(cell_type); - double local_y = outcellhd.north - (outcellhd.ns_res / 2) - (row * outcellhd.ns_res); - double local_x_start = outcellhd.west + (outcellhd.ew_res / 2); - - for (col = 0; col < outcellhd.cols; col++) { - void *obufptr = (void *)((unsigned char *)local_obuffer + (size_t)col * cell_size); - double x1 = local_x_start + (col * outcellhd.ew_res); - double y1 = local_y; - - if (GPJ_transform(&oproj, &iproj, &tproj, PJ_FWD, &x1, &y1, NULL) < 0) { - Rast_set_null_value(obufptr, 1, cell_type); - } else { - double c_idx = (x1 - incellhd.west) / incellhd.ew_res; - double r_idx = (incellhd.north - y1) / incellhd.ns_res; - - /* CALL OUR LOCK-FREE RAM INTERPOLATOR */ - interpolate_ram(full_map_array, obufptr, cell_type, c_idx, r_idx, &incellhd); + double t1 = omp_get_wtime(); +#pragma omp parallel + { + /* Per-thread PROJ context + private transform clone (KEEP: this is + * the bit-exact-verified, banding-agnostic part). oproj/iproj are + * read-only shared; the static METERS_in/out race is benign + * (constant CRS per run). */ + struct pj_info tproj_local = tproj; + PJ_CONTEXT *thread_ctx = proj_context_create(); + tproj_local.pj = proj_clone(thread_ctx, tproj.pj); + +#pragma omp for private(row, col) schedule(dynamic) + for (row = obr0; row < obr1; row++) { + void *out_row = + (unsigned char *)band_out + + (size_t)(row - obr0) * outcellhd.cols * cell_size; + double local_y = outcellhd.north - (outcellhd.ns_res / 2) - + (row * outcellhd.ns_res); + double local_x_start = outcellhd.west + (outcellhd.ew_res / 2); + + for (col = 0; col < outcellhd.cols; col++) { + void *obufptr = + (unsigned char *)out_row + (size_t)col * cell_size; + double x1 = local_x_start + (col * outcellhd.ew_res); + double y1 = local_y; + + if (GPJ_transform(&oproj, &iproj, &tproj_local, PJ_FWD, &x1, + &y1, NULL) < 0) { + Rast_set_null_value(obufptr, 1, cell_type); + } + else { + double c_idx = (x1 - incellhd.west) / incellhd.ew_res; + double r_idx = (incellhd.north - y1) / incellhd.ns_res; + interpolate_strip(strip, obufptr, cell_type, c_idx, + r_idx, &incellhd, imin, imax); + } + } } - } - #pragma omp critical - { - Rast_put_row(fdo, local_obuffer, cell_type); + proj_destroy(tproj_local.pj); + proj_context_destroy(thread_ctx); } - G_free(local_obuffer); + t_compute += omp_get_wtime() - t1; + + /* Serial in-order write of the band's rows (Rast_put_row sequential). + */ + double t2 = omp_get_wtime(); + for (row = obr0; row < obr1; row++) + Rast_put_row(fdo, + (unsigned char *)band_out + + (size_t)(row - obr0) * outcellhd.cols * cell_size, + cell_type); + t_write += omp_get_wtime() - t2; + + G_percent(obr1, outcellhd.rows, 5); + + if (strip) + G_free(strip); + G_free(band_out); + obr0 = obr1; } - /*RAM BYPASS END*/ + G_message("PHASE_TIMERS size=%.4f fill=%.4f compute=%.4f write=%.4f " + "bands=%d", + t_size, t_fill, t_compute, t_write, n_bands); + + /* Close input map in its own env, then the output map. */ + G_switch_env(); /* -> input */ + Rast_close(fdi); + G_switch_env(); /* -> output */ Rast_close(fdo); - release_cache(ibuffer); - G_free(full_map_array); /* Clean up our bypass buffer */ if (have_colors > 0) { Rast_write_colors(mapname, G_mapset(), &colr); @@ -854,4 +990,3 @@ char *make_ipol_desc(void) return buf; } - From 8420d2c2ce32ea304327474b6cea694f670f5207 Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Thu, 2 Jul 2026 00:09:18 -0700 Subject: [PATCH 04/39] r.proj: link libproj for direct proj_* calls --- raster/r.proj/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/raster/r.proj/Makefile b/raster/r.proj/Makefile index 083cd95c72f..147b47fe2d8 100644 --- a/raster/r.proj/Makefile +++ b/raster/r.proj/Makefile @@ -2,7 +2,7 @@ MODULE_TOPDIR = ../.. PGM = r.proj -LIBES = $(GPROJLIB) $(RASTERLIB) $(GISLIB) $(MATHLIB) $(PARSONLIB) +LIBES = $(GPROJLIB) $(RASTERLIB) $(GISLIB) $(MATHLIB) $(PARSONLIB) $(PROJLIB) DEPENDENCIES = $(GPROJDEP) $(RASTERDEP) $(GISDEP) EXTRA_LIBS = $(OPENMP_LIBPATH) $(OPENMP_LIB) From 781e9bfa0c90d50e27bd069bc0981eb1a875d6bf Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Thu, 2 Jul 2026 18:49:36 -0700 Subject: [PATCH 05/39] r.proj: address review comments (remove stray PACKAGE define, clarify comments) --- raster/r.proj/main.c | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index 7afc15e60e8..d4d181a6936 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -66,8 +66,6 @@ #include #include "r.proj.h" -#define PACKAGE "grassmods" - #include /* modify this table to add new methods */ @@ -104,8 +102,12 @@ static void interpolate_strip(void *strip, void *obufptr, int cell_type, return; } - /* Inside the map but outside the loaded strip: the span check under-sized - * the strip. This is a correctness failure, not a NULL. */ + /* This input row is inside the input map (the check above already handled + * coordinates that fall outside it), but it is not among the rows we + * preloaded into this band's strip. That cannot happen if the band's + * footprint estimate was right, so it means the estimate was wrong: a bug + * in band sizing, not a normal case. Fail loudly rather than write a NULL + * and silently produce wrong output. */ if (r < imin || r > imax) G_fatal_error(_("Band strip under-sized: input row %d outside loaded " "range [%d, %d] at column %d"), @@ -863,6 +865,14 @@ int main(int argc, char **argv) G_malloc((size_t)band_orows * outcellhd.cols * cell_size); double t1 = omp_get_wtime(); + /* Each band runs one parallel region. This is not nested parallelism: + * the "omp for" below does not create a second thread team, it only + * divides the band's output rows among the threads that this "omp + * parallel" created. The two directives are kept separate instead of a + * combined "omp parallel for" because every thread must clone its own + * PROJ context before the row loop starts and destroy it after the loop + * ends, and that per-thread setup has to sit inside the parallel region + * but outside the for. */ #pragma omp parallel { /* Per-thread PROJ context + private transform clone (KEEP: this is From eda6d1ee900295bb1459261caa559a85f14da532 Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Tue, 7 Jul 2026 20:24:03 -0700 Subject: [PATCH 06/39] r.proj: guard OpenMP timer calls for non-OpenMP builds The banding timers call omp_get_wtime(), which is undefined when GRASS is built without OpenMP and breaks the link in the minimum-config build. Wrap omp.h and omp_get_wtime() behind _OPENMP via a small rproj_wtime() helper that returns 0.0 without OpenMP. Also demote the PHASE_TIMERS line from G_message to G_debug, since it is benchmark scaffolding, not user output. --- raster/r.proj/main.c | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index d4d181a6936..5c126fd8ae0 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -66,7 +66,18 @@ #include #include "r.proj.h" +#ifdef _OPENMP #include +static inline double rproj_wtime(void) +{ + return omp_get_wtime(); +} +#else +static inline double rproj_wtime(void) +{ + return 0.0; +} +#endif /* modify this table to add new methods */ struct menu menu[] = { @@ -811,7 +822,7 @@ int main(int argc, char **argv) * strip plus the band's output buffer fit the cap. band_input_row_span * is RE-RUN for every candidate height -- the previous band's span is * never reused. */ - double ts = omp_get_wtime(); + double ts = rproj_wtime(); int band_orows = outcellhd.rows - obr0; int imin = 0, imax = -1; for (;;) { @@ -834,7 +845,7 @@ int main(int argc, char **argv) strip_rows, cap_mb); band_orows = (band_orows + 1) / 2; /* halve (round up), re-sample */ } - t_size += omp_get_wtime() - ts; + t_size += rproj_wtime() - ts; int obr1 = obr0 + band_orows; int strip_rows = imax - imin + 1; @@ -848,7 +859,7 @@ int main(int argc, char **argv) void *strip = NULL; if (strip_rows > 0) { strip = G_malloc((size_t)strip_rows * incellhd.cols * cell_size); - double t0 = omp_get_wtime(); + double t0 = rproj_wtime(); G_switch_env(); /* -> input */ for (int r = imin; r <= imax; r++) Rast_get_row(fdi, @@ -856,7 +867,7 @@ int main(int argc, char **argv) (size_t)(r - imin) * incellhd.cols * cell_size, r, cell_type); G_switch_env(); /* -> output */ - t_fill += omp_get_wtime() - t0; + t_fill += rproj_wtime() - t0; } /* Per-band output buffer, lock-free disjoint row slots (band-relative @@ -864,7 +875,7 @@ int main(int argc, char **argv) void *band_out = G_malloc((size_t)band_orows * outcellhd.cols * cell_size); - double t1 = omp_get_wtime(); + double t1 = rproj_wtime(); /* Each band runs one parallel region. This is not nested parallelism: * the "omp for" below does not create a second thread team, it only * divides the band's output rows among the threads that this "omp @@ -914,17 +925,17 @@ int main(int argc, char **argv) proj_destroy(tproj_local.pj); proj_context_destroy(thread_ctx); } - t_compute += omp_get_wtime() - t1; + t_compute += rproj_wtime() - t1; /* Serial in-order write of the band's rows (Rast_put_row sequential). */ - double t2 = omp_get_wtime(); + double t2 = rproj_wtime(); for (row = obr0; row < obr1; row++) Rast_put_row(fdo, (unsigned char *)band_out + (size_t)(row - obr0) * outcellhd.cols * cell_size, cell_type); - t_write += omp_get_wtime() - t2; + t_write += rproj_wtime() - t2; G_percent(obr1, outcellhd.rows, 5); @@ -934,9 +945,9 @@ int main(int argc, char **argv) obr0 = obr1; } - G_message("PHASE_TIMERS size=%.4f fill=%.4f compute=%.4f write=%.4f " - "bands=%d", - t_size, t_fill, t_compute, t_write, n_bands); + G_debug(1, + "PHASE_TIMERS size=%.4f fill=%.4f compute=%.4f write=%.4f bands=%d", + t_size, t_fill, t_compute, t_write, n_bands); /* Close input map in its own env, then the output map. */ G_switch_env(); /* -> input */ From 266f7fece0dc2c606a17e02de241de4f334c58de Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Wed, 8 Jul 2026 19:26:09 -0700 Subject: [PATCH 07/39] lib/proj: add per-thread transform clone helpers PROJ transformation objects are not safe for concurrent use, so a parallel module needs a private clone per thread. Add GPJ_clone_transform() and GPJ_free_transform_clone(), which bundle a cloned transform with its private PROJ context in struct gpj_transform_clone so ownership is a single unit. r.proj's parallel banding created the per-thread context with proj_context_create(), proj_clone(), and proj_destroy() directly; switch it to these helpers so the PROJ calls live in lib/proj and the module makes none. --- include/grass/defs/gprojects.h | 2 ++ include/grass/gprojects.h | 9 +++++++++ lib/proj/do_proj.c | 35 ++++++++++++++++++++++++++++++++++ raster/r.proj/main.c | 12 +++++------- 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/include/grass/defs/gprojects.h b/include/grass/defs/gprojects.h index 1270ed66c5a..2f21ba91230 100644 --- a/include/grass/defs/gprojects.h +++ b/include/grass/defs/gprojects.h @@ -9,6 +9,8 @@ int GPJ_transform(const struct pj_info *, const struct pj_info *, int GPJ_transform_array(const struct pj_info *, const struct pj_info *, const struct pj_info *, int, double *, double *, double *, int); +void GPJ_clone_transform(const struct pj_info *, struct gpj_transform_clone *); +void GPJ_free_transform_clone(struct gpj_transform_clone *); /* old API, to be removed */ int pj_do_proj(double *, double *, const struct pj_info *, diff --git a/include/grass/gprojects.h b/include/grass/gprojects.h index 803eecf8477..8fc71b8f2a9 100644 --- a/include/grass/gprojects.h +++ b/include/grass/gprojects.h @@ -48,6 +48,15 @@ struct pj_info { char *wkt; }; +/* Per-thread clone of a transform, filled by GPJ_clone_transform() and + * released by GPJ_free_transform_clone(). Bundles the cloned transform with + * the private PROJ context it was cloned into, so ownership is a single unit. + */ +struct gpj_transform_clone { + struct pj_info info; + PJ_CONTEXT *ctx; +}; + struct gpj_datum { char *name, *longname, *ellps; double dx, dy, dz; diff --git a/lib/proj/do_proj.c b/lib/proj/do_proj.c index 22c635310cc..66be1c7e5ed 100644 --- a/lib/proj/do_proj.c +++ b/lib/proj/do_proj.c @@ -1414,3 +1414,38 @@ int pj_do_transform(int count, double *x, double *y, double *h, } return ok; } + +/*! + * \brief Clone a transform into a fresh per-thread PROJ context + * + * PROJ transformation objects are not safe for concurrent use, so each thread + * needs its own. This fills \p clone with a copy of \p src whose PJ is cloned + * into a new private context. Release it with GPJ_free_transform_clone(). + * + * Safe to call concurrently from multiple threads with the same \p src, + * provided \p src is not modified during the calls: each call clones into its + * own new context and touches no shared mutable state. + * + * \param src source transform (as set up by GPJ_init_transform()) + * \param[out] clone receives the per-thread clone (info plus private context) + */ +void GPJ_clone_transform(const struct pj_info *src, + struct gpj_transform_clone *clone) +{ + clone->ctx = proj_context_create(); + clone->info = *src; + clone->info.pj = proj_clone(clone->ctx, src->pj); +} + +/*! + * \brief Free a per-thread transform clone and its context + * + * \param clone clone filled by GPJ_clone_transform(); its cloned PJ is set to + * NULL after release + */ +void GPJ_free_transform_clone(struct gpj_transform_clone *clone) +{ + proj_destroy(clone->info.pj); + proj_context_destroy(clone->ctx); + clone->info.pj = NULL; +} diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index 5c126fd8ae0..fec3a319ca9 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -890,9 +890,8 @@ int main(int argc, char **argv) * the bit-exact-verified, banding-agnostic part). oproj/iproj are * read-only shared; the static METERS_in/out race is benign * (constant CRS per run). */ - struct pj_info tproj_local = tproj; - PJ_CONTEXT *thread_ctx = proj_context_create(); - tproj_local.pj = proj_clone(thread_ctx, tproj.pj); + struct gpj_transform_clone tproj_local; + GPJ_clone_transform(&tproj, &tproj_local); #pragma omp for private(row, col) schedule(dynamic) for (row = obr0; row < obr1; row++) { @@ -909,8 +908,8 @@ int main(int argc, char **argv) double x1 = local_x_start + (col * outcellhd.ew_res); double y1 = local_y; - if (GPJ_transform(&oproj, &iproj, &tproj_local, PJ_FWD, &x1, - &y1, NULL) < 0) { + if (GPJ_transform(&oproj, &iproj, &tproj_local.info, PJ_FWD, + &x1, &y1, NULL) < 0) { Rast_set_null_value(obufptr, 1, cell_type); } else { @@ -922,8 +921,7 @@ int main(int argc, char **argv) } } - proj_destroy(tproj_local.pj); - proj_context_destroy(thread_ctx); + GPJ_free_transform_clone(&tproj_local); } t_compute += rproj_wtime() - t1; From e2a7d9f6b55df6478d092624725279f646337f17 Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Wed, 8 Jul 2026 15:38:09 -0700 Subject: [PATCH 08/39] r.proj: experimental per-thread-fd parallel strip reads Each band's input strip is read in parallel: read_nprocs fresh per-thread fds (Rast_open_old), a static block split of the strip rows across threads, each thread reading its disjoint rows through its own fd into its own strip slice. Rast_disable_omp_on_mask gates the parallelism (serial when a mask is present or without OpenMP); fdi remains the serial-fallback path. Env-switch choreography, compute region, write loop, band sizing, and PJ context cloning are unchanged. Experimental, not for merge. --- raster/r.proj/main.c | 65 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index fec3a319ca9..13d90126c7a 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -784,6 +784,32 @@ int main(int argc, char **argv) cell_type = FCELL_TYPE; cell_size = Rast_cell_size(cell_type); + /* Parallel input reads: decide the read-thread count here, in the INPUT + * env, so the mask guard checks the source mapset's mask (the mask that + * would apply to Rast_get_row on fdi). Rast_disable_omp_on_mask returns 1 + * (serial) if a mask is present or without OpenMP, and does NOT touch the + * thread count when no mask exists (lib/raster/mask_info.c:226-231), so the + * compute region's threads are unperturbed in the common case. When + * read_nprocs > 1 we open that many FRESH read fds (one per thread); fdi is + * used only by the serial fallback. + * INFERRED-safe (not yet runtime-verified; the gate converts it): + * concurrent Rast_open_old fds on the same map across locations is sound + * from the r.neighbors same-location precedent (in_fd[t]) plus the Stage 1 + * fcb analysis (each fd carries its own cur_row/data/data_fd; reads depend + * only on the fcb and R__.rd_window). */ +#ifdef _OPENMP + int want_nprocs = omp_get_max_threads(); +#else + int want_nprocs = 1; +#endif + int read_nprocs = Rast_disable_omp_on_mask(want_nprocs); + int *fd_read = NULL; + if (read_nprocs > 1) { + fd_read = G_malloc((size_t)read_nprocs * sizeof(int)); + for (int t = 0; t < read_nprocs; t++) + fd_read[t] = Rast_open_old(inmap->answer, setname); + } + /* Back to the output location: set output window, init transform, open * output map. Both fds now stay open; rd_window/wr_window are set and * survive env switches, so reads/writes use the right windows throughout. @@ -861,11 +887,35 @@ int main(int argc, char **argv) strip = G_malloc((size_t)strip_rows * incellhd.cols * cell_size); double t0 = rproj_wtime(); G_switch_env(); /* -> input */ - for (int r = imin; r <= imax; r++) - Rast_get_row(fdi, - (unsigned char *)strip + - (size_t)(r - imin) * incellhd.cols * cell_size, - r, cell_type); + if (read_nprocs > 1) { +#ifdef _OPENMP + /* Parallel read: each thread reads a contiguous, disjoint + * block of strip rows (schedule(static)) through its OWN fd + * into its own disjoint strip slice (slice = row r - imin). + * No two threads share an fd or a strip row. */ +#pragma omp parallel num_threads(read_nprocs) + { + int t = omp_get_thread_num(); +#pragma omp for schedule(static) + for (int r = imin; r <= imax; r++) + Rast_get_row(fd_read[t], + (unsigned char *)strip + + (size_t)(r - imin) * incellhd.cols * + cell_size, + r, cell_type); + } +#endif + } + else { + /* Serial fallback (nprocs==1, mask present, or no OpenMP): + * original loop, unchanged, through fdi. */ + for (int r = imin; r <= imax; r++) + Rast_get_row(fdi, + (unsigned char *)strip + (size_t)(r - imin) * + incellhd.cols * + cell_size, + r, cell_type); + } G_switch_env(); /* -> output */ t_fill += rproj_wtime() - t0; } @@ -950,6 +1000,11 @@ int main(int argc, char **argv) /* Close input map in its own env, then the output map. */ G_switch_env(); /* -> input */ Rast_close(fdi); + if (fd_read) { + for (int t = 0; t < read_nprocs; t++) + Rast_close(fd_read[t]); + G_free(fd_read); + } G_switch_env(); /* -> output */ Rast_close(fdo); From a42445f4b1e40b28923509d7a911b0f8400d84de Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Thu, 9 Jul 2026 23:24:42 -0700 Subject: [PATCH 09/39] r.proj: add adaptive column tiling for oblique reprojections The memory-bounded banding path halves the band height until a full-width input strip fits the memory cap. On oblique projections a single output row can back-project to an input footprint larger than the cap at any height, and that path bailed out. Add column tiling as a second search dimension. Phase 1 is identical to the current banding: halve the band height while the input strip spans the full input width, and use that result whenever a full-width band fits. Phase 2 runs only when a single full-width row still exceeds the cap. It keeps the band as tall as its output buffer allows and halves the tile width instead, so each column tile back-projects to a smaller input row span and the per-band parallel region stays populated rather than collapsing to a single row. The width search runs in two tiers to stay cheap. The upper tier estimates the worst tile strip by probing a bounded, evenly spaced subset of tiles, which is a lower bound on the true worst, and narrows to a candidate width. The lower tier validates that width with the exact per-tile edge walk and narrows further if the estimate was optimistic, so the accepted width is always exact-sized against the cap. Input strips stay full width because the raster API reads whole rows, so a tile strip is its input row span times the full input width, and tiling shrinks the row span rather than the width. Each tile loads one strip whose row span comes from the exact edge walk, one tile at a time, bounding peak memory to the worst tile rather than the whole band. Every output cell is computed once and written in row order, so the result is bit-exact with the serial output. Retain the existing fatal error only for the degenerate tile whose footprint cannot fit the cap at any width, at minimum band height; that footprint needs the tile-cache path, which is not implemented. --- raster/r.proj/main.c | 388 +++++++++++++++++++++++++++++++------------ 1 file changed, 278 insertions(+), 110 deletions(-) diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index 13d90126c7a..e968b79c740 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -130,29 +130,30 @@ static void interpolate_strip(void *strip, void *obufptr, int cell_type, memcpy(obufptr, src, cell_size); } -/* Dense edge-walk of an output band's rectangle [obr0, obr1) projected into - * input space; returns the min/max INPUT ROW touched, plus a 2-cell margin, - * clamped to the input map. Samples the band's top and bottom rows across all - * columns and its left and right columns across all band rows (bordwalk-style), - * so a curved transform's interior-edge extremum is caught -- corner-only - * sampling can under-size the strip. Called serially, before the parallel - * region, so the shared tproj is safe here. Returns imax < imin for a band - * that projects entirely outside the input. */ +/* Dense edge-walk of an output tile's rectangle [obr0, obr1) x [obc0, obc1) + * projected into input space; returns the min/max INPUT ROW touched, plus a + * 2-cell margin, clamped to the input map. Samples the tile's top and bottom + * rows across its columns [obc0, obc1) and its left and right columns across + * its rows (bordwalk-style), so a curved transform's interior-edge extremum is + * caught -- corner-only sampling can under-size the strip. A full-width band is + * the case obc0=0, obc1=cols. Called serially, before the parallel region, so + * the shared tproj is safe here. Returns imax < imin for a tile that projects + * entirely outside the input. */ static void band_input_row_span(const struct Cell_head *ohd, const struct Cell_head *ihd, const struct pj_info *oproj, const struct pj_info *iproj, const struct pj_info *tproj, int obr0, int obr1, - int *imin, int *imax) + int obc0, int obc1, int *imin, int *imax) { double rmin = 1e300, rmax = -1e300; int e, r, c; - /* top edge (row obr0) and bottom edge (row obr1-1), all columns */ + /* top edge (row obr0) and bottom edge (row obr1-1), tile columns */ for (e = 0; e < 2; e++) { int orow = (e == 0) ? obr0 : (obr1 - 1); double y = ohd->north - (orow + 0.5) * ohd->ns_res; - for (c = 0; c < ohd->cols; c++) { + for (c = obc0; c < obc1; c++) { double x = ohd->west + (c + 0.5) * ohd->ew_res; double xx = x, yy = y; if (GPJ_transform(oproj, iproj, tproj, PJ_FWD, &xx, &yy, NULL) < 0) @@ -164,9 +165,9 @@ static void band_input_row_span(const struct Cell_head *ohd, rmax = ri; } } - /* left edge (col 0) and right edge (col cols-1), all band rows */ + /* left edge (col obc0) and right edge (col obc1-1), all band rows */ for (e = 0; e < 2; e++) { - int ocol = (e == 0) ? 0 : (ohd->cols - 1); + int ocol = (e == 0) ? obc0 : (obc1 - 1); double x = ohd->west + (ocol + 0.5) * ohd->ew_res; for (r = obr0; r < obr1; r++) { double y = ohd->north - (r + 0.5) * ohd->ns_res; @@ -197,6 +198,75 @@ static void band_input_row_span(const struct Cell_head *ohd, *imax = hi; } +/* Largest input-row strip (in rows) among the column tiles of width tilew that + * partition output columns [0, ohd->cols) for the band [obr0, obr1). Tiles are + * loaded one at a time, so peak strip memory is set by the worst tile, not the + * union of the band's tiles; the fit search sizes this against the cap. Every + * call is a full tile edge-walk, so this is O(tiles * perimeter) -- paid in the + * serial size phase, and only when column splitting is actually entered. + * Returns 0 if every tile projects entirely outside the input. */ +static int worst_tile_strip_rows(const struct Cell_head *ohd, + const struct Cell_head *ihd, + const struct pj_info *oproj, + const struct pj_info *iproj, + const struct pj_info *tproj, int obr0, + int obr1, int tilew) +{ + int worst = 0, obc0; + + for (obc0 = 0; obc0 < ohd->cols; obc0 += tilew) { + int obc1 = obc0 + tilew; + int imin, imax, rows; + + if (obc1 > ohd->cols) + obc1 = ohd->cols; + band_input_row_span(ohd, ihd, oproj, iproj, tproj, obr0, obr1, obc0, + obc1, &imin, &imax); + rows = imax - imin + 1; /* imax < imin (empty) -> <= 0, ignored */ + if (rows > worst) + worst = rows; + } + return worst; +} + +#define TILE_PROBE 16 /* tiles sampled by the Phase-2 width-search estimate */ + +/* Cheap estimate of worst_tile_strip_rows: the largest input-row strip among + * at most `probe` column tiles, evenly spaced across the band width and always + * including the first and last. A subset max is a LOWER bound on the true + * worst, so it only PRUNES the Phase-2 search; the chosen width is exact- + * validated by worst_tile_strip_rows before use. */ +static int est_worst_tile_strip_rows(const struct Cell_head *ohd, + const struct Cell_head *ihd, + const struct pj_info *oproj, + const struct pj_info *iproj, + const struct pj_info *tproj, int obr0, + int obr1, int tilew, int probe) +{ + int ntiles = (ohd->cols + tilew - 1) / tilew; + int worst = 0, k; + + if (probe < 1) + probe = 1; + if (probe > ntiles) + probe = ntiles; + for (k = 0; k < probe; k++) { + int ti = (probe == 1) ? 0 : (int)((long)k * (ntiles - 1) / (probe - 1)); + int obc0 = ti * tilew; + int obc1 = obc0 + tilew; + int imin, imax, rows; + + if (obc1 > ohd->cols) + obc1 = ohd->cols; + band_input_row_span(ohd, ihd, oproj, iproj, tproj, obr0, obr1, obc0, + obc1, &imin, &imax); + rows = imax - imin + 1; + if (rows > worst) + worst = rows; + } + return worst; +} + int main(int argc, char **argv) { char *mapname, /* ptr to name of output layer */ @@ -839,21 +909,28 @@ int main(int argc, char **argv) size_t cap_bytes = (size_t)(cap_mb * 1024.0 * 1024.0); double t_size = 0.0, t_fill = 0.0, t_compute = 0.0, t_write = 0.0; int n_bands = 0; + int max_tiles = 1; /* most column tiles used by any single band */ G_important_message(_("Projecting (banded, per-thread PROJ context)...")); int obr0 = 0; while (obr0 < outcellhd.rows) { - /* Band height: start from all remaining rows, halve until the input - * strip plus the band's output buffer fit the cap. band_input_row_span - * is RE-RUN for every candidate height -- the previous band's span is - * never reused. */ + /* Fit search. Phase 1 (fast path, unchanged): halve the band height + * until the FULL-WIDTH strip plus the band output buffer fit the cap; + * the span is re-run per candidate height. Phase 2 (oblique fallback): + * only if a single full-width output row still busts the cap, split the + * row into column tiles and halve tile WIDTH until the worst tile's + * strip fits. Strips are full input width (the raster API reads whole + * rows), so width splitting shrinks a tile's input ROW span, not its + * width. Easy pairs never leave Phase 1. */ double ts = rproj_wtime(); int band_orows = outcellhd.rows - obr0; + int tilew = outcellhd.cols; int imin = 0, imax = -1; for (;;) { band_input_row_span(&outcellhd, &incellhd, &oproj, &iproj, &tproj, - obr0, obr0 + band_orows, &imin, &imax); + obr0, obr0 + band_orows, 0, outcellhd.cols, + &imin, &imax); int strip_rows = imax - imin + 1; size_t strip_bytes = strip_rows > 0 ? (size_t)strip_rows * incellhd.cols * cell_size @@ -862,121 +939,213 @@ int main(int argc, char **argv) if (strip_bytes + out_bytes <= cap_bytes) break; if (band_orows == 1) - G_fatal_error( - _("A single output row needs %.1f MB (input footprint %d " - "rows), exceeding the memory cap (%.1f MB). This " - "large-halo/oblique case needs the tile-cache path, " - "which is not implemented."), - (double)(strip_bytes + out_bytes) / (1024.0 * 1024.0), - strip_rows, cap_mb); + break; /* height exhausted: fall through to column splitting */ band_orows = (band_orows + 1) / 2; /* halve (round up), re-sample */ } + if (band_orows == 1) { + /* Phase 2 (oblique only): Phase 1 could not fit even a single + * full-width row, so prefer a TALL tiled band instead of a 1-row + * one. Rescan from the full remaining height downward; at the + * tallest height whose output buffer fits the cap, halve tile WIDTH + * until the worst column tile's strip fits, and only reduce height + * when no width fits. Keeping the band tall gives the per-band + * parallel region many output rows. Runs only on this path, so the + * easy-pair size phase (Phase 1) is unaffected. */ + band_orows = outcellhd.rows - obr0; + for (;;) { + size_t out_bytes = + (size_t)band_orows * outcellhd.cols * cell_size; + if (out_bytes <= cap_bytes) { + /* Upper tier: cheap probe estimate narrows to a candidate + * width, checking down to tilew==1. The estimate is a lower + * bound, so est-no-fit at tilew==1 implies exact-no-fit -> + * the exact validation below is skipped entirely at heights + * where no width can fit (this is what keeps the search + * cheap; scanning every tile there was the cost). */ + tilew = outcellhd.cols; + int est_fit = 0; + for (;;) { + int est = est_worst_tile_strip_rows( + &outcellhd, &incellhd, &oproj, &iproj, &tproj, obr0, + obr0 + band_orows, tilew, TILE_PROBE); + size_t est_bytes = + est > 0 ? (size_t)est * incellhd.cols * cell_size + : 0; + if (est_bytes + out_bytes <= cap_bytes) { + est_fit = 1; + break; + } + if (tilew == 1) + break; + tilew = (tilew + 1) / 2; + } + /* Lower tier: EXACT validation, only when the estimate + * found a candidate. Narrow and re-validate if the estimate + * was optimistic; this exact-sizes the accepted width so + * the cap is honored. */ + int fit = 0; + if (est_fit) { + for (;;) { + int worst = worst_tile_strip_rows( + &outcellhd, &incellhd, &oproj, &iproj, &tproj, + obr0, obr0 + band_orows, tilew); + size_t strip_bytes = + worst > 0 + ? (size_t)worst * incellhd.cols * cell_size + : 0; + if (strip_bytes + out_bytes <= cap_bytes) { + fit = 1; + break; + } + if (tilew == 1) + break; /* exact: no width fits at this height */ + tilew = (tilew + 1) / 2; + } + } + if (fit) + break; + } + if (band_orows == 1) { + /* Single output row at minimum width still over cap = + * singular/large-halo; needs the tile-cache path. */ + size_t out1 = (size_t)outcellhd.cols * cell_size; + int worst = worst_tile_strip_rows(&outcellhd, &incellhd, + &oproj, &iproj, &tproj, + obr0, obr0 + 1, 1); + size_t strip_bytes = + worst > 0 ? (size_t)worst * incellhd.cols * cell_size + : 0; + G_fatal_error( + _("A single output row needs %.1f MB (input footprint " + "%d rows), exceeding the memory cap (%.1f MB). This " + "large-halo/oblique case needs the tile-cache path, " + "which is not implemented."), + (double)(strip_bytes + out1) / (1024.0 * 1024.0), worst, + cap_mb); + } + band_orows = (band_orows + 1) / 2; /* shrink height, retry */ + } + } t_size += rproj_wtime() - ts; int obr1 = obr0 + band_orows; - int strip_rows = imax - imin + 1; n_bands++; + int n_tiles = (outcellhd.cols + tilew - 1) / tilew; + if (n_tiles > max_tiles) + max_tiles = n_tiles; - /* Serial strip load (single fd -> get_row not thread-safe). Reads in - * the INPUT env (matching the serial code's invariant), then back to - * OUTPUT for compute+write. EMPTY BAND: strip_rows <= 0 means the band - * projects entirely outside the input -> no malloc, no read; its cells - * become NULL via interpolate_strip's out-of-map path. */ - void *strip = NULL; - if (strip_rows > 0) { - strip = G_malloc((size_t)strip_rows * incellhd.cols * cell_size); - double t0 = rproj_wtime(); - G_switch_env(); /* -> input */ - if (read_nprocs > 1) { + /* Per-band output buffer, lock-free disjoint row slots, filled column + * tile by column tile and written once after all tiles. Full width + * regardless of tiling. */ + void *band_out = + G_malloc((size_t)band_orows * outcellhd.cols * cell_size); + + /* Column tiles processed one at a time: only the current tile's strip + * is resident, so peak strip memory is the worst tile, not the band's + * union. tilew == cols is the single-tile fast path (obc0=0, + * obc1=cols), identical to un-tiled banding. */ + for (int obc0 = 0; obc0 < outcellhd.cols; obc0 += tilew) { + int obc1 = obc0 + tilew; + if (obc1 > outcellhd.cols) + obc1 = outcellhd.cols; + + /* Per-tile input row span (full-width strip: the raster API reads + * whole rows, so columns are not cropped). */ + band_input_row_span(&outcellhd, &incellhd, &oproj, &iproj, &tproj, + obr0, obr1, obc0, obc1, &imin, &imax); + int strip_rows = imax - imin + 1; + + /* Serial strip load (single fd -> get_row not thread-safe). EMPTY + * TILE: strip_rows <= 0 -> projects outside input, no read; cells + * become NULL via interpolate_strip's out-of-map path. */ + void *strip = NULL; + if (strip_rows > 0) { + strip = + G_malloc((size_t)strip_rows * incellhd.cols * cell_size); + double t0 = rproj_wtime(); + G_switch_env(); /* -> input */ + if (read_nprocs > 1) { #ifdef _OPENMP - /* Parallel read: each thread reads a contiguous, disjoint - * block of strip rows (schedule(static)) through its OWN fd - * into its own disjoint strip slice (slice = row r - imin). - * No two threads share an fd or a strip row. */ + /* Parallel read: each thread reads a contiguous, disjoint + * block of strip rows through its OWN fd into its own + * disjoint strip slice. No two threads share an fd/row. */ #pragma omp parallel num_threads(read_nprocs) - { - int t = omp_get_thread_num(); + { + int t = omp_get_thread_num(); #pragma omp for schedule(static) + for (int r = imin; r <= imax; r++) + Rast_get_row(fd_read[t], + (unsigned char *)strip + + (size_t)(r - imin) * + incellhd.cols * cell_size, + r, cell_type); + } +#endif + } + else { + /* Serial fallback (nprocs==1, mask, or no OpenMP). */ for (int r = imin; r <= imax; r++) - Rast_get_row(fd_read[t], + Rast_get_row(fdi, (unsigned char *)strip + (size_t)(r - imin) * incellhd.cols * cell_size, r, cell_type); } -#endif + G_switch_env(); /* -> output */ + t_fill += rproj_wtime() - t0; } - else { - /* Serial fallback (nprocs==1, mask present, or no OpenMP): - * original loop, unchanged, through fdi. */ - for (int r = imin; r <= imax; r++) - Rast_get_row(fdi, - (unsigned char *)strip + (size_t)(r - imin) * - incellhd.cols * - cell_size, - r, cell_type); - } - G_switch_env(); /* -> output */ - t_fill += rproj_wtime() - t0; - } - - /* Per-band output buffer, lock-free disjoint row slots (band-relative - * index), mirroring r.neighbors' outputs[i].buf. */ - void *band_out = - G_malloc((size_t)band_orows * outcellhd.cols * cell_size); - double t1 = rproj_wtime(); - /* Each band runs one parallel region. This is not nested parallelism: - * the "omp for" below does not create a second thread team, it only - * divides the band's output rows among the threads that this "omp - * parallel" created. The two directives are kept separate instead of a - * combined "omp parallel for" because every thread must clone its own - * PROJ context before the row loop starts and destroy it after the loop - * ends, and that per-thread setup has to sit inside the parallel region - * but outside the for. */ + double t1 = rproj_wtime(); + /* One parallel region per tile. Not nested: the "omp for" divides + * the band's output rows among this region's threads. Separate + * directives so each thread clones its PROJ context before the row + * loop and destroys it after. */ #pragma omp parallel - { - /* Per-thread PROJ context + private transform clone (KEEP: this is - * the bit-exact-verified, banding-agnostic part). oproj/iproj are - * read-only shared; the static METERS_in/out race is benign - * (constant CRS per run). */ - struct gpj_transform_clone tproj_local; - GPJ_clone_transform(&tproj, &tproj_local); + { + struct gpj_transform_clone tproj_local; + GPJ_clone_transform(&tproj, &tproj_local); #pragma omp for private(row, col) schedule(dynamic) - for (row = obr0; row < obr1; row++) { - void *out_row = - (unsigned char *)band_out + - (size_t)(row - obr0) * outcellhd.cols * cell_size; - double local_y = outcellhd.north - (outcellhd.ns_res / 2) - - (row * outcellhd.ns_res); - double local_x_start = outcellhd.west + (outcellhd.ew_res / 2); - - for (col = 0; col < outcellhd.cols; col++) { - void *obufptr = - (unsigned char *)out_row + (size_t)col * cell_size; - double x1 = local_x_start + (col * outcellhd.ew_res); - double y1 = local_y; - - if (GPJ_transform(&oproj, &iproj, &tproj_local.info, PJ_FWD, - &x1, &y1, NULL) < 0) { - Rast_set_null_value(obufptr, 1, cell_type); - } - else { - double c_idx = (x1 - incellhd.west) / incellhd.ew_res; - double r_idx = (incellhd.north - y1) / incellhd.ns_res; - interpolate_strip(strip, obufptr, cell_type, c_idx, - r_idx, &incellhd, imin, imax); + for (row = obr0; row < obr1; row++) { + void *out_row = + (unsigned char *)band_out + + (size_t)(row - obr0) * outcellhd.cols * cell_size; + double local_y = outcellhd.north - (outcellhd.ns_res / 2) - + (row * outcellhd.ns_res); + double local_x_start = + outcellhd.west + (outcellhd.ew_res / 2); + + for (col = obc0; col < obc1; col++) { + void *obufptr = + (unsigned char *)out_row + (size_t)col * cell_size; + double x1 = local_x_start + (col * outcellhd.ew_res); + double y1 = local_y; + + if (GPJ_transform(&oproj, &iproj, &tproj_local.info, + PJ_FWD, &x1, &y1, NULL) < 0) { + Rast_set_null_value(obufptr, 1, cell_type); + } + else { + double c_idx = + (x1 - incellhd.west) / incellhd.ew_res; + double r_idx = + (incellhd.north - y1) / incellhd.ns_res; + interpolate_strip(strip, obufptr, cell_type, c_idx, + r_idx, &incellhd, imin, imax); + } } } + + GPJ_free_transform_clone(&tproj_local); } + t_compute += rproj_wtime() - t1; - GPJ_free_transform_clone(&tproj_local); + if (strip) + G_free(strip); } - t_compute += rproj_wtime() - t1; - /* Serial in-order write of the band's rows (Rast_put_row sequential). - */ + /* Serial in-order write of the band's rows once all tiles filled + * band_out (Rast_put_row sequential). */ double t2 = rproj_wtime(); for (row = obr0; row < obr1; row++) Rast_put_row(fdo, @@ -987,15 +1156,14 @@ int main(int argc, char **argv) G_percent(obr1, outcellhd.rows, 5); - if (strip) - G_free(strip); G_free(band_out); obr0 = obr1; } G_debug(1, - "PHASE_TIMERS size=%.4f fill=%.4f compute=%.4f write=%.4f bands=%d", - t_size, t_fill, t_compute, t_write, n_bands); + "PHASE_TIMERS size=%.4f fill=%.4f compute=%.4f write=%.4f bands=%d " + "tiles=%d", + t_size, t_fill, t_compute, t_write, n_bands, max_tiles); /* Close input map in its own env, then the output map. */ G_switch_env(); /* -> input */ From 70b6f1c0b0e9b7df0b2f87e0b6a66150df20867b Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Mon, 13 Jul 2026 12:57:41 -0700 Subject: [PATCH 10/39] r.proj: reuse previous band size in the tile sizing search The tile sizing search finds, for each output band, the tallest band height and its column tiling whose input strip fits the memory cap. Neighboring bands almost always end up with the same size, since the projection changes gradually from one band to the next. The search now tries the previous band's accepted height and width first instead of restarting the descending scan from the top every time. The previous band's size is checked with the same exact edge walk acceptance test the full search uses, and the next taller height is checked to make sure it does not fit. Together these two checks confirm the reused size is the tallest fitting answer, the same result the full scan would have returned. If either check fails, the code falls back to the full descending search, so the worst case costs the same as before. Because acceptance is decided by the same test in both paths, the resulting bands and tiles are identical to before and the output is bit for bit unchanged. In the common case a band is sized in two edge walks instead of a full descending scan. --- raster/r.proj/main.c | 210 +++++++++++++++++++++++++++++-------------- 1 file changed, 144 insertions(+), 66 deletions(-) diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index e968b79c740..682d64f7289 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -267,6 +267,78 @@ static int est_worst_tile_strip_rows(const struct Cell_head *ohd, return worst; } +/* Exact per-height fit test for the Phase-2 height search: 1 iff a band of + * height h at obr0 has an output buffer within the cap AND some column-tile + * width whose worst input strip fits (setting *acc_tilew to that width, via the + * same upper-tier estimate then lower-tier exact validation the search uses); + * 0 if no width fits or the output buffer alone exceeds the cap. */ +static int phase2_width_fit(const struct Cell_head *ohd, + const struct Cell_head *ihd, + const struct pj_info *oproj, + const struct pj_info *iproj, + const struct pj_info *tproj, int obr0, int h, + size_t cap_bytes, int cell_size, int *acc_tilew) +{ + size_t out_bytes = (size_t)h * ohd->cols * cell_size; + int tilew, est_fit; + + if (out_bytes > cap_bytes) + return 0; + tilew = ohd->cols; + est_fit = 0; + for (;;) { + int est = est_worst_tile_strip_rows(ohd, ihd, oproj, iproj, tproj, obr0, + obr0 + h, tilew, TILE_PROBE); + size_t est_bytes = est > 0 ? (size_t)est * ihd->cols * cell_size : 0; + if (est_bytes + out_bytes <= cap_bytes) { + est_fit = 1; + break; + } + if (tilew == 1) + break; + tilew = (tilew + 1) / 2; + } + if (est_fit) { + for (;;) { + int worst = worst_tile_strip_rows(ohd, ihd, oproj, iproj, tproj, + obr0, obr0 + h, tilew); + size_t strip_bytes = + worst > 0 ? (size_t)worst * ihd->cols * cell_size : 0; + if (strip_bytes + out_bytes <= cap_bytes) { + *acc_tilew = tilew; + return 1; + } + if (tilew == 1) + break; + tilew = (tilew + 1) / 2; + } + } + return 0; +} + +/* Full-width fit test for the Phase-1 height search: 1 iff a band of height h + * at obr0 has its full-width input strip plus output buffer within the cap. + * Short-circuits on the output buffer alone (no edge walk) when it already + * exceeds the cap. Used only by the seed peek; the walk keeps its inline test, + * so the miss path is byte-for-byte today's execution. */ +static int phase1_fits(const struct Cell_head *ohd, const struct Cell_head *ihd, + const struct pj_info *oproj, const struct pj_info *iproj, + const struct pj_info *tproj, int obr0, int h, + size_t cap_bytes, int cell_size) +{ + int imin, imax, strip_rows; + size_t out_bytes = (size_t)h * ohd->cols * cell_size, strip_bytes; + + if (out_bytes > cap_bytes) + return 0; + band_input_row_span(ohd, ihd, oproj, iproj, tproj, obr0, obr0 + h, 0, + ohd->cols, &imin, &imax); + strip_rows = imax - imin + 1; + strip_bytes = + strip_rows > 0 ? (size_t)strip_rows * ihd->cols * cell_size : 0; + return strip_bytes + out_bytes <= cap_bytes; +} + int main(int argc, char **argv) { char *mapname, /* ptr to name of output layer */ @@ -909,7 +981,11 @@ int main(int argc, char **argv) size_t cap_bytes = (size_t)(cap_mb * 1024.0 * 1024.0); double t_size = 0.0, t_fill = 0.0, t_compute = 0.0, t_write = 0.0; int n_bands = 0; - int max_tiles = 1; /* most column tiles used by any single band */ + int max_tiles = 1; /* most column tiles used by any single band */ + int seed_h = 0, seed_w = 0; /* previous Phase-2 band's accepted sizing */ + int seed_hits = 0, phase2_bands = 0; /* seed hit rate on the Phase-2 path */ + int seed_h1 = 0; /* previous Phase-1 band's accepted height */ + int p1_hits = 0, p1_bands = 0; /* seed hit rate on the Phase-1 path */ G_important_message(_("Projecting (banded, per-thread PROJ context)...")); @@ -924,9 +1000,31 @@ int main(int argc, char **argv) * rows), so width splitting shrinks a tile's input ROW span, not its * width. Easy pairs never leave Phase 1. */ double ts = rproj_wtime(); - int band_orows = outcellhd.rows - obr0; int tilew = outcellhd.cols; int imin = 0, imax = -1; + /* Phase-1 neighbor seed (hit path): seed_h1 (previous Phase-1 accepted + * height) is close to this band's. Take g_seed, the grid height just + * ABOVE seed_h1 on this band's descending lattice; if it does not fit + * then (height-monotone span) nothing taller fits, so the tallest + * fitting height is at or below (g_seed+1)/2 and the walk can start + * there, skipping the tall full-width edge walks. Any miss (no seed, + * seed_h1 too tall, or g_seed fits) starts from the full remaining + * height -- byte-for-byte the walk below. Same lattice, same acceptance + * line -> identical accepted height and partition; the hit only skips + * heights it has shown cannot fit. */ + int band_orows = outcellhd.rows - obr0; + int p1_seeded = 0; + if (seed_h1 > 0 && seed_h1 < band_orows) { + int gs = band_orows; + + while ((gs + 1) / 2 > seed_h1) + gs = (gs + 1) / 2; + if (!phase1_fits(&outcellhd, &incellhd, &oproj, &iproj, &tproj, + obr0, gs, cap_bytes, cell_size)) { + band_orows = (gs + 1) / 2; + p1_seeded = 1; + } + } for (;;) { band_input_row_span(&outcellhd, &incellhd, &oproj, &iproj, &tproj, obr0, obr0 + band_orows, 0, outcellhd.cols, @@ -942,69 +1040,46 @@ int main(int argc, char **argv) break; /* height exhausted: fall through to column splitting */ band_orows = (band_orows + 1) / 2; /* halve (round up), re-sample */ } + if (band_orows > 1) { /* Phase-1 accepted a full-width band */ + seed_h1 = band_orows; + p1_bands++; + if (p1_seeded) + p1_hits++; + } if (band_orows == 1) { - /* Phase 2 (oblique only): Phase 1 could not fit even a single - * full-width row, so prefer a TALL tiled band instead of a 1-row - * one. Rescan from the full remaining height downward; at the - * tallest height whose output buffer fits the cap, halve tile WIDTH - * until the worst column tile's strip fits, and only reduce height - * when no width fits. Keeping the band tall gives the per-band - * parallel region many output rows. Runs only on this path, so the - * easy-pair size phase (Phase 1) is unaffected. */ - band_orows = outcellhd.rows - obr0; - for (;;) { - size_t out_bytes = - (size_t)band_orows * outcellhd.cols * cell_size; - if (out_bytes <= cap_bytes) { - /* Upper tier: cheap probe estimate narrows to a candidate - * width, checking down to tilew==1. The estimate is a lower - * bound, so est-no-fit at tilew==1 implies exact-no-fit -> - * the exact validation below is skipped entirely at heights - * where no width can fit (this is what keeps the search - * cheap; scanning every tile there was the cost). */ - tilew = outcellhd.cols; - int est_fit = 0; - for (;;) { - int est = est_worst_tile_strip_rows( - &outcellhd, &incellhd, &oproj, &iproj, &tproj, obr0, - obr0 + band_orows, tilew, TILE_PROBE); - size_t est_bytes = - est > 0 ? (size_t)est * incellhd.cols * cell_size - : 0; - if (est_bytes + out_bytes <= cap_bytes) { - est_fit = 1; - break; - } - if (tilew == 1) - break; - tilew = (tilew + 1) / 2; - } - /* Lower tier: EXACT validation, only when the estimate - * found a candidate. Narrow and re-validate if the estimate - * was optimistic; this exact-sizes the accepted width so - * the cap is honored. */ - int fit = 0; - if (est_fit) { - for (;;) { - int worst = worst_tile_strip_rows( - &outcellhd, &incellhd, &oproj, &iproj, &tproj, - obr0, obr0 + band_orows, tilew); - size_t strip_bytes = - worst > 0 - ? (size_t)worst * incellhd.cols * cell_size - : 0; - if (strip_bytes + out_bytes <= cap_bytes) { - fit = 1; - break; - } - if (tilew == 1) - break; /* exact: no width fits at this height */ - tilew = (tilew + 1) / 2; - } - } - if (fit) - break; + /* Phase 2 (oblique only): find the tallest band height on the + * descending grid whose worst column tile fits the cap, then that + * height's widest fitting tile width. Neighbor seed (hit path): the + * previous Phase-2 band's height (seed_h) is close to this band's + * H*. Take g_seed, the grid height just ABOVE seed_h; if it does + * not fit then (for an input-row span monotone in band height) + * nothing taller fits, so H* is at or below g_seed and the walk can + * start there, skipping the tall no-fit heights. On a miss (no + * seed, seed_h too tall, or g_seed fits) start from the full + * remaining height -- byte-for-byte the unseeded walk. Both starts + * lie on the same grid and accept via the same phase2_width_fit, so + * H*, W* and the partition are identical; the hit path only skips + * heights it has shown cannot fit. */ + phase2_bands++; + int start_h = outcellhd.rows - obr0; + if (seed_w > 0 && seed_h < start_h) { + int gs = start_h, w; + + while ((gs + 1) / 2 > seed_h) + gs = (gs + 1) / 2; + if (!phase2_width_fit(&outcellhd, &incellhd, &oproj, &iproj, + &tproj, obr0, gs, cap_bytes, cell_size, + &w)) { + start_h = (gs + 1) / 2; + seed_hits++; } + } + band_orows = start_h; + for (;;) { + if (phase2_width_fit(&outcellhd, &incellhd, &oproj, &iproj, + &tproj, obr0, band_orows, cap_bytes, + cell_size, &tilew)) + break; if (band_orows == 1) { /* Single output row at minimum width still over cap = * singular/large-halo; needs the tile-cache path. */ @@ -1023,8 +1098,10 @@ int main(int argc, char **argv) (double)(strip_bytes + out1) / (1024.0 * 1024.0), worst, cap_mb); } - band_orows = (band_orows + 1) / 2; /* shrink height, retry */ + band_orows = (band_orows + 1) / 2; } + seed_h = band_orows; + seed_w = tilew; } t_size += rproj_wtime() - ts; @@ -1162,8 +1239,9 @@ int main(int argc, char **argv) G_debug(1, "PHASE_TIMERS size=%.4f fill=%.4f compute=%.4f write=%.4f bands=%d " - "tiles=%d", - t_size, t_fill, t_compute, t_write, n_bands, max_tiles); + "tiles=%d seed_hits=%d phase2_bands=%d p1_hits=%d p1_bands=%d", + t_size, t_fill, t_compute, t_write, n_bands, max_tiles, seed_hits, + phase2_bands, p1_hits, p1_bands); /* Close input map in its own env, then the output map. */ G_switch_env(); /* -> input */ From c621ef37f43803e490410dbf78809eb44a47c4dd Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Tue, 14 Jul 2026 15:23:21 -0700 Subject: [PATCH 11/39] r.proj: size the input strip for tiles containing a pole The memory-bounded band sizing walks each output tile's perimeter to bound the range of input rows the tile needs, then loads that strip. A tile whose interior contains a geographic pole has its northmost or southmost latitude at the pole, in the tile interior, where the perimeter walk never samples it. The strip was therefore sized too small, and projecting a pole-containing map aborted with a "Band strip under-sized" error at every memory setting, though the projection itself was well defined. This computes, once per map, each geographic pole that lies within the input's latitude coverage: its coordinate in the output projection and its input row. When an output tile's rectangle contains a pole, that pole's input row is folded into the tile's row span, so the height and width search sees the true footprint and shrinks pole tiles until they fit the memory cap. The loaded strip then covers every row the fill reads. Only lat/lon input is handled, where a pole is at latitude 90 or -90. If the pole's coordinate transform fails or returns a non-finite value the pole is skipped and the existing under-size guard stays as the backstop. A map with no pole in the output frame is unaffected: the row spans, the band and tile partition, and the output are byte for byte unchanged. --- raster/r.proj/main.c | 135 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 107 insertions(+), 28 deletions(-) diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index 682d64f7289..9d59f4e9f56 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -130,6 +130,21 @@ static void interpolate_strip(void *strip, void *obufptr, int cell_type, memcpy(obufptr, src, cell_size); } +/* Geographic poles within the input map's latitude coverage. + * band_input_row_span samples only the tile perimeter, so a tile whose interior + * holds a pole has an input-row (latitude) extremum the perimeter misses; the + * pole's row is folded into that tile's span. Each pole is stored as its + * output-CRS coordinate (for a point-in-tile test) and its input row. Filled + * once per map, and empty (n == 0) whenever no pole is in frame, so pole + * handling is a no-op on such maps. Assumes a pole maps to a single output + * point (azimuthal/stereographic); for a projection that images a pole as a + * line or arc, the under-size guard remains the backstop. */ +struct pole_set { + int n; /* active poles, 0..2 */ + double ox[2], oy[2]; /* pole coordinates in the output CRS */ + double ri[2]; /* pole input row index */ +}; + /* Dense edge-walk of an output tile's rectangle [obr0, obr1) x [obc0, obc1) * projected into input space; returns the min/max INPUT ROW touched, plus a * 2-cell margin, clamped to the input map. Samples the tile's top and bottom @@ -144,7 +159,8 @@ static void band_input_row_span(const struct Cell_head *ohd, const struct pj_info *oproj, const struct pj_info *iproj, const struct pj_info *tproj, int obr0, int obr1, - int obc0, int obc1, int *imin, int *imax) + int obc0, int obc1, int *imin, int *imax, + const struct pole_set *poles, int *pole_widened) { double rmin = 1e300, rmax = -1e300; int e, r, c; @@ -182,6 +198,32 @@ static void band_input_row_span(const struct Cell_head *ohd, } } + /* Fold in any pole whose output point lies in this tile's rect: the + * perimeter walk cannot see an interior latitude extremum. A pole exactly + * on a tile edge (inclusive test) is caught by both adjacent tiles, which + * is harmless -- it only widens a strip that is loaded anyway. Placed + * before the empty-tile check so a pole inside an otherwise-outside tile + * still yields a valid span. */ + if (poles) { + double x_lo = ohd->west + obc0 * ohd->ew_res; + double x_hi = ohd->west + obc1 * ohd->ew_res; + double y_lo = ohd->north - obr1 * ohd->ns_res; + double y_hi = ohd->north - obr0 * ohd->ns_res; + int k; + + for (k = 0; k < poles->n; k++) { + if (poles->ox[k] < x_lo || poles->ox[k] > x_hi || + poles->oy[k] < y_lo || poles->oy[k] > y_hi) + continue; + if (poles->ri[k] < rmin) + rmin = poles->ri[k]; + if (poles->ri[k] > rmax) + rmax = poles->ri[k]; + if (pole_widened) + *pole_widened = k + 1; /* 1-based pole index, 0 == none */ + } + } + if (rmax < rmin) { /* band projects entirely outside the input */ *imin = 0; *imax = -1; @@ -205,12 +247,11 @@ static void band_input_row_span(const struct Cell_head *ohd, * call is a full tile edge-walk, so this is O(tiles * perimeter) -- paid in the * serial size phase, and only when column splitting is actually entered. * Returns 0 if every tile projects entirely outside the input. */ -static int worst_tile_strip_rows(const struct Cell_head *ohd, - const struct Cell_head *ihd, - const struct pj_info *oproj, - const struct pj_info *iproj, - const struct pj_info *tproj, int obr0, - int obr1, int tilew) +static int +worst_tile_strip_rows(const struct Cell_head *ohd, const struct Cell_head *ihd, + const struct pj_info *oproj, const struct pj_info *iproj, + const struct pj_info *tproj, int obr0, int obr1, + int tilew, const struct pole_set *poles) { int worst = 0, obc0; @@ -221,7 +262,7 @@ static int worst_tile_strip_rows(const struct Cell_head *ohd, if (obc1 > ohd->cols) obc1 = ohd->cols; band_input_row_span(ohd, ihd, oproj, iproj, tproj, obr0, obr1, obc0, - obc1, &imin, &imax); + obc1, &imin, &imax, poles, NULL); rows = imax - imin + 1; /* imax < imin (empty) -> <= 0, ignored */ if (rows > worst) worst = rows; @@ -241,7 +282,8 @@ static int est_worst_tile_strip_rows(const struct Cell_head *ohd, const struct pj_info *oproj, const struct pj_info *iproj, const struct pj_info *tproj, int obr0, - int obr1, int tilew, int probe) + int obr1, int tilew, int probe, + const struct pole_set *poles) { int ntiles = (ohd->cols + tilew - 1) / tilew; int worst = 0, k; @@ -259,7 +301,7 @@ static int est_worst_tile_strip_rows(const struct Cell_head *ohd, if (obc1 > ohd->cols) obc1 = ohd->cols; band_input_row_span(ohd, ihd, oproj, iproj, tproj, obr0, obr1, obc0, - obc1, &imin, &imax); + obc1, &imin, &imax, poles, NULL); rows = imax - imin + 1; if (rows > worst) worst = rows; @@ -272,12 +314,11 @@ static int est_worst_tile_strip_rows(const struct Cell_head *ohd, * width whose worst input strip fits (setting *acc_tilew to that width, via the * same upper-tier estimate then lower-tier exact validation the search uses); * 0 if no width fits or the output buffer alone exceeds the cap. */ -static int phase2_width_fit(const struct Cell_head *ohd, - const struct Cell_head *ihd, - const struct pj_info *oproj, - const struct pj_info *iproj, - const struct pj_info *tproj, int obr0, int h, - size_t cap_bytes, int cell_size, int *acc_tilew) +static int +phase2_width_fit(const struct Cell_head *ohd, const struct Cell_head *ihd, + const struct pj_info *oproj, const struct pj_info *iproj, + const struct pj_info *tproj, int obr0, int h, size_t cap_bytes, + int cell_size, int *acc_tilew, const struct pole_set *poles) { size_t out_bytes = (size_t)h * ohd->cols * cell_size; int tilew, est_fit; @@ -288,7 +329,7 @@ static int phase2_width_fit(const struct Cell_head *ohd, est_fit = 0; for (;;) { int est = est_worst_tile_strip_rows(ohd, ihd, oproj, iproj, tproj, obr0, - obr0 + h, tilew, TILE_PROBE); + obr0 + h, tilew, TILE_PROBE, poles); size_t est_bytes = est > 0 ? (size_t)est * ihd->cols * cell_size : 0; if (est_bytes + out_bytes <= cap_bytes) { est_fit = 1; @@ -301,7 +342,7 @@ static int phase2_width_fit(const struct Cell_head *ohd, if (est_fit) { for (;;) { int worst = worst_tile_strip_rows(ohd, ihd, oproj, iproj, tproj, - obr0, obr0 + h, tilew); + obr0, obr0 + h, tilew, poles); size_t strip_bytes = worst > 0 ? (size_t)worst * ihd->cols * cell_size : 0; if (strip_bytes + out_bytes <= cap_bytes) { @@ -324,7 +365,8 @@ static int phase2_width_fit(const struct Cell_head *ohd, static int phase1_fits(const struct Cell_head *ohd, const struct Cell_head *ihd, const struct pj_info *oproj, const struct pj_info *iproj, const struct pj_info *tproj, int obr0, int h, - size_t cap_bytes, int cell_size) + size_t cap_bytes, int cell_size, + const struct pole_set *poles) { int imin, imax, strip_rows; size_t out_bytes = (size_t)h * ohd->cols * cell_size, strip_bytes; @@ -332,7 +374,7 @@ static int phase1_fits(const struct Cell_head *ohd, const struct Cell_head *ihd, if (out_bytes > cap_bytes) return 0; band_input_row_span(ohd, ihd, oproj, iproj, tproj, obr0, obr0 + h, 0, - ohd->cols, &imin, &imax); + ohd->cols, &imin, &imax, poles, NULL); strip_rows = imax - imin + 1; strip_bytes = strip_rows > 0 ? (size_t)strip_rows * ihd->cols * cell_size : 0; @@ -987,6 +1029,35 @@ int main(int argc, char **argv) int seed_h1 = 0; /* previous Phase-1 band's accepted height */ int p1_hits = 0, p1_bands = 0; /* seed hit rate on the Phase-1 path */ + /* Pole footprint fix: a tile whose interior holds a geographic pole has an + * input-row extremum the perimeter walk misses. Precompute each in-range + * pole's output coordinate and input row (lat/lon input only, where a pole + * is at latitude +/- 90). On transform failure or a non-finite result the + * pole is skipped and the strip under-size guard stays the backstop. Uses + * the adjusted incellhd, matching what band_input_row_span sees. */ + struct pole_set poles; + + poles.n = 0; + if (incellhd.proj == PROJECTION_LL) { + double polelat[2] = {90.0, -90.0}; + + for (int p = 0; p < 2; p++) { + double px = 0.0, py = polelat[p]; + + if (polelat[p] > incellhd.north + 0.5 * incellhd.ns_res || + polelat[p] < incellhd.south - 0.5 * incellhd.ns_res) + continue; + if (GPJ_transform(&oproj, &iproj, &tproj, PJ_INV, &px, &py, NULL) < + 0 || + !isfinite(px) || !isfinite(py)) + continue; + poles.ox[poles.n] = px; + poles.oy[poles.n] = py; + poles.ri[poles.n] = (incellhd.north - polelat[p]) / incellhd.ns_res; + poles.n++; + } + } + G_important_message(_("Projecting (banded, per-thread PROJ context)...")); int obr0 = 0; @@ -1020,7 +1091,7 @@ int main(int argc, char **argv) while ((gs + 1) / 2 > seed_h1) gs = (gs + 1) / 2; if (!phase1_fits(&outcellhd, &incellhd, &oproj, &iproj, &tproj, - obr0, gs, cap_bytes, cell_size)) { + obr0, gs, cap_bytes, cell_size, &poles)) { band_orows = (gs + 1) / 2; p1_seeded = 1; } @@ -1028,7 +1099,7 @@ int main(int argc, char **argv) for (;;) { band_input_row_span(&outcellhd, &incellhd, &oproj, &iproj, &tproj, obr0, obr0 + band_orows, 0, outcellhd.cols, - &imin, &imax); + &imin, &imax, &poles, NULL); int strip_rows = imax - imin + 1; size_t strip_bytes = strip_rows > 0 ? (size_t)strip_rows * incellhd.cols * cell_size @@ -1069,7 +1140,7 @@ int main(int argc, char **argv) gs = (gs + 1) / 2; if (!phase2_width_fit(&outcellhd, &incellhd, &oproj, &iproj, &tproj, obr0, gs, cap_bytes, cell_size, - &w)) { + &w, &poles)) { start_h = (gs + 1) / 2; seed_hits++; } @@ -1078,15 +1149,15 @@ int main(int argc, char **argv) for (;;) { if (phase2_width_fit(&outcellhd, &incellhd, &oproj, &iproj, &tproj, obr0, band_orows, cap_bytes, - cell_size, &tilew)) + cell_size, &tilew, &poles)) break; if (band_orows == 1) { /* Single output row at minimum width still over cap = * singular/large-halo; needs the tile-cache path. */ size_t out1 = (size_t)outcellhd.cols * cell_size; - int worst = worst_tile_strip_rows(&outcellhd, &incellhd, - &oproj, &iproj, &tproj, - obr0, obr0 + 1, 1); + int worst = worst_tile_strip_rows( + &outcellhd, &incellhd, &oproj, &iproj, &tproj, obr0, + obr0 + 1, 1, &poles); size_t strip_bytes = worst > 0 ? (size_t)worst * incellhd.cols * cell_size : 0; @@ -1128,8 +1199,16 @@ int main(int argc, char **argv) /* Per-tile input row span (full-width strip: the raster API reads * whole rows, so columns are not cropped). */ + int pole_widened = 0; + band_input_row_span(&outcellhd, &incellhd, &oproj, &iproj, &tproj, - obr0, obr1, obc0, obc1, &imin, &imax); + obr0, obr1, obc0, obc1, &imin, &imax, &poles, + &pole_widened); + if (pole_widened) + G_verbose_message( + _("Pole (input row %d) in output tile rows [%d, %d) cols " + "[%d, %d): input strip extended to reach it"), + (int)poles.ri[pole_widened - 1], obr0, obr1, obc0, obc1); int strip_rows = imax - imin + 1; /* Serial strip load (single fd -> get_row not thread-safe). EMPTY From 2f5747e00aa504f44667395ad9247458b9be04d5 Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Fri, 17 Jul 2026 21:41:12 -0700 Subject: [PATCH 12/39] r.proj: add strip-based non-nearest interpolation to the banded path The banded compute path dispatched a nearest-only strip reader for every resampling method, so bilinear, bicubic, lanczos and their fallback variants silently produced nearest-neighbor output. Add interp_strip.c with strip counterparts of the cache kernels (strip_bilinear, strip_cubic, strip_lanczos, and the three _f fallbacks). They read the in-RAM full-width FCELL band strip the banded path already loads, using the same base index, bounds, weights, and null fallback as the readcell-cache kernels in bilinear.c, cubic.c, and lanczos.c. A strip_kernels[] table, ordered like menu[], resolves each method to its strip counterpart once after option parsing; nearest keeps the existing interpolate_strip reader in slot 0. Output is bitwise identical to serial r.proj for all seven methods across the test datasets. --- raster/r.proj/interp_strip.c | 257 +++++++++++++++++++++++++++++++++++ raster/r.proj/main.c | 14 +- raster/r.proj/r.proj.h | 20 +++ 3 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 raster/r.proj/interp_strip.c diff --git a/raster/r.proj/interp_strip.c b/raster/r.proj/interp_strip.c new file mode 100644 index 00000000000..c1a18899286 --- /dev/null +++ b/raster/r.proj/interp_strip.c @@ -0,0 +1,257 @@ +/* + * interp_strip.c - strip-based interpolation kernels for the banded r.proj + * compute path. These mirror the cache-based kernels (bilinear.c, cubic.c, + * lanczos.c and their _f variants) but read an in-RAM FCELL band strip + * holding input rows [imin, imax] instead of the readcell block cache. + * Nearest is handled by interpolate_strip() in main.c and is not duplicated. + */ + +#include +#include +#include +#include +#include "r.proj.h" + +/* Read one FCELL from the band strip. The strip holds full-width input rows + * [imin, imax] contiguously; input row r maps to strip row (r - imin), the same + * addressing as interpolate_strip(). Every read is guarded by the same + * under-size tripwire as interpolate_strip: a stencil row inside the input map + * but outside the loaded strip means a sizing/indexing bug, so fail loudly + * rather than read out of bounds. Each kernel runs its full-map bounds check + * first (setting NULL for out-of-map stencils), so this tripwire only ever + * fires on a bug. */ +static inline FCELL strip_val(const void *strip, int r, int c, int imin, + int imax, int cols) +{ + if (r < imin || r > imax) + G_fatal_error(_("Band strip under-sized: input row %d outside loaded " + "range [%d, %d] at column %d"), + r, imin, imax, c); + return ((const FCELL *)strip)[(size_t)(r - imin) * cols + c]; +} + +void strip_bilinear(void *strip, void *obufptr, int cell_type, double col_idx, + double row_idx, struct Cell_head *incellhd, int imin, + int imax) +{ + int row, col, i, j; + FCELL t, u, result; + FCELL c[2][2]; + + row = (int)floor(row_idx - 0.5); + col = (int)floor(col_idx - 0.5); + + /* Full-map bounds check runs before any strip read: an out-of-map stencil + * sets NULL and returns, so strip_val is never reached out of range. */ + if (row < 0 || row + 1 >= incellhd->rows || col < 0 || + col + 1 >= incellhd->cols) { + Rast_set_null_value(obufptr, 1, cell_type); + return; + } + + for (i = 0; i < 2; i++) + for (j = 0; j < 2; j++) { + const FCELL cell = + strip_val(strip, row + i, col + j, imin, imax, incellhd->cols); + + if (Rast_is_f_null_value(&cell)) { + Rast_set_null_value(obufptr, 1, cell_type); + return; + } + c[i][j] = cell; + } + + t = col_idx - 0.5 - col; + u = row_idx - 0.5 - row; + + result = Rast_interp_bilinear(t, u, c[0][0], c[0][1], c[1][0], c[1][1]); + + Rast_set_f_value(obufptr, result, cell_type); +} + +void strip_cubic(void *strip, void *obufptr, int cell_type, double col_idx, + double row_idx, struct Cell_head *incellhd, int imin, int imax) +{ + int row, col, i, j; + FCELL t, u, result; + FCELL val[4]; + FCELL c[4][4]; + + row = (int)floor(row_idx - 0.5); + col = (int)floor(col_idx - 0.5); + + /* Full-map bounds check runs before any strip read. */ + if (row - 1 < 0 || row + 2 >= incellhd->rows || col - 1 < 0 || + col + 2 >= incellhd->cols) { + Rast_set_null_value(obufptr, 1, cell_type); + return; + } + + for (i = 0; i < 4; i++) + for (j = 0; j < 4; j++) { + const FCELL cell = strip_val(strip, row - 1 + i, col - 1 + j, imin, + imax, incellhd->cols); + + if (Rast_is_f_null_value(&cell)) { + Rast_set_null_value(obufptr, 1, cell_type); + return; + } + c[i][j] = cell; + } + + t = col_idx - 0.5 - col; + u = row_idx - 0.5 - row; + + for (i = 0; i < 4; i++) { + const FCELL *tmp = c[i]; + + val[i] = Rast_interp_cubic(t, tmp[0], tmp[1], tmp[2], tmp[3]); + } + + result = Rast_interp_cubic(u, val[0], val[1], val[2], val[3]); + + Rast_set_f_value(obufptr, result, cell_type); +} + +void strip_lanczos(void *strip, void *obufptr, int cell_type, double col_idx, + double row_idx, struct Cell_head *incellhd, int imin, + int imax) +{ + int row, col, i, j, k; + double t, u; + FCELL result; + DCELL c[25]; + + row = (int)floor(row_idx); + col = (int)floor(col_idx); + + /* Full-map bounds check runs before any strip read. */ + if (row - 2 < 0 || row + 2 >= incellhd->rows || col - 2 < 0 || + col + 2 >= incellhd->cols) { + Rast_set_null_value(obufptr, 1, cell_type); + return; + } + + k = 0; + for (i = 0; i < 5; i++) { + for (j = 0; j < 5; j++) { + const FCELL cell = strip_val(strip, row - 2 + i, col - 2 + j, imin, + imax, incellhd->cols); + + if (Rast_is_f_null_value(&cell)) { + Rast_set_null_value(obufptr, 1, cell_type); + return; + } + c[k++] = cell; + } + } + + t = col_idx - 0.5 - col; + u = row_idx - 0.5 - row; + + result = Rast_interp_lanczos(t, u, c); + + Rast_set_f_value(obufptr, result, cell_type); +} + +void strip_bilinear_f(void *strip, void *obufptr, int cell_type, double col_idx, + double row_idx, struct Cell_head *incellhd, int imin, + int imax) +{ + int row, col; + FCELL cell; + + row = (int)floor(row_idx); + col = (int)floor(col_idx); + + if (row < 0 || row >= incellhd->rows || col < 0 || col >= incellhd->cols) { + Rast_set_null_value(obufptr, 1, cell_type); + return; + } + + cell = strip_val(strip, row, col, imin, imax, incellhd->cols); + /* if nearest is null, all the other interps will be null */ + if (Rast_is_f_null_value(&cell)) { + Rast_set_null_value(obufptr, 1, cell_type); + return; + } + + strip_bilinear(strip, obufptr, cell_type, col_idx, row_idx, incellhd, imin, + imax); + /* fallback to nearest if bilinear is null */ + if (Rast_is_f_null_value(obufptr)) + Rast_set_f_value(obufptr, cell, cell_type); +} + +void strip_cubic_f(void *strip, void *obufptr, int cell_type, double col_idx, + double row_idx, struct Cell_head *incellhd, int imin, + int imax) +{ + int row, col; + FCELL cell; + + row = (int)floor(row_idx); + col = (int)floor(col_idx); + + if (row < 0 || row >= incellhd->rows || col < 0 || col >= incellhd->cols) { + Rast_set_null_value(obufptr, 1, cell_type); + return; + } + + cell = strip_val(strip, row, col, imin, imax, incellhd->cols); + /* if nearest is null, all the other interps will be null */ + if (Rast_is_f_null_value(&cell)) { + Rast_set_null_value(obufptr, 1, cell_type); + return; + } + + strip_cubic(strip, obufptr, cell_type, col_idx, row_idx, incellhd, imin, + imax); + /* fallback to bilinear if cubic is null */ + if (Rast_is_f_null_value(obufptr)) { + strip_bilinear(strip, obufptr, cell_type, col_idx, row_idx, incellhd, + imin, imax); + /* fallback to nearest if bilinear is null */ + if (Rast_is_f_null_value(obufptr)) + Rast_set_f_value(obufptr, cell, cell_type); + } +} + +void strip_lanczos_f(void *strip, void *obufptr, int cell_type, double col_idx, + double row_idx, struct Cell_head *incellhd, int imin, + int imax) +{ + int row, col; + FCELL cell; + + row = (int)floor(row_idx); + col = (int)floor(col_idx); + + if (row < 0 || row >= incellhd->rows || col < 0 || col >= incellhd->cols) { + Rast_set_null_value(obufptr, 1, cell_type); + return; + } + + cell = strip_val(strip, row, col, imin, imax, incellhd->cols); + /* if nearest is null, all the other interps will be null */ + if (Rast_is_f_null_value(&cell)) { + Rast_set_null_value(obufptr, 1, cell_type); + return; + } + + strip_lanczos(strip, obufptr, cell_type, col_idx, row_idx, incellhd, imin, + imax); + /* fallback to bicubic if lanczos is null */ + if (Rast_is_f_null_value(obufptr)) { + strip_cubic(strip, obufptr, cell_type, col_idx, row_idx, incellhd, imin, + imax); + /* fallback to bilinear if cubic is null */ + if (Rast_is_f_null_value(obufptr)) { + strip_bilinear(strip, obufptr, cell_type, col_idx, row_idx, + incellhd, imin, imax); + /* fallback to nearest if bilinear is null */ + if (Rast_is_f_null_value(obufptr)) + Rast_set_f_value(obufptr, cell, cell_type); + } + } +} diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index 9d59f4e9f56..99d76e9e695 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -130,6 +130,13 @@ static void interpolate_strip(void *strip, void *obufptr, int cell_type, memcpy(obufptr, src, cell_size); } +/* Strip-based kernels for the banded compute path, in the same order as menu[]: + * slot i is the strip counterpart of menu[i].method. Slot 0 is nearest + * (interpolate_strip above); slots 1-6 are the interp_strip.c kernels. */ +static const strip_func strip_kernels[] = { + interpolate_strip, strip_bilinear, strip_cubic, strip_lanczos, + strip_bilinear_f, strip_cubic_f, strip_lanczos_f}; + /* Geographic poles within the input map's latitude coverage. * band_input_row_span samples only the tile perimeter, so a tile whose interior * holds a pole has an input-row (latitude) extremum the perimeter misses; the @@ -571,6 +578,9 @@ int main(int argc, char **argv) G_fatal_error(_("<%s=%s> unknown %s"), interpol->key, interpol->answer, interpol->key); + /* Resolve the strip kernel once; menu[] and strip_kernels[] share order. */ + strip_func interp = strip_kernels[method]; + mapname = outmap->answer ? outmap->answer : inmap->answer; if (mapname && !list->answer && !overwrite && !print_bounds->answer && outputFormat != SHELL && G_find_raster(mapname, G_mapset())) @@ -1286,8 +1296,8 @@ int main(int argc, char **argv) (x1 - incellhd.west) / incellhd.ew_res; double r_idx = (incellhd.north - y1) / incellhd.ns_res; - interpolate_strip(strip, obufptr, cell_type, c_idx, - r_idx, &incellhd, imin, imax); + interp(strip, obufptr, cell_type, c_idx, r_idx, + &incellhd, imin, imax); } } } diff --git a/raster/r.proj/r.proj.h b/raster/r.proj/r.proj.h index 935415c1e41..28e46a503bb 100644 --- a/raster/r.proj/r.proj.h +++ b/raster/r.proj/r.proj.h @@ -27,6 +27,12 @@ struct cache { typedef void (*func)(struct cache *, void *, int, double, double, struct Cell_head *); +/* Strip-based interpolation kernels (interp_strip.c) for the banded compute + * path read an in-RAM FCELL strip holding input rows [imin, imax] instead of + * the readcell block cache, so they take imin/imax in place of struct cache. */ +typedef void (*strip_func)(void *, void *, int, double, double, + struct Cell_head *, int, int); + struct menu { func method; /* routine to interpolate new value */ char *name; /* method name */ @@ -67,6 +73,20 @@ extern void p_lanczos(struct cache *, void *, int, double, double, extern void p_lanczos_f(struct cache *, void *, int, double, double, struct Cell_head *); +/* interp_strip.c - strip variants for the banded compute path */ +extern void strip_bilinear(void *, void *, int, double, double, + struct Cell_head *, int, int); +extern void strip_cubic(void *, void *, int, double, double, struct Cell_head *, + int, int); +extern void strip_lanczos(void *, void *, int, double, double, + struct Cell_head *, int, int); +extern void strip_bilinear_f(void *, void *, int, double, double, + struct Cell_head *, int, int); +extern void strip_cubic_f(void *, void *, int, double, double, + struct Cell_head *, int, int); +extern void strip_lanczos_f(void *, void *, int, double, double, + struct Cell_head *, int, int); + #if 1 #define BKIDX(c, y, x) ((y) * (c)->stride + (x)) From 03cdd9768fed56dc1ff24a4c0b33af059b4e296f Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Fri, 17 Jul 2026 22:13:46 -0700 Subject: [PATCH 13/39] r.proj: fix output row northing mismatch with serial Serial r.proj computes each output row's northing by subtracting ns_res row by row. The banded path computed it directly as north - ns_res/2 - row * ns_res, which can differ by one ulp when ns_res is not exactly representable. Nearest is unaffected, but for the other methods the shifted interpolation weights changed a few cells (29 of 76M on the EPSG:3035 test). Precompute the row northings once with the serial recurrence and use that array in both the sizing walk and the compute loop. --- raster/r.proj/main.c | 135 ++++++++++++++++++++++++++----------------- 1 file changed, 82 insertions(+), 53 deletions(-) diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index 99d76e9e695..0338206221d 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -161,13 +161,12 @@ struct pole_set { * the case obc0=0, obc1=cols. Called serially, before the parallel region, so * the shared tproj is safe here. Returns imax < imin for a tile that projects * entirely outside the input. */ -static void band_input_row_span(const struct Cell_head *ohd, - const struct Cell_head *ihd, - const struct pj_info *oproj, - const struct pj_info *iproj, - const struct pj_info *tproj, int obr0, int obr1, - int obc0, int obc1, int *imin, int *imax, - const struct pole_set *poles, int *pole_widened) +static void +band_input_row_span(const struct Cell_head *ohd, const struct Cell_head *ihd, + const struct pj_info *oproj, const struct pj_info *iproj, + const struct pj_info *tproj, const double *y_center, + int obr0, int obr1, int obc0, int obc1, int *imin, + int *imax, const struct pole_set *poles, int *pole_widened) { double rmin = 1e300, rmax = -1e300; int e, r, c; @@ -175,7 +174,7 @@ static void band_input_row_span(const struct Cell_head *ohd, /* top edge (row obr0) and bottom edge (row obr1-1), tile columns */ for (e = 0; e < 2; e++) { int orow = (e == 0) ? obr0 : (obr1 - 1); - double y = ohd->north - (orow + 0.5) * ohd->ns_res; + double y = y_center[orow]; for (c = obc0; c < obc1; c++) { double x = ohd->west + (c + 0.5) * ohd->ew_res; double xx = x, yy = y; @@ -193,7 +192,7 @@ static void band_input_row_span(const struct Cell_head *ohd, int ocol = (e == 0) ? obc0 : (obc1 - 1); double x = ohd->west + (ocol + 0.5) * ohd->ew_res; for (r = obr0; r < obr1; r++) { - double y = ohd->north - (r + 0.5) * ohd->ns_res; + double y = y_center[r]; double xx = x, yy = y; if (GPJ_transform(oproj, iproj, tproj, PJ_FWD, &xx, &yy, NULL) < 0) continue; @@ -254,11 +253,13 @@ static void band_input_row_span(const struct Cell_head *ohd, * call is a full tile edge-walk, so this is O(tiles * perimeter) -- paid in the * serial size phase, and only when column splitting is actually entered. * Returns 0 if every tile projects entirely outside the input. */ -static int -worst_tile_strip_rows(const struct Cell_head *ohd, const struct Cell_head *ihd, - const struct pj_info *oproj, const struct pj_info *iproj, - const struct pj_info *tproj, int obr0, int obr1, - int tilew, const struct pole_set *poles) +static int worst_tile_strip_rows(const struct Cell_head *ohd, + const struct Cell_head *ihd, + const struct pj_info *oproj, + const struct pj_info *iproj, + const struct pj_info *tproj, + const double *y_center, int obr0, int obr1, + int tilew, const struct pole_set *poles) { int worst = 0, obc0; @@ -268,8 +269,8 @@ worst_tile_strip_rows(const struct Cell_head *ohd, const struct Cell_head *ihd, if (obc1 > ohd->cols) obc1 = ohd->cols; - band_input_row_span(ohd, ihd, oproj, iproj, tproj, obr0, obr1, obc0, - obc1, &imin, &imax, poles, NULL); + band_input_row_span(ohd, ihd, oproj, iproj, tproj, y_center, obr0, obr1, + obc0, obc1, &imin, &imax, poles, NULL); rows = imax - imin + 1; /* imax < imin (empty) -> <= 0, ignored */ if (rows > worst) worst = rows; @@ -284,13 +285,11 @@ worst_tile_strip_rows(const struct Cell_head *ohd, const struct Cell_head *ihd, * including the first and last. A subset max is a LOWER bound on the true * worst, so it only PRUNES the Phase-2 search; the chosen width is exact- * validated by worst_tile_strip_rows before use. */ -static int est_worst_tile_strip_rows(const struct Cell_head *ohd, - const struct Cell_head *ihd, - const struct pj_info *oproj, - const struct pj_info *iproj, - const struct pj_info *tproj, int obr0, - int obr1, int tilew, int probe, - const struct pole_set *poles) +static int est_worst_tile_strip_rows( + const struct Cell_head *ohd, const struct Cell_head *ihd, + const struct pj_info *oproj, const struct pj_info *iproj, + const struct pj_info *tproj, const double *y_center, int obr0, int obr1, + int tilew, int probe, const struct pole_set *poles) { int ntiles = (ohd->cols + tilew - 1) / tilew; int worst = 0, k; @@ -307,8 +306,8 @@ static int est_worst_tile_strip_rows(const struct Cell_head *ohd, if (obc1 > ohd->cols) obc1 = ohd->cols; - band_input_row_span(ohd, ihd, oproj, iproj, tproj, obr0, obr1, obc0, - obc1, &imin, &imax, poles, NULL); + band_input_row_span(ohd, ihd, oproj, iproj, tproj, y_center, obr0, obr1, + obc0, obc1, &imin, &imax, poles, NULL); rows = imax - imin + 1; if (rows > worst) worst = rows; @@ -321,11 +320,13 @@ static int est_worst_tile_strip_rows(const struct Cell_head *ohd, * width whose worst input strip fits (setting *acc_tilew to that width, via the * same upper-tier estimate then lower-tier exact validation the search uses); * 0 if no width fits or the output buffer alone exceeds the cap. */ -static int -phase2_width_fit(const struct Cell_head *ohd, const struct Cell_head *ihd, - const struct pj_info *oproj, const struct pj_info *iproj, - const struct pj_info *tproj, int obr0, int h, size_t cap_bytes, - int cell_size, int *acc_tilew, const struct pole_set *poles) +static int phase2_width_fit(const struct Cell_head *ohd, + const struct Cell_head *ihd, + const struct pj_info *oproj, + const struct pj_info *iproj, + const struct pj_info *tproj, const double *y_center, + int obr0, int h, size_t cap_bytes, int cell_size, + int *acc_tilew, const struct pole_set *poles) { size_t out_bytes = (size_t)h * ohd->cols * cell_size; int tilew, est_fit; @@ -335,8 +336,9 @@ phase2_width_fit(const struct Cell_head *ohd, const struct Cell_head *ihd, tilew = ohd->cols; est_fit = 0; for (;;) { - int est = est_worst_tile_strip_rows(ohd, ihd, oproj, iproj, tproj, obr0, - obr0 + h, tilew, TILE_PROBE, poles); + int est = + est_worst_tile_strip_rows(ohd, ihd, oproj, iproj, tproj, y_center, + obr0, obr0 + h, tilew, TILE_PROBE, poles); size_t est_bytes = est > 0 ? (size_t)est * ihd->cols * cell_size : 0; if (est_bytes + out_bytes <= cap_bytes) { est_fit = 1; @@ -348,8 +350,9 @@ phase2_width_fit(const struct Cell_head *ohd, const struct Cell_head *ihd, } if (est_fit) { for (;;) { - int worst = worst_tile_strip_rows(ohd, ihd, oproj, iproj, tproj, - obr0, obr0 + h, tilew, poles); + int worst = + worst_tile_strip_rows(ohd, ihd, oproj, iproj, tproj, y_center, + obr0, obr0 + h, tilew, poles); size_t strip_bytes = worst > 0 ? (size_t)worst * ihd->cols * cell_size : 0; if (strip_bytes + out_bytes <= cap_bytes) { @@ -371,8 +374,8 @@ phase2_width_fit(const struct Cell_head *ohd, const struct Cell_head *ihd, * so the miss path is byte-for-byte today's execution. */ static int phase1_fits(const struct Cell_head *ohd, const struct Cell_head *ihd, const struct pj_info *oproj, const struct pj_info *iproj, - const struct pj_info *tproj, int obr0, int h, - size_t cap_bytes, int cell_size, + const struct pj_info *tproj, const double *y_center, + int obr0, int h, size_t cap_bytes, int cell_size, const struct pole_set *poles) { int imin, imax, strip_rows; @@ -380,8 +383,8 @@ static int phase1_fits(const struct Cell_head *ohd, const struct Cell_head *ihd, if (out_bytes > cap_bytes) return 0; - band_input_row_span(ohd, ihd, oproj, iproj, tproj, obr0, obr0 + h, 0, - ohd->cols, &imin, &imax, poles, NULL); + band_input_row_span(ohd, ihd, oproj, iproj, tproj, y_center, obr0, obr0 + h, + 0, ohd->cols, &imin, &imax, poles, NULL); strip_rows = imax - imin + 1; strip_bytes = strip_rows > 0 ? (size_t)strip_rows * ihd->cols * cell_size : 0; @@ -1039,6 +1042,29 @@ int main(int argc, char **argv) int seed_h1 = 0; /* previous Phase-1 band's accepted height */ int p1_hits = 0, p1_bands = 0; /* seed hit rate on the Phase-1 path */ + /* Output-row center northings, precomputed once by the serial version's + * recurrence: ycoord2 = north - ns_res/2, then ycoord2 -= ns_res per row. + * The banded fill loop and the strip-sizing perimeter walk both read these + * instead of computing north - ns_res/2 - row*ns_res directly. The direct + * multiply and the accumulated subtraction differ by up to one ULP when + * ns_res is not exactly representable; for non-nearest interpolation that + * shifts the sampling weights and diverges from the serial result by up to + * one FCELL ULP. The recurrence is reproduced here deliberately + * (bug-compatible rounding) so the parallel output stays bitwise identical + * to the serial reference; the direct multiply is the numerically cleaner + * form, so any future change away from the recurrence should be made in + * both code paths as an explicit accuracy decision. Both the fill loop and + * the sizing walk read these values, so sizing and fill stay on the same y + * and the loaded strip covers exactly the rows fill probes. */ + double *y_center = G_malloc((size_t)outcellhd.rows * sizeof(double)); + { + double yc = outcellhd.north - (outcellhd.ns_res / 2); + for (int r = 0; r < outcellhd.rows; r++) { + y_center[r] = yc; + yc -= outcellhd.ns_res; + } + } + /* Pole footprint fix: a tile whose interior holds a geographic pole has an * input-row extremum the perimeter walk misses. Precompute each in-range * pole's output coordinate and input row (lat/lon input only, where a pole @@ -1101,15 +1127,16 @@ int main(int argc, char **argv) while ((gs + 1) / 2 > seed_h1) gs = (gs + 1) / 2; if (!phase1_fits(&outcellhd, &incellhd, &oproj, &iproj, &tproj, - obr0, gs, cap_bytes, cell_size, &poles)) { + y_center, obr0, gs, cap_bytes, cell_size, + &poles)) { band_orows = (gs + 1) / 2; p1_seeded = 1; } } for (;;) { band_input_row_span(&outcellhd, &incellhd, &oproj, &iproj, &tproj, - obr0, obr0 + band_orows, 0, outcellhd.cols, - &imin, &imax, &poles, NULL); + y_center, obr0, obr0 + band_orows, 0, + outcellhd.cols, &imin, &imax, &poles, NULL); int strip_rows = imax - imin + 1; size_t strip_bytes = strip_rows > 0 ? (size_t)strip_rows * incellhd.cols * cell_size @@ -1149,8 +1176,8 @@ int main(int argc, char **argv) while ((gs + 1) / 2 > seed_h) gs = (gs + 1) / 2; if (!phase2_width_fit(&outcellhd, &incellhd, &oproj, &iproj, - &tproj, obr0, gs, cap_bytes, cell_size, - &w, &poles)) { + &tproj, y_center, obr0, gs, cap_bytes, + cell_size, &w, &poles)) { start_h = (gs + 1) / 2; seed_hits++; } @@ -1158,16 +1185,16 @@ int main(int argc, char **argv) band_orows = start_h; for (;;) { if (phase2_width_fit(&outcellhd, &incellhd, &oproj, &iproj, - &tproj, obr0, band_orows, cap_bytes, - cell_size, &tilew, &poles)) + &tproj, y_center, obr0, band_orows, + cap_bytes, cell_size, &tilew, &poles)) break; if (band_orows == 1) { /* Single output row at minimum width still over cap = * singular/large-halo; needs the tile-cache path. */ size_t out1 = (size_t)outcellhd.cols * cell_size; int worst = worst_tile_strip_rows( - &outcellhd, &incellhd, &oproj, &iproj, &tproj, obr0, - obr0 + 1, 1, &poles); + &outcellhd, &incellhd, &oproj, &iproj, &tproj, y_center, + obr0, obr0 + 1, 1, &poles); size_t strip_bytes = worst > 0 ? (size_t)worst * incellhd.cols * cell_size : 0; @@ -1212,8 +1239,8 @@ int main(int argc, char **argv) int pole_widened = 0; band_input_row_span(&outcellhd, &incellhd, &oproj, &iproj, &tproj, - obr0, obr1, obc0, obc1, &imin, &imax, &poles, - &pole_widened); + y_center, obr0, obr1, obc0, obc1, &imin, &imax, + &poles, &pole_widened); if (pole_widened) G_verbose_message( _("Pole (input row %d) in output tile rows [%d, %d) cols " @@ -1276,8 +1303,7 @@ int main(int argc, char **argv) void *out_row = (unsigned char *)band_out + (size_t)(row - obr0) * outcellhd.cols * cell_size; - double local_y = outcellhd.north - (outcellhd.ns_res / 2) - - (row * outcellhd.ns_res); + double local_y = y_center[row]; double local_x_start = outcellhd.west + (outcellhd.ew_res / 2); @@ -1326,9 +1352,12 @@ int main(int argc, char **argv) obr0 = obr1; } + G_free(y_center); + G_debug(1, - "PHASE_TIMERS size=%.4f fill=%.4f compute=%.4f write=%.4f bands=%d " - "tiles=%d seed_hits=%d phase2_bands=%d p1_hits=%d p1_bands=%d", + "PHASE_TIMERS size=%.4f fill=%.4f compute=%.4f write=%.4f " + "bands=%d tiles=%d seed_hits=%d phase2_bands=%d p1_hits=%d " + "p1_bands=%d", t_size, t_fill, t_compute, t_write, n_bands, max_tiles, seed_hits, phase2_bands, p1_hits, p1_bands); From a60cfa52baa6286104cc574db0d87c5b16c19da3 Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Fri, 17 Jul 2026 22:19:17 -0700 Subject: [PATCH 14/39] r.proj: fall back to serial tile cache instead of aborting The banded path aborted when the memory cap could not hold even one output row's input strip. In practice that needs an input wider than about cap/20 columns; poles and oblique projections do not trigger it. Warn with the minimum memory that keeps the parallel path, then finish the run through the old serial readcell cache. Output is bitwise identical to serial r.proj. Failed transforms set the cell NULL like the banded path does. R_PROJ_FORCE_TILECACHE forces the fallback for testing. --- raster/r.proj/main.c | 163 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 140 insertions(+), 23 deletions(-) diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index 0338206221d..66de006ce80 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -391,6 +391,60 @@ static int phase1_fits(const struct Cell_head *ohd, const struct Cell_head *ihd, return strip_bytes + out_bytes <= cap_bytes; } +/* Serial tile-cache fallback for the large-halo/oblique corner: when even a + * single output row's full-width input strip busts the memory cap (the bail in + * the band loop), the banded strip path cannot proceed. This finishes the run + * from output row obr0 onward using the classic readcell block cache (faults + * blocks on demand, bounded by the same memory option via nblocks) and the + * CVAL cache kernels (menu[].method), exactly as the serial r.proj does. + * + * Runs strictly serially: get_block mutates shared cache state and is not + * thread-safe. Rows [0, obr0) were already written by the banded path; each + * output row is independent of the others, so the banded prefix followed by + * this serial suffix is bit-identical to a pure serial run. y_center supplies + * the same output-row northings the banded prefix used (and that serial's + * ycoord2 recurrence produces), so the seam at obr0 is seamless. A transform + * failure sets NULL here (matching the banded strip path) rather than the old + * serial fatal; identical on data where transforms succeed. */ +static void +fallback_serial_cache(int fdi, int fdo, int cell_type, int method, + const struct pj_info *oproj, const struct pj_info *iproj, + const struct pj_info *tproj, struct Cell_head *incellhd, + struct Cell_head *outcellhd, const double *y_center, + int obr0, const char *memory) +{ + struct cache *ibuffer = readcell(fdi, memory); + func interpolate = menu[method].method; + void *obuffer = Rast_allocate_output_buf(cell_type); + int cell_size = Rast_cell_size(cell_type); + double local_x_start = outcellhd->west + (outcellhd->ew_res / 2); + + for (int row = obr0; row < outcellhd->rows; row++) { + G_percent(row - obr0, outcellhd->rows - obr0, 5); + for (int col = 0; col < outcellhd->cols; col++) { + void *obufptr = (unsigned char *)obuffer + (size_t)col * cell_size; + double x1 = local_x_start + col * outcellhd->ew_res; + double y1 = y_center[row]; + + if (GPJ_transform(oproj, iproj, tproj, PJ_FWD, &x1, &y1, NULL) < + 0) { + Rast_set_null_value(obufptr, 1, cell_type); + } + else { + double col_idx = (x1 - incellhd->west) / incellhd->ew_res; + double row_idx = (incellhd->north - y1) / incellhd->ns_res; + + interpolate(ibuffer, obufptr, cell_type, col_idx, row_idx, + incellhd); + } + } + Rast_put_row(fdo, obuffer, cell_type); + } + + release_cache(ibuffer); + G_free(obuffer); +} + int main(int argc, char **argv) { char *mapname, /* ptr to name of output layer */ @@ -1096,6 +1150,8 @@ int main(int argc, char **argv) G_important_message(_("Projecting (banded, per-thread PROJ context)...")); + int used_fallback = 0; /* set when the serial tile-cache fallback runs */ + int force_tilecache = getenv("R_PROJ_FORCE_TILECACHE") != NULL; int obr0 = 0; while (obr0 < outcellhd.rows) { /* Fit search. Phase 1 (fast path, unchanged): halve the band height @@ -1107,6 +1163,39 @@ int main(int argc, char **argv) * rows), so width splitting shrinks a tile's input ROW span, not its * width. Easy pairs never leave Phase 1. */ double ts = rproj_wtime(); + /* Band-0 early-out for the wide-input corner: if a single output row at + * the finest tiling already busts the cap, take the serial fallback now + * instead of running the height/width search only to bail. Uses the + * same worst_tile_strip_rows(obr0, obr0+1, 1) the Phase-2 bail uses, + * probed only at the first band so its O(cols) cost is paid once, not + * per band. Later-band (pole) busts still fall through to the Phase-2 + * bail. force_tilecache is deliberately not handled here, so the forced + * override keeps routing through that bail unchanged. */ + if (obr0 == 0) { + size_t out1 = (size_t)outcellhd.cols * cell_size; + int worst1 = worst_tile_strip_rows(&outcellhd, &incellhd, &oproj, + &iproj, &tproj, y_center, obr0, + obr0 + 1, 1, &poles); + size_t strip1 = + worst1 > 0 ? (size_t)worst1 * incellhd.cols * cell_size : 0; + if (strip1 + out1 > cap_bytes) { + int needed_mb = + (int)ceil((double)(strip1 + out1) / (1024.0 * 1024.0)) + 1; + G_warning(_("Memory cap (%.1f MB) is below what one output row " + "needs (input footprint %d rows, %.1f MB). Falling " + "back to the serial tile-cache path for output " + "rows %d-%d; this path is slower. Raise memory= to " + "at least %d MB to use the parallel path."), + cap_mb, worst1, + (double)(strip1 + out1) / (1024.0 * 1024.0), obr0, + outcellhd.rows - 1, needed_mb); + fallback_serial_cache(fdi, fdo, cell_type, method, &oproj, + &iproj, &tproj, &incellhd, &outcellhd, + y_center, obr0, memory->answer); + used_fallback = 1; + goto fallback_done; + } + } int tilew = outcellhd.cols; int imin = 0, imax = -1; /* Phase-1 neighbor seed (hit path): seed_h1 (previous Phase-1 accepted @@ -1142,7 +1231,7 @@ int main(int argc, char **argv) strip_rows > 0 ? (size_t)strip_rows * incellhd.cols * cell_size : 0; size_t out_bytes = (size_t)band_orows * outcellhd.cols * cell_size; - if (strip_bytes + out_bytes <= cap_bytes) + if (!force_tilecache && strip_bytes + out_bytes <= cap_bytes) break; if (band_orows == 1) break; /* height exhausted: fall through to column splitting */ @@ -1184,27 +1273,51 @@ int main(int argc, char **argv) } band_orows = start_h; for (;;) { - if (phase2_width_fit(&outcellhd, &incellhd, &oproj, &iproj, + if (!force_tilecache && + phase2_width_fit(&outcellhd, &incellhd, &oproj, &iproj, &tproj, y_center, obr0, band_orows, cap_bytes, cell_size, &tilew, &poles)) break; if (band_orows == 1) { /* Single output row at minimum width still over cap = - * singular/large-halo; needs the tile-cache path. */ - size_t out1 = (size_t)outcellhd.cols * cell_size; - int worst = worst_tile_strip_rows( - &outcellhd, &incellhd, &oproj, &iproj, &tproj, y_center, - obr0, obr0 + 1, 1, &poles); - size_t strip_bytes = - worst > 0 ? (size_t)worst * incellhd.cols * cell_size - : 0; - G_fatal_error( - _("A single output row needs %.1f MB (input footprint " - "%d rows), exceeding the memory cap (%.1f MB). This " - "large-halo/oblique case needs the tile-cache path, " - "which is not implemented."), - (double)(strip_bytes + out1) / (1024.0 * 1024.0), worst, - cap_mb); + * singular/large-halo; take the serial tile-cache path. + * Also reached from band 0 when R_PROJ_FORCE_TILECACHE is + * set, which routes normal data through this identical + * block for testing. */ + if (force_tilecache) { + G_warning( + _("R_PROJ_FORCE_TILECACHE is set: taking the " + "serial tile-cache path for all output rows " + "(testing override).")); + } + else { + size_t out1 = (size_t)outcellhd.cols * cell_size; + int worst = worst_tile_strip_rows( + &outcellhd, &incellhd, &oproj, &iproj, &tproj, + y_center, obr0, obr0 + 1, 1, &poles); + size_t strip_bytes = + worst > 0 + ? (size_t)worst * incellhd.cols * cell_size + : 0; + int needed_mb = (int)ceil((double)(strip_bytes + out1) / + (1024.0 * 1024.0)) + + 1; + G_warning( + _("Memory cap (%.1f MB) is below what one output " + "row needs (input footprint %d rows, %.1f MB). " + "Falling back to the serial tile-cache path for " + "output rows %d-%d; this path is slower. Raise " + "memory= to at least %d MB to use the parallel " + "path."), + cap_mb, worst, + (double)(strip_bytes + out1) / (1024.0 * 1024.0), + obr0, outcellhd.rows - 1, needed_mb); + } + fallback_serial_cache(fdi, fdo, cell_type, method, &oproj, + &iproj, &tproj, &incellhd, &outcellhd, + y_center, obr0, memory->answer); + used_fallback = 1; + goto fallback_done; } band_orows = (band_orows + 1) / 2; } @@ -1352,14 +1465,18 @@ int main(int argc, char **argv) obr0 = obr1; } +fallback_done: G_free(y_center); - G_debug(1, - "PHASE_TIMERS size=%.4f fill=%.4f compute=%.4f write=%.4f " - "bands=%d tiles=%d seed_hits=%d phase2_bands=%d p1_hits=%d " - "p1_bands=%d", - t_size, t_fill, t_compute, t_write, n_bands, max_tiles, seed_hits, - phase2_bands, p1_hits, p1_bands); + if (used_fallback) + G_debug(1, "PHASE_TIMERS fallback=1 fallback_from_row=%d", obr0); + else + G_debug(1, + "PHASE_TIMERS size=%.4f fill=%.4f compute=%.4f write=%.4f " + "bands=%d tiles=%d seed_hits=%d phase2_bands=%d p1_hits=%d " + "p1_bands=%d", + t_size, t_fill, t_compute, t_write, n_bands, max_tiles, + seed_hits, phase2_bands, p1_hits, p1_bands); /* Close input map in its own env, then the output map. */ G_switch_env(); /* -> input */ From 5419dd8b00409992670d11f53e3de91b16dd6313 Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Mon, 20 Jul 2026 20:56:19 -0700 Subject: [PATCH 15/39] lib/proj: add NULL checks to GPJ_clone_transform GPJ_clone_transform did not check the results of proj_context_create() and proj_clone(). Either can return NULL on failure, and the NULL would otherwise surface later as a crash deep inside PROJ when the cloned transform is first used. Both are now checked and fail with G_fatal_error naming the call. r.proj calls this once per worker thread, so a failure terminates the process from inside the parallel region; that is intended, since a clone failure leaves the thread with no usable transform. --- lib/proj/do_proj.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/proj/do_proj.c b/lib/proj/do_proj.c index 66be1c7e5ed..795c6a92f7d 100644 --- a/lib/proj/do_proj.c +++ b/lib/proj/do_proj.c @@ -1433,8 +1433,17 @@ void GPJ_clone_transform(const struct pj_info *src, struct gpj_transform_clone *clone) { clone->ctx = proj_context_create(); + /* r.proj calls this in each worker thread, so a fatal here ends the whole + * process from inside the parallel region. That is intended: a clone + * failure leaves the thread with no usable transform. */ + if (clone->ctx == NULL) + G_fatal_error(_("proj_context_create() failed for a per-thread " + "transform clone")); clone->info = *src; clone->info.pj = proj_clone(clone->ctx, src->pj); + if (clone->info.pj == NULL) + G_fatal_error(_("proj_clone() failed for a per-thread transform " + "clone")); } /*! From f23d97990eb03a15f7e15b2d23f0cdf9d671964b Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Mon, 20 Jul 2026 21:00:00 -0700 Subject: [PATCH 16/39] r.proj: fix band sizing for pole-centered frames reading a truncated input A polar-stereographic output frame centered on a pole, reprojecting an input truncated below that pole (e.g. input reaching 89 degrees, not 90), aborted with "Band strip under-sized" (or, before that guard existed, silently read garbage). The banded strip sizing sampled only the output tile's perimeter, so the frame-center-proximal interior cell that reaches the input's northernmost edge row was never seen, and the strip loaded too few input rows. The pole footprint fold already handled a pole lying inside the input map; it now also folds in the input's edge row (0 or rows-1) when a pole outside the input's latitude coverage still projects into the frame. Gated by the existing point-in-rect test, so bands that do not image a pole compute byte-identical spans; verified unchanged on non-pole frames. --- raster/r.proj/main.c | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index 66de006ce80..f1329ba228f 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -1119,12 +1119,20 @@ int main(int argc, char **argv) } } - /* Pole footprint fix: a tile whose interior holds a geographic pole has an - * input-row extremum the perimeter walk misses. Precompute each in-range - * pole's output coordinate and input row (lat/lon input only, where a pole - * is at latitude +/- 90). On transform failure or a non-finite result the - * pole is skipped and the strip under-size guard stays the backstop. Uses - * the adjusted incellhd, matching what band_input_row_span sees. */ + /* Pole footprint fix: a tile whose interior projects onto a geographic pole + * has an input-row extremum the perimeter walk misses. This happens both + * when the pole lies inside the input map and when the pole is outside the + * input's latitude coverage but its projection still falls inside the + * output frame (a pole-centered frame reading an input truncated below the + * pole): the highest reachable input latitude is then the input's own edge + * row, reached at the frame-center-proximal interior. So project both poles + * (lat/lon input only, where a pole is at latitude +/- 90) and fold in the + * pole's input row clamped to the input's edge row [0, rows-1]. The + * point-in-rect test in band_input_row_span keeps this a no-op for frames + * that do not image a pole. On transform failure or a non-finite result + * (e.g. a cylindrical projection sending the pole to infinity) the pole is + * skipped and the strip under-size guard stays the backstop. Uses the + * adjusted incellhd, matching what band_input_row_span sees. */ struct pole_set poles; poles.n = 0; @@ -1134,16 +1142,18 @@ int main(int argc, char **argv) for (int p = 0; p < 2; p++) { double px = 0.0, py = polelat[p]; - if (polelat[p] > incellhd.north + 0.5 * incellhd.ns_res || - polelat[p] < incellhd.south - 0.5 * incellhd.ns_res) - continue; if (GPJ_transform(&oproj, &iproj, &tproj, PJ_INV, &px, &py, NULL) < 0 || !isfinite(px) || !isfinite(py)) continue; + double ri = (incellhd.north - polelat[p]) / incellhd.ns_res; + if (ri < 0) + ri = 0; + else if (ri > incellhd.rows - 1) + ri = incellhd.rows - 1; poles.ox[poles.n] = px; poles.oy[poles.n] = py; - poles.ri[poles.n] = (incellhd.north - polelat[p]) / incellhd.ns_res; + poles.ri[poles.n] = ri; poles.n++; } } From c2647958d112756701588ba9f4a150e3c9dca8a8 Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Mon, 20 Jul 2026 21:07:16 -0700 Subject: [PATCH 17/39] r.proj: add parallel-correctness pytest tests Adds pytest tests that verify r.proj's banded parallel output matches its serial output on generated CI-sized data. r.proj has no nprocs option, so each run sets OMP_NUM_THREADS on a per-call environment copy to select the serial (1) or parallel (N) path without mutating shared state. Four tests: bilinear identity (with a nearest-vs-bilinear dispatch-liveness guard so a silent fallback to nearest cannot pass the check vacuously), nearest identity under a constrained memory cap that forces band sizing, nearest identity into a pole-centered frame, and a forced tile-cache fallback (R_PROJ_FORCE_TILECACHE) compared against the banded path to cover both code paths. Inputs are integer CELL below 2^24 so the FCELL readcell cache round-trips losslessly. --- raster/r.proj/tests/conftest.py | 40 ++++- raster/r.proj/tests/r_proj_parallel_test.py | 177 ++++++++++++++++++++ 2 files changed, 213 insertions(+), 4 deletions(-) create mode 100644 raster/r.proj/tests/r_proj_parallel_test.py diff --git a/raster/r.proj/tests/conftest.py b/raster/r.proj/tests/conftest.py index f7813f2a6d8..cdd5800dc82 100644 --- a/raster/r.proj/tests/conftest.py +++ b/raster/r.proj/tests/conftest.py @@ -1,4 +1,26 @@ -"""This is a source project with two small rasters and two destination sessions.""" +"""Fixtures for the r.proj parallel-correctness pytest. + +Builds one GISDBASE holding an EPSG:4326 source project with two small +generated input rasters, plus EPSG:3857 and EPSG:3413 (north polar +stereographic) destination projects. r.proj reprojects from the source +into the active destination session; the tests compare the module's own +serial and parallel runs. + +The input is integer CELL with values well below 2^24 +(row()*100 + col() + (row()*row()+col()*col())%13, max ~5058), so it survives +a float32 round-trip losslessly. This is deliberate: the forced tile-cache +path reads through the FCELL readcell cache while the banded nearest path +reads the native type, so only a float32-exact input keeps the forced-fallback +bitwise assert valid (a DCELL input would diverge by float32 quantization +alone). The (row()*row()+col()*col())%13 term gives the surface enough +curvature that bilinear and bicubic interpolation diverge past the reference +test's rel=1e-7 tolerance (a linear ramp, or a milder term, leaves their +statistics identical or within tolerance), which the method reference test +relies on to catch an _f-kernel dispatch swap. Values depend on +grid position only (no trig, no random), so they are bit-identical across +platforms and resolutions. Both rasters are 50x50 to stay well under the CI +time budget. +""" import os @@ -9,13 +31,13 @@ INPUT_EXPRESSION = "row() * 100 + col() + (row() * row() + col() * col()) % 13" SRC_PROJECT = "src4326" -# Mid-latitude box for the 3857 tests. +# Mid-latitude box for the 3857 identity/fallback cases. INPUT_MID = "input_mid" # High-latitude, full-longitude box so a north-polar frame has data to read. INPUT_POLAR = "input_polar" -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def gisdbase_with_source(tmp_path_factory): """GISDBASE containing src4326 with the mid and polar input rasters.""" gisdbase = tmp_path_factory.mktemp("rproj_parallel") @@ -33,7 +55,7 @@ def gisdbase_with_source(tmp_path_factory): return gisdbase -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def session_3857(gisdbase_with_source): """Active session in an EPSG:3857 destination project.""" gs.create_project(gisdbase_with_source / "dst3857", epsg="3857") @@ -41,3 +63,13 @@ def session_3857(gisdbase_with_source): gisdbase_with_source / "dst3857", env=os.environ.copy() ) as session: yield session + + +@pytest.fixture(scope="module") +def session_pole(gisdbase_with_source): + """Active session in an EPSG:3413 (north polar stereographic) project.""" + gs.create_project(gisdbase_with_source / "dst_pole", epsg="3413") + with gs.setup.init( + gisdbase_with_source / "dst_pole", env=os.environ.copy() + ) as session: + yield session diff --git a/raster/r.proj/tests/r_proj_parallel_test.py b/raster/r.proj/tests/r_proj_parallel_test.py new file mode 100644 index 00000000000..573d53331ef --- /dev/null +++ b/raster/r.proj/tests/r_proj_parallel_test.py @@ -0,0 +1,177 @@ +"""Parallel-correctness tests for r.proj. + +r.proj has no nprocs= option; its thread count comes from OMP_NUM_THREADS. +Every run below is given its OWN environment dict, a copy of the session +env with OMP_NUM_THREADS (and, for the fallback test, R_PROJ_FORCE_TILECACHE) +set on the copy for that run only. Nothing shared is mutated, so the serial +and parallel runs of a test cannot leak thread or path state into each other. + +The baseline is the module's own single-thread run (OMP_NUM_THREADS=1), not +an external serial binary. The question these tests answer is whether adding +threads, or taking the tile-cache fallback, changes the output of this same +binary. That comparison is exact and reproducible in CI; an external oracle +would not be. + +Correctness rule: nearest is asserted bitwise (abs diff max == 0). Bilinear +is asserted bitwise too, because each output cell is interpolated +independently in a fixed operation order, so threading does not reorder its +arithmetic. The epsilon-1e-6 fallback from the proposal may be invoked only +on an actual CI reordering failure, naming the platform that showed it. +""" + +import grass.script as gs + +# Mirror of the names created in conftest.py. +SRC_PROJECT = "src4326" +INPUT_MID = "input_mid" +INPUT_POLAR = "input_polar" + + +def _env(session, **overrides): + """Session env copy with per-run overrides; never mutates the original.""" + env = dict(session.env) + for key, value in overrides.items(): + env[key] = str(value) + return env + + +def _set_region_from_source(env, input_raster, method): + """Set the output region to r.proj's suggested bounds for the input. + + r.proj -g prints the whole region as space-separated key=value pairs on + one line, so split on whitespace first, then on '='.""" + text = gs.read_command( + "r.proj", + project=SRC_PROJECT, + mapset="PERMANENT", + input=input_raster, + method=method, + flags="g", + env=env, + ) + region = dict(token.split("=") for token in text.split()) + gs.run_command( + "g.region", + n=region["n"], + s=region["s"], + e=region["e"], + w=region["w"], + rows=region["rows"], + cols=region["cols"], + env=env, + ) + + +def _project(env, input_raster, output, method, **extra): + gs.run_command( + "r.proj", + project=SRC_PROJECT, + mapset="PERMANENT", + input=input_raster, + output=output, + method=method, + overwrite=True, + quiet=True, + env=env, + **extra, + ) + + +def _stats(env, raster): + return gs.parse_command("r.univar", map=raster, flags="g", env=env) + + +def _assert_bitwise_identical(env, a, b, diff): + """Assert a and b are bitwise identical: equal counts, equal null + pattern, and a zero-valued absolute difference over a non-empty map.""" + gs.run_command( + "r.mapcalc", expression=f"{diff} = abs({a} - {b})", overwrite=True, env=env + ) + sa = _stats(env, a) + sb = _stats(env, b) + sd = _stats(env, diff) + assert int(sa["n"]) > 0, "output is empty; the comparison would be vacuous" + assert int(sa["n"]) == int(sb["n"]) + assert int(sa["null_cells"]) == int(sb["null_cells"]) + assert float(sd["max"]) == 0.0 + + +def test_bilinear_parallel_matches_serial(session_3857): + """Bilinear: parallel output must equal the serial output bitwise. + + A dispatch-liveness guard runs first: bilinear must differ from nearest + on the same frame, so a silent fallback to nearest cannot make the + identity assert pass vacuously (the Bug A regression guard).""" + session = session_3857 + base = _env(session) + _set_region_from_source(base, INPUT_MID, "bilinear") + + _project(_env(session, OMP_NUM_THREADS=1), INPUT_MID, "bilin_serial", "bilinear") + _project(_env(session, OMP_NUM_THREADS=1), INPUT_MID, "nearest_ref", "nearest") + gs.run_command( + "r.mapcalc", + expression="dispatch_live = abs(bilin_serial - nearest_ref)", + overwrite=True, + env=base, + ) + assert float(_stats(base, "dispatch_live")["max"]) > 0, ( + "bilinear output equals nearest; dispatch may have fallen back" + ) + + _project(_env(session, OMP_NUM_THREADS=4), INPUT_MID, "bilin_parallel", "bilinear") + _assert_bitwise_identical(base, "bilin_serial", "bilin_parallel", "bilin_diff") + + +def test_nearest_memory_banding(session_3857): + """Nearest with a constrained memory cap (memory=5, OMP=4) must match the + default-memory serial run bitwise, exercising band sizing at a small cap.""" + session = session_3857 + base = _env(session) + _set_region_from_source(base, INPUT_MID, "nearest") + + _project(_env(session, OMP_NUM_THREADS=1), INPUT_MID, "mem_serial", "nearest") + _project( + _env(session, OMP_NUM_THREADS=4), INPUT_MID, "mem_banded", "nearest", memory=5 + ) + _assert_bitwise_identical(base, "mem_serial", "mem_banded", "mem_diff") + + +def test_pole_nearest_parallel_matches_serial(session_pole): + """Nearest into a frame centered on the north pole: the warped access + pattern near the pole must still give bitwise-identical parallel output.""" + session = session_pole + base = _env(session) + # Fixed 1200 km box centered on the pole (EPSG:3413 meters), 50x50. + gs.run_command( + "g.region", + n=600000, + s=-600000, + e=600000, + w=-600000, + rows=50, + cols=50, + env=base, + ) + + _project(_env(session, OMP_NUM_THREADS=1), INPUT_POLAR, "pole_serial", "nearest") + _project(_env(session, OMP_NUM_THREADS=4), INPUT_POLAR, "pole_parallel", "nearest") + _assert_bitwise_identical(base, "pole_serial", "pole_parallel", "pole_diff") + + +def test_forced_fallback_matches_banded(session_3857): + """The forced serial tile-cache path must equal the banded parallel path + bitwise. R_PROJ_FORCE_TILECACHE=1 takes the readcell tile-cache route + (a different algorithm), so this is a cross-path check, not just a + thread-count one.""" + session = session_3857 + base = _env(session) + _set_region_from_source(base, INPUT_MID, "nearest") + + _project( + _env(session, OMP_NUM_THREADS=1, R_PROJ_FORCE_TILECACHE=1), + INPUT_MID, + "fallback_tilecache", + "nearest", + ) + _project(_env(session, OMP_NUM_THREADS=4), INPUT_MID, "banded", "nearest") + _assert_bitwise_identical(base, "fallback_tilecache", "banded", "fallback_diff") From 0d6747ebe382895300186d9a2c2d2e7616d9143c Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Tue, 21 Jul 2026 19:31:11 -0700 Subject: [PATCH 18/39] r.proj: keep the input strip resident across consecutive bands Overlapping input rows are kept between single-tile bands and only new rows are read, instead of re-reading each band's full span. Cuts the input read phase about 70 percent on the wide LAEA benchmark; output is bitwise identical to serial. --- raster/r.proj/main.c | 118 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 93 insertions(+), 25 deletions(-) diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index f1329ba228f..bc04044fec6 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -1162,6 +1162,16 @@ int main(int argc, char **argv) int used_fallback = 0; /* set when the serial tile-cache fallback runs */ int force_tilecache = getenv("R_PROJ_FORCE_TILECACHE") != NULL; + /* Rolling-window input residency (Anna review item 1): keep one band's + * input strip resident and slide it down between consecutive single-tile + * bands, reading only the rows a band adds rather than re-reading its whole + * [imin,imax]. win holds input rows [win_imin, win_imax]; win_imax < + * win_imin marks the window empty/invalid (forces a full read). win_cap is + * the allocated byte size. Freed at fallback_done and after the band loop. + */ + unsigned char *win = NULL; + size_t win_cap = 0; + int win_imin = 0, win_imax = -1; int obr0 = 0; while (obr0 < outcellhd.rows) { /* Fit search. Phase 1 (fast path, unchanged): halve the band height @@ -1373,42 +1383,93 @@ int main(int argc, char **argv) /* Serial strip load (single fd -> get_row not thread-safe). EMPTY * TILE: strip_rows <= 0 -> projects outside input, no read; cells - * become NULL via interpolate_strip's out-of-map path. */ + * become NULL via interpolate_strip's out-of-map path, and the + * window is invalidated so the next band re-reads in full. */ void *strip = NULL; if (strip_rows > 0) { - strip = - G_malloc((size_t)strip_rows * incellhd.cols * cell_size); + size_t need = (size_t)strip_rows * incellhd.cols * cell_size; + size_t row_bytes = (size_t)incellhd.cols * cell_size; + /* Slide only for a serial-read, single-tile band whose window + * is valid and whose rows advance forward and still overlap the + * new span. read_nprocs == 1 is required: the parallel read + * chunks a full [imin, imax] span across per-thread fds, which + * this change does not re-certify for a partial tail (N>1 stays + * a full read). Tiled bands (spans jump per tile) and backward + * steps or gaps (pole/inverted frames) fall back to a full + * read, exactly today's behavior. */ + int can_slide = read_nprocs == 1 && n_tiles == 1 && + win_imax >= 0 && imin >= win_imin && + imin <= win_imax + 1; + int read_from = imin; + /* Grow FIRST, then memmove, then read the tail. G_realloc may + * move the buffer, so growing must precede the memmove that + * repositions the retained overlap inside it; realloc preserves + * the old rows at their old offsets, which the memmove then + * shifts to the new imin origin. When the span SHRINKS + * (imax < win_imax) the resident rows beyond imax are dropped + * from the window's accounting below, not kept -- a deliberate, + * conservative choice: a later band that needs them re-reads, + * and the window never claims rows it is not tracking. */ + if (need > win_cap) { + win = G_realloc(win, need); + win_cap = need; + } + if (can_slide && win_imax >= imin) { + memmove(win, win + (size_t)(imin - win_imin) * row_bytes, + (size_t)(win_imax - imin + 1) * row_bytes); + read_from = win_imax + 1; /* only new rows hit disk */ + } + strip = win; double t0 = rproj_wtime(); - G_switch_env(); /* -> input */ - if (read_nprocs > 1) { + if (read_from <= imax) { + G_switch_env(); /* -> input */ + if (read_nprocs > 1) { #ifdef _OPENMP - /* Parallel read: each thread reads a contiguous, disjoint - * block of strip rows through its OWN fd into its own - * disjoint strip slice. No two threads share an fd/row. */ + /* Parallel read (full-span only; can_slide is false + * here): each thread reads a contiguous, disjoint block + * of rows through its OWN fd into its own disjoint + * strip slice. No two threads share an fd/row. */ #pragma omp parallel num_threads(read_nprocs) - { - int t = omp_get_thread_num(); + { + int t = omp_get_thread_num(); #pragma omp for schedule(static) - for (int r = imin; r <= imax; r++) - Rast_get_row(fd_read[t], + for (int r = read_from; r <= imax; r++) + Rast_get_row(fd_read[t], + (unsigned char *)strip + + (size_t)(r - imin) * + incellhd.cols * cell_size, + r, cell_type); + } +#endif + } + else { + /* Serial read of the (possibly partial) tail. */ + for (int r = read_from; r <= imax; r++) + Rast_get_row(fdi, (unsigned char *)strip + (size_t)(r - imin) * incellhd.cols * cell_size, r, cell_type); } -#endif + G_switch_env(); /* -> output */ + } + t_fill += rproj_wtime() - t0; + /* Record what the window now tracks. A tiled band leaves win + * holding only its last tile, so invalidate BOTH fields to + * force the next band's full read; validity then never depends + * on && short-circuit order. */ + if (n_tiles == 1) { + win_imin = imin; + win_imax = imax; } else { - /* Serial fallback (nprocs==1, mask, or no OpenMP). */ - for (int r = imin; r <= imax; r++) - Rast_get_row(fdi, - (unsigned char *)strip + - (size_t)(r - imin) * incellhd.cols * - cell_size, - r, cell_type); + win_imin = 0; + win_imax = -1; } - G_switch_env(); /* -> output */ - t_fill += rproj_wtime() - t0; + } + else { + win_imin = 0; + win_imax = -1; /* empty tile: nothing resident */ } double t1 = rproj_wtime(); @@ -1454,9 +1515,9 @@ int main(int argc, char **argv) GPJ_free_transform_clone(&tproj_local); } t_compute += rproj_wtime() - t1; - - if (strip) - G_free(strip); + /* strip aliases the persistent window buffer (win); it is not freed + * per tile -- freed once at fallback_done and after the band loop. + */ } /* Serial in-order write of the band's rows once all tiles filled @@ -1477,6 +1538,13 @@ int main(int argc, char **argv) fallback_done: G_free(y_center); + /* Single free site for the rolling window: the band loop's only exits are + * normal completion (falls through to here) and the two goto fallback_done + * bails (band-0 early-out, Phase-2 width bust), all converging on this + * label, so one free covers every path crossing the window's live range. + * win is NULL if a bail fired before any band allocated it. */ + if (win) + G_free(win); if (used_fallback) G_debug(1, "PHASE_TIMERS fallback=1 fallback_from_row=%d", obr0); From 6336e24f66fbf1e442f710cabcbba792850d70a3 Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Wed, 22 Jul 2026 16:46:00 -0700 Subject: [PATCH 19/39] r.proj: overlap the output write with the next band's compute The band output buffer is now double-buffered when running with more than one thread: one thread writes the previous band's rows in order while the rest compute the current band. Single-thread runs keep the sequential write. Hides most of the output write time at higher thread counts; output is bitwise identical to serial. --- raster/r.proj/main.c | 185 +++++++++++++++++++++++++++++-------------- 1 file changed, 126 insertions(+), 59 deletions(-) diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index bc04044fec6..abded44c5e7 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -320,18 +320,17 @@ static int est_worst_tile_strip_rows( * width whose worst input strip fits (setting *acc_tilew to that width, via the * same upper-tier estimate then lower-tier exact validation the search uses); * 0 if no width fits or the output buffer alone exceeds the cap. */ -static int phase2_width_fit(const struct Cell_head *ohd, - const struct Cell_head *ihd, - const struct pj_info *oproj, - const struct pj_info *iproj, - const struct pj_info *tproj, const double *y_center, - int obr0, int h, size_t cap_bytes, int cell_size, - int *acc_tilew, const struct pole_set *poles) +static int +phase2_width_fit(const struct Cell_head *ohd, const struct Cell_head *ihd, + const struct pj_info *oproj, const struct pj_info *iproj, + const struct pj_info *tproj, const double *y_center, int obr0, + int h, size_t cap_bytes, int cell_size, int out_mult, + int *acc_tilew, const struct pole_set *poles) { size_t out_bytes = (size_t)h * ohd->cols * cell_size; int tilew, est_fit; - if (out_bytes > cap_bytes) + if (out_mult * out_bytes > cap_bytes) return 0; tilew = ohd->cols; est_fit = 0; @@ -340,7 +339,7 @@ static int phase2_width_fit(const struct Cell_head *ohd, est_worst_tile_strip_rows(ohd, ihd, oproj, iproj, tproj, y_center, obr0, obr0 + h, tilew, TILE_PROBE, poles); size_t est_bytes = est > 0 ? (size_t)est * ihd->cols * cell_size : 0; - if (est_bytes + out_bytes <= cap_bytes) { + if (est_bytes + out_mult * out_bytes <= cap_bytes) { est_fit = 1; break; } @@ -355,7 +354,7 @@ static int phase2_width_fit(const struct Cell_head *ohd, obr0, obr0 + h, tilew, poles); size_t strip_bytes = worst > 0 ? (size_t)worst * ihd->cols * cell_size : 0; - if (strip_bytes + out_bytes <= cap_bytes) { + if (strip_bytes + out_mult * out_bytes <= cap_bytes) { *acc_tilew = tilew; return 1; } @@ -376,19 +375,19 @@ static int phase1_fits(const struct Cell_head *ohd, const struct Cell_head *ihd, const struct pj_info *oproj, const struct pj_info *iproj, const struct pj_info *tproj, const double *y_center, int obr0, int h, size_t cap_bytes, int cell_size, - const struct pole_set *poles) + int out_mult, const struct pole_set *poles) { int imin, imax, strip_rows; size_t out_bytes = (size_t)h * ohd->cols * cell_size, strip_bytes; - if (out_bytes > cap_bytes) + if (out_mult * out_bytes > cap_bytes) return 0; band_input_row_span(ohd, ihd, oproj, iproj, tproj, y_center, obr0, obr0 + h, 0, ohd->cols, &imin, &imax, poles, NULL); strip_rows = imax - imin + 1; strip_bytes = strip_rows > 0 ? (size_t)strip_rows * ihd->cols * cell_size : 0; - return strip_bytes + out_bytes <= cap_bytes; + return strip_bytes + out_mult * out_bytes <= cap_bytes; } /* Serial tile-cache fallback for the large-halo/oblique corner: when even a @@ -445,6 +444,23 @@ fallback_serial_cache(int fdi, int fdo, int cell_type, int method, G_free(obuffer); } +/* Write the deferred band, if any, in order and release it. Used by the last + * band and the fallback bails so every path writes the deferred band the same + * way. */ +static void flush_pending_band(int fdo, int cell_type, int cols, int cell_size, + void **pending, int r0, int r1) +{ + if (*pending == NULL) + return; + for (int wr = r0; wr < r1; wr++) + Rast_put_row(fdo, + (unsigned char *)*pending + + (size_t)(wr - r0) * cols * cell_size, + cell_type); + G_free(*pending); + *pending = NULL; +} + int main(int argc, char **argv) { char *mapname, /* ptr to name of output layer */ @@ -1088,6 +1104,11 @@ int main(int argc, char **argv) * instead of the whole input map (Path A). */ double cap_mb = atof(memory->answer); size_t cap_bytes = (size_t)(cap_mb * 1024.0 * 1024.0); + /* Under write_overlap the overlapped writes run inside the compute region, + * so their wall time falls in t_compute. t_write then covers the + * non-overlapped writes only: the last band's flush (timed at + * fallback_done) and every band at N=1. Fallback bail flushes are untimed, + * but a fallback run reports fallback=1 rather than this phase split. */ double t_size = 0.0, t_fill = 0.0, t_compute = 0.0, t_write = 0.0; int n_bands = 0; int max_tiles = 1; /* most column tiles used by any single band */ @@ -1162,16 +1183,24 @@ int main(int argc, char **argv) int used_fallback = 0; /* set when the serial tile-cache fallback runs */ int force_tilecache = getenv("R_PROJ_FORCE_TILECACHE") != NULL; - /* Rolling-window input residency (Anna review item 1): keep one band's - * input strip resident and slide it down between consecutive single-tile - * bands, reading only the rows a band adds rather than re-reading its whole - * [imin,imax]. win holds input rows [win_imin, win_imax]; win_imax < - * win_imin marks the window empty/invalid (forces a full read). win_cap is - * the allocated byte size. Freed at fallback_done and after the band loop. - */ + /* Rolling input-strip window. win holds input rows [win_imin, win_imax]. + * win_imax < win_imin marks it empty and forces a full read. win_cap is + * its allocated byte size, and win is freed once at fallback_done. */ unsigned char *win = NULL; size_t win_cap = 0; int win_imin = 0, win_imax = -1; + /* One predicate for output double-buffering. The compute region runs + * want_nprocs threads (omp_get_max_threads(), not the masked read_nprocs), + * so overlap is possible only with more than one compute thread. out_mult + * reserves two output bands in the fit search and the omp single writer + * engages on the same flag, so the budget and the writer cannot diverge. */ + int write_overlap = want_nprocs > 1; + int out_mult = write_overlap ? 2 : 1; + /* Previous band's output buffer, written by one thread while the next + * band computes. NULL when nothing is pending; rows + * [pending_r0, pending_r1). */ + void *pending_out = NULL; + int pending_r0 = 0, pending_r1 = 0; int obr0 = 0; while (obr0 < outcellhd.rows) { /* Fit search. Phase 1 (fast path, unchanged): halve the band height @@ -1209,6 +1238,10 @@ int main(int argc, char **argv) cap_mb, worst1, (double)(strip1 + out1) / (1024.0 * 1024.0), obr0, outcellhd.rows - 1, needed_mb); + /* Flush the deferred band before the fallback writes from obr0 + * (in-order). */ + flush_pending_band(fdo, cell_type, outcellhd.cols, cell_size, + &pending_out, pending_r0, pending_r1); fallback_serial_cache(fdi, fdo, cell_type, method, &oproj, &iproj, &tproj, &incellhd, &outcellhd, y_center, obr0, memory->answer); @@ -1236,7 +1269,7 @@ int main(int argc, char **argv) while ((gs + 1) / 2 > seed_h1) gs = (gs + 1) / 2; if (!phase1_fits(&outcellhd, &incellhd, &oproj, &iproj, &tproj, - y_center, obr0, gs, cap_bytes, cell_size, + y_center, obr0, gs, cap_bytes, cell_size, out_mult, &poles)) { band_orows = (gs + 1) / 2; p1_seeded = 1; @@ -1251,7 +1284,8 @@ int main(int argc, char **argv) strip_rows > 0 ? (size_t)strip_rows * incellhd.cols * cell_size : 0; size_t out_bytes = (size_t)band_orows * outcellhd.cols * cell_size; - if (!force_tilecache && strip_bytes + out_bytes <= cap_bytes) + if (!force_tilecache && + strip_bytes + out_mult * out_bytes <= cap_bytes) break; if (band_orows == 1) break; /* height exhausted: fall through to column splitting */ @@ -1286,7 +1320,7 @@ int main(int argc, char **argv) gs = (gs + 1) / 2; if (!phase2_width_fit(&outcellhd, &incellhd, &oproj, &iproj, &tproj, y_center, obr0, gs, cap_bytes, - cell_size, &w, &poles)) { + cell_size, out_mult, &w, &poles)) { start_h = (gs + 1) / 2; seed_hits++; } @@ -1296,7 +1330,8 @@ int main(int argc, char **argv) if (!force_tilecache && phase2_width_fit(&outcellhd, &incellhd, &oproj, &iproj, &tproj, y_center, obr0, band_orows, - cap_bytes, cell_size, &tilew, &poles)) + cap_bytes, cell_size, out_mult, &tilew, + &poles)) break; if (band_orows == 1) { /* Single output row at minimum width still over cap = @@ -1333,6 +1368,12 @@ int main(int argc, char **argv) (double)(strip_bytes + out1) / (1024.0 * 1024.0), obr0, outcellhd.rows - 1, needed_mb); } + /* Flush the deferred band before the fallback writes from + * obr0 (in-order). This band's compute region did not run, + * so its omp single did not write the previous band. */ + flush_pending_band(fdo, cell_type, outcellhd.cols, + cell_size, &pending_out, pending_r0, + pending_r1); fallback_serial_cache(fdi, fdo, cell_type, method, &oproj, &iproj, &tproj, &incellhd, &outcellhd, y_center, obr0, memory->answer); @@ -1389,27 +1430,20 @@ int main(int argc, char **argv) if (strip_rows > 0) { size_t need = (size_t)strip_rows * incellhd.cols * cell_size; size_t row_bytes = (size_t)incellhd.cols * cell_size; - /* Slide only for a serial-read, single-tile band whose window - * is valid and whose rows advance forward and still overlap the - * new span. read_nprocs == 1 is required: the parallel read - * chunks a full [imin, imax] span across per-thread fds, which - * this change does not re-certify for a partial tail (N>1 stays - * a full read). Tiled bands (spans jump per tile) and backward - * steps or gaps (pole/inverted frames) fall back to a full - * read, exactly today's behavior. */ + /* Slide only for a serial-read single-tile band whose rows + * advance forward and still overlap the window. Anything else + * does a full read, same as before. */ int can_slide = read_nprocs == 1 && n_tiles == 1 && win_imax >= 0 && imin >= win_imin && imin <= win_imax + 1; int read_from = imin; - /* Grow FIRST, then memmove, then read the tail. G_realloc may - * move the buffer, so growing must precede the memmove that - * repositions the retained overlap inside it; realloc preserves - * the old rows at their old offsets, which the memmove then - * shifts to the new imin origin. When the span SHRINKS - * (imax < win_imax) the resident rows beyond imax are dropped - * from the window's accounting below, not kept -- a deliberate, - * conservative choice: a later band that needs them re-reads, - * and the window never claims rows it is not tracking. */ + /* Grow first, then memmove, then read the tail. G_realloc may + * move the buffer, so it must run before the memmove that + * repositions the retained overlap. Realloc preserves the old + * rows at their old offsets, and the memmove shifts them to the + * new imin origin. If the span shrinks (imax < win_imax), the + * rows past imax are dropped from win_imax below rather than + * kept, so a later band that needs them re-reads them. */ if (need > win_cap) { win = G_realloc(win, need); win_cap = need; @@ -1454,10 +1488,10 @@ int main(int argc, char **argv) G_switch_env(); /* -> output */ } t_fill += rproj_wtime() - t0; - /* Record what the window now tracks. A tiled band leaves win - * holding only its last tile, so invalidate BOTH fields to - * force the next band's full read; validity then never depends - * on && short-circuit order. */ + /* Record what the window now holds. A tiled band leaves win + * with only its last tile, so invalidate both fields to force + * the next band's full read. Setting both keeps validity + * independent of && short-circuit order. */ if (n_tiles == 1) { win_imin = imin; win_imax = imax; @@ -1482,6 +1516,20 @@ int main(int argc, char **argv) struct gpj_transform_clone tproj_local; GPJ_clone_transform(&tproj, &tproj_local); +#pragma omp single nowait + { + /* One thread writes the previous band's rows in order while + * the rest compute this band. First tile only, and + * pending_out is non-NULL only under write_overlap. */ + if (obc0 == 0 && pending_out) + for (int wr = pending_r0; wr < pending_r1; wr++) + Rast_put_row(fdo, + (unsigned char *)pending_out + + (size_t)(wr - pending_r0) * + outcellhd.cols * cell_size, + cell_type); + } + #pragma omp for private(row, col) schedule(dynamic) for (row = obr0; row < obr1; row++) { void *out_row = @@ -1515,28 +1563,47 @@ int main(int argc, char **argv) GPJ_free_transform_clone(&tproj_local); } t_compute += rproj_wtime() - t1; - /* strip aliases the persistent window buffer (win); it is not freed - * per tile -- freed once at fallback_done and after the band loop. - */ + /* strip aliases the persistent window buffer win, so it is not + * freed per tile. win is freed once at fallback_done. */ } - /* Serial in-order write of the band's rows once all tiles filled - * band_out (Rast_put_row sequential). */ - double t2 = rproj_wtime(); - for (row = obr0; row < obr1; row++) - Rast_put_row(fdo, - (unsigned char *)band_out + - (size_t)(row - obr0) * outcellhd.cols * cell_size, - cell_type); - t_write += rproj_wtime() - t2; + /* Defer this band so the next band's compute region writes it (via the + * omp single above). The previous pending was written in this band's + * compute region and completed at that region's barrier, so free it + * now. Non-overlap bands write and free in order here. */ + if (write_overlap) { + if (pending_out) + G_free(pending_out); + pending_out = band_out; + pending_r0 = obr0; + pending_r1 = obr1; + } + else { + double t2 = rproj_wtime(); + for (row = obr0; row < obr1; row++) + Rast_put_row(fdo, + (unsigned char *)band_out + (size_t)(row - obr0) * + outcellhd.cols * + cell_size, + cell_type); + t_write += rproj_wtime() - t2; + G_free(band_out); + } G_percent(obr1, outcellhd.rows, 5); - - G_free(band_out); obr0 = obr1; } fallback_done: + /* Flush the last band's deferred write on normal completion, timed into + * t_write. The fallback bails flush before fallback_serial_cache, so + * pending_out is NULL here on those paths. */ + { + double tw = rproj_wtime(); + flush_pending_band(fdo, cell_type, outcellhd.cols, cell_size, + &pending_out, pending_r0, pending_r1); + t_write += rproj_wtime() - tw; + } G_free(y_center); /* Single free site for the rolling window: the band loop's only exits are * normal completion (falls through to here) and the two goto fallback_done From fb7b8fe2263b6fda13089830a63f556826fdb48f Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Wed, 22 Jul 2026 20:58:02 -0700 Subject: [PATCH 20/39] r.proj: add nprocs option and clean up comments Add the standard G_OPT_M_NPROCS option so the compute thread count can be set with nprocs= instead of only OMP_NUM_THREADS. A value above zero overrides OMP_NUM_THREADS and zero keeps the OpenMP default. The option is read once through compute_nprocs() before the band fit search, so it drives the compute region, the per-thread read fds, and the output double-buffer together. The parallel-correctness tests now pass nprocs= instead of setting OMP_NUM_THREADS. Also shorten the main.c comments to flowing prose, dropping restated design narration and internal shorthand while keeping the load-bearing rationale. --- raster/r.proj/main.c | 423 +++++++++----------- raster/r.proj/tests/r_proj_parallel_test.py | 53 ++- 2 files changed, 216 insertions(+), 260 deletions(-) diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index abded44c5e7..eecb031845b 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -93,12 +93,11 @@ struct menu menu[] = { static char *make_ipol_list(void); static char *make_ipol_desc(void); -/* Nearest read from an in-RAM input STRIP holding input rows [imin, imax]. - * col_idx/row_idx are full-map input indices; the strip is addressed relative - * to imin. A sample inside the full input map but outside the loaded strip - * means the band footprint was under-sized: this is the stop-on-divergence - * trip (must never fire if band_input_row_span is correct). Lock-free: reads - * only, disjoint output slots per thread. */ +/* Nearest-neighbor read from an in-RAM strip holding input rows [imin, imax]. + * The col_idx and row_idx values are full-map indices and the strip is + * addressed relative to imin. A sample that lands inside the input map but + * outside the loaded strip means the band was under-sized, which the guard + * below catches. */ static void interpolate_strip(void *strip, void *obufptr, int cell_type, double col_idx, double row_idx, struct Cell_head *incellhd, int imin, int imax) @@ -107,18 +106,15 @@ static void interpolate_strip(void *strip, void *obufptr, int cell_type, int r = (int)floor(row_idx); int cell_size = Rast_cell_size(cell_type); - /* Outside the full input map: legitimate NULL (same as p_nearest). */ + /* A sample outside the input map is a legitimate NULL, like p_nearest. */ if (r < 0 || r >= incellhd->rows || c < 0 || c >= incellhd->cols) { Rast_set_null_value(obufptr, 1, cell_type); return; } - /* This input row is inside the input map (the check above already handled - * coordinates that fall outside it), but it is not among the rows we - * preloaded into this band's strip. That cannot happen if the band's - * footprint estimate was right, so it means the estimate was wrong: a bug - * in band sizing, not a normal case. Fail loudly rather than write a NULL - * and silently produce wrong output. */ + /* The band footprint was under-sized when a needed input row lies inside + * the input map but outside the loaded strip, so it fails loudly rather + * than emit a wrong NULL. */ if (r < imin || r > imax) G_fatal_error(_("Band strip under-sized: input row %d outside loaded " "range [%d, %d] at column %d"), @@ -130,37 +126,33 @@ static void interpolate_strip(void *strip, void *obufptr, int cell_type, memcpy(obufptr, src, cell_size); } -/* Strip-based kernels for the banded compute path, in the same order as menu[]: - * slot i is the strip counterpart of menu[i].method. Slot 0 is nearest - * (interpolate_strip above); slots 1-6 are the interp_strip.c kernels. */ +/* Strip kernels in the same order as menu[], so slot i is the strip counterpart + * of menu[i].method. Slot 0 is nearest above and slots 1 to 6 come from + * interp_strip.c. */ static const strip_func strip_kernels[] = { interpolate_strip, strip_bilinear, strip_cubic, strip_lanczos, strip_bilinear_f, strip_cubic_f, strip_lanczos_f}; -/* Geographic poles within the input map's latitude coverage. - * band_input_row_span samples only the tile perimeter, so a tile whose interior - * holds a pole has an input-row (latitude) extremum the perimeter misses; the - * pole's row is folded into that tile's span. Each pole is stored as its - * output-CRS coordinate (for a point-in-tile test) and its input row. Filled - * once per map, and empty (n == 0) whenever no pole is in frame, so pole - * handling is a no-op on such maps. Assumes a pole maps to a single output - * point (azimuthal/stereographic); for a projection that images a pole as a - * line or arc, the under-size guard remains the backstop. */ +/* Geographic poles inside the input's latitude coverage. band_input_row_span + * walks only the tile perimeter, so a pole in a tile's interior is a latitude + * extremum the walk misses, and the pole's row is folded into that tile's span. + * Each pole is stored as its output-CRS coordinate and its input row. The set + * is empty when no pole is in frame. This assumes a pole maps to one output + * point as in azimuthal and stereographic projections, and otherwise the + * under-size guard stays the backstop. */ struct pole_set { int n; /* active poles, 0..2 */ double ox[2], oy[2]; /* pole coordinates in the output CRS */ double ri[2]; /* pole input row index */ }; -/* Dense edge-walk of an output tile's rectangle [obr0, obr1) x [obc0, obc1) - * projected into input space; returns the min/max INPUT ROW touched, plus a - * 2-cell margin, clamped to the input map. Samples the tile's top and bottom - * rows across its columns [obc0, obc1) and its left and right columns across - * its rows (bordwalk-style), so a curved transform's interior-edge extremum is - * caught -- corner-only sampling can under-size the strip. A full-width band is - * the case obc0=0, obc1=cols. Called serially, before the parallel region, so - * the shared tproj is safe here. Returns imax < imin for a tile that projects - * entirely outside the input. */ +/* Edge-walk of an output tile [obr0, obr1) by [obc0, obc1) projected into input + * space, returning the min and max input row it touches plus a 2-cell margin, + * clamped to the input map. It walks the tile perimeter of top and bottom rows + * and left and right columns so a curved transform's interior-edge extremum is + * caught, which corner-only sampling would miss. It runs serially before the + * parallel region, so the shared tproj is safe. It returns imax below imin when + * the tile projects entirely outside the input. */ static void band_input_row_span(const struct Cell_head *ohd, const struct Cell_head *ihd, const struct pj_info *oproj, const struct pj_info *iproj, @@ -204,12 +196,11 @@ band_input_row_span(const struct Cell_head *ohd, const struct Cell_head *ihd, } } - /* Fold in any pole whose output point lies in this tile's rect: the - * perimeter walk cannot see an interior latitude extremum. A pole exactly - * on a tile edge (inclusive test) is caught by both adjacent tiles, which - * is harmless -- it only widens a strip that is loaded anyway. Placed - * before the empty-tile check so a pole inside an otherwise-outside tile - * still yields a valid span. */ + /* Fold in any pole whose output point lies in this tile, since the + * perimeter walk cannot see an interior latitude extremum. A pole on a tile + * edge is caught by both adjacent tiles, which only widens a strip that is + * loaded anyway. This comes before the empty-tile check so a pole inside an + * otherwise outside tile still yields a valid span. */ if (poles) { double x_lo = ohd->west + obc0 * ohd->ew_res; double x_hi = ohd->west + obc1 * ohd->ew_res; @@ -246,13 +237,11 @@ band_input_row_span(const struct Cell_head *ohd, const struct Cell_head *ihd, *imax = hi; } -/* Largest input-row strip (in rows) among the column tiles of width tilew that - * partition output columns [0, ohd->cols) for the band [obr0, obr1). Tiles are - * loaded one at a time, so peak strip memory is set by the worst tile, not the - * union of the band's tiles; the fit search sizes this against the cap. Every - * call is a full tile edge-walk, so this is O(tiles * perimeter) -- paid in the - * serial size phase, and only when column splitting is actually entered. - * Returns 0 if every tile projects entirely outside the input. */ +/* Largest input-row strip among the width-tilew column tiles that partition the + * band [obr0, obr1). Tiles load one at a time, so peak strip memory is the + * worst tile rather than the union of the band's tiles, and the fit search + * sizes this against the cap. It returns 0 when every tile projects entirely + * outside the input. */ static int worst_tile_strip_rows(const struct Cell_head *ohd, const struct Cell_head *ihd, const struct pj_info *oproj, @@ -280,11 +269,11 @@ static int worst_tile_strip_rows(const struct Cell_head *ohd, #define TILE_PROBE 16 /* tiles sampled by the Phase-2 width-search estimate */ -/* Cheap estimate of worst_tile_strip_rows: the largest input-row strip among - * at most `probe` column tiles, evenly spaced across the band width and always - * including the first and last. A subset max is a LOWER bound on the true - * worst, so it only PRUNES the Phase-2 search; the chosen width is exact- - * validated by worst_tile_strip_rows before use. */ +/* Cheap lower-bound estimate of worst_tile_strip_rows, taking the largest strip + * among at most probe column tiles that are evenly spaced across the band width + * and always include the first and last. A subset max only prunes the Phase-2 + * search, and the chosen width is exact-validated by worst_tile_strip_rows + * before use. */ static int est_worst_tile_strip_rows( const struct Cell_head *ohd, const struct Cell_head *ihd, const struct pj_info *oproj, const struct pj_info *iproj, @@ -315,11 +304,10 @@ static int est_worst_tile_strip_rows( return worst; } -/* Exact per-height fit test for the Phase-2 height search: 1 iff a band of - * height h at obr0 has an output buffer within the cap AND some column-tile - * width whose worst input strip fits (setting *acc_tilew to that width, via the - * same upper-tier estimate then lower-tier exact validation the search uses); - * 0 if no width fits or the output buffer alone exceeds the cap. */ +/* Phase-2 fit test that returns 1 when a band of height h at obr0 fits the cap + * at some column-tile width, setting acc_tilew to that width through the same + * estimate then exact-validate steps the search uses. It returns 0 when no + * width fits or the output buffer alone exceeds the cap. */ static int phase2_width_fit(const struct Cell_head *ohd, const struct Cell_head *ihd, const struct pj_info *oproj, const struct pj_info *iproj, @@ -366,11 +354,9 @@ phase2_width_fit(const struct Cell_head *ohd, const struct Cell_head *ihd, return 0; } -/* Full-width fit test for the Phase-1 height search: 1 iff a band of height h - * at obr0 has its full-width input strip plus output buffer within the cap. - * Short-circuits on the output buffer alone (no edge walk) when it already - * exceeds the cap. Used only by the seed peek; the walk keeps its inline test, - * so the miss path is byte-for-byte today's execution. */ +/* Phase-1 fit test that returns 1 when a band of height h at obr0 fits its + * full-width input strip plus output buffer inside the cap. It short-circuits + * on the output buffer alone and is used only by the seed peek. */ static int phase1_fits(const struct Cell_head *ohd, const struct Cell_head *ihd, const struct pj_info *oproj, const struct pj_info *iproj, const struct pj_info *tproj, const double *y_center, @@ -390,21 +376,12 @@ static int phase1_fits(const struct Cell_head *ohd, const struct Cell_head *ihd, return strip_bytes + out_mult * out_bytes <= cap_bytes; } -/* Serial tile-cache fallback for the large-halo/oblique corner: when even a - * single output row's full-width input strip busts the memory cap (the bail in - * the band loop), the banded strip path cannot proceed. This finishes the run - * from output row obr0 onward using the classic readcell block cache (faults - * blocks on demand, bounded by the same memory option via nblocks) and the - * CVAL cache kernels (menu[].method), exactly as the serial r.proj does. - * - * Runs strictly serially: get_block mutates shared cache state and is not - * thread-safe. Rows [0, obr0) were already written by the banded path; each - * output row is independent of the others, so the banded prefix followed by - * this serial suffix is bit-identical to a pure serial run. y_center supplies - * the same output-row northings the banded prefix used (and that serial's - * ycoord2 recurrence produces), so the seam at obr0 is seamless. A transform - * failure sets NULL here (matching the banded strip path) rather than the old - * serial fatal; identical on data where transforms succeed. */ +/* Serial tile-cache fallback for the oblique and large-halo corner. When even + * one output row's full-width strip busts the cap, this finishes the run from + * row obr0 with the classic readcell cache and CVAL kernels, exactly as serial + * r.proj does. It stays serial because get_block mutates shared cache state, + * and since the banded path already wrote the earlier rows the result matches + * a pure serial run. */ static void fallback_serial_cache(int fdi, int fdo, int cell_type, int method, const struct pj_info *oproj, const struct pj_info *iproj, @@ -444,7 +421,7 @@ fallback_serial_cache(int fdi, int fdo, int cell_type, int method, G_free(obuffer); } -/* Write the deferred band, if any, in order and release it. Used by the last +/* Write the deferred band, if any, in order and release it. Shared by the last * band and the fallback bails so every path writes the deferred band the same * way. */ static void flush_pending_band(int fdo, int cell_type, int cols, int cell_size, @@ -461,6 +438,14 @@ static void flush_pending_band(int fdo, int cell_type, int cols, int cell_size, *pending = NULL; } +/* Thread count for compute and write overlap, where nprocs above zero + * overrides OMP_NUM_THREADS. Set before the fit search so the write overlap's + * two reserved output buffers match the band sizing. */ +static int compute_nprocs(struct Option *nprocs) +{ + return G_set_omp_num_threads(nprocs); +} + int main(int argc, char **argv) { char *mapname, /* ptr to name of output layer */ @@ -508,6 +493,7 @@ int main(int argc, char **argv) *indbase, /* name of input database */ *interpol, /* interpolation method */ *memory, /* amount of memory for cache */ + *nprocs, /* number of compute threads */ *res, /* resolution of target map */ *format; /* output format */ @@ -565,6 +551,13 @@ int main(int argc, char **argv) memory = G_define_standard_option(G_OPT_MEMORYMB); + nprocs = G_define_standard_option(G_OPT_M_NPROCS); + nprocs->description = _( + "Number of threads for parallel computing. 0, the default, uses the " + "OpenMP default and honors OMP_NUM_THREADS if set. A value above zero " + "overrides OMP_NUM_THREADS, and a value below zero leaves that many " + "cores free"); + res = G_define_option(); res->key = "resolution"; res->type = TYPE_DOUBLE; @@ -1040,9 +1033,9 @@ int main(int argc, char **argv) G_message(_("NS-res: %f"), outcellhd.ns_res); G_message(" "); - /* Open the input map (input location env). Banding loads only per-band - * input strips, not the whole map, so fdi stays open across the band loop. - */ + /* Open the input map in the input env. Banding loads only per-band input + * strips rather than the whole map, so fdi stays open across the band + * loop. */ G_switch_env(); Rast_set_input_window(&incellhd); fdi = Rast_open_old(inmap->answer, setname); @@ -1051,24 +1044,16 @@ int main(int argc, char **argv) cell_type = FCELL_TYPE; cell_size = Rast_cell_size(cell_type); - /* Parallel input reads: decide the read-thread count here, in the INPUT - * env, so the mask guard checks the source mapset's mask (the mask that - * would apply to Rast_get_row on fdi). Rast_disable_omp_on_mask returns 1 - * (serial) if a mask is present or without OpenMP, and does NOT touch the - * thread count when no mask exists (lib/raster/mask_info.c:226-231), so the - * compute region's threads are unperturbed in the common case. When - * read_nprocs > 1 we open that many FRESH read fds (one per thread); fdi is - * used only by the serial fallback. - * INFERRED-safe (not yet runtime-verified; the gate converts it): - * concurrent Rast_open_old fds on the same map across locations is sound - * from the r.neighbors same-location precedent (in_fd[t]) plus the Stage 1 - * fcb analysis (each fd carries its own cur_row/data/data_fd; reads depend - * only on the fcb and R__.rd_window). */ -#ifdef _OPENMP - int want_nprocs = omp_get_max_threads(); -#else - int want_nprocs = 1; -#endif + /* The read-thread count is decided here in the input env so the mask guard + * checks the source mapset's mask. Rast_disable_omp_on_mask returns 1 and + * forces serial under a mask or without OpenMP, and leaves the count + * untouched otherwise (lib/raster/mask_info.c lines 226-231). When + * read_nprocs is above one, each thread opens its own fresh fd and fdi + * serves only the serial fallback. Concurrent fds on the same map across + * locations follow the r.neighbors in_fd[] precedent, where each fd carries + * its own cur_row, data, and data_fd. */ + /* Runs before the fit search. See compute_nprocs(). */ + int want_nprocs = compute_nprocs(nprocs); int read_nprocs = Rast_disable_omp_on_mask(want_nprocs); int *fd_read = NULL; if (read_nprocs > 1) { @@ -1077,10 +1062,9 @@ int main(int argc, char **argv) fd_read[t] = Rast_open_old(inmap->answer, setname); } - /* Back to the output location: set output window, init transform, open - * output map. Both fds now stay open; rd_window/wr_window are set and - * survive env switches, so reads/writes use the right windows throughout. - */ + /* Back in the output env, set the output window, init the transform, and + * open the output map. Both fds stay open and their windows survive env + * switches, so reads and writes use the right window throughout. */ G_switch_env(); Rast_set_output_window(&outcellhd); G_unset_window(); @@ -1098,17 +1082,15 @@ int main(int argc, char **argv) else fdo = Rast_open_fp_new(mapname); - /* Banding (r.neighbors two-level structure): outer serial band loop -> - * serial strip load -> parallel compute into a per-band buffer -> serial - * in-order per-band write -> next band. Bounds peak memory by the cap - * instead of the whole input map (Path A). */ + /* Banding runs a serial band loop of serial strip load, parallel compute + * into a per-band buffer, and an in-order per-band write. This bounds peak + * memory by the cap rather than the whole input map. */ double cap_mb = atof(memory->answer); size_t cap_bytes = (size_t)(cap_mb * 1024.0 * 1024.0); /* Under write_overlap the overlapped writes run inside the compute region, - * so their wall time falls in t_compute. t_write then covers the - * non-overlapped writes only: the last band's flush (timed at - * fallback_done) and every band at N=1. Fallback bail flushes are untimed, - * but a fallback run reports fallback=1 rather than this phase split. */ + * so their time falls in t_compute. t_write then covers only the + * non-overlapped writes, which are the last band's flush and every band at + * one thread. The fallback bail flushes are untimed. */ double t_size = 0.0, t_fill = 0.0, t_compute = 0.0, t_write = 0.0; int n_bands = 0; int max_tiles = 1; /* most column tiles used by any single band */ @@ -1117,20 +1099,11 @@ int main(int argc, char **argv) int seed_h1 = 0; /* previous Phase-1 band's accepted height */ int p1_hits = 0, p1_bands = 0; /* seed hit rate on the Phase-1 path */ - /* Output-row center northings, precomputed once by the serial version's - * recurrence: ycoord2 = north - ns_res/2, then ycoord2 -= ns_res per row. - * The banded fill loop and the strip-sizing perimeter walk both read these - * instead of computing north - ns_res/2 - row*ns_res directly. The direct - * multiply and the accumulated subtraction differ by up to one ULP when - * ns_res is not exactly representable; for non-nearest interpolation that - * shifts the sampling weights and diverges from the serial result by up to - * one FCELL ULP. The recurrence is reproduced here deliberately - * (bug-compatible rounding) so the parallel output stays bitwise identical - * to the serial reference; the direct multiply is the numerically cleaner - * form, so any future change away from the recurrence should be made in - * both code paths as an explicit accuracy decision. Both the fill loop and - * the sizing walk read these values, so sizing and fill stay on the same y - * and the loaded strip covers exactly the rows fill probes. */ + /* Output-row center northings from the serial recurrence, starting at north + * minus ns_res/2 and subtracting ns_res per row. The direct form differs by + * up to one ULP when ns_res is not exactly representable, so the recurrence + * is kept to stay bitwise identical to serial. The fill loop and the sizing + * walk share these values. */ double *y_center = G_malloc((size_t)outcellhd.rows * sizeof(double)); { double yc = outcellhd.north - (outcellhd.ns_res / 2); @@ -1140,20 +1113,16 @@ int main(int argc, char **argv) } } - /* Pole footprint fix: a tile whose interior projects onto a geographic pole - * has an input-row extremum the perimeter walk misses. This happens both - * when the pole lies inside the input map and when the pole is outside the - * input's latitude coverage but its projection still falls inside the - * output frame (a pole-centered frame reading an input truncated below the - * pole): the highest reachable input latitude is then the input's own edge - * row, reached at the frame-center-proximal interior. So project both poles - * (lat/lon input only, where a pole is at latitude +/- 90) and fold in the - * pole's input row clamped to the input's edge row [0, rows-1]. The - * point-in-rect test in band_input_row_span keeps this a no-op for frames - * that do not image a pole. On transform failure or a non-finite result - * (e.g. a cylindrical projection sending the pole to infinity) the pole is - * skipped and the strip under-size guard stays the backstop. Uses the - * adjusted incellhd, matching what band_input_row_span sees. */ + /* Pole footprint fix. A tile that projects onto a geographic pole has an + * input-row extremum the perimeter walk misses. This happens when the pole + * lies inside the input map, and also when the pole is outside the input's + * latitude coverage but still projects into the output frame, as with a + * pole-centered frame reading an input truncated below the pole, where the + * highest reachable input latitude is the input's own edge row. So it + * projects both poles for lat/lon input and folds in the pole's input row + * clamped to [0, rows-1]. The point-in-rect test keeps this a no-op for + * frames that image no pole, and a transform failure or non-finite result + * skips the pole and leaves the under-size guard as the backstop. */ struct pole_set poles; poles.n = 0; @@ -1189,37 +1158,34 @@ int main(int argc, char **argv) unsigned char *win = NULL; size_t win_cap = 0; int win_imin = 0, win_imax = -1; - /* One predicate for output double-buffering. The compute region runs - * want_nprocs threads (omp_get_max_threads(), not the masked read_nprocs), - * so overlap is possible only with more than one compute thread. out_mult - * reserves two output bands in the fit search and the omp single writer - * engages on the same flag, so the budget and the writer cannot diverge. */ + /* Output double-buffer predicate. The compute region runs want_nprocs + * threads rather than the masked read_nprocs, so overlap needs more than + * one compute thread. out_mult reserves two output bands in the fit search + * on the same flag the writer uses, so the budget and the writer stay in + * step. */ int write_overlap = want_nprocs > 1; int out_mult = write_overlap ? 2 : 1; - /* Previous band's output buffer, written by one thread while the next - * band computes. NULL when nothing is pending; rows + /* Previous band's output buffer, written by one thread while the next band + * computes. It is NULL when nothing is pending and holds rows * [pending_r0, pending_r1). */ void *pending_out = NULL; int pending_r0 = 0, pending_r1 = 0; int obr0 = 0; while (obr0 < outcellhd.rows) { - /* Fit search. Phase 1 (fast path, unchanged): halve the band height - * until the FULL-WIDTH strip plus the band output buffer fit the cap; - * the span is re-run per candidate height. Phase 2 (oblique fallback): - * only if a single full-width output row still busts the cap, split the - * row into column tiles and halve tile WIDTH until the worst tile's - * strip fits. Strips are full input width (the raster API reads whole - * rows), so width splitting shrinks a tile's input ROW span, not its + /* Fit search. Phase 1 halves the band height until the full-width strip + * plus output buffer fit the cap. Phase 2 runs only when a single + * full-width row still busts the cap, splitting the row into column + * tiles and halving tile width until the worst tile's strip fits. + * Strips are full input width because the raster API reads whole rows, + * so width splitting shrinks a tile's input row span rather than its * width. Easy pairs never leave Phase 1. */ double ts = rproj_wtime(); - /* Band-0 early-out for the wide-input corner: if a single output row at - * the finest tiling already busts the cap, take the serial fallback now - * instead of running the height/width search only to bail. Uses the - * same worst_tile_strip_rows(obr0, obr0+1, 1) the Phase-2 bail uses, - * probed only at the first band so its O(cols) cost is paid once, not - * per band. Later-band (pole) busts still fall through to the Phase-2 - * bail. force_tilecache is deliberately not handled here, so the forced - * override keeps routing through that bail unchanged. */ + /* Band-0 early-out for the wide-input corner. When a single output row + * at the finest tiling already busts the cap, take the serial fallback + * now instead of running the search only to bail. This is probed once + * at the first band. Later-band pole busts still fall through to the + * Phase-2 bail, and force_tilecache is deliberately not handled here so + * the override keeps routing through that bail. */ if (obr0 == 0) { size_t out1 = (size_t)outcellhd.cols * cell_size; int worst1 = worst_tile_strip_rows(&outcellhd, &incellhd, &oproj, @@ -1251,16 +1217,14 @@ int main(int argc, char **argv) } int tilew = outcellhd.cols; int imin = 0, imax = -1; - /* Phase-1 neighbor seed (hit path): seed_h1 (previous Phase-1 accepted - * height) is close to this band's. Take g_seed, the grid height just - * ABOVE seed_h1 on this band's descending lattice; if it does not fit - * then (height-monotone span) nothing taller fits, so the tallest - * fitting height is at or below (g_seed+1)/2 and the walk can start - * there, skipping the tall full-width edge walks. Any miss (no seed, - * seed_h1 too tall, or g_seed fits) starts from the full remaining - * height -- byte-for-byte the walk below. Same lattice, same acceptance - * line -> identical accepted height and partition; the hit only skips - * heights it has shown cannot fit. */ + /* Phase-1 neighbor seed. seed_h1 is the previous accepted height and is + * close to this band's. Take g_seed, the grid height just above + * seed_h1, and if it does not fit then nothing taller fits, so the walk + * starts at (g_seed+1)/2 and skips the tall full-width edge walks. Any + * miss starts from the full remaining height. The lattice and + * acceptance line are the same, so the accepted height and partition + * are identical and the hit only skips heights already shown not to + * fit. */ int band_orows = outcellhd.rows - obr0; int p1_seeded = 0; if (seed_h1 > 0 && seed_h1 < band_orows) { @@ -1288,7 +1252,7 @@ int main(int argc, char **argv) strip_bytes + out_mult * out_bytes <= cap_bytes) break; if (band_orows == 1) - break; /* height exhausted: fall through to column splitting */ + break; /* height exhausted, fall through to column splitting */ band_orows = (band_orows + 1) / 2; /* halve (round up), re-sample */ } if (band_orows > 1) { /* Phase-1 accepted a full-width band */ @@ -1298,19 +1262,14 @@ int main(int argc, char **argv) p1_hits++; } if (band_orows == 1) { - /* Phase 2 (oblique only): find the tallest band height on the - * descending grid whose worst column tile fits the cap, then that - * height's widest fitting tile width. Neighbor seed (hit path): the - * previous Phase-2 band's height (seed_h) is close to this band's - * H*. Take g_seed, the grid height just ABOVE seed_h; if it does - * not fit then (for an input-row span monotone in band height) - * nothing taller fits, so H* is at or below g_seed and the walk can - * start there, skipping the tall no-fit heights. On a miss (no - * seed, seed_h too tall, or g_seed fits) start from the full - * remaining height -- byte-for-byte the unseeded walk. Both starts - * lie on the same grid and accept via the same phase2_width_fit, so - * H*, W* and the partition are identical; the hit path only skips - * heights it has shown cannot fit. */ + /* Phase 2, oblique only, finds the tallest grid height whose worst + * column tile fits, then that height's widest fitting tile width. + * seed_h is the previous Phase-2 height and is close to H*. Take + * g_seed just above seed_h, and if it does not fit then nothing + * taller fits, so the walk starts at (g_seed+1)/2. A miss starts + * from the full remaining height. The grid and phase2_width_fit + * acceptance are the same, so H*, W*, and the partition are + * identical. */ phase2_bands++; int start_h = outcellhd.rows - obr0; if (seed_w > 0 && seed_h < start_h) { @@ -1334,11 +1293,11 @@ int main(int argc, char **argv) &poles)) break; if (band_orows == 1) { - /* Single output row at minimum width still over cap = - * singular/large-halo; take the serial tile-cache path. - * Also reached from band 0 when R_PROJ_FORCE_TILECACHE is - * set, which routes normal data through this identical - * block for testing. */ + /* A single output row at minimum width still over the cap + * is a singular or large-halo case, so take the serial + * tile-cache path. This is also reached from band 0 when + * R_PROJ_FORCE_TILECACHE routes normal data here for + * testing. */ if (force_tilecache) { G_warning( _("R_PROJ_FORCE_TILECACHE is set: taking the " @@ -1369,8 +1328,8 @@ int main(int argc, char **argv) obr0, outcellhd.rows - 1, needed_mb); } /* Flush the deferred band before the fallback writes from - * obr0 (in-order). This band's compute region did not run, - * so its omp single did not write the previous band. */ + * obr0. This band's compute region did not run, so its + * writer never fired. */ flush_pending_band(fdo, cell_type, outcellhd.cols, cell_size, &pending_out, pending_r0, pending_r1); @@ -1393,23 +1352,22 @@ int main(int argc, char **argv) if (n_tiles > max_tiles) max_tiles = n_tiles; - /* Per-band output buffer, lock-free disjoint row slots, filled column - * tile by column tile and written once after all tiles. Full width - * regardless of tiling. */ + /* Per-band output buffer at full width, filled tile by tile and written + * once after all tiles. The row slots are disjoint, so compute is + * lock-free. */ void *band_out = G_malloc((size_t)band_orows * outcellhd.cols * cell_size); - /* Column tiles processed one at a time: only the current tile's strip - * is resident, so peak strip memory is the worst tile, not the band's - * union. tilew == cols is the single-tile fast path (obc0=0, - * obc1=cols), identical to un-tiled banding. */ + /* Column tiles are processed one at a time, so peak strip memory is the + * worst tile rather than the band's union. A tilew equal to cols is the + * single-tile fast path. */ for (int obc0 = 0; obc0 < outcellhd.cols; obc0 += tilew) { int obc1 = obc0 + tilew; if (obc1 > outcellhd.cols) obc1 = outcellhd.cols; - /* Per-tile input row span (full-width strip: the raster API reads - * whole rows, so columns are not cropped). */ + /* Per-tile input row span. The strip is full input width because + * the raster API reads whole rows, so columns are not cropped. */ int pole_widened = 0; band_input_row_span(&outcellhd, &incellhd, &oproj, &iproj, &tproj, @@ -1422,10 +1380,11 @@ int main(int argc, char **argv) (int)poles.ri[pole_widened - 1], obr0, obr1, obc0, obc1); int strip_rows = imax - imin + 1; - /* Serial strip load (single fd -> get_row not thread-safe). EMPTY - * TILE: strip_rows <= 0 -> projects outside input, no read; cells - * become NULL via interpolate_strip's out-of-map path, and the - * window is invalidated so the next band re-reads in full. */ + /* Serial strip load, since a single fd makes get_row unsafe to + * share. An empty tile with strip_rows at or below zero projects + * outside the input and is not read, its cells become NULL through + * interpolate_strip's out-of-map path, and the window is + * invalidated so the next band re-reads in full. */ void *strip = NULL; if (strip_rows > 0) { size_t need = (size_t)strip_rows * incellhd.cols * cell_size; @@ -1437,13 +1396,12 @@ int main(int argc, char **argv) win_imax >= 0 && imin >= win_imin && imin <= win_imax + 1; int read_from = imin; - /* Grow first, then memmove, then read the tail. G_realloc may - * move the buffer, so it must run before the memmove that + /* Grow, then memmove, then read the tail. G_realloc may move + * the buffer, so it must run before the memmove that * repositions the retained overlap. Realloc preserves the old - * rows at their old offsets, and the memmove shifts them to the - * new imin origin. If the span shrinks (imax < win_imax), the - * rows past imax are dropped from win_imax below rather than - * kept, so a later band that needs them re-reads them. */ + * rows at their old offsets and the memmove shifts them to the + * new imin origin. Rows past a shrunk imax are dropped through + * win_imax below and re-read if a later band needs them. */ if (need > win_cap) { win = G_realloc(win, need); win_cap = need; @@ -1459,10 +1417,11 @@ int main(int argc, char **argv) G_switch_env(); /* -> input */ if (read_nprocs > 1) { #ifdef _OPENMP - /* Parallel read (full-span only; can_slide is false - * here): each thread reads a contiguous, disjoint block - * of rows through its OWN fd into its own disjoint - * strip slice. No two threads share an fd/row. */ + /* Parallel read of the full span only, since can_slide + * is false here. Each thread reads a contiguous + * disjoint block of rows through its own fd into its + * own strip slice, so no two threads share an fd or a + * row. */ #pragma omp parallel num_threads(read_nprocs) { int t = omp_get_thread_num(); @@ -1491,7 +1450,7 @@ int main(int argc, char **argv) /* Record what the window now holds. A tiled band leaves win * with only its last tile, so invalidate both fields to force * the next band's full read. Setting both keeps validity - * independent of && short-circuit order. */ + * independent of the && short-circuit order. */ if (n_tiles == 1) { win_imin = imin; win_imax = imax; @@ -1503,13 +1462,13 @@ int main(int argc, char **argv) } else { win_imin = 0; - win_imax = -1; /* empty tile: nothing resident */ + win_imax = -1; /* empty tile, nothing resident */ } double t1 = rproj_wtime(); - /* One parallel region per tile. Not nested: the "omp for" divides - * the band's output rows among this region's threads. Separate - * directives so each thread clones its PROJ context before the row + /* One parallel region per tile. The omp for divides the band's + * output rows among this region's threads. The directives are + * separate so each thread clones its PROJ context before the row * loop and destroys it after. */ #pragma omp parallel { @@ -1519,8 +1478,8 @@ int main(int argc, char **argv) #pragma omp single nowait { /* One thread writes the previous band's rows in order while - * the rest compute this band. First tile only, and - * pending_out is non-NULL only under write_overlap. */ + * the rest compute this band. This is the first tile only, + * and pending_out is non-NULL only under write_overlap. */ if (obc0 == 0 && pending_out) for (int wr = pending_r0; wr < pending_r1; wr++) Rast_put_row(fdo, @@ -1567,10 +1526,10 @@ int main(int argc, char **argv) * freed per tile. win is freed once at fallback_done. */ } - /* Defer this band so the next band's compute region writes it (via the - * omp single above). The previous pending was written in this band's - * compute region and completed at that region's barrier, so free it - * now. Non-overlap bands write and free in order here. */ + /* Defer this band so the next band's compute region writes it through + * the omp single above. The previous pending completed at this band's + * compute barrier, so free it now. Non-overlap bands write and free in + * order here. */ if (write_overlap) { if (pending_out) G_free(pending_out); @@ -1596,8 +1555,8 @@ int main(int argc, char **argv) fallback_done: /* Flush the last band's deferred write on normal completion, timed into - * t_write. The fallback bails flush before fallback_serial_cache, so - * pending_out is NULL here on those paths. */ + * t_write. The fallback bails already flushed, so pending_out is NULL on + * those paths. */ { double tw = rproj_wtime(); flush_pending_band(fdo, cell_type, outcellhd.cols, cell_size, @@ -1605,11 +1564,9 @@ int main(int argc, char **argv) t_write += rproj_wtime() - tw; } G_free(y_center); - /* Single free site for the rolling window: the band loop's only exits are - * normal completion (falls through to here) and the two goto fallback_done - * bails (band-0 early-out, Phase-2 width bust), all converging on this - * label, so one free covers every path crossing the window's live range. - * win is NULL if a bail fired before any band allocated it. */ + /* Single free site for the rolling window. Normal completion and both + * fallback_done bails converge here, so one free covers every path. win is + * NULL when a bail fired before any band allocated it. */ if (win) G_free(win); diff --git a/raster/r.proj/tests/r_proj_parallel_test.py b/raster/r.proj/tests/r_proj_parallel_test.py index 573d53331ef..02b4dc89630 100644 --- a/raster/r.proj/tests/r_proj_parallel_test.py +++ b/raster/r.proj/tests/r_proj_parallel_test.py @@ -1,22 +1,22 @@ """Parallel-correctness tests for r.proj. -r.proj has no nprocs= option; its thread count comes from OMP_NUM_THREADS. -Every run below is given its OWN environment dict, a copy of the session -env with OMP_NUM_THREADS (and, for the fallback test, R_PROJ_FORCE_TILECACHE) -set on the copy for that run only. Nothing shared is mutated, so the serial -and parallel runs of a test cannot leak thread or path state into each other. - -The baseline is the module's own single-thread run (OMP_NUM_THREADS=1), not -an external serial binary. The question these tests answer is whether adding +r.proj takes a nprocs= option that sets the compute thread count, so each run +below passes nprocs= for that run, and the fallback test also sets +R_PROJ_FORCE_TILECACHE on its own env copy. Nothing shared is mutated, so the +serial and parallel runs of a test cannot leak thread or path state into each +other. + +The baseline is the module's own single-thread run at nprocs=1 rather than an +external serial binary. The question these tests answer is whether adding threads, or taking the tile-cache fallback, changes the output of this same -binary. That comparison is exact and reproducible in CI; an external oracle -would not be. - -Correctness rule: nearest is asserted bitwise (abs diff max == 0). Bilinear -is asserted bitwise too, because each output cell is interpolated -independently in a fixed operation order, so threading does not reorder its -arithmetic. The epsilon-1e-6 fallback from the proposal may be invoked only -on an actual CI reordering failure, naming the platform that showed it. +binary. That comparison is exact and reproducible in CI where an external +oracle would not be. + +Nearest is asserted bitwise with an absolute diff max of zero. Bilinear is +asserted bitwise too because each output cell is interpolated independently in +a fixed operation order, so threading does not reorder its arithmetic. The +epsilon-1e-6 fallback from the proposal may be invoked only on an actual CI +reordering failure, naming the platform that showed it. """ import grass.script as gs @@ -106,8 +106,8 @@ def test_bilinear_parallel_matches_serial(session_3857): base = _env(session) _set_region_from_source(base, INPUT_MID, "bilinear") - _project(_env(session, OMP_NUM_THREADS=1), INPUT_MID, "bilin_serial", "bilinear") - _project(_env(session, OMP_NUM_THREADS=1), INPUT_MID, "nearest_ref", "nearest") + _project(base, INPUT_MID, "bilin_serial", "bilinear", nprocs=1) + _project(base, INPUT_MID, "nearest_ref", "nearest", nprocs=1) gs.run_command( "r.mapcalc", expression="dispatch_live = abs(bilin_serial - nearest_ref)", @@ -118,7 +118,7 @@ def test_bilinear_parallel_matches_serial(session_3857): "bilinear output equals nearest; dispatch may have fallen back" ) - _project(_env(session, OMP_NUM_THREADS=4), INPUT_MID, "bilin_parallel", "bilinear") + _project(base, INPUT_MID, "bilin_parallel", "bilinear", nprocs=4) _assert_bitwise_identical(base, "bilin_serial", "bilin_parallel", "bilin_diff") @@ -129,10 +129,8 @@ def test_nearest_memory_banding(session_3857): base = _env(session) _set_region_from_source(base, INPUT_MID, "nearest") - _project(_env(session, OMP_NUM_THREADS=1), INPUT_MID, "mem_serial", "nearest") - _project( - _env(session, OMP_NUM_THREADS=4), INPUT_MID, "mem_banded", "nearest", memory=5 - ) + _project(base, INPUT_MID, "mem_serial", "nearest", nprocs=1) + _project(base, INPUT_MID, "mem_banded", "nearest", nprocs=4, memory=5) _assert_bitwise_identical(base, "mem_serial", "mem_banded", "mem_diff") @@ -153,8 +151,8 @@ def test_pole_nearest_parallel_matches_serial(session_pole): env=base, ) - _project(_env(session, OMP_NUM_THREADS=1), INPUT_POLAR, "pole_serial", "nearest") - _project(_env(session, OMP_NUM_THREADS=4), INPUT_POLAR, "pole_parallel", "nearest") + _project(base, INPUT_POLAR, "pole_serial", "nearest", nprocs=1) + _project(base, INPUT_POLAR, "pole_parallel", "nearest", nprocs=4) _assert_bitwise_identical(base, "pole_serial", "pole_parallel", "pole_diff") @@ -168,10 +166,11 @@ def test_forced_fallback_matches_banded(session_3857): _set_region_from_source(base, INPUT_MID, "nearest") _project( - _env(session, OMP_NUM_THREADS=1, R_PROJ_FORCE_TILECACHE=1), + _env(session, R_PROJ_FORCE_TILECACHE=1), INPUT_MID, "fallback_tilecache", "nearest", + nprocs=1, ) - _project(_env(session, OMP_NUM_THREADS=4), INPUT_MID, "banded", "nearest") + _project(base, INPUT_MID, "banded", "nearest", nprocs=4) _assert_bitwise_identical(base, "fallback_tilecache", "banded", "fallback_diff") From 0826fae833cddeee51905c4ea7554052349c9235 Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Wed, 22 Jul 2026 20:58:26 -0700 Subject: [PATCH 21/39] r.proj: add thread scaling benchmark script Add a benchmark that sweeps the nprocs= thread count from 1 to 8 at two memory caps and plots the time, speedup, and efficiency metrics, following the r.param.scale benchmark template with grass.benchmark. It builds a source project and reprojects a generated raster from EPSG:4326 into EPSG:3857 in a temporary database, so it is self-contained. --- raster/r.proj/benchmark/benchmark_r_proj.py | 135 ++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 raster/r.proj/benchmark/benchmark_r_proj.py diff --git a/raster/r.proj/benchmark/benchmark_r_proj.py b/raster/r.proj/benchmark/benchmark_r_proj.py new file mode 100644 index 00000000000..acbd77abb6e --- /dev/null +++ b/raster/r.proj/benchmark/benchmark_r_proj.py @@ -0,0 +1,135 @@ +"""Benchmarking of r.proj thread scaling +raster (2D) + +This follows the r.param.scale benchmark structure, sweeping raster size at a +fixed memory and then memory at a fixed raster size, and plotting the time, +speedup, and efficiency metrics with grass.benchmark. r.proj sweeps its compute +thread count through the nprocs= option. Each cell generates a source raster in +an EPSG:4326 project and reprojects it into EPSG:3857 in a temporary database, +so the script is self-contained. Run it with +grass --exec python benchmark_r_proj.py or from any GRASS session. +""" + +import os +import tempfile + +from grass.exceptions import CalledModuleError, GrassError +from grass.pygrass.modules import Module +import grass.script as gs +import grass.benchmark as bm + +# Baselines held fixed while one dimension is swept. +BASE_MAPSIZE = 50e6 # cells +BASE_MEMORY = 300 # MB +MAPSIZES = [10e6, 50e6, 100e6] +MEMORIES = [50, 100, 300, 1000] +METRICS = ["time", "speedup", "efficiency"] +MAX_NPROCS = 8 +REPEAT = 3 + +SRC_PROJECT = "src4326" +DST_PROJECT = "dst3857" +INPUT = "benchmark_r_proj_reference" +OUTPUT = "benchmark_r_proj" + + +def main(): + gisdbase = tempfile.mkdtemp(prefix="bench_r_proj_") + gs.create_project(os.path.join(gisdbase, SRC_PROJECT), epsg="4326") + gs.create_project(os.path.join(gisdbase, DST_PROJECT), epsg="3857") + + # Sweep raster size at the baseline memory. + results = [] + for mapsize in MAPSIZES: + benchmark( + gisdbase, + size=int(mapsize**0.5), + memory=BASE_MEMORY, + label=f"r.proj_{int(mapsize / 1e6)}M", + results=results, + ) + plot(results, "rastersize") + + # Sweep memory at the baseline raster size. + results = [] + for memory in MEMORIES: + benchmark( + gisdbase, + size=int(BASE_MAPSIZE**0.5), + memory=memory, + label=f"r.proj_memory_{memory}MB", + results=results, + ) + plot(results, "memory") + + +def benchmark(gisdbase, size, memory, label, results): + generate_input(gisdbase, size) + with gs.setup.init( + os.path.join(gisdbase, DST_PROJECT), env=os.environ.copy() + ) as session: + env = session.env + # Output region from r.proj's own suggested bounds for this input. + text = gs.read_command( + "r.proj", + project=SRC_PROJECT, + mapset="PERMANENT", + dbase=gisdbase, + input=INPUT, + method="nearest", + flags="g", + env=env, + ) + region = dict(token.split("=") for token in text.split()) + gs.run_command("g.region", env=env, **region) + + module = Module( + "r.proj", + project=SRC_PROJECT, + mapset="PERMANENT", + dbase=gisdbase, + input=INPUT, + output=OUTPUT, + method="nearest", + memory=memory, + env_=env, + run_=False, + overwrite=True, + ) + results.append( + bm.benchmark_nprocs( + module, label=label, max_nprocs=MAX_NPROCS, repeat=REPEAT + ) + ) + + +def generate_input(gisdbase, size): + """Generate the size by size source raster in the EPSG:4326 project, + mirroring the r.param.scale benchmark by trying r.surf.fractal and falling + back to r.random.surface when fractal is unavailable, for example in a build + without FFTW.""" + with gs.setup.init( + os.path.join(gisdbase, SRC_PROJECT), env=os.environ.copy() + ) as session: + env = session.env + gs.run_command( + "g.region", n=50, s=40, w=-110, e=-90, rows=size, cols=size, env=env + ) + try: + Module("r.surf.fractal", output=INPUT, overwrite=True, env_=env) + except (CalledModuleError, GrassError): + Module("r.random.surface", output=INPUT, overwrite=True, env_=env) + + +def plot(results, sweep): + for metric in METRICS: + bm.nprocs_plot( + results, + filename=f"r_proj_{sweep}_{metric}.svg", + title=f"r.proj {sweep} {metric}", + metric=metric, + ) + + +if __name__ == "__main__": + main() From 3874ab7099ad90a75665be118617943d3a55cc5f Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Fri, 31 Jul 2026 22:47:09 -0700 Subject: [PATCH 22/39] r.proj: add footprint grid measurement alongside the fit search Add a footprint grid of input row spans, one grid row per output row and thirty-two column blocks wide, in a boundary-sampled and a column-exact variant. The grid only observes in this commit. The live fit search still steers every band height and tile width, and the grid span is compared against the search span for each rectangle the search evaluates. The comparison is gated by the R_PROJ_FG_VERIFY environment variable, so a normal run builds no grid and prints nothing. Each boundary cell carries a one row sampling margin, since the column samples can miss a curve between them by a fraction of a row. The margin is applied after the two variants are compared, so the variant report still measures the raw sampling error. --- raster/r.proj/footprint.c | 271 ++++++++++++++++++++++++++++++++++++++ raster/r.proj/main.c | 84 ++++++++++-- raster/r.proj/r.proj.h | 24 ++++ 3 files changed, 367 insertions(+), 12 deletions(-) create mode 100644 raster/r.proj/footprint.c diff --git a/raster/r.proj/footprint.c b/raster/r.proj/footprint.c new file mode 100644 index 00000000000..2ee92fb531a --- /dev/null +++ b/raster/r.proj/footprint.c @@ -0,0 +1,271 @@ +/* + * footprint.c - grid of input row spans for the output map. + * + * Each cell covers one output row and one column block and holds the range of + * input rows that block reaches. + */ + +#include +#include +#include + +#include +#include + +#include "r.proj.h" + +struct fg_cell { + double rmin, rmax; /* rmax below rmin marks an empty cell */ +}; + +struct footprint_grid { + int variant; /* FG_BOUNDARY or FG_EXACT */ + int grows, nb; /* grid rows and column blocks */ + int ocols; /* output columns */ + int irows; /* input rows */ + struct fg_cell *cell; /* grows by nb cells in row major order */ +}; + +/* The samples can miss a curve between columns by a fraction of a row, so each + * cell is widened by one row. */ +#define FG_SAMPLING_MARGIN 1.0 + +/* Returns the first output column of block b. */ +static int block_c0(const struct footprint_grid *g, int b) +{ + return (int)((long)b * g->ocols / g->nb); +} + +/* Returns the block that contains output column c. */ +static int block_of_col(const struct footprint_grid *g, int c) +{ + int b; + + for (b = 0; b < g->nb - 1; b++) + if (c < block_c0(g, b + 1)) + return b; + return g->nb - 1; +} + +/* Projects the center of output cell (r, c) to an input row index. Returns 0 on + * a failed transform and leaves ri unchanged. */ +static int sample_ri(const struct Cell_head *ohd, const struct Cell_head *ihd, + const struct pj_info *oproj, const struct pj_info *iproj, + const struct pj_info *tproj, const double *y_center, int r, + int c, double *ri) +{ + double xx = ohd->west + (c + 0.5) * ohd->ew_res; + double yy = y_center[r]; + + if (GPJ_transform(oproj, iproj, tproj, PJ_FWD, &xx, &yy, NULL) < 0) + return 0; + *ri = (ihd->north - yy) / ihd->ns_res; + return 1; +} + +/* Widens cell (r, b) to include any pole whose output point falls inside the + * cell rectangle. */ +static void fold_poles(const struct footprint_grid *g, + const struct Cell_head *ohd, + const struct pole_set *poles, int r, int b, + struct fg_cell *cell) +{ + int c0 = block_c0(g, b), c1 = block_c0(g, b + 1), k; + double x_lo = ohd->west + c0 * ohd->ew_res; + double x_hi = ohd->west + c1 * ohd->ew_res; + double y_lo = ohd->north - (r + 1) * ohd->ns_res; + double y_hi = ohd->north - r * ohd->ns_res; + + if (!poles) + return; + for (k = 0; k < poles->n; k++) { + if (poles->ox[k] < x_lo || poles->ox[k] > x_hi || poles->oy[k] < y_lo || + poles->oy[k] > y_hi) + continue; + if (poles->ri[k] < cell->rmin) + cell->rmin = poles->ri[k]; + if (poles->ri[k] > cell->rmax) + cell->rmax = poles->ri[k]; + } +} + +/* Builds the grid using boundary samples or every column. */ +struct footprint_grid * +fg_build(const struct Cell_head *ohd, const struct Cell_head *ihd, + const struct pj_info *oproj, const struct pj_info *iproj, + const struct pj_info *tproj, const double *y_center, + const struct pole_set *poles, int variant) +{ + struct footprint_grid *g = G_malloc(sizeof(*g)); + int r, b; + double *bnd = NULL; + + g->variant = variant; + g->grows = ohd->rows; + g->nb = ohd->cols < 32 ? ohd->cols : 32; + g->ocols = ohd->cols; + g->irows = ihd->rows; + g->cell = G_malloc((size_t)g->grows * g->nb * sizeof(struct fg_cell)); + + if (variant == FG_BOUNDARY) + bnd = G_malloc((size_t)(g->nb + 1) * sizeof(double)); + + for (r = 0; r < g->grows; r++) { + if (variant == FG_BOUNDARY) { + /* Sample the NB plus one block boundaries for this row. The last + * boundary uses the final valid column. */ + int k; + + for (k = 0; k <= g->nb; k++) { + int c = block_c0(g, k); + + if (c > g->ocols - 1) + c = g->ocols - 1; + if (!sample_ri(ohd, ihd, oproj, iproj, tproj, y_center, r, c, + &bnd[k])) + bnd[k] = + DBL_MAX; /* a failed sample is left out of the range */ + } + } + for (b = 0; b < g->nb; b++) { + struct fg_cell *cell = &g->cell[(size_t)r * g->nb + b]; + + cell->rmin = DBL_MAX; + cell->rmax = -DBL_MAX; + if (variant == FG_BOUNDARY) { + double lo = bnd[b] < bnd[b + 1] ? bnd[b] : bnd[b + 1]; + double hi = bnd[b] > bnd[b + 1] ? bnd[b] : bnd[b + 1]; + + if (bnd[b] != DBL_MAX && bnd[b + 1] != DBL_MAX) { + cell->rmin = lo; + cell->rmax = hi; + } + else if (bnd[b] != DBL_MAX) { + cell->rmin = cell->rmax = bnd[b]; + } + else if (bnd[b + 1] != DBL_MAX) { + cell->rmin = cell->rmax = bnd[b + 1]; + } + } + else { + /* Scan every column in the block. */ + int c0 = block_c0(g, b), c1 = block_c0(g, b + 1), c; + + for (c = c0; c < c1; c++) { + double ri; + + if (!sample_ri(ohd, ihd, oproj, iproj, tproj, y_center, r, + c, &ri)) + continue; + if (ri < cell->rmin) + cell->rmin = ri; + if (ri > cell->rmax) + cell->rmax = ri; + } + } + fold_poles(g, ohd, poles, r, b, cell); + } + } + if (bnd) + G_free(bnd); + return g; +} + +/* Returns the input row span covering the output rectangle. Includes every + * block the rectangle touches and adds a two cell margin. The grid holds one + * row per output row, so every output row in the rectangle indexes a grid row. + */ +void fg_span(const struct footprint_grid *g, int obr0, int obr1, int obc0, + int obc1, int *imin, int *imax) +{ + double rmin = DBL_MAX, rmax = -DBL_MAX; + int b_lo = block_of_col(g, obc0), b_hi = block_of_col(g, obc1 - 1); + int r, b; + + if (obr1 > g->grows) + G_fatal_error(_("Footprint grid has %d rows but output row %d was " + "requested"), + g->grows, obr1 - 1); + + for (r = obr0; r < obr1; r++) + for (b = b_lo; b <= b_hi; b++) { + const struct fg_cell *cell = &g->cell[(size_t)r * g->nb + b]; + + if (cell->rmax < cell->rmin) + continue; /* empty cell */ + if (cell->rmin < rmin) + rmin = cell->rmin; + if (cell->rmax > rmax) + rmax = cell->rmax; + } + + if (rmax < rmin) { /* every touched cell empty */ + *imin = 0; + *imax = -1; + return; + } + int lo = (int)floor(rmin) - 2; + int hi = (int)floor(rmax) + 2; + + if (lo < 0) + lo = 0; + if (hi > g->irows - 1) + hi = g->irows - 1; + *imin = lo; + *imax = hi; +} + +/* Reports how many cells the exact variant makes wider than the boundary + * variant, with the largest widening on each side. */ +void fg_compare_variants(const struct footprint_grid *b, + const struct footprint_grid *e) +{ + size_t n = (size_t)b->grows * b->nb, i; + long differ = 0; + double max_lo_gap = 0.0, max_hi_gap = 0.0; + + for (i = 0; i < n; i++) { + const struct fg_cell *cb = &b->cell[i], *ce = &e->cell[i]; + double lo_gap, hi_gap; + + if (cb->rmax < cb->rmin && ce->rmax < ce->rmin) + continue; + lo_gap = cb->rmin - ce->rmin; /* exact reaches this much lower */ + hi_gap = ce->rmax - cb->rmax; /* exact reaches this much higher */ + if (lo_gap > 0.0 || hi_gap > 0.0) { + differ++; + if (lo_gap > max_lo_gap) + max_lo_gap = lo_gap; + if (hi_gap > max_hi_gap) + max_hi_gap = hi_gap; + } + } + fprintf(stderr, + "FG_VAR cells=%ld differ=%ld max_lo_gap=%.3f max_hi_gap=%.3f\n", + (long)n, differ, max_lo_gap, max_hi_gap); +} + +/* Widens every non-empty cell of a boundary grid by the sampling margin. */ +void fg_apply_sampling_margin(struct footprint_grid *g) +{ + size_t n = (size_t)g->grows * g->nb, i; + + if (g->variant != FG_BOUNDARY) + return; + for (i = 0; i < n; i++) { + struct fg_cell *cell = &g->cell[i]; + + if (cell->rmax >= cell->rmin) { + cell->rmin -= FG_SAMPLING_MARGIN; + cell->rmax += FG_SAMPLING_MARGIN; + } + } +} + +void fg_free(struct footprint_grid *g) +{ + if (!g) + return; + G_free(g->cell); + G_free(g); +} diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index eecb031845b..ca16e125ef2 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -133,18 +133,59 @@ static const strip_func strip_kernels[] = { interpolate_strip, strip_bilinear, strip_cubic, strip_lanczos, strip_bilinear_f, strip_cubic_f, strip_lanczos_f}; -/* Geographic poles inside the input's latitude coverage. band_input_row_span - * walks only the tile perimeter, so a pole in a tile's interior is a latitude - * extremum the walk misses, and the pole's row is folded into that tile's span. - * Each pole is stored as its output-CRS coordinate and its input row. The set - * is empty when no pole is in frame. This assumes a pole maps to one output - * point as in azimuthal and stereographic projections, and otherwise the - * under-size guard stays the backstop. */ -struct pole_set { - int n; /* active poles, 0..2 */ - double ox[2], oy[2]; /* pole coordinates in the output CRS */ - double ri[2]; /* pole input row index */ -}; +/* Footprint grid used for comparison and the counters printed at the end. */ +static struct footprint_grid *g_fg_boundary = NULL; +static int g_fg_verify = 0; +static long g_fg_ncmp = 0; /* comparisons made */ +static long g_fg_fail = 0; /* cover failures */ +static int g_fg_min_slack = 0; /* smallest margin between grid and search */ +static int g_fg_max_overread = + 0; /* most extra input rows the grid would load */ +static int g_fg_have_stats = 0; /* set once a non-empty span is compared */ + +/* Compares one search span against the grid span and records the result. Prints + * a line only when the grid fails to cover the search. */ +static void fg_verify_emit(int obr0, int obr1, int obc0, int obc1, int s_imin, + int s_imax) +{ + int g_imin, g_imax, cover, low_slack, high_slack, slack, overread; + + if (!g_fg_verify) + return; + fg_span(g_fg_boundary, obr0, obr1, obc0, obc1, &g_imin, &g_imax); + g_fg_ncmp++; + if (s_imax < s_imin) /* empty search span is always covered */ + return; + cover = g_imin <= s_imin && g_imax >= s_imax; + low_slack = s_imin - g_imin; + high_slack = g_imax - s_imax; + slack = low_slack < high_slack ? low_slack : high_slack; + overread = (g_imax - g_imin) - (s_imax - s_imin); + if (!g_fg_have_stats || slack < g_fg_min_slack) + g_fg_min_slack = slack; + if (overread > g_fg_max_overread) + g_fg_max_overread = overread; + g_fg_have_stats = 1; + if (!cover) { + g_fg_fail++; + fprintf( + stderr, + "FG_CMP r[%d,%d) c[%d,%d) search=[%d,%d] grid=[%d,%d] cover=0\n", + obr0, obr1, obc0, obc1, s_imin, s_imax, g_imin, g_imax); + } +} + +/* Prints one line with the totals from all comparisons. */ +static void fg_verify_summary(void) +{ + if (!g_fg_verify) + return; + fprintf( + stderr, + "FG_SUM comparisons=%ld cover_fail=%ld min_slack=%d max_overread=%d\n", + g_fg_ncmp, g_fg_fail, g_fg_have_stats ? g_fg_min_slack : 0, + g_fg_max_overread); +} /* Edge-walk of an output tile [obr0, obr1) by [obc0, obc1) projected into input * space, returning the min and max input row it touches plus a 2-cell margin, @@ -224,6 +265,7 @@ band_input_row_span(const struct Cell_head *ohd, const struct Cell_head *ihd, if (rmax < rmin) { /* band projects entirely outside the input */ *imin = 0; *imax = -1; + fg_verify_emit(obr0, obr1, obc0, obc1, *imin, *imax); return; } @@ -235,6 +277,7 @@ band_input_row_span(const struct Cell_head *ohd, const struct Cell_head *ihd, hi = ihd->rows - 1; *imin = lo; *imax = hi; + fg_verify_emit(obr0, obr1, obc0, obc1, *imin, *imax); } /* Largest input-row strip among the width-tilew column tiles that partition the @@ -1148,6 +1191,18 @@ int main(int argc, char **argv) } } + /* Build both grids and compare them when R_PROJ_FG_VERIFY is set. */ + struct footprint_grid *fg_exact = NULL; + if (getenv("R_PROJ_FG_VERIFY")) { + g_fg_boundary = fg_build(&outcellhd, &incellhd, &oproj, &iproj, &tproj, + y_center, &poles, FG_BOUNDARY); + fg_exact = fg_build(&outcellhd, &incellhd, &oproj, &iproj, &tproj, + y_center, &poles, FG_EXACT); + fg_compare_variants(g_fg_boundary, fg_exact); + fg_apply_sampling_margin(g_fg_boundary); + g_fg_verify = 1; + } + G_important_message(_("Projecting (banded, per-thread PROJ context)...")); int used_fallback = 0; /* set when the serial tile-cache fallback runs */ @@ -1564,6 +1619,11 @@ int main(int argc, char **argv) t_write += rproj_wtime() - tw; } G_free(y_center); + fg_verify_summary(); + if (g_fg_boundary) + fg_free(g_fg_boundary); + if (fg_exact) + fg_free(fg_exact); /* Single free site for the rolling window. Normal completion and both * fallback_done bails converge here, so one free covers every path. win is * NULL when a bail fired before any band allocated it. */ diff --git a/raster/r.proj/r.proj.h b/raster/r.proj/r.proj.h index 28e46a503bb..6a2216ce6a8 100644 --- a/raster/r.proj/r.proj.h +++ b/raster/r.proj/r.proj.h @@ -41,6 +41,30 @@ struct menu { enum OutputFormat { PLAIN, SHELL, JSON }; +/* Geographic poles that fall inside the input map, folded into the input row + * span of the tile that contains them. Empty when no pole is in frame. */ +struct pole_set { + int n; /* active poles, 0 to 2 */ + double ox[2], oy[2]; /* pole coordinates in the output CRS */ + double ri[2]; /* pole input row index */ +}; + +/* Footprint grid of input row spans for the output map, built in footprint.c. + */ +enum fg_variant { FG_BOUNDARY, FG_EXACT }; +struct footprint_grid; +extern struct footprint_grid * +fg_build(const struct Cell_head *ohd, const struct Cell_head *ihd, + const struct pj_info *oproj, const struct pj_info *iproj, + const struct pj_info *tproj, const double *y_center, + const struct pole_set *poles, int variant); +extern void fg_span(const struct footprint_grid *g, int obr0, int obr1, + int obc0, int obc1, int *imin, int *imax); +extern void fg_compare_variants(const struct footprint_grid *b, + const struct footprint_grid *e); +extern void fg_apply_sampling_margin(struct footprint_grid *g); +extern void fg_free(struct footprint_grid *g); + extern void bordwalk(const struct Cell_head *, struct Cell_head *, const struct pj_info *, const struct pj_info *, const struct pj_info *, int); From f86ff66b1516bc9a2f76c03d602eeba1810cd40a Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Sat, 1 Aug 2026 00:29:33 -0700 Subject: [PATCH 23/39] r.proj: size band heights from the footprint grid Band heights now come from a scan of the footprint grid instead of the Phase-1 halving search, which is removed along with its seed machinery. Phase 2 and the rest of the pipeline are unchanged, and the output stays bitwise identical to the serial reference. --- raster/r.proj/footprint.c | 45 ++++++++++++++ raster/r.proj/main.c | 126 +++++++++++++------------------------- raster/r.proj/r.proj.h | 3 + 3 files changed, 92 insertions(+), 82 deletions(-) diff --git a/raster/r.proj/footprint.c b/raster/r.proj/footprint.c index 2ee92fb531a..26e3020a2cf 100644 --- a/raster/r.proj/footprint.c +++ b/raster/r.proj/footprint.c @@ -215,6 +215,51 @@ void fg_span(const struct footprint_grid *g, int obr0, int obr1, int obc0, *imax = hi; } +/* Find the tallest band at obr0 whose strip and output still fit the cap, and + * never return less than one row. */ +int fg_band_height(const struct footprint_grid *g, int obr0, size_t cap_bytes, + int out_mult, int cell_size, int in_cols) +{ + double rmin = DBL_MAX, rmax = -DBL_MAX; + int max_h = g->grows - obr0, accepted = 1, h, b; + + for (h = 0; h < max_h; h++) { + int r = obr0 + h, strip_rows; + size_t strip_bytes, out_bytes; + + for (b = 0; b < g->nb; b++) { + const struct fg_cell *cell = &g->cell[(size_t)r * g->nb + b]; + + if (cell->rmax < cell->rmin) + continue; + if (cell->rmin < rmin) + rmin = cell->rmin; + if (cell->rmax > rmax) + rmax = cell->rmax; + } + if (rmax < rmin) { + strip_rows = 0; + } + else { + int lo = (int)floor(rmin) - 2; + int hi = (int)floor(rmax) + 2; + + if (lo < 0) + lo = 0; + if (hi > g->irows - 1) + hi = g->irows - 1; + strip_rows = hi - lo + 1; + } + strip_bytes = + strip_rows > 0 ? (size_t)strip_rows * in_cols * cell_size : 0; + out_bytes = (size_t)(h + 1) * g->ocols * cell_size; + if (!(strip_bytes + out_mult * out_bytes <= cap_bytes)) + break; + accepted = h + 1; + } + return accepted; +} + /* Reports how many cells the exact variant makes wider than the boundary * variant, with the largest widening on each side. */ void fg_compare_variants(const struct footprint_grid *b, diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index ca16e125ef2..4da5bde5975 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -142,6 +142,8 @@ static int g_fg_min_slack = 0; /* smallest margin between grid and search */ static int g_fg_max_overread = 0; /* most extra input rows the grid would load */ static int g_fg_have_stats = 0; /* set once a non-empty span is compared */ +static long g_fg_band_audit_fail = + 0; /* bands whose grid strip came out smaller than the walk */ /* Compares one search span against the grid span and records the result. Prints * a line only when the grid fails to cover the search. */ @@ -180,11 +182,11 @@ static void fg_verify_summary(void) { if (!g_fg_verify) return; - fprintf( - stderr, - "FG_SUM comparisons=%ld cover_fail=%ld min_slack=%d max_overread=%d\n", - g_fg_ncmp, g_fg_fail, g_fg_have_stats ? g_fg_min_slack : 0, - g_fg_max_overread); + fprintf(stderr, + "FG_SUM comparisons=%ld cover_fail=%ld min_slack=%d " + "max_overread=%d fg_band_audit_fail=%ld\n", + g_fg_ncmp, g_fg_fail, g_fg_have_stats ? g_fg_min_slack : 0, + g_fg_max_overread, g_fg_band_audit_fail); } /* Edge-walk of an output tile [obr0, obr1) by [obc0, obc1) projected into input @@ -397,28 +399,6 @@ phase2_width_fit(const struct Cell_head *ohd, const struct Cell_head *ihd, return 0; } -/* Phase-1 fit test that returns 1 when a band of height h at obr0 fits its - * full-width input strip plus output buffer inside the cap. It short-circuits - * on the output buffer alone and is used only by the seed peek. */ -static int phase1_fits(const struct Cell_head *ohd, const struct Cell_head *ihd, - const struct pj_info *oproj, const struct pj_info *iproj, - const struct pj_info *tproj, const double *y_center, - int obr0, int h, size_t cap_bytes, int cell_size, - int out_mult, const struct pole_set *poles) -{ - int imin, imax, strip_rows; - size_t out_bytes = (size_t)h * ohd->cols * cell_size, strip_bytes; - - if (out_mult * out_bytes > cap_bytes) - return 0; - band_input_row_span(ohd, ihd, oproj, iproj, tproj, y_center, obr0, obr0 + h, - 0, ohd->cols, &imin, &imax, poles, NULL); - strip_rows = imax - imin + 1; - strip_bytes = - strip_rows > 0 ? (size_t)strip_rows * ihd->cols * cell_size : 0; - return strip_bytes + out_mult * out_bytes <= cap_bytes; -} - /* Serial tile-cache fallback for the oblique and large-halo corner. When even * one output row's full-width strip busts the cap, this finishes the run from * row obr0 with the classic readcell cache and CVAL kernels, exactly as serial @@ -1139,8 +1119,6 @@ int main(int argc, char **argv) int max_tiles = 1; /* most column tiles used by any single band */ int seed_h = 0, seed_w = 0; /* previous Phase-2 band's accepted sizing */ int seed_hits = 0, phase2_bands = 0; /* seed hit rate on the Phase-2 path */ - int seed_h1 = 0; /* previous Phase-1 band's accepted height */ - int p1_hits = 0, p1_bands = 0; /* seed hit rate on the Phase-1 path */ /* Output-row center northings from the serial recurrence, starting at north * minus ns_res/2 and subtracting ns_res per row. The direct form differs by @@ -1191,17 +1169,23 @@ int main(int argc, char **argv) } } - /* Build both grids and compare them when R_PROJ_FG_VERIFY is set. */ + /* Build the grid that sizes band heights. */ + g_fg_boundary = fg_build(&outcellhd, &incellhd, &oproj, &iproj, &tproj, + y_center, &poles, FG_BOUNDARY); + /* Under the verify flag build the exact grid and compare it before the + * margin is added. */ + int fg_verify_env = getenv("R_PROJ_FG_VERIFY") != NULL; struct footprint_grid *fg_exact = NULL; - if (getenv("R_PROJ_FG_VERIFY")) { - g_fg_boundary = fg_build(&outcellhd, &incellhd, &oproj, &iproj, &tproj, - y_center, &poles, FG_BOUNDARY); + if (fg_verify_env) { fg_exact = fg_build(&outcellhd, &incellhd, &oproj, &iproj, &tproj, y_center, &poles, FG_EXACT); fg_compare_variants(g_fg_boundary, fg_exact); - fg_apply_sampling_margin(g_fg_boundary); - g_fg_verify = 1; } + /* The margin covers what the samples can miss between columns. */ + fg_apply_sampling_margin(g_fg_boundary); + /* Turn on the audit once the grid is ready. */ + if (fg_verify_env) + g_fg_verify = 1; G_important_message(_("Projecting (banded, per-thread PROJ context)...")); @@ -1272,50 +1256,13 @@ int main(int argc, char **argv) } int tilew = outcellhd.cols; int imin = 0, imax = -1; - /* Phase-1 neighbor seed. seed_h1 is the previous accepted height and is - * close to this band's. Take g_seed, the grid height just above - * seed_h1, and if it does not fit then nothing taller fits, so the walk - * starts at (g_seed+1)/2 and skips the tall full-width edge walks. Any - * miss starts from the full remaining height. The lattice and - * acceptance line are the same, so the accepted height and partition - * are identical and the hit only skips heights already shown not to - * fit. */ - int band_orows = outcellhd.rows - obr0; - int p1_seeded = 0; - if (seed_h1 > 0 && seed_h1 < band_orows) { - int gs = band_orows; - - while ((gs + 1) / 2 > seed_h1) - gs = (gs + 1) / 2; - if (!phase1_fits(&outcellhd, &incellhd, &oproj, &iproj, &tproj, - y_center, obr0, gs, cap_bytes, cell_size, out_mult, - &poles)) { - band_orows = (gs + 1) / 2; - p1_seeded = 1; - } - } - for (;;) { - band_input_row_span(&outcellhd, &incellhd, &oproj, &iproj, &tproj, - y_center, obr0, obr0 + band_orows, 0, - outcellhd.cols, &imin, &imax, &poles, NULL); - int strip_rows = imax - imin + 1; - size_t strip_bytes = - strip_rows > 0 ? (size_t)strip_rows * incellhd.cols * cell_size - : 0; - size_t out_bytes = (size_t)band_orows * outcellhd.cols * cell_size; - if (!force_tilecache && - strip_bytes + out_mult * out_bytes <= cap_bytes) - break; - if (band_orows == 1) - break; /* height exhausted, fall through to column splitting */ - band_orows = (band_orows + 1) / 2; /* halve (round up), re-sample */ - } - if (band_orows > 1) { /* Phase-1 accepted a full-width band */ - seed_h1 = band_orows; - p1_bands++; - if (p1_seeded) - p1_hits++; - } + /* Grow the band while the strip and output still fit the cap, then step + * back one. */ + int band_orows = + force_tilecache + ? 1 + : fg_band_height(g_fg_boundary, obr0, cap_bytes, out_mult, + cell_size, incellhd.cols); if (band_orows == 1) { /* Phase 2, oblique only, finds the tallest grid height whose worst * column tile fits, then that height's widest fitting tile width. @@ -1401,6 +1348,22 @@ int main(int argc, char **argv) } t_size += rproj_wtime() - ts; + /* Walk the accepted band once and flag it when the grid strip is + * smaller than the walk. */ + if (g_fg_verify) { + int gi0, gi1, si0, si1, grid_rows, walk_rows; + + fg_span(g_fg_boundary, obr0, obr0 + band_orows, 0, outcellhd.cols, + &gi0, &gi1); + band_input_row_span(&outcellhd, &incellhd, &oproj, &iproj, &tproj, + y_center, obr0, obr0 + band_orows, 0, + outcellhd.cols, &si0, &si1, &poles, NULL); + grid_rows = gi1 >= gi0 ? gi1 - gi0 + 1 : 0; + walk_rows = si1 >= si0 ? si1 - si0 + 1 : 0; + if (grid_rows < walk_rows) + g_fg_band_audit_fail++; + } + int obr1 = obr0 + band_orows; n_bands++; int n_tiles = (outcellhd.cols + tilew - 1) / tilew; @@ -1635,10 +1598,9 @@ int main(int argc, char **argv) else G_debug(1, "PHASE_TIMERS size=%.4f fill=%.4f compute=%.4f write=%.4f " - "bands=%d tiles=%d seed_hits=%d phase2_bands=%d p1_hits=%d " - "p1_bands=%d", + "bands=%d tiles=%d seed_hits=%d phase2_bands=%d", t_size, t_fill, t_compute, t_write, n_bands, max_tiles, - seed_hits, phase2_bands, p1_hits, p1_bands); + seed_hits, phase2_bands); /* Close input map in its own env, then the output map. */ G_switch_env(); /* -> input */ diff --git a/raster/r.proj/r.proj.h b/raster/r.proj/r.proj.h index 6a2216ce6a8..0afa38c72d8 100644 --- a/raster/r.proj/r.proj.h +++ b/raster/r.proj/r.proj.h @@ -60,6 +60,9 @@ fg_build(const struct Cell_head *ohd, const struct Cell_head *ihd, const struct pole_set *poles, int variant); extern void fg_span(const struct footprint_grid *g, int obr0, int obr1, int obc0, int obc1, int *imin, int *imax); +extern int fg_band_height(const struct footprint_grid *g, int obr0, + size_t cap_bytes, int out_mult, int cell_size, + int in_cols); extern void fg_compare_variants(const struct footprint_grid *b, const struct footprint_grid *e); extern void fg_apply_sampling_margin(struct footprint_grid *g); From 3c15ccca4f2d197e3ab7a792a9a0533db8fd9269 Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Sat, 1 Aug 2026 15:06:52 -0700 Subject: [PATCH 24/39] r.proj: size tile widths from the footprint grid Tile widths now come from a scan of the footprint grid, with tiles built as runs of whole column blocks and a single over-cap check that routes to the serial fallback. The former tile-width search is removed and the output stays bitwise identical to the serial reference. --- raster/r.proj/footprint.c | 54 ++++++ raster/r.proj/main.c | 338 ++++++++------------------------------ raster/r.proj/r.proj.h | 5 + 3 files changed, 131 insertions(+), 266 deletions(-) diff --git a/raster/r.proj/footprint.c b/raster/r.proj/footprint.c index 26e3020a2cf..13a81f090ad 100644 --- a/raster/r.proj/footprint.c +++ b/raster/r.proj/footprint.c @@ -260,6 +260,60 @@ int fg_band_height(const struct footprint_grid *g, int obr0, size_t cap_bytes, return accepted; } +/* Number of column blocks in the grid. */ +int fg_num_blocks(const struct footprint_grid *g) +{ + return g->nb; +} + +/* First output column of block b. Block g->nb starts at the output width. */ +int fg_block_start(const struct footprint_grid *g, int b) +{ + return block_c0(g, b); +} + +/* Worst strip among the tiles that pack k whole blocks each across the band. */ +static int worst_ktile_rows(const struct footprint_grid *g, int obr0, int obr1, + int k) +{ + int worst = 0, tb; + + for (tb = 0; tb < g->nb; tb += k) { + int te = tb + k < g->nb ? tb + k : g->nb; + int imin, imax, rows; + + fg_span(g, obr0, obr1, block_c0(g, tb), block_c0(g, te), &imin, &imax); + rows = imax - imin + 1; + if (rows > worst) + worst = rows; + } + return worst; +} + +/* Widest tile in whole blocks whose worst strip and the output still fit the + * cap, or zero when even one block per tile busts. Reports the worst single + * block strip for the caller message. */ +int fg_tile_blocks(const struct footprint_grid *g, int obr0, int obr1, + size_t cap_bytes, int out_mult, int cell_size, int in_cols, + int *worst_block_rows) +{ + size_t out_bytes = (size_t)(obr1 - obr0) * g->ocols * cell_size; + int k; + + *worst_block_rows = worst_ktile_rows(g, obr0, obr1, 1); + if (out_mult * out_bytes > cap_bytes) + return 0; + for (k = g->nb; k >= 1; k--) { + int worst = worst_ktile_rows(g, obr0, obr1, k); + size_t strip_bytes = + worst > 0 ? (size_t)worst * in_cols * cell_size : 0; + + if (strip_bytes + out_mult * out_bytes <= cap_bytes) + return k; + } + return 0; +} + /* Reports how many cells the exact variant makes wider than the boundary * variant, with the largest widening on each side. */ void fg_compare_variants(const struct footprint_grid *b, diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index 4da5bde5975..4b1e630ccf6 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -282,123 +282,6 @@ band_input_row_span(const struct Cell_head *ohd, const struct Cell_head *ihd, fg_verify_emit(obr0, obr1, obc0, obc1, *imin, *imax); } -/* Largest input-row strip among the width-tilew column tiles that partition the - * band [obr0, obr1). Tiles load one at a time, so peak strip memory is the - * worst tile rather than the union of the band's tiles, and the fit search - * sizes this against the cap. It returns 0 when every tile projects entirely - * outside the input. */ -static int worst_tile_strip_rows(const struct Cell_head *ohd, - const struct Cell_head *ihd, - const struct pj_info *oproj, - const struct pj_info *iproj, - const struct pj_info *tproj, - const double *y_center, int obr0, int obr1, - int tilew, const struct pole_set *poles) -{ - int worst = 0, obc0; - - for (obc0 = 0; obc0 < ohd->cols; obc0 += tilew) { - int obc1 = obc0 + tilew; - int imin, imax, rows; - - if (obc1 > ohd->cols) - obc1 = ohd->cols; - band_input_row_span(ohd, ihd, oproj, iproj, tproj, y_center, obr0, obr1, - obc0, obc1, &imin, &imax, poles, NULL); - rows = imax - imin + 1; /* imax < imin (empty) -> <= 0, ignored */ - if (rows > worst) - worst = rows; - } - return worst; -} - -#define TILE_PROBE 16 /* tiles sampled by the Phase-2 width-search estimate */ - -/* Cheap lower-bound estimate of worst_tile_strip_rows, taking the largest strip - * among at most probe column tiles that are evenly spaced across the band width - * and always include the first and last. A subset max only prunes the Phase-2 - * search, and the chosen width is exact-validated by worst_tile_strip_rows - * before use. */ -static int est_worst_tile_strip_rows( - const struct Cell_head *ohd, const struct Cell_head *ihd, - const struct pj_info *oproj, const struct pj_info *iproj, - const struct pj_info *tproj, const double *y_center, int obr0, int obr1, - int tilew, int probe, const struct pole_set *poles) -{ - int ntiles = (ohd->cols + tilew - 1) / tilew; - int worst = 0, k; - - if (probe < 1) - probe = 1; - if (probe > ntiles) - probe = ntiles; - for (k = 0; k < probe; k++) { - int ti = (probe == 1) ? 0 : (int)((long)k * (ntiles - 1) / (probe - 1)); - int obc0 = ti * tilew; - int obc1 = obc0 + tilew; - int imin, imax, rows; - - if (obc1 > ohd->cols) - obc1 = ohd->cols; - band_input_row_span(ohd, ihd, oproj, iproj, tproj, y_center, obr0, obr1, - obc0, obc1, &imin, &imax, poles, NULL); - rows = imax - imin + 1; - if (rows > worst) - worst = rows; - } - return worst; -} - -/* Phase-2 fit test that returns 1 when a band of height h at obr0 fits the cap - * at some column-tile width, setting acc_tilew to that width through the same - * estimate then exact-validate steps the search uses. It returns 0 when no - * width fits or the output buffer alone exceeds the cap. */ -static int -phase2_width_fit(const struct Cell_head *ohd, const struct Cell_head *ihd, - const struct pj_info *oproj, const struct pj_info *iproj, - const struct pj_info *tproj, const double *y_center, int obr0, - int h, size_t cap_bytes, int cell_size, int out_mult, - int *acc_tilew, const struct pole_set *poles) -{ - size_t out_bytes = (size_t)h * ohd->cols * cell_size; - int tilew, est_fit; - - if (out_mult * out_bytes > cap_bytes) - return 0; - tilew = ohd->cols; - est_fit = 0; - for (;;) { - int est = - est_worst_tile_strip_rows(ohd, ihd, oproj, iproj, tproj, y_center, - obr0, obr0 + h, tilew, TILE_PROBE, poles); - size_t est_bytes = est > 0 ? (size_t)est * ihd->cols * cell_size : 0; - if (est_bytes + out_mult * out_bytes <= cap_bytes) { - est_fit = 1; - break; - } - if (tilew == 1) - break; - tilew = (tilew + 1) / 2; - } - if (est_fit) { - for (;;) { - int worst = - worst_tile_strip_rows(ohd, ihd, oproj, iproj, tproj, y_center, - obr0, obr0 + h, tilew, poles); - size_t strip_bytes = - worst > 0 ? (size_t)worst * ihd->cols * cell_size : 0; - if (strip_bytes + out_mult * out_bytes <= cap_bytes) { - *acc_tilew = tilew; - return 1; - } - if (tilew == 1) - break; - tilew = (tilew + 1) / 2; - } - } - return 0; -} - /* Serial tile-cache fallback for the oblique and large-halo corner. When even * one output row's full-width strip busts the cap, this finishes the run from * row obr0 with the classic readcell cache and CVAL kernels, exactly as serial @@ -1116,9 +999,7 @@ int main(int argc, char **argv) * one thread. The fallback bail flushes are untimed. */ double t_size = 0.0, t_fill = 0.0, t_compute = 0.0, t_write = 0.0; int n_bands = 0; - int max_tiles = 1; /* most column tiles used by any single band */ - int seed_h = 0, seed_w = 0; /* previous Phase-2 band's accepted sizing */ - int seed_hits = 0, phase2_bands = 0; /* seed hit rate on the Phase-2 path */ + int max_tiles = 1; /* most column tiles used by any single band */ /* Output-row center northings from the serial recurrence, starting at north * minus ns_res/2 and subtracting ns_res per row. The direct form differs by @@ -1211,140 +1092,64 @@ int main(int argc, char **argv) int pending_r0 = 0, pending_r1 = 0; int obr0 = 0; while (obr0 < outcellhd.rows) { - /* Fit search. Phase 1 halves the band height until the full-width strip - * plus output buffer fit the cap. Phase 2 runs only when a single - * full-width row still busts the cap, splitting the row into column - * tiles and halving tile width until the worst tile's strip fits. - * Strips are full input width because the raster API reads whole rows, - * so width splitting shrinks a tile's input row span rather than its - * width. Easy pairs never leave Phase 1. */ + /* Size this band. Take the tallest full-width band that fits, and when + * even one full-width row does not fit split it into whole column + * blocks and take the widest tile that fits. */ double ts = rproj_wtime(); - /* Band-0 early-out for the wide-input corner. When a single output row - * at the finest tiling already busts the cap, take the serial fallback - * now instead of running the search only to bail. This is probed once - * at the first band. Later-band pole busts still fall through to the - * Phase-2 bail, and force_tilecache is deliberately not handled here so - * the override keeps routing through that bail. */ - if (obr0 == 0) { - size_t out1 = (size_t)outcellhd.cols * cell_size; - int worst1 = worst_tile_strip_rows(&outcellhd, &incellhd, &oproj, - &iproj, &tproj, y_center, obr0, - obr0 + 1, 1, &poles); - size_t strip1 = - worst1 > 0 ? (size_t)worst1 * incellhd.cols * cell_size : 0; - if (strip1 + out1 > cap_bytes) { - int needed_mb = - (int)ceil((double)(strip1 + out1) / (1024.0 * 1024.0)) + 1; - G_warning(_("Memory cap (%.1f MB) is below what one output row " - "needs (input footprint %d rows, %.1f MB). Falling " - "back to the serial tile-cache path for output " - "rows %d-%d; this path is slower. Raise memory= to " - "at least %d MB to use the parallel path."), - cap_mb, worst1, - (double)(strip1 + out1) / (1024.0 * 1024.0), obr0, - outcellhd.rows - 1, needed_mb); - /* Flush the deferred band before the fallback writes from obr0 - * (in-order). */ - flush_pending_band(fdo, cell_type, outcellhd.cols, cell_size, - &pending_out, pending_r0, pending_r1); - fallback_serial_cache(fdi, fdo, cell_type, method, &oproj, - &iproj, &tproj, &incellhd, &outcellhd, - y_center, obr0, memory->answer); - used_fallback = 1; - goto fallback_done; - } - } - int tilew = outcellhd.cols; int imin = 0, imax = -1; - /* Grow the band while the strip and output still fit the cap, then step - * back one. */ int band_orows = force_tilecache ? 1 : fg_band_height(g_fg_boundary, obr0, cap_bytes, out_mult, cell_size, incellhd.cols); + int tile_blocks = fg_num_blocks(g_fg_boundary); if (band_orows == 1) { - /* Phase 2, oblique only, finds the tallest grid height whose worst - * column tile fits, then that height's widest fitting tile width. - * seed_h is the previous Phase-2 height and is close to H*. Take - * g_seed just above seed_h, and if it does not fit then nothing - * taller fits, so the walk starts at (g_seed+1)/2. A miss starts - * from the full remaining height. The grid and phase2_width_fit - * acceptance are the same, so H*, W*, and the partition are - * identical. */ - phase2_bands++; - int start_h = outcellhd.rows - obr0; - if (seed_w > 0 && seed_h < start_h) { - int gs = start_h, w; - - while ((gs + 1) / 2 > seed_h) - gs = (gs + 1) / 2; - if (!phase2_width_fit(&outcellhd, &incellhd, &oproj, &iproj, - &tproj, y_center, obr0, gs, cap_bytes, - cell_size, out_mult, &w, &poles)) { - start_h = (gs + 1) / 2; - seed_hits++; + int worst_block_rows = 0; + + tile_blocks = + force_tilecache + ? 0 + : fg_tile_blocks(g_fg_boundary, obr0, obr0 + band_orows, + cap_bytes, out_mult, cell_size, + incellhd.cols, &worst_block_rows); + if (tile_blocks == 0) { + /* Even the finest tiling busts the cap, so finish from obr0 on + * the serial tile-cache path. */ + if (force_tilecache) { + G_warning( + _("R_PROJ_FORCE_TILECACHE is set: taking the serial " + "tile-cache path for all output rows (testing " + "override).")); } - } - band_orows = start_h; - for (;;) { - if (!force_tilecache && - phase2_width_fit(&outcellhd, &incellhd, &oproj, &iproj, - &tproj, y_center, obr0, band_orows, - cap_bytes, cell_size, out_mult, &tilew, - &poles)) - break; - if (band_orows == 1) { - /* A single output row at minimum width still over the cap - * is a singular or large-halo case, so take the serial - * tile-cache path. This is also reached from band 0 when - * R_PROJ_FORCE_TILECACHE routes normal data here for - * testing. */ - if (force_tilecache) { - G_warning( - _("R_PROJ_FORCE_TILECACHE is set: taking the " - "serial tile-cache path for all output rows " - "(testing override).")); - } - else { - size_t out1 = (size_t)outcellhd.cols * cell_size; - int worst = worst_tile_strip_rows( - &outcellhd, &incellhd, &oproj, &iproj, &tproj, - y_center, obr0, obr0 + 1, 1, &poles); - size_t strip_bytes = - worst > 0 - ? (size_t)worst * incellhd.cols * cell_size - : 0; - int needed_mb = (int)ceil((double)(strip_bytes + out1) / - (1024.0 * 1024.0)) + - 1; - G_warning( - _("Memory cap (%.1f MB) is below what one output " - "row needs (input footprint %d rows, %.1f MB). " - "Falling back to the serial tile-cache path for " - "output rows %d-%d; this path is slower. Raise " - "memory= to at least %d MB to use the parallel " - "path."), - cap_mb, worst, - (double)(strip_bytes + out1) / (1024.0 * 1024.0), - obr0, outcellhd.rows - 1, needed_mb); - } - /* Flush the deferred band before the fallback writes from - * obr0. This band's compute region did not run, so its - * writer never fired. */ - flush_pending_band(fdo, cell_type, outcellhd.cols, - cell_size, &pending_out, pending_r0, - pending_r1); - fallback_serial_cache(fdi, fdo, cell_type, method, &oproj, - &iproj, &tproj, &incellhd, &outcellhd, - y_center, obr0, memory->answer); - used_fallback = 1; - goto fallback_done; + else { + size_t out1 = (size_t)outcellhd.cols * cell_size; + size_t strip_bytes = worst_block_rows > 0 + ? (size_t)worst_block_rows * + incellhd.cols * cell_size + : 0; + int needed_mb = (int)ceil((double)(strip_bytes + out1) / + (1024.0 * 1024.0)) + + 1; + G_warning( + _("Memory cap (%.1f MB) is below what one output row " + "needs (input footprint %d rows, %.1f MB). Falling " + "back to the serial tile-cache path for output rows " + "%d-%d; this path is slower. Raise memory= to at " + "least %d MB to use the parallel path."), + cap_mb, worst_block_rows, + (double)(strip_bytes + out1) / (1024.0 * 1024.0), obr0, + outcellhd.rows - 1, needed_mb); } - band_orows = (band_orows + 1) / 2; + /* Flush the deferred band before the fallback writes from obr0 + * in order. */ + flush_pending_band(fdo, cell_type, outcellhd.cols, cell_size, + &pending_out, pending_r0, pending_r1); + fallback_serial_cache(fdi, fdo, cell_type, method, &oproj, + &iproj, &tproj, &incellhd, &outcellhd, + y_center, obr0, memory->answer); + used_fallback = 1; + goto fallback_done; } - seed_h = band_orows; - seed_w = tilew; } t_size += rproj_wtime() - ts; @@ -1366,7 +1171,8 @@ int main(int argc, char **argv) int obr1 = obr0 + band_orows; n_bands++; - int n_tiles = (outcellhd.cols + tilew - 1) / tilew; + int nb = fg_num_blocks(g_fg_boundary); + int n_tiles = (nb + tile_blocks - 1) / tile_blocks; if (n_tiles > max_tiles) max_tiles = n_tiles; @@ -1377,25 +1183,26 @@ int main(int argc, char **argv) G_malloc((size_t)band_orows * outcellhd.cols * cell_size); /* Column tiles are processed one at a time, so peak strip memory is the - * worst tile rather than the band's union. A tilew equal to cols is the - * single-tile fast path. */ - for (int obc0 = 0; obc0 < outcellhd.cols; obc0 += tilew) { - int obc1 = obc0 + tilew; - if (obc1 > outcellhd.cols) - obc1 = outcellhd.cols; - - /* Per-tile input row span. The strip is full input width because - * the raster API reads whole rows, so columns are not cropped. */ - int pole_widened = 0; - - band_input_row_span(&outcellhd, &incellhd, &oproj, &iproj, &tproj, - y_center, obr0, obr1, obc0, obc1, &imin, &imax, - &poles, &pole_widened); - if (pole_widened) - G_verbose_message( - _("Pole (input row %d) in output tile rows [%d, %d) cols " - "[%d, %d): input strip extended to reach it"), - (int)poles.ri[pole_widened - 1], obr0, obr1, obc0, obc1); + * worst tile rather than the band's union. A single tile spanning every + * block is the full-width fast path. */ + for (int tb = 0; tb < nb; tb += tile_blocks) { + int te = tb + tile_blocks < nb ? tb + tile_blocks : nb; + int obc0 = fg_block_start(g_fg_boundary, tb); + int obc1 = fg_block_start(g_fg_boundary, te); + + /* Fill spans come from the grid. The strip is full input width + * because the raster API reads whole rows, so columns are not + * cropped. */ + fg_span(g_fg_boundary, obr0, obr1, obc0, obc1, &imin, &imax); + /* Under the verify flag walk the tile too so the hook checks the + * grid against the walk. */ + if (g_fg_verify) { + int wi0, wi1; + + band_input_row_span(&outcellhd, &incellhd, &oproj, &iproj, + &tproj, y_center, obr0, obr1, obc0, obc1, + &wi0, &wi1, &poles, NULL); + } int strip_rows = imax - imin + 1; /* Serial strip load, since a single fd makes get_row unsafe to @@ -1598,9 +1405,8 @@ int main(int argc, char **argv) else G_debug(1, "PHASE_TIMERS size=%.4f fill=%.4f compute=%.4f write=%.4f " - "bands=%d tiles=%d seed_hits=%d phase2_bands=%d", - t_size, t_fill, t_compute, t_write, n_bands, max_tiles, - seed_hits, phase2_bands); + "bands=%d tiles=%d", + t_size, t_fill, t_compute, t_write, n_bands, max_tiles); /* Close input map in its own env, then the output map. */ G_switch_env(); /* -> input */ diff --git a/raster/r.proj/r.proj.h b/raster/r.proj/r.proj.h index 0afa38c72d8..5d51940565d 100644 --- a/raster/r.proj/r.proj.h +++ b/raster/r.proj/r.proj.h @@ -63,6 +63,11 @@ extern void fg_span(const struct footprint_grid *g, int obr0, int obr1, extern int fg_band_height(const struct footprint_grid *g, int obr0, size_t cap_bytes, int out_mult, int cell_size, int in_cols); +extern int fg_num_blocks(const struct footprint_grid *g); +extern int fg_block_start(const struct footprint_grid *g, int b); +extern int fg_tile_blocks(const struct footprint_grid *g, int obr0, int obr1, + size_t cap_bytes, int out_mult, int cell_size, + int in_cols, int *worst_block_rows); extern void fg_compare_variants(const struct footprint_grid *b, const struct footprint_grid *e); extern void fg_apply_sampling_margin(struct footprint_grid *g); From da0991b23c74332b0533375bc7c441c7182a669d Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Sun, 2 Aug 2026 00:53:48 -0700 Subject: [PATCH 25/39] r.proj: match the serial rounding for large integer values The parallel nearest path now rounds each input value through a 32-bit float as the strip is read, the same step the serial cache-based read uses. Outputs then match the serial reference for every input, including integers above 2^24 where the float step changes the value. --- raster/r.proj/main.c | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index 4b1e630ccf6..f786ccb41a6 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -93,6 +93,28 @@ struct menu menu[] = { static char *make_ipol_list(void); static char *make_ipol_desc(void); +/* Round whole numbers through 32 bit float so every read path + returns the same values. */ +static void quantize_cell_row(void *row, int cols, int cell_type) +{ + int i; + + if (cell_type == CELL_TYPE) { + CELL *p = row; + + for (i = 0; i < cols; i++) + if (!Rast_is_c_null_value(&p[i])) + Rast_set_f_value(&p[i], (FCELL)p[i], CELL_TYPE); + } + else if (cell_type == DCELL_TYPE) { + DCELL *p = row; + + for (i = 0; i < cols; i++) + if (!Rast_is_d_null_value(&p[i])) + Rast_set_f_value(&p[i], (FCELL)p[i], DCELL_TYPE); + } +} + /* Nearest-neighbor read from an in-RAM strip holding input rows [imin, imax]. * The col_idx and row_idx values are full-map indices and the strip is * addressed relative to imin. A sample that lands inside the input map but @@ -1270,6 +1292,11 @@ int main(int argc, char **argv) r, cell_type); } G_switch_env(); /* -> output */ + for (int r = read_from; r <= imax; r++) + quantize_cell_row((unsigned char *)strip + + (size_t)(r - imin) * + incellhd.cols * cell_size, + incellhd.cols, cell_type); } t_fill += rproj_wtime() - t0; /* Record what the window now holds. A tiled band leaves win From da8019bac7e3a08930b6a81734abfbcb18291eac Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Sun, 2 Aug 2026 12:52:16 -0700 Subject: [PATCH 26/39] r.proj: remove the grid verification scaffolding The footprint grid was validated against the perimeter-walk search while it was being brought up. That verification path, its environment flag, and the counters it printed are no longer needed, and the runtime under-size check remains as the backstop. --- raster/r.proj/footprint.c | 116 ++++++----------------- raster/r.proj/main.c | 188 +------------------------------------- raster/r.proj/r.proj.h | 31 +------ 3 files changed, 30 insertions(+), 305 deletions(-) diff --git a/raster/r.proj/footprint.c b/raster/r.proj/footprint.c index 13a81f090ad..cf3f7db479e 100644 --- a/raster/r.proj/footprint.c +++ b/raster/r.proj/footprint.c @@ -7,7 +7,6 @@ #include #include -#include #include #include @@ -19,7 +18,6 @@ struct fg_cell { }; struct footprint_grid { - int variant; /* FG_BOUNDARY or FG_EXACT */ int grows, nb; /* grid rows and column blocks */ int ocols; /* output columns */ int irows; /* input rows */ @@ -89,85 +87,59 @@ static void fold_poles(const struct footprint_grid *g, } } -/* Builds the grid using boundary samples or every column. */ +/* Builds the grid from block boundary samples. */ struct footprint_grid * fg_build(const struct Cell_head *ohd, const struct Cell_head *ihd, const struct pj_info *oproj, const struct pj_info *iproj, const struct pj_info *tproj, const double *y_center, - const struct pole_set *poles, int variant) + const struct pole_set *poles) { struct footprint_grid *g = G_malloc(sizeof(*g)); int r, b; - double *bnd = NULL; + double *bnd; - g->variant = variant; g->grows = ohd->rows; g->nb = ohd->cols < 32 ? ohd->cols : 32; g->ocols = ohd->cols; g->irows = ihd->rows; g->cell = G_malloc((size_t)g->grows * g->nb * sizeof(struct fg_cell)); - - if (variant == FG_BOUNDARY) - bnd = G_malloc((size_t)(g->nb + 1) * sizeof(double)); + bnd = G_malloc((size_t)(g->nb + 1) * sizeof(double)); for (r = 0; r < g->grows; r++) { - if (variant == FG_BOUNDARY) { - /* Sample the NB plus one block boundaries for this row. The last - * boundary uses the final valid column. */ - int k; - - for (k = 0; k <= g->nb; k++) { - int c = block_c0(g, k); - - if (c > g->ocols - 1) - c = g->ocols - 1; - if (!sample_ri(ohd, ihd, oproj, iproj, tproj, y_center, r, c, - &bnd[k])) - bnd[k] = - DBL_MAX; /* a failed sample is left out of the range */ - } + /* Sample the NB plus one block boundaries for this row. The last + * boundary uses the final valid column. */ + int k; + + for (k = 0; k <= g->nb; k++) { + int c = block_c0(g, k); + + if (c > g->ocols - 1) + c = g->ocols - 1; + if (!sample_ri(ohd, ihd, oproj, iproj, tproj, y_center, r, c, + &bnd[k])) + bnd[k] = DBL_MAX; /* a failed sample is left out of the range */ } for (b = 0; b < g->nb; b++) { struct fg_cell *cell = &g->cell[(size_t)r * g->nb + b]; + double lo = bnd[b] < bnd[b + 1] ? bnd[b] : bnd[b + 1]; + double hi = bnd[b] > bnd[b + 1] ? bnd[b] : bnd[b + 1]; cell->rmin = DBL_MAX; cell->rmax = -DBL_MAX; - if (variant == FG_BOUNDARY) { - double lo = bnd[b] < bnd[b + 1] ? bnd[b] : bnd[b + 1]; - double hi = bnd[b] > bnd[b + 1] ? bnd[b] : bnd[b + 1]; - - if (bnd[b] != DBL_MAX && bnd[b + 1] != DBL_MAX) { - cell->rmin = lo; - cell->rmax = hi; - } - else if (bnd[b] != DBL_MAX) { - cell->rmin = cell->rmax = bnd[b]; - } - else if (bnd[b + 1] != DBL_MAX) { - cell->rmin = cell->rmax = bnd[b + 1]; - } + if (bnd[b] != DBL_MAX && bnd[b + 1] != DBL_MAX) { + cell->rmin = lo; + cell->rmax = hi; } - else { - /* Scan every column in the block. */ - int c0 = block_c0(g, b), c1 = block_c0(g, b + 1), c; - - for (c = c0; c < c1; c++) { - double ri; - - if (!sample_ri(ohd, ihd, oproj, iproj, tproj, y_center, r, - c, &ri)) - continue; - if (ri < cell->rmin) - cell->rmin = ri; - if (ri > cell->rmax) - cell->rmax = ri; - } + else if (bnd[b] != DBL_MAX) { + cell->rmin = cell->rmax = bnd[b]; + } + else if (bnd[b + 1] != DBL_MAX) { + cell->rmin = cell->rmax = bnd[b + 1]; } fold_poles(g, ohd, poles, r, b, cell); } } - if (bnd) - G_free(bnd); + G_free(bnd); return g; } @@ -314,43 +286,11 @@ int fg_tile_blocks(const struct footprint_grid *g, int obr0, int obr1, return 0; } -/* Reports how many cells the exact variant makes wider than the boundary - * variant, with the largest widening on each side. */ -void fg_compare_variants(const struct footprint_grid *b, - const struct footprint_grid *e) -{ - size_t n = (size_t)b->grows * b->nb, i; - long differ = 0; - double max_lo_gap = 0.0, max_hi_gap = 0.0; - - for (i = 0; i < n; i++) { - const struct fg_cell *cb = &b->cell[i], *ce = &e->cell[i]; - double lo_gap, hi_gap; - - if (cb->rmax < cb->rmin && ce->rmax < ce->rmin) - continue; - lo_gap = cb->rmin - ce->rmin; /* exact reaches this much lower */ - hi_gap = ce->rmax - cb->rmax; /* exact reaches this much higher */ - if (lo_gap > 0.0 || hi_gap > 0.0) { - differ++; - if (lo_gap > max_lo_gap) - max_lo_gap = lo_gap; - if (hi_gap > max_hi_gap) - max_hi_gap = hi_gap; - } - } - fprintf(stderr, - "FG_VAR cells=%ld differ=%ld max_lo_gap=%.3f max_hi_gap=%.3f\n", - (long)n, differ, max_lo_gap, max_hi_gap); -} - -/* Widens every non-empty cell of a boundary grid by the sampling margin. */ +/* Widens every non-empty cell by the sampling margin. */ void fg_apply_sampling_margin(struct footprint_grid *g) { size_t n = (size_t)g->grows * g->nb, i; - if (g->variant != FG_BOUNDARY) - return; for (i = 0; i < n; i++) { struct fg_cell *cell = &g->cell[i]; diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index f786ccb41a6..8efc5b4089e 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -157,152 +157,6 @@ static const strip_func strip_kernels[] = { /* Footprint grid used for comparison and the counters printed at the end. */ static struct footprint_grid *g_fg_boundary = NULL; -static int g_fg_verify = 0; -static long g_fg_ncmp = 0; /* comparisons made */ -static long g_fg_fail = 0; /* cover failures */ -static int g_fg_min_slack = 0; /* smallest margin between grid and search */ -static int g_fg_max_overread = - 0; /* most extra input rows the grid would load */ -static int g_fg_have_stats = 0; /* set once a non-empty span is compared */ -static long g_fg_band_audit_fail = - 0; /* bands whose grid strip came out smaller than the walk */ - -/* Compares one search span against the grid span and records the result. Prints - * a line only when the grid fails to cover the search. */ -static void fg_verify_emit(int obr0, int obr1, int obc0, int obc1, int s_imin, - int s_imax) -{ - int g_imin, g_imax, cover, low_slack, high_slack, slack, overread; - - if (!g_fg_verify) - return; - fg_span(g_fg_boundary, obr0, obr1, obc0, obc1, &g_imin, &g_imax); - g_fg_ncmp++; - if (s_imax < s_imin) /* empty search span is always covered */ - return; - cover = g_imin <= s_imin && g_imax >= s_imax; - low_slack = s_imin - g_imin; - high_slack = g_imax - s_imax; - slack = low_slack < high_slack ? low_slack : high_slack; - overread = (g_imax - g_imin) - (s_imax - s_imin); - if (!g_fg_have_stats || slack < g_fg_min_slack) - g_fg_min_slack = slack; - if (overread > g_fg_max_overread) - g_fg_max_overread = overread; - g_fg_have_stats = 1; - if (!cover) { - g_fg_fail++; - fprintf( - stderr, - "FG_CMP r[%d,%d) c[%d,%d) search=[%d,%d] grid=[%d,%d] cover=0\n", - obr0, obr1, obc0, obc1, s_imin, s_imax, g_imin, g_imax); - } -} - -/* Prints one line with the totals from all comparisons. */ -static void fg_verify_summary(void) -{ - if (!g_fg_verify) - return; - fprintf(stderr, - "FG_SUM comparisons=%ld cover_fail=%ld min_slack=%d " - "max_overread=%d fg_band_audit_fail=%ld\n", - g_fg_ncmp, g_fg_fail, g_fg_have_stats ? g_fg_min_slack : 0, - g_fg_max_overread, g_fg_band_audit_fail); -} - -/* Edge-walk of an output tile [obr0, obr1) by [obc0, obc1) projected into input - * space, returning the min and max input row it touches plus a 2-cell margin, - * clamped to the input map. It walks the tile perimeter of top and bottom rows - * and left and right columns so a curved transform's interior-edge extremum is - * caught, which corner-only sampling would miss. It runs serially before the - * parallel region, so the shared tproj is safe. It returns imax below imin when - * the tile projects entirely outside the input. */ -static void -band_input_row_span(const struct Cell_head *ohd, const struct Cell_head *ihd, - const struct pj_info *oproj, const struct pj_info *iproj, - const struct pj_info *tproj, const double *y_center, - int obr0, int obr1, int obc0, int obc1, int *imin, - int *imax, const struct pole_set *poles, int *pole_widened) -{ - double rmin = 1e300, rmax = -1e300; - int e, r, c; - - /* top edge (row obr0) and bottom edge (row obr1-1), tile columns */ - for (e = 0; e < 2; e++) { - int orow = (e == 0) ? obr0 : (obr1 - 1); - double y = y_center[orow]; - for (c = obc0; c < obc1; c++) { - double x = ohd->west + (c + 0.5) * ohd->ew_res; - double xx = x, yy = y; - if (GPJ_transform(oproj, iproj, tproj, PJ_FWD, &xx, &yy, NULL) < 0) - continue; - double ri = (ihd->north - yy) / ihd->ns_res; - if (ri < rmin) - rmin = ri; - if (ri > rmax) - rmax = ri; - } - } - /* left edge (col obc0) and right edge (col obc1-1), all band rows */ - for (e = 0; e < 2; e++) { - int ocol = (e == 0) ? obc0 : (obc1 - 1); - double x = ohd->west + (ocol + 0.5) * ohd->ew_res; - for (r = obr0; r < obr1; r++) { - double y = y_center[r]; - double xx = x, yy = y; - if (GPJ_transform(oproj, iproj, tproj, PJ_FWD, &xx, &yy, NULL) < 0) - continue; - double ri = (ihd->north - yy) / ihd->ns_res; - if (ri < rmin) - rmin = ri; - if (ri > rmax) - rmax = ri; - } - } - - /* Fold in any pole whose output point lies in this tile, since the - * perimeter walk cannot see an interior latitude extremum. A pole on a tile - * edge is caught by both adjacent tiles, which only widens a strip that is - * loaded anyway. This comes before the empty-tile check so a pole inside an - * otherwise outside tile still yields a valid span. */ - if (poles) { - double x_lo = ohd->west + obc0 * ohd->ew_res; - double x_hi = ohd->west + obc1 * ohd->ew_res; - double y_lo = ohd->north - obr1 * ohd->ns_res; - double y_hi = ohd->north - obr0 * ohd->ns_res; - int k; - - for (k = 0; k < poles->n; k++) { - if (poles->ox[k] < x_lo || poles->ox[k] > x_hi || - poles->oy[k] < y_lo || poles->oy[k] > y_hi) - continue; - if (poles->ri[k] < rmin) - rmin = poles->ri[k]; - if (poles->ri[k] > rmax) - rmax = poles->ri[k]; - if (pole_widened) - *pole_widened = k + 1; /* 1-based pole index, 0 == none */ - } - } - - if (rmax < rmin) { /* band projects entirely outside the input */ - *imin = 0; - *imax = -1; - fg_verify_emit(obr0, obr1, obc0, obc1, *imin, *imax); - return; - } - - int lo = (int)floor(rmin) - 2; /* 2-cell margin for interp stencils */ - int hi = (int)floor(rmax) + 2; - if (lo < 0) - lo = 0; - if (hi > ihd->rows - 1) - hi = ihd->rows - 1; - *imin = lo; - *imax = hi; - fg_verify_emit(obr0, obr1, obc0, obc1, *imin, *imax); -} /* Serial tile-cache fallback for the oblique and large-halo corner. When even * one output row's full-width strip busts the cap, this finishes the run from @@ -1074,21 +928,9 @@ int main(int argc, char **argv) /* Build the grid that sizes band heights. */ g_fg_boundary = fg_build(&outcellhd, &incellhd, &oproj, &iproj, &tproj, - y_center, &poles, FG_BOUNDARY); - /* Under the verify flag build the exact grid and compare it before the - * margin is added. */ - int fg_verify_env = getenv("R_PROJ_FG_VERIFY") != NULL; - struct footprint_grid *fg_exact = NULL; - if (fg_verify_env) { - fg_exact = fg_build(&outcellhd, &incellhd, &oproj, &iproj, &tproj, - y_center, &poles, FG_EXACT); - fg_compare_variants(g_fg_boundary, fg_exact); - } + y_center, &poles); /* The margin covers what the samples can miss between columns. */ fg_apply_sampling_margin(g_fg_boundary); - /* Turn on the audit once the grid is ready. */ - if (fg_verify_env) - g_fg_verify = 1; G_important_message(_("Projecting (banded, per-thread PROJ context)...")); @@ -1175,22 +1017,6 @@ int main(int argc, char **argv) } t_size += rproj_wtime() - ts; - /* Walk the accepted band once and flag it when the grid strip is - * smaller than the walk. */ - if (g_fg_verify) { - int gi0, gi1, si0, si1, grid_rows, walk_rows; - - fg_span(g_fg_boundary, obr0, obr0 + band_orows, 0, outcellhd.cols, - &gi0, &gi1); - band_input_row_span(&outcellhd, &incellhd, &oproj, &iproj, &tproj, - y_center, obr0, obr0 + band_orows, 0, - outcellhd.cols, &si0, &si1, &poles, NULL); - grid_rows = gi1 >= gi0 ? gi1 - gi0 + 1 : 0; - walk_rows = si1 >= si0 ? si1 - si0 + 1 : 0; - if (grid_rows < walk_rows) - g_fg_band_audit_fail++; - } - int obr1 = obr0 + band_orows; n_bands++; int nb = fg_num_blocks(g_fg_boundary); @@ -1216,15 +1042,6 @@ int main(int argc, char **argv) * because the raster API reads whole rows, so columns are not * cropped. */ fg_span(g_fg_boundary, obr0, obr1, obc0, obc1, &imin, &imax); - /* Under the verify flag walk the tile too so the hook checks the - * grid against the walk. */ - if (g_fg_verify) { - int wi0, wi1; - - band_input_row_span(&outcellhd, &incellhd, &oproj, &iproj, - &tproj, y_center, obr0, obr1, obc0, obc1, - &wi0, &wi1, &poles, NULL); - } int strip_rows = imax - imin + 1; /* Serial strip load, since a single fd makes get_row unsafe to @@ -1416,11 +1233,8 @@ int main(int argc, char **argv) t_write += rproj_wtime() - tw; } G_free(y_center); - fg_verify_summary(); if (g_fg_boundary) fg_free(g_fg_boundary); - if (fg_exact) - fg_free(fg_exact); /* Single free site for the rolling window. Normal completion and both * fallback_done bails converge here, so one free covers every path. win is * NULL when a bail fired before any band allocated it. */ diff --git a/raster/r.proj/r.proj.h b/raster/r.proj/r.proj.h index 5d51940565d..76dc9ff00d3 100644 --- a/raster/r.proj/r.proj.h +++ b/raster/r.proj/r.proj.h @@ -51,13 +51,12 @@ struct pole_set { /* Footprint grid of input row spans for the output map, built in footprint.c. */ -enum fg_variant { FG_BOUNDARY, FG_EXACT }; struct footprint_grid; extern struct footprint_grid * fg_build(const struct Cell_head *ohd, const struct Cell_head *ihd, const struct pj_info *oproj, const struct pj_info *iproj, const struct pj_info *tproj, const double *y_center, - const struct pole_set *poles, int variant); + const struct pole_set *poles); extern void fg_span(const struct footprint_grid *g, int obr0, int obr1, int obc0, int obc1, int *imin, int *imax); extern int fg_band_height(const struct footprint_grid *g, int obr0, @@ -68,8 +67,6 @@ extern int fg_block_start(const struct footprint_grid *g, int b); extern int fg_tile_blocks(const struct footprint_grid *g, int obr0, int obr1, size_t cap_bytes, int out_mult, int cell_size, int in_cols, int *worst_block_rows); -extern void fg_compare_variants(const struct footprint_grid *b, - const struct footprint_grid *e); extern void fg_apply_sampling_margin(struct footprint_grid *g); extern void fg_free(struct footprint_grid *g); @@ -119,8 +116,6 @@ extern void strip_cubic_f(void *, void *, int, double, double, extern void strip_lanczos_f(void *, void *, int, double, double, struct Cell_head *, int, int); -#if 1 - #define BKIDX(c, y, x) ((y) * (c)->stride + (x)) #define BKPTR(c, y, x) ((c)->grid[BKIDX((c), (y), (x))]) #define BLOCK(c, y, x) \ @@ -128,28 +123,4 @@ extern void strip_lanczos_f(void *, void *, int, double, double, : get_block((c), BKIDX((c), (y), (x)))) #define CVAL(c, y, x) ((*BLOCK((c), HI((y)), HI((x))))[LO((y))][LO((x))]) -#else - -static inline int BKIDX(const struct cache *c, int y, int x) -{ - return y * c->stride + x; -} - -static inline block *BKPTR(const struct cache *c, int y, int x) -{ - return c->grid[BKIDX(c, y, x)]; -} - -static inline block *BLOCK(struct cache *c, int y, int x) -{ - return BKPTR(c, y, x) ? BKPTR(c, y, x) : get_block(c, BKIDX(c, y, x)); -} - -static inline FCELL *CPTR(struct cache *c, int y, int x) -{ - return &(*BLOCK(c, HI(y), HI(x)))[LO(y)][LO(x)]; -} - -#endif - #endif From 8b73c2b36d98bad176d26aea3c8d550d23372081 Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Sun, 2 Aug 2026 16:23:57 -0700 Subject: [PATCH 27/39] r.proj: rename the sizing variables for clarity The nearest strip reader is renamed to strip_nearest to match its sibling kernels, the footprint grid pointer to band_grid now that there is only one grid, and the pole row field to pole_row. --- raster/r.proj/footprint.c | 8 ++++---- raster/r.proj/main.c | 40 +++++++++++++++++++-------------------- raster/r.proj/r.proj.h | 2 +- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/raster/r.proj/footprint.c b/raster/r.proj/footprint.c index cf3f7db479e..0b79b0347a5 100644 --- a/raster/r.proj/footprint.c +++ b/raster/r.proj/footprint.c @@ -80,10 +80,10 @@ static void fold_poles(const struct footprint_grid *g, if (poles->ox[k] < x_lo || poles->ox[k] > x_hi || poles->oy[k] < y_lo || poles->oy[k] > y_hi) continue; - if (poles->ri[k] < cell->rmin) - cell->rmin = poles->ri[k]; - if (poles->ri[k] > cell->rmax) - cell->rmax = poles->ri[k]; + if (poles->pole_row[k] < cell->rmin) + cell->rmin = poles->pole_row[k]; + if (poles->pole_row[k] > cell->rmax) + cell->rmax = poles->pole_row[k]; } } diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index 8efc5b4089e..044bbe80cbc 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -120,9 +120,9 @@ static void quantize_cell_row(void *row, int cols, int cell_type) * addressed relative to imin. A sample that lands inside the input map but * outside the loaded strip means the band was under-sized, which the guard * below catches. */ -static void interpolate_strip(void *strip, void *obufptr, int cell_type, - double col_idx, double row_idx, - struct Cell_head *incellhd, int imin, int imax) +static void strip_nearest(void *strip, void *obufptr, int cell_type, + double col_idx, double row_idx, + struct Cell_head *incellhd, int imin, int imax) { int c = (int)floor(col_idx); int r = (int)floor(row_idx); @@ -152,11 +152,11 @@ static void interpolate_strip(void *strip, void *obufptr, int cell_type, * of menu[i].method. Slot 0 is nearest above and slots 1 to 6 come from * interp_strip.c. */ static const strip_func strip_kernels[] = { - interpolate_strip, strip_bilinear, strip_cubic, strip_lanczos, - strip_bilinear_f, strip_cubic_f, strip_lanczos_f}; + strip_nearest, strip_bilinear, strip_cubic, strip_lanczos, + strip_bilinear_f, strip_cubic_f, strip_lanczos_f}; /* Footprint grid used for comparison and the counters printed at the end. */ -static struct footprint_grid *g_fg_boundary = NULL; +static struct footprint_grid *band_grid = NULL; /* Serial tile-cache fallback for the oblique and large-halo corner. When even * one output row's full-width strip busts the cap, this finishes the run from @@ -921,16 +921,16 @@ int main(int argc, char **argv) ri = incellhd.rows - 1; poles.ox[poles.n] = px; poles.oy[poles.n] = py; - poles.ri[poles.n] = ri; + poles.pole_row[poles.n] = ri; poles.n++; } } /* Build the grid that sizes band heights. */ - g_fg_boundary = fg_build(&outcellhd, &incellhd, &oproj, &iproj, &tproj, - y_center, &poles); + band_grid = fg_build(&outcellhd, &incellhd, &oproj, &iproj, &tproj, + y_center, &poles); /* The margin covers what the samples can miss between columns. */ - fg_apply_sampling_margin(g_fg_boundary); + fg_apply_sampling_margin(band_grid); G_important_message(_("Projecting (banded, per-thread PROJ context)...")); @@ -964,16 +964,16 @@ int main(int argc, char **argv) int band_orows = force_tilecache ? 1 - : fg_band_height(g_fg_boundary, obr0, cap_bytes, out_mult, + : fg_band_height(band_grid, obr0, cap_bytes, out_mult, cell_size, incellhd.cols); - int tile_blocks = fg_num_blocks(g_fg_boundary); + int tile_blocks = fg_num_blocks(band_grid); if (band_orows == 1) { int worst_block_rows = 0; tile_blocks = force_tilecache ? 0 - : fg_tile_blocks(g_fg_boundary, obr0, obr0 + band_orows, + : fg_tile_blocks(band_grid, obr0, obr0 + band_orows, cap_bytes, out_mult, cell_size, incellhd.cols, &worst_block_rows); if (tile_blocks == 0) { @@ -1019,7 +1019,7 @@ int main(int argc, char **argv) int obr1 = obr0 + band_orows; n_bands++; - int nb = fg_num_blocks(g_fg_boundary); + int nb = fg_num_blocks(band_grid); int n_tiles = (nb + tile_blocks - 1) / tile_blocks; if (n_tiles > max_tiles) max_tiles = n_tiles; @@ -1035,19 +1035,19 @@ int main(int argc, char **argv) * block is the full-width fast path. */ for (int tb = 0; tb < nb; tb += tile_blocks) { int te = tb + tile_blocks < nb ? tb + tile_blocks : nb; - int obc0 = fg_block_start(g_fg_boundary, tb); - int obc1 = fg_block_start(g_fg_boundary, te); + int obc0 = fg_block_start(band_grid, tb); + int obc1 = fg_block_start(band_grid, te); /* Fill spans come from the grid. The strip is full input width * because the raster API reads whole rows, so columns are not * cropped. */ - fg_span(g_fg_boundary, obr0, obr1, obc0, obc1, &imin, &imax); + fg_span(band_grid, obr0, obr1, obc0, obc1, &imin, &imax); int strip_rows = imax - imin + 1; /* Serial strip load, since a single fd makes get_row unsafe to * share. An empty tile with strip_rows at or below zero projects * outside the input and is not read, its cells become NULL through - * interpolate_strip's out-of-map path, and the window is + * strip_nearest's out-of-map path, and the window is * invalidated so the next band re-reads in full. */ void *strip = NULL; if (strip_rows > 0) { @@ -1233,8 +1233,8 @@ int main(int argc, char **argv) t_write += rproj_wtime() - tw; } G_free(y_center); - if (g_fg_boundary) - fg_free(g_fg_boundary); + if (band_grid) + fg_free(band_grid); /* Single free site for the rolling window. Normal completion and both * fallback_done bails converge here, so one free covers every path. win is * NULL when a bail fired before any band allocated it. */ diff --git a/raster/r.proj/r.proj.h b/raster/r.proj/r.proj.h index 76dc9ff00d3..3a142ee5f2e 100644 --- a/raster/r.proj/r.proj.h +++ b/raster/r.proj/r.proj.h @@ -46,7 +46,7 @@ enum OutputFormat { PLAIN, SHELL, JSON }; struct pole_set { int n; /* active poles, 0 to 2 */ double ox[2], oy[2]; /* pole coordinates in the output CRS */ - double ri[2]; /* pole input row index */ + double pole_row[2]; /* pole input row index */ }; /* Footprint grid of input row spans for the output map, built in footprint.c. From fb6ee011c47337d58a41b23764db224b1242ee8f Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Sun, 2 Aug 2026 23:56:47 -0700 Subject: [PATCH 28/39] r.proj: shorten and clarify the comments Shorten the longer comments to plain descriptions of what the code does and drop the internal jargon. No code changes. --- lib/proj/do_proj.c | 7 ++- raster/r.proj/interp_strip.c | 24 ++++----- raster/r.proj/main.c | 95 +++++++++++++----------------------- raster/r.proj/r.proj.h | 11 ++--- 4 files changed, 51 insertions(+), 86 deletions(-) diff --git a/lib/proj/do_proj.c b/lib/proj/do_proj.c index 795c6a92f7d..e3773007ecf 100644 --- a/lib/proj/do_proj.c +++ b/lib/proj/do_proj.c @@ -1423,7 +1423,7 @@ int pj_do_transform(int count, double *x, double *y, double *h, * into a new private context. Release it with GPJ_free_transform_clone(). * * Safe to call concurrently from multiple threads with the same \p src, - * provided \p src is not modified during the calls: each call clones into its + * provided \p src is not modified during the calls. Each call clones into its * own new context and touches no shared mutable state. * * \param src source transform (as set up by GPJ_init_transform()) @@ -1433,9 +1433,8 @@ void GPJ_clone_transform(const struct pj_info *src, struct gpj_transform_clone *clone) { clone->ctx = proj_context_create(); - /* r.proj calls this in each worker thread, so a fatal here ends the whole - * process from inside the parallel region. That is intended: a clone - * failure leaves the thread with no usable transform. */ + /* A failed context leaves the thread with no usable transform, so this + * aborts the run. */ if (clone->ctx == NULL) G_fatal_error(_("proj_context_create() failed for a per-thread " "transform clone")); diff --git a/raster/r.proj/interp_strip.c b/raster/r.proj/interp_strip.c index c1a18899286..193f8258814 100644 --- a/raster/r.proj/interp_strip.c +++ b/raster/r.proj/interp_strip.c @@ -1,9 +1,7 @@ /* - * interp_strip.c - strip-based interpolation kernels for the banded r.proj - * compute path. These mirror the cache-based kernels (bilinear.c, cubic.c, - * lanczos.c and their _f variants) but read an in-RAM FCELL band strip - * holding input rows [imin, imax] instead of the readcell block cache. - * Nearest is handled by interpolate_strip() in main.c and is not duplicated. + * interp_strip.c - strip versions of the resampling methods. They read the + * input rows from a strip held in memory as floats, instead of the block + * cache. Nearest is handled by strip_nearest() in main.c. */ #include @@ -12,14 +10,9 @@ #include #include "r.proj.h" -/* Read one FCELL from the band strip. The strip holds full-width input rows - * [imin, imax] contiguously; input row r maps to strip row (r - imin), the same - * addressing as interpolate_strip(). Every read is guarded by the same - * under-size tripwire as interpolate_strip: a stencil row inside the input map - * but outside the loaded strip means a sizing/indexing bug, so fail loudly - * rather than read out of bounds. Each kernel runs its full-map bounds check - * first (setting NULL for out-of-map stencils), so this tripwire only ever - * fires on a bug. */ +/* Read one value from the strip. Input row r maps to strip row (r - imin). A + * row inside the input map but outside the loaded strip is a bug, so fail + * rather than read out of bounds. */ static inline FCELL strip_val(const void *strip, int r, int c, int imin, int imax, int cols) { @@ -41,8 +34,9 @@ void strip_bilinear(void *strip, void *obufptr, int cell_type, double col_idx, row = (int)floor(row_idx - 0.5); col = (int)floor(col_idx - 0.5); - /* Full-map bounds check runs before any strip read: an out-of-map stencil - * sets NULL and returns, so strip_val is never reached out of range. */ + /* Full-map bounds check runs before any strip read. A sample outside the + * input map is set to NULL and returned, so strip_val is never asked for a + * row outside the strip. */ if (row < 0 || row + 1 >= incellhd->rows || col < 0 || col + 1 >= incellhd->cols) { Rast_set_null_value(obufptr, 1, cell_type); diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index 044bbe80cbc..d9b0934ad57 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -115,11 +115,8 @@ static void quantize_cell_row(void *row, int cols, int cell_type) } } -/* Nearest-neighbor read from an in-RAM strip holding input rows [imin, imax]. - * The col_idx and row_idx values are full-map indices and the strip is - * addressed relative to imin. A sample that lands inside the input map but - * outside the loaded strip means the band was under-sized, which the guard - * below catches. */ +/* Nearest read from the in-RAM input strip covering rows imin to imax. Indices + * are full-map and addressed relative to imin. */ static void strip_nearest(void *strip, void *obufptr, int cell_type, double col_idx, double row_idx, struct Cell_head *incellhd, int imin, int imax) @@ -134,9 +131,8 @@ static void strip_nearest(void *strip, void *obufptr, int cell_type, return; } - /* The band footprint was under-sized when a needed input row lies inside - * the input map but outside the loaded strip, so it fails loudly rather - * than emit a wrong NULL. */ + /* Fail loudly when a needed input row is inside the map but outside the + * loaded strip. */ if (r < imin || r > imax) G_fatal_error(_("Band strip under-sized: input row %d outside loaded " "range [%d, %d] at column %d"), @@ -148,22 +144,17 @@ static void strip_nearest(void *strip, void *obufptr, int cell_type, memcpy(obufptr, src, cell_size); } -/* Strip kernels in the same order as menu[], so slot i is the strip counterpart - * of menu[i].method. Slot 0 is nearest above and slots 1 to 6 come from - * interp_strip.c. */ +/* Strip kernels in menu[] order, so slot i matches menu[i].method. */ static const strip_func strip_kernels[] = { strip_nearest, strip_bilinear, strip_cubic, strip_lanczos, strip_bilinear_f, strip_cubic_f, strip_lanczos_f}; -/* Footprint grid used for comparison and the counters printed at the end. */ +/* Grid that sizes band heights and column tiles. */ static struct footprint_grid *band_grid = NULL; -/* Serial tile-cache fallback for the oblique and large-halo corner. When even - * one output row's full-width strip busts the cap, this finishes the run from - * row obr0 with the classic readcell cache and CVAL kernels, exactly as serial - * r.proj does. It stays serial because get_block mutates shared cache state, - * and since the banded path already wrote the earlier rows the result matches - * a pure serial run. */ +/* Serial tile-cache path for output rows whose input footprint is too tall to + * band. Finishes the run from row obr0 with the readcell cache so it matches + * serial r.proj. */ static void fallback_serial_cache(int fdi, int fdo, int cell_type, int method, const struct pj_info *oproj, const struct pj_info *iproj, @@ -203,9 +194,8 @@ fallback_serial_cache(int fdi, int fdo, int cell_type, int method, G_free(obuffer); } -/* Write the deferred band, if any, in order and release it. Shared by the last - * band and the fallback bails so every path writes the deferred band the same - * way. */ +/* Write the previous band's output rows that were held for the overlapped + * write, then free the buffer. Returns when nothing is held. */ static void flush_pending_band(int fdo, int cell_type, int cols, int cell_size, void **pending, int r0, int r1) { @@ -220,9 +210,7 @@ static void flush_pending_band(int fdo, int cell_type, int cols, int cell_size, *pending = NULL; } -/* Thread count for compute and write overlap, where nprocs above zero - * overrides OMP_NUM_THREADS. Set before the fit search so the write overlap's - * two reserved output buffers match the band sizing. */ +/* Return the compute thread count, with nprocs overriding OMP_NUM_THREADS. */ static int compute_nprocs(struct Option *nprocs) { return G_set_omp_num_threads(nprocs); @@ -571,7 +559,7 @@ int main(int argc, char **argv) if (G_verbose() > G_verbose_std()) pj_print_proj_params(&iproj, &oproj); - /* this call causes r.proj to read the entire map into memory */ + /* Read the input map's rows, columns, resolution, and bounds. */ Rast_get_cellhd(inmap->answer, setname, &incellhd); if (G_projection() == PROJECTION_XY) @@ -815,9 +803,8 @@ int main(int argc, char **argv) G_message(_("NS-res: %f"), outcellhd.ns_res); G_message(" "); - /* Open the input map in the input env. Banding loads only per-band input - * strips rather than the whole map, so fdi stays open across the band - * loop. */ + /* Open the input map in the input env. fdi stays open across the band loop + * because each band reads only its own input strip. */ G_switch_env(); Rast_set_input_window(&incellhd); fdi = Rast_open_old(inmap->answer, setname); @@ -826,15 +813,10 @@ int main(int argc, char **argv) cell_type = FCELL_TYPE; cell_size = Rast_cell_size(cell_type); - /* The read-thread count is decided here in the input env so the mask guard - * checks the source mapset's mask. Rast_disable_omp_on_mask returns 1 and - * forces serial under a mask or without OpenMP, and leaves the count - * untouched otherwise (lib/raster/mask_info.c lines 226-231). When - * read_nprocs is above one, each thread opens its own fresh fd and fdi - * serves only the serial fallback. Concurrent fds on the same map across - * locations follow the r.neighbors in_fd[] precedent, where each fd carries - * its own cur_row, data, and data_fd. */ - /* Runs before the fit search. See compute_nprocs(). */ + /* Pick the read-thread count in the input env so the mask guard checks the + * source mapset's mask. Rast_disable_omp_on_mask forces serial under a mask + * or without OpenMP. Above one thread each thread opens its own fd and fdi + * serves only the serial fallback. */ int want_nprocs = compute_nprocs(nprocs); int read_nprocs = Rast_disable_omp_on_mask(want_nprocs); int *fd_read = NULL; @@ -869,19 +851,16 @@ int main(int argc, char **argv) * memory by the cap rather than the whole input map. */ double cap_mb = atof(memory->answer); size_t cap_bytes = (size_t)(cap_mb * 1024.0 * 1024.0); - /* Under write_overlap the overlapped writes run inside the compute region, - * so their time falls in t_compute. t_write then covers only the - * non-overlapped writes, which are the last band's flush and every band at - * one thread. The fallback bail flushes are untimed. */ + /* Output write time that overlap the compute time are counted in the + * compute time. The write time only counts the writes that run on their + * own. The fallback path is not timed. */ double t_size = 0.0, t_fill = 0.0, t_compute = 0.0, t_write = 0.0; int n_bands = 0; int max_tiles = 1; /* most column tiles used by any single band */ - /* Output-row center northings from the serial recurrence, starting at north - * minus ns_res/2 and subtracting ns_res per row. The direct form differs by - * up to one ULP when ns_res is not exactly representable, so the recurrence - * is kept to stay bitwise identical to serial. The fill loop and the sizing - * walk share these values. */ + /* Holds the north-to-south coordinate of the center of each output row. It + * starts at the top edge and steps down one cell per row. The step uses + * repeated subtraction to match serial r.proj exactly. */ double *y_center = G_malloc((size_t)outcellhd.rows * sizeof(double)); { double yc = outcellhd.north - (outcellhd.ns_res / 2); @@ -891,16 +870,11 @@ int main(int argc, char **argv) } } - /* Pole footprint fix. A tile that projects onto a geographic pole has an - * input-row extremum the perimeter walk misses. This happens when the pole - * lies inside the input map, and also when the pole is outside the input's - * latitude coverage but still projects into the output frame, as with a - * pole-centered frame reading an input truncated below the pole, where the - * highest reachable input latitude is the input's own edge row. So it - * projects both poles for lat/lon input and folds in the pole's input row - * clamped to [0, rows-1]. The point-in-rect test keeps this a no-op for - * frames that image no pole, and a transform failure or non-finite result - * skips the pole and leaves the under-size guard as the backstop. */ + /* For a lat/lon input, project the north and south poles into the output + * and record each pole's input row, clamped to the map. A pole is the + * highest or lowest latitude, which the column samples can step over, so + * keeping its row makes sure the loaded strip reaches it. Does nothing when + * no pole lands inside the output map. */ struct pole_set poles; poles.n = 0; @@ -942,11 +916,10 @@ int main(int argc, char **argv) unsigned char *win = NULL; size_t win_cap = 0; int win_imin = 0, win_imax = -1; - /* Output double-buffer predicate. The compute region runs want_nprocs - * threads rather than the masked read_nprocs, so overlap needs more than - * one compute thread. out_mult reserves two output bands in the fit search - * on the same flag the writer uses, so the budget and the writer stay in - * step. */ + /* Turn on output double buffering when more than one compute thread runs, + * so one thread can write the previous band while the next one computes. + * out_mult then reserves two output bands so the memory budget matches the + * writer. */ int write_overlap = want_nprocs > 1; int out_mult = write_overlap ? 2 : 1; /* Previous band's output buffer, written by one thread while the next band diff --git a/raster/r.proj/r.proj.h b/raster/r.proj/r.proj.h index 3a142ee5f2e..c1c359d9d46 100644 --- a/raster/r.proj/r.proj.h +++ b/raster/r.proj/r.proj.h @@ -27,9 +27,8 @@ struct cache { typedef void (*func)(struct cache *, void *, int, double, double, struct Cell_head *); -/* Strip-based interpolation kernels (interp_strip.c) for the banded compute - * path read an in-RAM FCELL strip holding input rows [imin, imax] instead of - * the readcell block cache, so they take imin/imax in place of struct cache. */ +/* Strip interpolation kernels (interp_strip.c) read the input rows imin to imax + * from a strip held in memory, and take imin/imax in place of struct cache. */ typedef void (*strip_func)(void *, void *, int, double, double, struct Cell_head *, int, int); @@ -41,8 +40,8 @@ struct menu { enum OutputFormat { PLAIN, SHELL, JSON }; -/* Geographic poles that fall inside the input map, folded into the input row - * span of the tile that contains them. Empty when no pole is in frame. */ +/* Geographic poles that land inside the output map, each stored as its output + * position and its input row. Empty when no pole lands inside. */ struct pole_set { int n; /* active poles, 0 to 2 */ double ox[2], oy[2]; /* pole coordinates in the output CRS */ @@ -102,7 +101,7 @@ extern void p_lanczos(struct cache *, void *, int, double, double, extern void p_lanczos_f(struct cache *, void *, int, double, double, struct Cell_head *); -/* interp_strip.c - strip variants for the banded compute path */ +/* interp_strip.c - strip versions of the resampling methods */ extern void strip_bilinear(void *, void *, int, double, double, struct Cell_head *, int, int); extern void strip_cubic(void *, void *, int, double, double, struct Cell_head *, From 9b209052c77b8b823d1549b343606b32249d9b66 Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Tue, 4 Aug 2026 11:51:43 -0700 Subject: [PATCH 29/39] r.proj: co-size band heights and tile widths from the footprint grid The band loop co-sizes each band's height and tile width from the footprint grid. It grows the height while a full-width band fits the memory cap and turns to whole column tiles only when even one full-width row does not fit, so an output row whose columns project across a wide span of input rows is read in tall bands instead of one row at a time. The output values are identical to the serial result. --- raster/r.proj/footprint.c | 112 ++++++++++++++++++++------------------ raster/r.proj/main.c | 94 ++++++++++++++------------------ raster/r.proj/r.proj.h | 10 ++-- 3 files changed, 105 insertions(+), 111 deletions(-) diff --git a/raster/r.proj/footprint.c b/raster/r.proj/footprint.c index 0b79b0347a5..f8c77606032 100644 --- a/raster/r.proj/footprint.c +++ b/raster/r.proj/footprint.c @@ -187,51 +187,6 @@ void fg_span(const struct footprint_grid *g, int obr0, int obr1, int obc0, *imax = hi; } -/* Find the tallest band at obr0 whose strip and output still fit the cap, and - * never return less than one row. */ -int fg_band_height(const struct footprint_grid *g, int obr0, size_t cap_bytes, - int out_mult, int cell_size, int in_cols) -{ - double rmin = DBL_MAX, rmax = -DBL_MAX; - int max_h = g->grows - obr0, accepted = 1, h, b; - - for (h = 0; h < max_h; h++) { - int r = obr0 + h, strip_rows; - size_t strip_bytes, out_bytes; - - for (b = 0; b < g->nb; b++) { - const struct fg_cell *cell = &g->cell[(size_t)r * g->nb + b]; - - if (cell->rmax < cell->rmin) - continue; - if (cell->rmin < rmin) - rmin = cell->rmin; - if (cell->rmax > rmax) - rmax = cell->rmax; - } - if (rmax < rmin) { - strip_rows = 0; - } - else { - int lo = (int)floor(rmin) - 2; - int hi = (int)floor(rmax) + 2; - - if (lo < 0) - lo = 0; - if (hi > g->irows - 1) - hi = g->irows - 1; - strip_rows = hi - lo + 1; - } - strip_bytes = - strip_rows > 0 ? (size_t)strip_rows * in_cols * cell_size : 0; - out_bytes = (size_t)(h + 1) * g->ocols * cell_size; - if (!(strip_bytes + out_mult * out_bytes <= cap_bytes)) - break; - accepted = h + 1; - } - return accepted; -} - /* Number of column blocks in the grid. */ int fg_num_blocks(const struct footprint_grid *g) { @@ -262,17 +217,15 @@ static int worst_ktile_rows(const struct footprint_grid *g, int obr0, int obr1, return worst; } -/* Widest tile in whole blocks whose worst strip and the output still fit the - * cap, or zero when even one block per tile busts. Reports the worst single - * block strip for the caller message. */ -int fg_tile_blocks(const struct footprint_grid *g, int obr0, int obr1, - size_t cap_bytes, int out_mult, int cell_size, int in_cols, - int *worst_block_rows) +/* Widest tile in whole blocks whose worst strip and the output fit the cap, or + * zero when even one block per tile busts. */ +static int tile_blocks_for_band(const struct footprint_grid *g, int obr0, + int obr1, size_t cap_bytes, int out_mult, + int cell_size, int in_cols) { size_t out_bytes = (size_t)(obr1 - obr0) * g->ocols * cell_size; int k; - *worst_block_rows = worst_ktile_rows(g, obr0, obr1, 1); if (out_mult * out_bytes > cap_bytes) return 0; for (k = g->nb; k >= 1; k--) { @@ -286,6 +239,61 @@ int fg_tile_blocks(const struct footprint_grid *g, int obr0, int obr1, return 0; } +/* Grows the band height by doubling, preferring full-width bands and tiling + * only when even one full-width row busts the cap, and takes the last fitting + * height with its widest tile. Reports the finest tile strip the fallback + * message needs and returns zero when even one tiled row busts. */ +int fg_band_geometry(const struct footprint_grid *g, int obr0, size_t cap_bytes, + int out_mult, int cell_size, int in_cols, + int *tile_blocks_out, int *worst_block_rows) +{ + int remaining = g->grows - obr0; + int best_h = 0, best_k = 0, h_cand; + + *worst_block_rows = worst_ktile_rows(g, obr0, obr0 + 1, 1); + + /* Prefer full-width bands, growing the height while the whole row still + * fits the cap as a single tile. */ + for (h_cand = 1;; h_cand *= 2) { + int h = h_cand < remaining ? h_cand : remaining; + int worst = worst_ktile_rows(g, obr0, obr0 + h, g->nb); + size_t strip_bytes = + worst > 0 ? (size_t)worst * in_cols * cell_size : 0; + size_t out_bytes = (size_t)h * g->ocols * cell_size; + + if (strip_bytes + out_mult * out_bytes > cap_bytes) + break; + best_h = h; + if (h == remaining) + break; + } + if (best_h > 0) { + *tile_blocks_out = g->nb; + return best_h; + } + + /* One full-width row busts the cap, so grow while the exhaustive scan finds + * any fitting whole-block tile. */ + for (h_cand = 1;; h_cand *= 2) { + int h = h_cand < remaining ? h_cand : remaining; + int k = tile_blocks_for_band(g, obr0, obr0 + h, cap_bytes, out_mult, + cell_size, in_cols); + + if (k == 0) + break; + best_h = h; + best_k = k; + if (h == remaining) + break; + } + if (best_h == 0) { + *tile_blocks_out = 0; + return 0; + } + *tile_blocks_out = best_k; + return best_h; +} + /* Widens every non-empty cell by the sampling margin. */ void fg_apply_sampling_margin(struct footprint_grid *g) { diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index d9b0934ad57..883cc9d70d7 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -929,64 +929,52 @@ int main(int argc, char **argv) int pending_r0 = 0, pending_r1 = 0; int obr0 = 0; while (obr0 < outcellhd.rows) { - /* Size this band. Take the tallest full-width band that fits, and when - * even one full-width row does not fit split it into whole column - * blocks and take the widest tile that fits. */ + /* Co-size this band's height and tile width from the grid. A zero + * height means even one row busts the cap so the run finishes from obr0 + * on the serial tile-cache path. */ double ts = rproj_wtime(); int imin = 0, imax = -1; + int tile_blocks = 0, worst_block_rows = 0; int band_orows = force_tilecache - ? 1 - : fg_band_height(band_grid, obr0, cap_bytes, out_mult, - cell_size, incellhd.cols); - int tile_blocks = fg_num_blocks(band_grid); - if (band_orows == 1) { - int worst_block_rows = 0; - - tile_blocks = - force_tilecache - ? 0 - : fg_tile_blocks(band_grid, obr0, obr0 + band_orows, - cap_bytes, out_mult, cell_size, - incellhd.cols, &worst_block_rows); - if (tile_blocks == 0) { - /* Even the finest tiling busts the cap, so finish from obr0 on - * the serial tile-cache path. */ - if (force_tilecache) { - G_warning( - _("R_PROJ_FORCE_TILECACHE is set: taking the serial " - "tile-cache path for all output rows (testing " - "override).")); - } - else { - size_t out1 = (size_t)outcellhd.cols * cell_size; - size_t strip_bytes = worst_block_rows > 0 - ? (size_t)worst_block_rows * - incellhd.cols * cell_size - : 0; - int needed_mb = (int)ceil((double)(strip_bytes + out1) / - (1024.0 * 1024.0)) + - 1; - G_warning( - _("Memory cap (%.1f MB) is below what one output row " - "needs (input footprint %d rows, %.1f MB). Falling " - "back to the serial tile-cache path for output rows " - "%d-%d; this path is slower. Raise memory= to at " - "least %d MB to use the parallel path."), - cap_mb, worst_block_rows, - (double)(strip_bytes + out1) / (1024.0 * 1024.0), obr0, - outcellhd.rows - 1, needed_mb); - } - /* Flush the deferred band before the fallback writes from obr0 - * in order. */ - flush_pending_band(fdo, cell_type, outcellhd.cols, cell_size, - &pending_out, pending_r0, pending_r1); - fallback_serial_cache(fdi, fdo, cell_type, method, &oproj, - &iproj, &tproj, &incellhd, &outcellhd, - y_center, obr0, memory->answer); - used_fallback = 1; - goto fallback_done; + ? 0 + : fg_band_geometry(band_grid, obr0, cap_bytes, out_mult, + cell_size, incellhd.cols, &tile_blocks, + &worst_block_rows); + if (band_orows == 0) { + if (force_tilecache) { + G_warning(_("R_PROJ_FORCE_TILECACHE is set: taking the serial " + "tile-cache path for all output rows (testing " + "override).")); + } + else { + size_t out1 = (size_t)outcellhd.cols * cell_size; + size_t strip_bytes = + worst_block_rows > 0 + ? (size_t)worst_block_rows * incellhd.cols * cell_size + : 0; + int needed_mb = (int)ceil((double)(strip_bytes + out1) / + (1024.0 * 1024.0)) + + 1; + G_warning( + _("Memory cap (%.1f MB) is below what one output row " + "needs (input footprint %d rows, %.1f MB). Falling " + "back to the serial tile-cache path for output rows " + "%d-%d; this path is slower. Raise memory= to at " + "least %d MB to use the parallel path."), + cap_mb, worst_block_rows, + (double)(strip_bytes + out1) / (1024.0 * 1024.0), obr0, + outcellhd.rows - 1, needed_mb); } + /* Flush the deferred band before the fallback writes from obr0 in + * order. */ + flush_pending_band(fdo, cell_type, outcellhd.cols, cell_size, + &pending_out, pending_r0, pending_r1); + fallback_serial_cache(fdi, fdo, cell_type, method, &oproj, &iproj, + &tproj, &incellhd, &outcellhd, y_center, obr0, + memory->answer); + used_fallback = 1; + goto fallback_done; } t_size += rproj_wtime() - ts; diff --git a/raster/r.proj/r.proj.h b/raster/r.proj/r.proj.h index c1c359d9d46..5bd766a1b76 100644 --- a/raster/r.proj/r.proj.h +++ b/raster/r.proj/r.proj.h @@ -58,14 +58,12 @@ fg_build(const struct Cell_head *ohd, const struct Cell_head *ihd, const struct pole_set *poles); extern void fg_span(const struct footprint_grid *g, int obr0, int obr1, int obc0, int obc1, int *imin, int *imax); -extern int fg_band_height(const struct footprint_grid *g, int obr0, - size_t cap_bytes, int out_mult, int cell_size, - int in_cols); extern int fg_num_blocks(const struct footprint_grid *g); extern int fg_block_start(const struct footprint_grid *g, int b); -extern int fg_tile_blocks(const struct footprint_grid *g, int obr0, int obr1, - size_t cap_bytes, int out_mult, int cell_size, - int in_cols, int *worst_block_rows); +extern int fg_band_geometry(const struct footprint_grid *g, int obr0, + size_t cap_bytes, int out_mult, int cell_size, + int in_cols, int *tile_blocks_out, + int *worst_block_rows); extern void fg_apply_sampling_margin(struct footprint_grid *g); extern void fg_free(struct footprint_grid *g); From a32fa3aa0057f9896dc2476bf3ac22349cae2f99 Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Wed, 5 Aug 2026 23:27:09 -0700 Subject: [PATCH 30/39] r.proj: shorten the test and benchmark comments Shortens the docstrings and comments in the tests and the benchmark script. --- raster/r.proj/benchmark/benchmark_r_proj.py | 23 ++++----- raster/r.proj/tests/conftest.py | 24 +--------- raster/r.proj/tests/r_proj_parallel_test.py | 52 ++++++--------------- 3 files changed, 23 insertions(+), 76 deletions(-) diff --git a/raster/r.proj/benchmark/benchmark_r_proj.py b/raster/r.proj/benchmark/benchmark_r_proj.py index acbd77abb6e..88cf48468ae 100644 --- a/raster/r.proj/benchmark/benchmark_r_proj.py +++ b/raster/r.proj/benchmark/benchmark_r_proj.py @@ -1,14 +1,9 @@ -"""Benchmarking of r.proj thread scaling -raster (2D) - -This follows the r.param.scale benchmark structure, sweeping raster size at a -fixed memory and then memory at a fixed raster size, and plotting the time, -speedup, and efficiency metrics with grass.benchmark. r.proj sweeps its compute -thread count through the nprocs= option. Each cell generates a source raster in -an EPSG:4326 project and reprojects it into EPSG:3857 in a temporary database, -so the script is self-contained. Run it with -grass --exec python benchmark_r_proj.py or from any GRASS session. -""" +"""This is a benchmark script for r.proj thread scaling with grass.benchmark. + +This script sweeps through a raster size at a fixed memory setting, and then +memory at a fixed size. It then plots time, speedup, and efficiency. Creates +its own source raster and projects, so it runs standalone with +grass --exec python benchmark_r_proj.py.""" import os import tempfile @@ -104,10 +99,8 @@ def benchmark(gisdbase, size, memory, label, results): def generate_input(gisdbase, size): - """Generate the size by size source raster in the EPSG:4326 project, - mirroring the r.param.scale benchmark by trying r.surf.fractal and falling - back to r.random.surface when fractal is unavailable, for example in a build - without FFTW.""" + """Generate the source raster in the EPSG:4326 project. Uses + r.surf.fractal, or r.random.surface when FFTW is unavailable.""" with gs.setup.init( os.path.join(gisdbase, SRC_PROJECT), env=os.environ.copy() ) as session: diff --git a/raster/r.proj/tests/conftest.py b/raster/r.proj/tests/conftest.py index cdd5800dc82..db5d8dfb930 100644 --- a/raster/r.proj/tests/conftest.py +++ b/raster/r.proj/tests/conftest.py @@ -1,26 +1,6 @@ -"""Fixtures for the r.proj parallel-correctness pytest. +"""This is a source project with two small rasters and two destination sessions.""" -Builds one GISDBASE holding an EPSG:4326 source project with two small -generated input rasters, plus EPSG:3857 and EPSG:3413 (north polar -stereographic) destination projects. r.proj reprojects from the source -into the active destination session; the tests compare the module's own -serial and parallel runs. - -The input is integer CELL with values well below 2^24 -(row()*100 + col() + (row()*row()+col()*col())%13, max ~5058), so it survives -a float32 round-trip losslessly. This is deliberate: the forced tile-cache -path reads through the FCELL readcell cache while the banded nearest path -reads the native type, so only a float32-exact input keeps the forced-fallback -bitwise assert valid (a DCELL input would diverge by float32 quantization -alone). The (row()*row()+col()*col())%13 term gives the surface enough -curvature that bilinear and bicubic interpolation diverge past the reference -test's rel=1e-7 tolerance (a linear ramp, or a milder term, leaves their -statistics identical or within tolerance), which the method reference test -relies on to catch an _f-kernel dispatch swap. Values depend on -grid position only (no trig, no random), so they are bit-identical across -platforms and resolutions. Both rasters are 50x50 to stay well under the CI -time budget. -""" +# Copied from the test PR. Drop this file when that PR merges. import os diff --git a/raster/r.proj/tests/r_proj_parallel_test.py b/raster/r.proj/tests/r_proj_parallel_test.py index 02b4dc89630..0b49291d011 100644 --- a/raster/r.proj/tests/r_proj_parallel_test.py +++ b/raster/r.proj/tests/r_proj_parallel_test.py @@ -1,23 +1,6 @@ -"""Parallel-correctness tests for r.proj. - -r.proj takes a nprocs= option that sets the compute thread count, so each run -below passes nprocs= for that run, and the fallback test also sets -R_PROJ_FORCE_TILECACHE on its own env copy. Nothing shared is mutated, so the -serial and parallel runs of a test cannot leak thread or path state into each -other. - -The baseline is the module's own single-thread run at nprocs=1 rather than an -external serial binary. The question these tests answer is whether adding -threads, or taking the tile-cache fallback, changes the output of this same -binary. That comparison is exact and reproducible in CI where an external -oracle would not be. - -Nearest is asserted bitwise with an absolute diff max of zero. Bilinear is -asserted bitwise too because each output cell is interpolated independently in -a fixed operation order, so threading does not reorder its arithmetic. The -epsilon-1e-6 fallback from the proposal may be invoked only on an actual CI -reordering failure, naming the platform that showed it. -""" +"""Parallel correctness tests for r.proj. Each test compares the +module's own nprocs=1 run against a multithreaded run, and the fallback +test forces the tile cache path.""" import grass.script as gs @@ -36,10 +19,7 @@ def _env(session, **overrides): def _set_region_from_source(env, input_raster, method): - """Set the output region to r.proj's suggested bounds for the input. - - r.proj -g prints the whole region as space-separated key=value pairs on - one line, so split on whitespace first, then on '='.""" + """Set the output region to r.proj's suggested bounds for the input.""" text = gs.read_command( "r.proj", project=SRC_PROJECT, @@ -82,8 +62,7 @@ def _stats(env, raster): def _assert_bitwise_identical(env, a, b, diff): - """Assert a and b are bitwise identical: equal counts, equal null - pattern, and a zero-valued absolute difference over a non-empty map.""" + """Check a and b are bitwise identical and have the same null cells.""" gs.run_command( "r.mapcalc", expression=f"{diff} = abs({a} - {b})", overwrite=True, env=env ) @@ -97,11 +76,9 @@ def _assert_bitwise_identical(env, a, b, diff): def test_bilinear_parallel_matches_serial(session_3857): - """Bilinear: parallel output must equal the serial output bitwise. - - A dispatch-liveness guard runs first: bilinear must differ from nearest - on the same frame, so a silent fallback to nearest cannot make the - identity assert pass vacuously (the Bug A regression guard).""" + """Bilinear method needs to match serial bitwise. It first checks that + bilinear and nearest outputs differ, so a silent fallback to nearest + cannot happen.""" session = session_3857 base = _env(session) _set_region_from_source(base, INPUT_MID, "bilinear") @@ -123,8 +100,8 @@ def test_bilinear_parallel_matches_serial(session_3857): def test_nearest_memory_banding(session_3857): - """Nearest with a constrained memory cap (memory=5, OMP=4) must match the - default-memory serial run bitwise, exercising band sizing at a small cap.""" + """The nearest method at a small memory cap (memory=5, nprocs=4) has to + match the default memory serial run bitwise.""" session = session_3857 base = _env(session) _set_region_from_source(base, INPUT_MID, "nearest") @@ -135,8 +112,7 @@ def test_nearest_memory_banding(session_3857): def test_pole_nearest_parallel_matches_serial(session_pole): - """Nearest into a frame centered on the north pole: the warped access - pattern near the pole must still give bitwise-identical parallel output.""" + """Nearest at the north pole matches serial bitwise.""" session = session_pole base = _env(session) # Fixed 1200 km box centered on the pole (EPSG:3413 meters), 50x50. @@ -157,10 +133,8 @@ def test_pole_nearest_parallel_matches_serial(session_pole): def test_forced_fallback_matches_banded(session_3857): - """The forced serial tile-cache path must equal the banded parallel path - bitwise. R_PROJ_FORCE_TILECACHE=1 takes the readcell tile-cache route - (a different algorithm), so this is a cross-path check, not just a - thread-count one.""" + """Forcing the tile cache with R_PROJ_FORCE_TILECACHE=1 gives the same + output as the banded path.""" session = session_3857 base = _env(session) _set_region_from_source(base, INPUT_MID, "nearest") From c125e3389435e30127fbdba0e8f481a5847f9c36 Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Thu, 13 Aug 2026 17:53:47 -0700 Subject: [PATCH 31/39] r.proj: reload an undersized strip instead of aborting --- raster/r.proj/interp_strip.c | 104 ++++++++++++++++++----------------- raster/r.proj/main.c | 104 +++++++++++++++++++++++++++-------- raster/r.proj/r.proj.h | 42 ++++++++------ 3 files changed, 163 insertions(+), 87 deletions(-) diff --git a/raster/r.proj/interp_strip.c b/raster/r.proj/interp_strip.c index 193f8258814..65c39df9c01 100644 --- a/raster/r.proj/interp_strip.c +++ b/raster/r.proj/interp_strip.c @@ -10,22 +10,28 @@ #include #include "r.proj.h" -/* Read one value from the strip. Input row r maps to strip row (r - imin). A - * row inside the input map but outside the loaded strip is a bug, so fail - * rather than read out of bounds. */ -static inline FCELL strip_val(const void *strip, int r, int c, int imin, - int imax, int cols) +/* Reads one value from the strip. A row outside the loaded range records the + * needed row through need_lo/need_hi and returns a null, so the caller nulls + * the cell and the band reloads and recomputes. */ +static inline FCELL strip_val(const struct strip *s, int r, int c, int *need_lo, + int *need_hi) { - if (r < imin || r > imax) - G_fatal_error(_("Band strip under-sized: input row %d outside loaded " - "range [%d, %d] at column %d"), - r, imin, imax, c); - return ((const FCELL *)strip)[(size_t)(r - imin) * cols + c]; + if (r < s->imin || r > s->imax) { + FCELL null_val; + + if (r < s->imin && r < *need_lo) + *need_lo = r; + if (r > s->imax && r > *need_hi) + *need_hi = r; + Rast_set_f_null_value(&null_val, 1); + return null_val; + } + return ((const FCELL *)s->data)[(size_t)(r - s->imin) * s->cols + c]; } -void strip_bilinear(void *strip, void *obufptr, int cell_type, double col_idx, - double row_idx, struct Cell_head *incellhd, int imin, - int imax) +void strip_bilinear(const struct strip *s, void *obufptr, int cell_type, + double col_idx, double row_idx, struct Cell_head *incellhd, + int *need_lo, int *need_hi) { int row, col, i, j; FCELL t, u, result; @@ -36,7 +42,7 @@ void strip_bilinear(void *strip, void *obufptr, int cell_type, double col_idx, /* Full-map bounds check runs before any strip read. A sample outside the * input map is set to NULL and returned, so strip_val is never asked for a - * row outside the strip. */ + * row outside the map. */ if (row < 0 || row + 1 >= incellhd->rows || col < 0 || col + 1 >= incellhd->cols) { Rast_set_null_value(obufptr, 1, cell_type); @@ -45,8 +51,7 @@ void strip_bilinear(void *strip, void *obufptr, int cell_type, double col_idx, for (i = 0; i < 2; i++) for (j = 0; j < 2; j++) { - const FCELL cell = - strip_val(strip, row + i, col + j, imin, imax, incellhd->cols); + const FCELL cell = strip_val(s, row + i, col + j, need_lo, need_hi); if (Rast_is_f_null_value(&cell)) { Rast_set_null_value(obufptr, 1, cell_type); @@ -63,8 +68,9 @@ void strip_bilinear(void *strip, void *obufptr, int cell_type, double col_idx, Rast_set_f_value(obufptr, result, cell_type); } -void strip_cubic(void *strip, void *obufptr, int cell_type, double col_idx, - double row_idx, struct Cell_head *incellhd, int imin, int imax) +void strip_cubic(const struct strip *s, void *obufptr, int cell_type, + double col_idx, double row_idx, struct Cell_head *incellhd, + int *need_lo, int *need_hi) { int row, col, i, j; FCELL t, u, result; @@ -83,8 +89,8 @@ void strip_cubic(void *strip, void *obufptr, int cell_type, double col_idx, for (i = 0; i < 4; i++) for (j = 0; j < 4; j++) { - const FCELL cell = strip_val(strip, row - 1 + i, col - 1 + j, imin, - imax, incellhd->cols); + const FCELL cell = + strip_val(s, row - 1 + i, col - 1 + j, need_lo, need_hi); if (Rast_is_f_null_value(&cell)) { Rast_set_null_value(obufptr, 1, cell_type); @@ -107,9 +113,9 @@ void strip_cubic(void *strip, void *obufptr, int cell_type, double col_idx, Rast_set_f_value(obufptr, result, cell_type); } -void strip_lanczos(void *strip, void *obufptr, int cell_type, double col_idx, - double row_idx, struct Cell_head *incellhd, int imin, - int imax) +void strip_lanczos(const struct strip *s, void *obufptr, int cell_type, + double col_idx, double row_idx, struct Cell_head *incellhd, + int *need_lo, int *need_hi) { int row, col, i, j, k; double t, u; @@ -129,8 +135,8 @@ void strip_lanczos(void *strip, void *obufptr, int cell_type, double col_idx, k = 0; for (i = 0; i < 5; i++) { for (j = 0; j < 5; j++) { - const FCELL cell = strip_val(strip, row - 2 + i, col - 2 + j, imin, - imax, incellhd->cols); + const FCELL cell = + strip_val(s, row - 2 + i, col - 2 + j, need_lo, need_hi); if (Rast_is_f_null_value(&cell)) { Rast_set_null_value(obufptr, 1, cell_type); @@ -148,9 +154,9 @@ void strip_lanczos(void *strip, void *obufptr, int cell_type, double col_idx, Rast_set_f_value(obufptr, result, cell_type); } -void strip_bilinear_f(void *strip, void *obufptr, int cell_type, double col_idx, - double row_idx, struct Cell_head *incellhd, int imin, - int imax) +void strip_bilinear_f(const struct strip *s, void *obufptr, int cell_type, + double col_idx, double row_idx, + struct Cell_head *incellhd, int *need_lo, int *need_hi) { int row, col; FCELL cell; @@ -163,23 +169,23 @@ void strip_bilinear_f(void *strip, void *obufptr, int cell_type, double col_idx, return; } - cell = strip_val(strip, row, col, imin, imax, incellhd->cols); + cell = strip_val(s, row, col, need_lo, need_hi); /* if nearest is null, all the other interps will be null */ if (Rast_is_f_null_value(&cell)) { Rast_set_null_value(obufptr, 1, cell_type); return; } - strip_bilinear(strip, obufptr, cell_type, col_idx, row_idx, incellhd, imin, - imax); + strip_bilinear(s, obufptr, cell_type, col_idx, row_idx, incellhd, need_lo, + need_hi); /* fallback to nearest if bilinear is null */ if (Rast_is_f_null_value(obufptr)) Rast_set_f_value(obufptr, cell, cell_type); } -void strip_cubic_f(void *strip, void *obufptr, int cell_type, double col_idx, - double row_idx, struct Cell_head *incellhd, int imin, - int imax) +void strip_cubic_f(const struct strip *s, void *obufptr, int cell_type, + double col_idx, double row_idx, struct Cell_head *incellhd, + int *need_lo, int *need_hi) { int row, col; FCELL cell; @@ -192,28 +198,28 @@ void strip_cubic_f(void *strip, void *obufptr, int cell_type, double col_idx, return; } - cell = strip_val(strip, row, col, imin, imax, incellhd->cols); + cell = strip_val(s, row, col, need_lo, need_hi); /* if nearest is null, all the other interps will be null */ if (Rast_is_f_null_value(&cell)) { Rast_set_null_value(obufptr, 1, cell_type); return; } - strip_cubic(strip, obufptr, cell_type, col_idx, row_idx, incellhd, imin, - imax); + strip_cubic(s, obufptr, cell_type, col_idx, row_idx, incellhd, need_lo, + need_hi); /* fallback to bilinear if cubic is null */ if (Rast_is_f_null_value(obufptr)) { - strip_bilinear(strip, obufptr, cell_type, col_idx, row_idx, incellhd, - imin, imax); + strip_bilinear(s, obufptr, cell_type, col_idx, row_idx, incellhd, + need_lo, need_hi); /* fallback to nearest if bilinear is null */ if (Rast_is_f_null_value(obufptr)) Rast_set_f_value(obufptr, cell, cell_type); } } -void strip_lanczos_f(void *strip, void *obufptr, int cell_type, double col_idx, - double row_idx, struct Cell_head *incellhd, int imin, - int imax) +void strip_lanczos_f(const struct strip *s, void *obufptr, int cell_type, + double col_idx, double row_idx, struct Cell_head *incellhd, + int *need_lo, int *need_hi) { int row, col; FCELL cell; @@ -226,23 +232,23 @@ void strip_lanczos_f(void *strip, void *obufptr, int cell_type, double col_idx, return; } - cell = strip_val(strip, row, col, imin, imax, incellhd->cols); + cell = strip_val(s, row, col, need_lo, need_hi); /* if nearest is null, all the other interps will be null */ if (Rast_is_f_null_value(&cell)) { Rast_set_null_value(obufptr, 1, cell_type); return; } - strip_lanczos(strip, obufptr, cell_type, col_idx, row_idx, incellhd, imin, - imax); + strip_lanczos(s, obufptr, cell_type, col_idx, row_idx, incellhd, need_lo, + need_hi); /* fallback to bicubic if lanczos is null */ if (Rast_is_f_null_value(obufptr)) { - strip_cubic(strip, obufptr, cell_type, col_idx, row_idx, incellhd, imin, - imax); + strip_cubic(s, obufptr, cell_type, col_idx, row_idx, incellhd, need_lo, + need_hi); /* fallback to bilinear if cubic is null */ if (Rast_is_f_null_value(obufptr)) { - strip_bilinear(strip, obufptr, cell_type, col_idx, row_idx, - incellhd, imin, imax); + strip_bilinear(s, obufptr, cell_type, col_idx, row_idx, incellhd, + need_lo, need_hi); /* fallback to nearest if bilinear is null */ if (Rast_is_f_null_value(obufptr)) Rast_set_f_value(obufptr, cell, cell_type); diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index 883cc9d70d7..c76e3732e16 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -117,9 +117,10 @@ static void quantize_cell_row(void *row, int cols, int cell_type) /* Nearest read from the in-RAM input strip covering rows imin to imax. Indices * are full-map and addressed relative to imin. */ -static void strip_nearest(void *strip, void *obufptr, int cell_type, +static void strip_nearest(const struct strip *s, void *obufptr, int cell_type, double col_idx, double row_idx, - struct Cell_head *incellhd, int imin, int imax) + struct Cell_head *incellhd, int *need_lo, + int *need_hi) { int c = (int)floor(col_idx); int r = (int)floor(row_idx); @@ -131,16 +132,19 @@ static void strip_nearest(void *strip, void *obufptr, int cell_type, return; } - /* Fail loudly when a needed input row is inside the map but outside the - * loaded strip. */ - if (r < imin || r > imax) - G_fatal_error(_("Band strip under-sized: input row %d outside loaded " - "range [%d, %d] at column %d"), - r, imin, imax, c); + /* A row inside the map but outside the loaded strip records the needed row + * and nulls the cell, so the band reloads and recomputes. */ + if (r < s->imin || r > s->imax) { + if (r < s->imin && r < *need_lo) + *need_lo = r; + if (r > s->imax && r > *need_hi) + *need_hi = r; + Rast_set_null_value(obufptr, 1, cell_type); + return; + } - unsigned char *src = - (unsigned char *)strip + - (((size_t)(r - imin) * incellhd->cols + c) * cell_size); + unsigned char *src = (unsigned char *)s->data + + (((size_t)(r - s->imin) * s->cols + c) * cell_size); memcpy(obufptr, src, cell_size); } @@ -910,6 +914,11 @@ int main(int argc, char **argv) int used_fallback = 0; /* set when the serial tile-cache fallback runs */ int force_tilecache = getenv("R_PROJ_FORCE_TILECACHE") != NULL; + /* For tests only. FG_SHRINK_SPAN=N loads each band N rows too short at the + * top and bottom, so the reload path runs. Zero leaves the strip full + * size. */ + const char *shrink_env = getenv("FG_SHRINK_SPAN"); + int shrink_span = shrink_env ? atoi(shrink_env) : 0; /* Rolling input-strip window. win holds input rows [win_imin, win_imax]. * win_imax < win_imin marks it empty and forces a full read. win_cap is * its allocated byte size, and win is freed once at fallback_done. */ @@ -1003,6 +1012,14 @@ int main(int argc, char **argv) * because the raster API reads whole rows, so columns are not * cropped. */ fg_span(band_grid, obr0, obr1, obc0, obc1, &imin, &imax); + /* The test hook shortens the span so the reload path runs. */ + if (shrink_span > 0 && imax >= imin) { + imin += shrink_span; + imax -= shrink_span; + } + int reloaded = 0; /* set once the span is widened and reread */ + + reload_tile:; int strip_rows = imax - imin + 1; /* Serial strip load, since a single fd makes get_row unsafe to @@ -1095,6 +1112,11 @@ int main(int argc, char **argv) win_imax = -1; /* empty tile, nothing resident */ } + struct strip sd = {strip, imin, imax, incellhd.cols}; + /* Widest input rows a kernel wants below imin and above imax. The + * reduction leaves them at imin and imax when the strip covered + * every sample. */ + int need_lo = imin, need_hi = imax; double t1 = rproj_wtime(); /* One parallel region per tile. The omp for divides the band's * output rows among this region's threads. The directives are @@ -1108,18 +1130,23 @@ int main(int argc, char **argv) #pragma omp single nowait { /* One thread writes the previous band's rows in order while - * the rest compute this band. This is the first tile only, - * and pending_out is non-NULL only under write_overlap. */ - if (obc0 == 0 && pending_out) + * the rest compute this band, then frees it. Only the first + * tile has a pending band, and clearing it keeps a reload + * from writing the same rows twice. */ + if (obc0 == 0 && pending_out) { for (int wr = pending_r0; wr < pending_r1; wr++) Rast_put_row(fdo, (unsigned char *)pending_out + (size_t)(wr - pending_r0) * outcellhd.cols * cell_size, cell_type); + G_free(pending_out); + pending_out = NULL; + } } -#pragma omp for private(row, col) schedule(dynamic) +#pragma omp for private(row, col) schedule(dynamic) reduction(min : need_lo) \ + reduction(max : need_hi) for (row = obr0; row < obr1; row++) { void *out_row = (unsigned char *)band_out + @@ -1143,8 +1170,8 @@ int main(int argc, char **argv) (x1 - incellhd.west) / incellhd.ew_res; double r_idx = (incellhd.north - y1) / incellhd.ns_res; - interp(strip, obufptr, cell_type, c_idx, r_idx, - &incellhd, imin, imax); + interp(&sd, obufptr, cell_type, c_idx, r_idx, + &incellhd, &need_lo, &need_hi); } } } @@ -1154,15 +1181,48 @@ int main(int argc, char **argv) t_compute += rproj_wtime() - t1; /* strip aliases the persistent window buffer win, so it is not * freed per tile. win is freed once at fallback_done. */ + + /* Reload once and recompute if a kernel needed an input row the + * strip left out. */ + if (need_lo < imin || need_hi > imax) { + if (reloaded) + G_fatal_error(_("Band strip still short after a reload, " + "input rows [%d, %d] but [%d, %d] needed"), + imin, imax, need_lo, need_hi); + size_t widened = + (size_t)(need_hi - need_lo + 1) * incellhd.cols * cell_size; + size_t out_bytes = + (size_t)band_orows * outcellhd.cols * cell_size; + /* If the widened strip no longer fits the memory cap, finish + * from obr0 on the serial tile-cache path, the same path the + * band sizing falls back to. */ + if (widened + (size_t)out_mult * out_bytes > cap_bytes) { + G_free(band_out); + flush_pending_band(fdo, cell_type, outcellhd.cols, + cell_size, &pending_out, pending_r0, + pending_r1); + fallback_serial_cache(fdi, fdo, cell_type, method, &oproj, + &iproj, &tproj, &incellhd, &outcellhd, + y_center, obr0, memory->answer); + used_fallback = 1; + goto fallback_done; + } + G_verbose_message(_("Reloading band strip for output rows " + "%d-%d, input rows [%d, %d] widen to " + "[%d, %d]"), + obr0, obr1 - 1, imin, imax, need_lo, need_hi); + imin = need_lo; + imax = need_hi; + reloaded = 1; + goto reload_tile; + } } /* Defer this band so the next band's compute region writes it through - * the omp single above. The previous pending completed at this band's - * compute barrier, so free it now. Non-overlap bands write and free in - * order here. */ + * the omp single above. The previous pending band was written and freed + * there, so pending_out is already clear. Non-overlap bands write in + * order below. */ if (write_overlap) { - if (pending_out) - G_free(pending_out); pending_out = band_out; pending_r0 = obr0; pending_r1 = obr1; diff --git a/raster/r.proj/r.proj.h b/raster/r.proj/r.proj.h index 5bd766a1b76..cfef94560d6 100644 --- a/raster/r.proj/r.proj.h +++ b/raster/r.proj/r.proj.h @@ -27,10 +27,20 @@ struct cache { typedef void (*func)(struct cache *, void *, int, double, double, struct Cell_head *); -/* Strip interpolation kernels (interp_strip.c) read the input rows imin to imax - * from a strip held in memory, and take imin/imax in place of struct cache. */ -typedef void (*strip_func)(void *, void *, int, double, double, - struct Cell_head *, int, int); +/* One input strip resident in memory, addressed by full-map input row. Rows + * outside imin to imax are not loaded. A kernel that needs one records it + * through the need arguments so the band reloads and recomputes. */ +struct strip { + void *data; /* input rows imin to imax at full input width */ + int imin, imax; /* loaded input row range */ + int cols; /* input columns, the row stride */ +}; + +/* Strip interpolation kernels (interp_strip.c) read from an in-memory strip. + * The last two arguments carry the widest input rows a kernel wanted below imin + * and above imax, left unchanged when the strip covered every sample. */ +typedef void (*strip_func)(const struct strip *, void *, int, double, double, + struct Cell_head *, int *, int *); struct menu { func method; /* routine to interpolate new value */ @@ -100,18 +110,18 @@ extern void p_lanczos_f(struct cache *, void *, int, double, double, struct Cell_head *); /* interp_strip.c - strip versions of the resampling methods */ -extern void strip_bilinear(void *, void *, int, double, double, - struct Cell_head *, int, int); -extern void strip_cubic(void *, void *, int, double, double, struct Cell_head *, - int, int); -extern void strip_lanczos(void *, void *, int, double, double, - struct Cell_head *, int, int); -extern void strip_bilinear_f(void *, void *, int, double, double, - struct Cell_head *, int, int); -extern void strip_cubic_f(void *, void *, int, double, double, - struct Cell_head *, int, int); -extern void strip_lanczos_f(void *, void *, int, double, double, - struct Cell_head *, int, int); +extern void strip_bilinear(const struct strip *, void *, int, double, double, + struct Cell_head *, int *, int *); +extern void strip_cubic(const struct strip *, void *, int, double, double, + struct Cell_head *, int *, int *); +extern void strip_lanczos(const struct strip *, void *, int, double, double, + struct Cell_head *, int *, int *); +extern void strip_bilinear_f(const struct strip *, void *, int, double, double, + struct Cell_head *, int *, int *); +extern void strip_cubic_f(const struct strip *, void *, int, double, double, + struct Cell_head *, int *, int *); +extern void strip_lanczos_f(const struct strip *, void *, int, double, double, + struct Cell_head *, int *, int *); #define BKIDX(c, y, x) ((y) * (c)->stride + (x)) #define BKPTR(c, y, x) ((c)->grid[BKIDX((c), (y), (x))]) From 9ec2bdef19819d6e3092d658b2f1ce7f4dd8bcb2 Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Thu, 13 Aug 2026 17:54:59 -0700 Subject: [PATCH 32/39] r.proj: add a test for the strip reload --- raster/r.proj/tests/r_proj_parallel_test.py | 50 +++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/raster/r.proj/tests/r_proj_parallel_test.py b/raster/r.proj/tests/r_proj_parallel_test.py index 0b49291d011..7ae5dcd7df1 100644 --- a/raster/r.proj/tests/r_proj_parallel_test.py +++ b/raster/r.proj/tests/r_proj_parallel_test.py @@ -148,3 +148,53 @@ def test_forced_fallback_matches_banded(session_3857): ) _project(base, INPUT_MID, "banded", "nearest", nprocs=4) _assert_bitwise_identical(base, "fallback_tilecache", "banded", "fallback_diff") + + +def _project_capture(env, input_raster, output, method, **extra): + """Run r.proj and return the messages it writes to stderr.""" + env = dict(env, GRASS_VERBOSE="3") + proc = gs.start_command( + "r.proj", + project=SRC_PROJECT, + mapset="PERMANENT", + input=input_raster, + output=output, + method=method, + overwrite=True, + env=env, + stderr=gs.PIPE, + **extra, + ) + stderr = proc.communicate()[1] + if isinstance(stderr, bytes): + stderr = stderr.decode() + assert proc.returncode == 0, stderr + return stderr + + +def test_forced_reload_matches_serial(session_3857): + """FG_SHRINK_SPAN shrinks each band so the reload runs, and the reloaded run + still matches the nprocs=1 run bitwise.""" + session = session_3857 + base = _env(session) + _set_region_from_source(base, INPUT_MID, "nearest") + + _project(base, INPUT_MID, "reload_serial", "nearest", nprocs=1) + + shrunk = _project_capture( + _env(session, FG_SHRINK_SPAN=2), + INPUT_MID, + "reload_shrunk", + "nearest", + nprocs=4, + ) + # The shrunk strips leave out rows the projection needs, so r.proj reloads + # and logs it here. Without this marker the run never hit the reload path. + assert "Reloading band strip" in shrunk + + # The full-size strips already cover every needed row, so this run never + # reloads. That is what ties the marker above to the shrink. + unshrunk = _project_capture(base, INPUT_MID, "reload_unshrunk", "nearest", nprocs=4) + assert "Reloading band strip" not in unshrunk + + _assert_bitwise_identical(base, "reload_serial", "reload_shrunk", "reload_diff") From 6c99bbfb3c62b4aa56803ba51544770e2626b649 Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Thu, 13 Aug 2026 19:49:18 -0700 Subject: [PATCH 33/39] r.proj: move the footprint code behind a small fp interface --- raster/r.proj/footprint.c | 137 +++++++++++++++++++++++++------------- raster/r.proj/main.c | 53 +++------------ raster/r.proj/r.proj.h | 36 ++++------ 3 files changed, 115 insertions(+), 111 deletions(-) diff --git a/raster/r.proj/footprint.c b/raster/r.proj/footprint.c index f8c77606032..b873209afe5 100644 --- a/raster/r.proj/footprint.c +++ b/raster/r.proj/footprint.c @@ -13,29 +13,37 @@ #include "r.proj.h" -struct fg_cell { +/* Geographic poles that land inside the output map, each stored as its output + * position and its input row. Empty when no pole lands inside. */ +struct pole_set { + int n; /* active poles, 0 to 2 */ + double ox[2], oy[2]; /* pole coordinates in the output CRS */ + double pole_row[2]; /* pole input row index */ +}; + +struct fp_cell { double rmin, rmax; /* rmax below rmin marks an empty cell */ }; -struct footprint_grid { +struct footprint { int grows, nb; /* grid rows and column blocks */ int ocols; /* output columns */ int irows; /* input rows */ - struct fg_cell *cell; /* grows by nb cells in row major order */ + struct fp_cell *cell; /* grows by nb cells in row major order */ }; /* The samples can miss a curve between columns by a fraction of a row, so each * cell is widened by one row. */ -#define FG_SAMPLING_MARGIN 1.0 +#define FP_SAMPLING_MARGIN 1.0 /* Returns the first output column of block b. */ -static int block_c0(const struct footprint_grid *g, int b) +static int block_c0(const struct footprint *g, int b) { return (int)((long)b * g->ocols / g->nb); } /* Returns the block that contains output column c. */ -static int block_of_col(const struct footprint_grid *g, int c) +static int block_of_col(const struct footprint *g, int c) { int b; @@ -63,10 +71,9 @@ static int sample_ri(const struct Cell_head *ohd, const struct Cell_head *ihd, /* Widens cell (r, b) to include any pole whose output point falls inside the * cell rectangle. */ -static void fold_poles(const struct footprint_grid *g, - const struct Cell_head *ohd, +static void fold_poles(const struct footprint *g, const struct Cell_head *ohd, const struct pole_set *poles, int r, int b, - struct fg_cell *cell) + struct fp_cell *cell) { int c0 = block_c0(g, b), c1 = block_c0(g, b + 1), k; double x_lo = ohd->west + c0 * ohd->ew_res; @@ -87,22 +94,74 @@ static void fold_poles(const struct footprint_grid *g, } } -/* Builds the grid from block boundary samples. */ -struct footprint_grid * -fg_build(const struct Cell_head *ohd, const struct Cell_head *ihd, - const struct pj_info *oproj, const struct pj_info *iproj, - const struct pj_info *tproj, const double *y_center, - const struct pole_set *poles) +/* For a lat/lon input, projects the north and south poles into the output and + * records each pole's input row, clamped to the map. A pole is the highest or + * lowest latitude, which the column samples can step over, so keeping its row + * makes sure the loaded strip reaches it. Leaves the set empty when no pole + * lands inside the output map. */ +static void build_pole_set(const struct Cell_head *ihd, + const struct pj_info *oproj, + const struct pj_info *iproj, + const struct pj_info *tproj, struct pole_set *poles) { - struct footprint_grid *g = G_malloc(sizeof(*g)); + poles->n = 0; + if (ihd->proj != PROJECTION_LL) + return; + double polelat[2] = {90.0, -90.0}; + + for (int p = 0; p < 2; p++) { + double px = 0.0, py = polelat[p]; + + if (GPJ_transform(oproj, iproj, tproj, PJ_INV, &px, &py, NULL) < 0 || + !isfinite(px) || !isfinite(py)) + continue; + double ri = (ihd->north - polelat[p]) / ihd->ns_res; + if (ri < 0) + ri = 0; + else if (ri > ihd->rows - 1) + ri = ihd->rows - 1; + poles->ox[poles->n] = px; + poles->oy[poles->n] = py; + poles->pole_row[poles->n] = ri; + poles->n++; + } +} + +/* Widens every non-empty cell by the sampling margin. */ +static void apply_sampling_margin(struct footprint *g) +{ + size_t n = (size_t)g->grows * g->nb, i; + + for (i = 0; i < n; i++) { + struct fp_cell *cell = &g->cell[i]; + + if (cell->rmax >= cell->rmin) { + cell->rmin -= FP_SAMPLING_MARGIN; + cell->rmax += FP_SAMPLING_MARGIN; + } + } +} + +/* Builds the footprint from block boundary samples, folds in any poles, and + * applies the sampling margin. */ +struct footprint *fp_create(const struct Cell_head *ohd, + const struct Cell_head *ihd, + const struct pj_info *oproj, + const struct pj_info *iproj, + const struct pj_info *tproj, const double *y_center) +{ + struct footprint *g = G_malloc(sizeof(*g)); + struct pole_set poles; int r, b; double *bnd; + build_pole_set(ihd, oproj, iproj, tproj, &poles); + g->grows = ohd->rows; g->nb = ohd->cols < 32 ? ohd->cols : 32; g->ocols = ohd->cols; g->irows = ihd->rows; - g->cell = G_malloc((size_t)g->grows * g->nb * sizeof(struct fg_cell)); + g->cell = G_malloc((size_t)g->grows * g->nb * sizeof(struct fp_cell)); bnd = G_malloc((size_t)(g->nb + 1) * sizeof(double)); for (r = 0; r < g->grows; r++) { @@ -120,7 +179,7 @@ fg_build(const struct Cell_head *ohd, const struct Cell_head *ihd, bnd[k] = DBL_MAX; /* a failed sample is left out of the range */ } for (b = 0; b < g->nb; b++) { - struct fg_cell *cell = &g->cell[(size_t)r * g->nb + b]; + struct fp_cell *cell = &g->cell[(size_t)r * g->nb + b]; double lo = bnd[b] < bnd[b + 1] ? bnd[b] : bnd[b + 1]; double hi = bnd[b] > bnd[b + 1] ? bnd[b] : bnd[b + 1]; @@ -136,10 +195,11 @@ fg_build(const struct Cell_head *ohd, const struct Cell_head *ihd, else if (bnd[b + 1] != DBL_MAX) { cell->rmin = cell->rmax = bnd[b + 1]; } - fold_poles(g, ohd, poles, r, b, cell); + fold_poles(g, ohd, &poles, r, b, cell); } } G_free(bnd); + apply_sampling_margin(g); return g; } @@ -147,8 +207,8 @@ fg_build(const struct Cell_head *ohd, const struct Cell_head *ihd, * block the rectangle touches and adds a two cell margin. The grid holds one * row per output row, so every output row in the rectangle indexes a grid row. */ -void fg_span(const struct footprint_grid *g, int obr0, int obr1, int obc0, - int obc1, int *imin, int *imax) +void fp_span(const struct footprint *g, int obr0, int obr1, int obc0, int obc1, + int *imin, int *imax) { double rmin = DBL_MAX, rmax = -DBL_MAX; int b_lo = block_of_col(g, obc0), b_hi = block_of_col(g, obc1 - 1); @@ -161,7 +221,7 @@ void fg_span(const struct footprint_grid *g, int obr0, int obr1, int obc0, for (r = obr0; r < obr1; r++) for (b = b_lo; b <= b_hi; b++) { - const struct fg_cell *cell = &g->cell[(size_t)r * g->nb + b]; + const struct fp_cell *cell = &g->cell[(size_t)r * g->nb + b]; if (cell->rmax < cell->rmin) continue; /* empty cell */ @@ -188,19 +248,19 @@ void fg_span(const struct footprint_grid *g, int obr0, int obr1, int obc0, } /* Number of column blocks in the grid. */ -int fg_num_blocks(const struct footprint_grid *g) +int fp_num_blocks(const struct footprint *g) { return g->nb; } /* First output column of block b. Block g->nb starts at the output width. */ -int fg_block_start(const struct footprint_grid *g, int b) +int fp_block_start(const struct footprint *g, int b) { return block_c0(g, b); } /* Worst strip among the tiles that pack k whole blocks each across the band. */ -static int worst_ktile_rows(const struct footprint_grid *g, int obr0, int obr1, +static int worst_ktile_rows(const struct footprint *g, int obr0, int obr1, int k) { int worst = 0, tb; @@ -209,7 +269,7 @@ static int worst_ktile_rows(const struct footprint_grid *g, int obr0, int obr1, int te = tb + k < g->nb ? tb + k : g->nb; int imin, imax, rows; - fg_span(g, obr0, obr1, block_c0(g, tb), block_c0(g, te), &imin, &imax); + fp_span(g, obr0, obr1, block_c0(g, tb), block_c0(g, te), &imin, &imax); rows = imax - imin + 1; if (rows > worst) worst = rows; @@ -219,9 +279,9 @@ static int worst_ktile_rows(const struct footprint_grid *g, int obr0, int obr1, /* Widest tile in whole blocks whose worst strip and the output fit the cap, or * zero when even one block per tile busts. */ -static int tile_blocks_for_band(const struct footprint_grid *g, int obr0, - int obr1, size_t cap_bytes, int out_mult, - int cell_size, int in_cols) +static int tile_blocks_for_band(const struct footprint *g, int obr0, int obr1, + size_t cap_bytes, int out_mult, int cell_size, + int in_cols) { size_t out_bytes = (size_t)(obr1 - obr0) * g->ocols * cell_size; int k; @@ -243,7 +303,7 @@ static int tile_blocks_for_band(const struct footprint_grid *g, int obr0, * only when even one full-width row busts the cap, and takes the last fitting * height with its widest tile. Reports the finest tile strip the fallback * message needs and returns zero when even one tiled row busts. */ -int fg_band_geometry(const struct footprint_grid *g, int obr0, size_t cap_bytes, +int fp_band_geometry(const struct footprint *g, int obr0, size_t cap_bytes, int out_mult, int cell_size, int in_cols, int *tile_blocks_out, int *worst_block_rows) { @@ -294,22 +354,7 @@ int fg_band_geometry(const struct footprint_grid *g, int obr0, size_t cap_bytes, return best_h; } -/* Widens every non-empty cell by the sampling margin. */ -void fg_apply_sampling_margin(struct footprint_grid *g) -{ - size_t n = (size_t)g->grows * g->nb, i; - - for (i = 0; i < n; i++) { - struct fg_cell *cell = &g->cell[i]; - - if (cell->rmax >= cell->rmin) { - cell->rmin -= FG_SAMPLING_MARGIN; - cell->rmax += FG_SAMPLING_MARGIN; - } - } -} - -void fg_free(struct footprint_grid *g) +void fp_free(struct footprint *g) { if (!g) return; diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index c76e3732e16..8de7358a620 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -154,7 +154,7 @@ static const strip_func strip_kernels[] = { strip_bilinear_f, strip_cubic_f, strip_lanczos_f}; /* Grid that sizes band heights and column tiles. */ -static struct footprint_grid *band_grid = NULL; +static struct footprint *band_grid = NULL; /* Serial tile-cache path for output rows whose input footprint is too tall to * band. Finishes the run from row obr0 with the readcell cache so it matches @@ -874,41 +874,10 @@ int main(int argc, char **argv) } } - /* For a lat/lon input, project the north and south poles into the output - * and record each pole's input row, clamped to the map. A pole is the - * highest or lowest latitude, which the column samples can step over, so - * keeping its row makes sure the loaded strip reaches it. Does nothing when - * no pole lands inside the output map. */ - struct pole_set poles; - - poles.n = 0; - if (incellhd.proj == PROJECTION_LL) { - double polelat[2] = {90.0, -90.0}; - - for (int p = 0; p < 2; p++) { - double px = 0.0, py = polelat[p]; - - if (GPJ_transform(&oproj, &iproj, &tproj, PJ_INV, &px, &py, NULL) < - 0 || - !isfinite(px) || !isfinite(py)) - continue; - double ri = (incellhd.north - polelat[p]) / incellhd.ns_res; - if (ri < 0) - ri = 0; - else if (ri > incellhd.rows - 1) - ri = incellhd.rows - 1; - poles.ox[poles.n] = px; - poles.oy[poles.n] = py; - poles.pole_row[poles.n] = ri; - poles.n++; - } - } - - /* Build the grid that sizes band heights. */ - band_grid = fg_build(&outcellhd, &incellhd, &oproj, &iproj, &tproj, - y_center, &poles); - /* The margin covers what the samples can miss between columns. */ - fg_apply_sampling_margin(band_grid); + /* Build the footprint that sizes the bands. It also projects any poles into + * the output and applies the sampling margin. */ + band_grid = + fp_create(&outcellhd, &incellhd, &oproj, &iproj, &tproj, y_center); G_important_message(_("Projecting (banded, per-thread PROJ context)...")); @@ -947,7 +916,7 @@ int main(int argc, char **argv) int band_orows = force_tilecache ? 0 - : fg_band_geometry(band_grid, obr0, cap_bytes, out_mult, + : fp_band_geometry(band_grid, obr0, cap_bytes, out_mult, cell_size, incellhd.cols, &tile_blocks, &worst_block_rows); if (band_orows == 0) { @@ -989,7 +958,7 @@ int main(int argc, char **argv) int obr1 = obr0 + band_orows; n_bands++; - int nb = fg_num_blocks(band_grid); + int nb = fp_num_blocks(band_grid); int n_tiles = (nb + tile_blocks - 1) / tile_blocks; if (n_tiles > max_tiles) max_tiles = n_tiles; @@ -1005,13 +974,13 @@ int main(int argc, char **argv) * block is the full-width fast path. */ for (int tb = 0; tb < nb; tb += tile_blocks) { int te = tb + tile_blocks < nb ? tb + tile_blocks : nb; - int obc0 = fg_block_start(band_grid, tb); - int obc1 = fg_block_start(band_grid, te); + int obc0 = fp_block_start(band_grid, tb); + int obc1 = fp_block_start(band_grid, te); /* Fill spans come from the grid. The strip is full input width * because the raster API reads whole rows, so columns are not * cropped. */ - fg_span(band_grid, obr0, obr1, obc0, obc1, &imin, &imax); + fp_span(band_grid, obr0, obr1, obc0, obc1, &imin, &imax); /* The test hook shortens the span so the reload path runs. */ if (shrink_span > 0 && imax >= imin) { imin += shrink_span; @@ -1255,7 +1224,7 @@ int main(int argc, char **argv) } G_free(y_center); if (band_grid) - fg_free(band_grid); + fp_free(band_grid); /* Single free site for the rolling window. Normal completion and both * fallback_done bails converge here, so one free covers every path. win is * NULL when a bail fired before any band allocated it. */ diff --git a/raster/r.proj/r.proj.h b/raster/r.proj/r.proj.h index cfef94560d6..e7dc9481974 100644 --- a/raster/r.proj/r.proj.h +++ b/raster/r.proj/r.proj.h @@ -50,32 +50,22 @@ struct menu { enum OutputFormat { PLAIN, SHELL, JSON }; -/* Geographic poles that land inside the output map, each stored as its output - * position and its input row. Empty when no pole lands inside. */ -struct pole_set { - int n; /* active poles, 0 to 2 */ - double ox[2], oy[2]; /* pole coordinates in the output CRS */ - double pole_row[2]; /* pole input row index */ -}; - -/* Footprint grid of input row spans for the output map, built in footprint.c. - */ -struct footprint_grid; -extern struct footprint_grid * -fg_build(const struct Cell_head *ohd, const struct Cell_head *ihd, - const struct pj_info *oproj, const struct pj_info *iproj, - const struct pj_info *tproj, const double *y_center, - const struct pole_set *poles); -extern void fg_span(const struct footprint_grid *g, int obr0, int obr1, - int obc0, int obc1, int *imin, int *imax); -extern int fg_num_blocks(const struct footprint_grid *g); -extern int fg_block_start(const struct footprint_grid *g, int b); -extern int fg_band_geometry(const struct footprint_grid *g, int obr0, +/* Footprint of input row spans for the output map, built in footprint.c. The + * struct is private to footprint.c. */ +struct footprint; +extern struct footprint * +fp_create(const struct Cell_head *ohd, const struct Cell_head *ihd, + const struct pj_info *oproj, const struct pj_info *iproj, + const struct pj_info *tproj, const double *y_center); +extern void fp_span(const struct footprint *g, int obr0, int obr1, int obc0, + int obc1, int *imin, int *imax); +extern int fp_num_blocks(const struct footprint *g); +extern int fp_block_start(const struct footprint *g, int b); +extern int fp_band_geometry(const struct footprint *g, int obr0, size_t cap_bytes, int out_mult, int cell_size, int in_cols, int *tile_blocks_out, int *worst_block_rows); -extern void fg_apply_sampling_margin(struct footprint_grid *g); -extern void fg_free(struct footprint_grid *g); +extern void fp_free(struct footprint *g); extern void bordwalk(const struct Cell_head *, struct Cell_head *, const struct pj_info *, const struct pj_info *, From e5bed463632f5e6151f67c3cfc86abc0a8364bf0 Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Fri, 14 Aug 2026 01:09:00 -0700 Subject: [PATCH 34/39] r.proj: create the fallback cache temp file in the output project --- raster/r.proj/footprint.c | 11 +++++++---- raster/r.proj/main.c | 11 +++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/raster/r.proj/footprint.c b/raster/r.proj/footprint.c index b873209afe5..e3c1bc7336b 100644 --- a/raster/r.proj/footprint.c +++ b/raster/r.proj/footprint.c @@ -39,7 +39,7 @@ struct footprint { /* Returns the first output column of block b. */ static int block_c0(const struct footprint *g, int b) { - return (int)((long)b * g->ocols / g->nb); + return (int)((long long)b * g->ocols / g->nb); } /* Returns the block that contains output column c. */ @@ -63,7 +63,8 @@ static int sample_ri(const struct Cell_head *ohd, const struct Cell_head *ihd, double xx = ohd->west + (c + 0.5) * ohd->ew_res; double yy = y_center[r]; - if (GPJ_transform(oproj, iproj, tproj, PJ_FWD, &xx, &yy, NULL) < 0) + if (GPJ_transform(oproj, iproj, tproj, PJ_FWD, &xx, &yy, NULL) < 0 || + !isfinite(yy)) return 0; *ri = (ihd->north - yy) / ihd->ns_res; return 1; @@ -314,7 +315,8 @@ int fp_band_geometry(const struct footprint *g, int obr0, size_t cap_bytes, /* Prefer full-width bands, growing the height while the whole row still * fits the cap as a single tile. */ - for (h_cand = 1;; h_cand *= 2) { + for (h_cand = 1;; + h_cand = h_cand > remaining / 2 ? remaining : h_cand * 2) { int h = h_cand < remaining ? h_cand : remaining; int worst = worst_ktile_rows(g, obr0, obr0 + h, g->nb); size_t strip_bytes = @@ -334,7 +336,8 @@ int fp_band_geometry(const struct footprint *g, int obr0, size_t cap_bytes, /* One full-width row busts the cap, so grow while the exhaustive scan finds * any fitting whole-block tile. */ - for (h_cand = 1;; h_cand *= 2) { + for (h_cand = 1;; + h_cand = h_cand > remaining / 2 ? remaining : h_cand * 2) { int h = h_cand < remaining ? h_cand : remaining; int k = tile_blocks_for_band(g, obr0, obr0 + h, cap_bytes, out_mult, cell_size, in_cols); diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index 8de7358a620..f3aedc1bb51 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -166,7 +166,10 @@ fallback_serial_cache(int fdi, int fdo, int cell_type, int method, struct Cell_head *outcellhd, const double *y_center, int obr0, const char *memory) { + /* readcell reads the input map, so it runs in the input env. */ + G_switch_env(); /* -> input */ struct cache *ibuffer = readcell(fdi, memory); + G_switch_env(); /* -> output */ func interpolate = menu[method].method; void *obuffer = Rast_allocate_output_buf(cell_type); int cell_size = Rast_cell_size(cell_type); @@ -992,10 +995,10 @@ int main(int argc, char **argv) int strip_rows = imax - imin + 1; /* Serial strip load, since a single fd makes get_row unsafe to - * share. An empty tile with strip_rows at or below zero projects - * outside the input and is not read, its cells become NULL through - * strip_nearest's out-of-map path, and the window is - * invalidated so the next band re-reads in full. */ + * share. An empty tile with strip_rows at or below zero is not + * read. Its cells project outside the input and the kernel sets + * them NULL, and the window is invalidated so the next band + * re-reads in full. */ void *strip = NULL; if (strip_rows > 0) { size_t need = (size_t)strip_rows * incellhd.cols * cell_size; From 71242972b6328d6182e766b4052b8c0fec71e5c5 Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Fri, 14 Aug 2026 01:09:09 -0700 Subject: [PATCH 35/39] r.proj: harden the parallel tests --- raster/r.proj/tests/r_proj_parallel_test.py | 67 ++++++++++++++------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/raster/r.proj/tests/r_proj_parallel_test.py b/raster/r.proj/tests/r_proj_parallel_test.py index 7ae5dcd7df1..a7c38503d1c 100644 --- a/raster/r.proj/tests/r_proj_parallel_test.py +++ b/raster/r.proj/tests/r_proj_parallel_test.py @@ -2,6 +2,8 @@ module's own nprocs=1 run against a multithreaded run, and the fallback test forces the tile cache path.""" +import pytest + import grass.script as gs # Mirror of the names created in conftest.py. @@ -18,18 +20,25 @@ def _env(session, **overrides): return env +# The suggested bounds depend only on the input, not the method, so cache them +# per input and avoid an r.proj -g call in every test. +_region_cache = {} + + def _set_region_from_source(env, input_raster, method): """Set the output region to r.proj's suggested bounds for the input.""" - text = gs.read_command( - "r.proj", - project=SRC_PROJECT, - mapset="PERMANENT", - input=input_raster, - method=method, - flags="g", - env=env, - ) - region = dict(token.split("=") for token in text.split()) + if input_raster not in _region_cache: + text = gs.read_command( + "r.proj", + project=SRC_PROJECT, + mapset="PERMANENT", + input=input_raster, + method=method, + flags="g", + env=env, + ) + _region_cache[input_raster] = dict(token.split("=") for token in text.split()) + region = _region_cache[input_raster] gs.run_command( "g.region", n=region["n"], @@ -62,10 +71,16 @@ def _stats(env, raster): def _assert_bitwise_identical(env, a, b, diff): - """Check a and b are bitwise identical and have the same null cells.""" + """Check a and b are bitwise identical and null in the same cells.""" gs.run_command( "r.mapcalc", expression=f"{diff} = abs({a} - {b})", overwrite=True, env=env ) + gs.run_command( + "r.mapcalc", + expression=f"{diff}_null = if(isnull({a}) != isnull({b}), 1, 0)", + overwrite=True, + env=env, + ) sa = _stats(env, a) sb = _stats(env, b) sd = _stats(env, diff) @@ -73,6 +88,9 @@ def _assert_bitwise_identical(env, a, b, diff): assert int(sa["n"]) == int(sb["n"]) assert int(sa["null_cells"]) == int(sb["null_cells"]) assert float(sd["max"]) == 0.0 + # abs(a - b) is null wherever either map is null, so a swapped null hides + # from sd. The null map catches a null that moved. + assert float(_stats(env, f"{diff}_null")["max"]) == 0.0 def test_bilinear_parallel_matches_serial(session_3857): @@ -99,16 +117,17 @@ def test_bilinear_parallel_matches_serial(session_3857): _assert_bitwise_identical(base, "bilin_serial", "bilin_parallel", "bilin_diff") -def test_nearest_memory_banding(session_3857): - """The nearest method at a small memory cap (memory=5, nprocs=4) has to - match the default memory serial run bitwise.""" +def test_nearest_low_memory_matches_serial(session_3857): + """The nearest method at a small memory cap (memory=5, nprocs=4) matches the + default memory serial run bitwise. The 50x50 fixture fits one band even at + this cap, so this checks the low-memory path rather than multi-band.""" session = session_3857 base = _env(session) _set_region_from_source(base, INPUT_MID, "nearest") _project(base, INPUT_MID, "mem_serial", "nearest", nprocs=1) - _project(base, INPUT_MID, "mem_banded", "nearest", nprocs=4, memory=5) - _assert_bitwise_identical(base, "mem_serial", "mem_banded", "mem_diff") + _project(base, INPUT_MID, "mem_low", "nearest", nprocs=4, memory=5) + _assert_bitwise_identical(base, "mem_serial", "mem_low", "mem_diff") def test_pole_nearest_parallel_matches_serial(session_pole): @@ -132,27 +151,29 @@ def test_pole_nearest_parallel_matches_serial(session_pole): _assert_bitwise_identical(base, "pole_serial", "pole_parallel", "pole_diff") -def test_forced_fallback_matches_banded(session_3857): - """Forcing the tile cache with R_PROJ_FORCE_TILECACHE=1 gives the same - output as the banded path.""" +@pytest.mark.parametrize("method", ["nearest", "bilinear", "lanczos"]) +def test_forced_fallback_matches_banded(session_3857, method): + """Forcing the tile cache with R_PROJ_FORCE_TILECACHE=1 gives the same output + as the banded path. One method per kernel family keeps the suite fast. This + pairs the legacy p_ cache kernels against the strip kernels.""" session = session_3857 base = _env(session) - _set_region_from_source(base, INPUT_MID, "nearest") + _set_region_from_source(base, INPUT_MID, method) _project( _env(session, R_PROJ_FORCE_TILECACHE=1), INPUT_MID, "fallback_tilecache", - "nearest", + method, nprocs=1, ) - _project(base, INPUT_MID, "banded", "nearest", nprocs=4) + _project(base, INPUT_MID, "banded", method, nprocs=4) _assert_bitwise_identical(base, "fallback_tilecache", "banded", "fallback_diff") def _project_capture(env, input_raster, output, method, **extra): """Run r.proj and return the messages it writes to stderr.""" - env = dict(env, GRASS_VERBOSE="3") + env = dict(env, GRASS_VERBOSE="3", LC_ALL="C") proc = gs.start_command( "r.proj", project=SRC_PROJECT, From c8128fb474ff307225057150859a51f1ce8fa77f Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Wed, 19 Aug 2026 10:19:54 -0700 Subject: [PATCH 36/39] r.proj: rename the footprint fields and simplify the block lookup --- raster/r.proj/footprint.c | 48 ++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/raster/r.proj/footprint.c b/raster/r.proj/footprint.c index e3c1bc7336b..5036ba83a1e 100644 --- a/raster/r.proj/footprint.c +++ b/raster/r.proj/footprint.c @@ -22,22 +22,28 @@ struct pole_set { }; struct fp_cell { + /* lowest and highest input row one block of one output row needs */ double rmin, rmax; /* rmax below rmin marks an empty cell */ }; struct footprint { - int grows, nb; /* grid rows and column blocks */ + int orows; /* output rows, one grid row per output row */ + int nb; /* output column blocks, at most 32 */ int ocols; /* output columns */ - int irows; /* input rows */ - struct fp_cell *cell; /* grows by nb cells in row major order */ + int irows; /* number of rows in the input map */ + struct fp_cell *cell; /* orows by nb cells in row major order */ }; -/* The samples can miss a curve between columns by a fraction of a row, so each - * cell is widened by one row. */ +/* A block's input row range comes from projecting just its first and last + column. A straight line between them would be exact, but projection + bends a little, so a column in the middle can land a fraction of a row + above or below those two points. The one row margin covers that. */ #define FP_SAMPLING_MARGIN 1.0 +/* When ocols does not divide evenly by nb, the extra columns are spread + out so no two blocks differ by more than one column. */ /* Returns the first output column of block b. */ -static int block_c0(const struct footprint *g, int b) +static int block_first_col(const struct footprint *g, int b) { return (int)((long long)b * g->ocols / g->nb); } @@ -45,12 +51,7 @@ static int block_c0(const struct footprint *g, int b) /* Returns the block that contains output column c. */ static int block_of_col(const struct footprint *g, int c) { - int b; - - for (b = 0; b < g->nb - 1; b++) - if (c < block_c0(g, b + 1)) - return b; - return g->nb - 1; + return (int)(((long long)(c + 1) * g->nb - 1) / g->ocols); } /* Projects the center of output cell (r, c) to an input row index. Returns 0 on @@ -76,7 +77,7 @@ static void fold_poles(const struct footprint *g, const struct Cell_head *ohd, const struct pole_set *poles, int r, int b, struct fp_cell *cell) { - int c0 = block_c0(g, b), c1 = block_c0(g, b + 1), k; + int c0 = block_first_col(g, b), c1 = block_first_col(g, b + 1), k; double x_lo = ohd->west + c0 * ohd->ew_res; double x_hi = ohd->west + c1 * ohd->ew_res; double y_lo = ohd->north - (r + 1) * ohd->ns_res; @@ -131,7 +132,7 @@ static void build_pole_set(const struct Cell_head *ihd, /* Widens every non-empty cell by the sampling margin. */ static void apply_sampling_margin(struct footprint *g) { - size_t n = (size_t)g->grows * g->nb, i; + size_t n = (size_t)g->orows * g->nb, i; for (i = 0; i < n; i++) { struct fp_cell *cell = &g->cell[i]; @@ -158,20 +159,20 @@ struct footprint *fp_create(const struct Cell_head *ohd, build_pole_set(ihd, oproj, iproj, tproj, &poles); - g->grows = ohd->rows; + g->orows = ohd->rows; g->nb = ohd->cols < 32 ? ohd->cols : 32; g->ocols = ohd->cols; g->irows = ihd->rows; - g->cell = G_malloc((size_t)g->grows * g->nb * sizeof(struct fp_cell)); + g->cell = G_malloc((size_t)g->orows * g->nb * sizeof(struct fp_cell)); bnd = G_malloc((size_t)(g->nb + 1) * sizeof(double)); - for (r = 0; r < g->grows; r++) { + for (r = 0; r < g->orows; r++) { /* Sample the NB plus one block boundaries for this row. The last * boundary uses the final valid column. */ int k; for (k = 0; k <= g->nb; k++) { - int c = block_c0(g, k); + int c = block_first_col(g, k); if (c > g->ocols - 1) c = g->ocols - 1; @@ -215,10 +216,10 @@ void fp_span(const struct footprint *g, int obr0, int obr1, int obc0, int obc1, int b_lo = block_of_col(g, obc0), b_hi = block_of_col(g, obc1 - 1); int r, b; - if (obr1 > g->grows) + if (obr1 > g->orows) G_fatal_error(_("Footprint grid has %d rows but output row %d was " "requested"), - g->grows, obr1 - 1); + g->orows, obr1 - 1); for (r = obr0; r < obr1; r++) for (b = b_lo; b <= b_hi; b++) { @@ -257,7 +258,7 @@ int fp_num_blocks(const struct footprint *g) /* First output column of block b. Block g->nb starts at the output width. */ int fp_block_start(const struct footprint *g, int b) { - return block_c0(g, b); + return block_first_col(g, b); } /* Worst strip among the tiles that pack k whole blocks each across the band. */ @@ -270,7 +271,8 @@ static int worst_ktile_rows(const struct footprint *g, int obr0, int obr1, int te = tb + k < g->nb ? tb + k : g->nb; int imin, imax, rows; - fp_span(g, obr0, obr1, block_c0(g, tb), block_c0(g, te), &imin, &imax); + fp_span(g, obr0, obr1, block_first_col(g, tb), block_first_col(g, te), + &imin, &imax); rows = imax - imin + 1; if (rows > worst) worst = rows; @@ -308,7 +310,7 @@ int fp_band_geometry(const struct footprint *g, int obr0, size_t cap_bytes, int out_mult, int cell_size, int in_cols, int *tile_blocks_out, int *worst_block_rows) { - int remaining = g->grows - obr0; + int remaining = g->orows - obr0; int best_h = 0, best_k = 0, h_cand; *worst_block_rows = worst_ktile_rows(g, obr0, obr0 + 1, 1); From fb4f7db214ce119c7983ff10cba7d9fd7854a789 Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Fri, 21 Aug 2026 22:49:31 -0700 Subject: [PATCH 37/39] r.proj: apply the footprint review renames and comments --- raster/r.proj/footprint.c | 187 ++++++++++++++++++++++---------------- raster/r.proj/r.proj.h | 6 +- 2 files changed, 114 insertions(+), 79 deletions(-) diff --git a/raster/r.proj/footprint.c b/raster/r.proj/footprint.c index 5036ba83a1e..26fc83830d2 100644 --- a/raster/r.proj/footprint.c +++ b/raster/r.proj/footprint.c @@ -13,6 +13,10 @@ #include "r.proj.h" +/* The output is cut into nb column slices called blocks. A band is a + chunk of consecutive output rows processed together. A tile is one + band tall and k blocks wide and each tile is read as one strip. */ + /* Geographic poles that land inside the output map, each stored as its output * position and its input row. Empty when no pole lands inside. */ struct pole_set { @@ -34,10 +38,11 @@ struct footprint { struct fp_cell *cell; /* orows by nb cells in row major order */ }; -/* A block's input row range comes from projecting just its first and last - column. A straight line between them would be exact, but projection - bends a little, so a column in the middle can land a fraction of a row - above or below those two points. The one row margin covers that. */ +/* A block's input row range comes from projecting its first and last + column. Those two samples give the exact range only when the input + row changes monotonically across the block. When it does not, a + middle column can sit a fraction of a row outside the two samples. + The margin below and the strip reload in main.c cover that case. */ #define FP_SAMPLING_MARGIN 1.0 /* When ocols does not divide evenly by nb, the extra columns are spread @@ -56,10 +61,12 @@ static int block_of_col(const struct footprint *g, int c) /* Projects the center of output cell (r, c) to an input row index. Returns 0 on * a failed transform and leaves ri unchanged. */ -static int sample_ri(const struct Cell_head *ohd, const struct Cell_head *ihd, - const struct pj_info *oproj, const struct pj_info *iproj, - const struct pj_info *tproj, const double *y_center, int r, - int c, double *ri) +static int sample_row_index(const struct Cell_head *ohd, + const struct Cell_head *ihd, + const struct pj_info *oproj, + const struct pj_info *iproj, + const struct pj_info *tproj, const double *y_center, + int r, int c, double *ri) { double xx = ohd->west + (c + 0.5) * ohd->ew_res; double yy = y_center[r]; @@ -77,14 +84,15 @@ static void fold_poles(const struct footprint *g, const struct Cell_head *ohd, const struct pole_set *poles, int r, int b, struct fp_cell *cell) { + if (!poles) + return; + int c0 = block_first_col(g, b), c1 = block_first_col(g, b + 1), k; double x_lo = ohd->west + c0 * ohd->ew_res; double x_hi = ohd->west + c1 * ohd->ew_res; double y_lo = ohd->north - (r + 1) * ohd->ns_res; double y_hi = ohd->north - r * ohd->ns_res; - if (!poles) - return; for (k = 0; k < poles->n; k++) { if (poles->ox[k] < x_lo || poles->ox[k] > x_hi || poles->oy[k] < y_lo || poles->oy[k] > y_hi) @@ -117,6 +125,11 @@ static void build_pole_set(const struct Cell_head *ihd, if (GPJ_transform(oproj, iproj, tproj, PJ_INV, &px, &py, NULL) < 0 || !isfinite(px) || !isfinite(py)) continue; + /* The pole sits outside the input's latitude range while its + projected position still ends up inside an output cell. The two edge + samples of that cell never reach the top of the input, so this line + forces the first or last input row into the cell's range. Without + this the strip for the cell holding the pole is too short. */ double ri = (ihd->north - polelat[p]) / ihd->ns_res; if (ri < 0) ri = 0; @@ -137,6 +150,8 @@ static void apply_sampling_margin(struct footprint *g) for (i = 0; i < n; i++) { struct fp_cell *cell = &g->cell[i]; + /* A cell stays empty when both of its boundary columns fail to + transform. */ if (cell->rmax >= cell->rmin) { cell->rmin -= FP_SAMPLING_MARGIN; cell->rmax += FP_SAMPLING_MARGIN; @@ -152,7 +167,7 @@ struct footprint *fp_create(const struct Cell_head *ohd, const struct pj_info *iproj, const struct pj_info *tproj, const double *y_center) { - struct footprint *g = G_malloc(sizeof(*g)); + struct footprint *g = G_malloc(sizeof *g); struct pole_set poles; int r, b; double *bnd; @@ -160,30 +175,29 @@ struct footprint *fp_create(const struct Cell_head *ohd, build_pole_set(ihd, oproj, iproj, tproj, &poles); g->orows = ohd->rows; - g->nb = ohd->cols < 32 ? ohd->cols : 32; + g->nb = MIN(ohd->cols, 32); g->ocols = ohd->cols; g->irows = ihd->rows; - g->cell = G_malloc((size_t)g->orows * g->nb * sizeof(struct fp_cell)); - bnd = G_malloc((size_t)(g->nb + 1) * sizeof(double)); + g->cell = G_malloc(sizeof *g->cell * (size_t)g->orows * g->nb); + /* nb blocks need nb plus 1 boundary columns. */ + bnd = G_malloc(sizeof *bnd * ((size_t)g->nb + 1)); for (r = 0; r < g->orows; r++) { - /* Sample the NB plus one block boundaries for this row. The last - * boundary uses the final valid column. */ + /* Sample the number of blocks (g->nb) plus one boundary columns for + * this row. The last boundary is the final column. */ int k; for (k = 0; k <= g->nb; k++) { - int c = block_first_col(g, k); + int c = k < g->nb ? block_first_col(g, k) : g->ocols - 1; - if (c > g->ocols - 1) - c = g->ocols - 1; - if (!sample_ri(ohd, ihd, oproj, iproj, tproj, y_center, r, c, - &bnd[k])) + if (!sample_row_index(ohd, ihd, oproj, iproj, tproj, y_center, r, c, + &bnd[k])) bnd[k] = DBL_MAX; /* a failed sample is left out of the range */ } for (b = 0; b < g->nb; b++) { struct fp_cell *cell = &g->cell[(size_t)r * g->nb + b]; - double lo = bnd[b] < bnd[b + 1] ? bnd[b] : bnd[b + 1]; - double hi = bnd[b] > bnd[b + 1] ? bnd[b] : bnd[b + 1]; + double lo = MIN(bnd[b], bnd[b + 1]); + double hi = MAX(bnd[b], bnd[b + 1]); cell->rmin = DBL_MAX; cell->rmax = -DBL_MAX; @@ -205,23 +219,25 @@ struct footprint *fp_create(const struct Cell_head *ohd, return g; } -/* Returns the input row span covering the output rectangle. Includes every - * block the rectangle touches and adds a two cell margin. The grid holds one - * row per output row, so every output row in the rectangle indexes a grid row. - */ -void fp_span(const struct footprint *g, int obr0, int obr1, int obc0, int obc1, - int *imin, int *imax) +/* Fills *imin and *imax with the input rows needed for the given output + rows and columns. end_row is one past the last row and end_col is one + past the last column, same as the loop bounds, so the last band ends + with end_row equal to orows. Looks at every grid cell in that rectangle, + skips the empty ones, takes the min and max, then adds two pad rows for + the bicubic and lanczos method reads. */ +void fp_span(const struct footprint *g, int first_row, int end_row, + int first_col, int end_col, int *imin, int *imax) { double rmin = DBL_MAX, rmax = -DBL_MAX; - int b_lo = block_of_col(g, obc0), b_hi = block_of_col(g, obc1 - 1); + int b_lo = block_of_col(g, first_col), b_hi = block_of_col(g, end_col - 1); int r, b; - if (obr1 > g->orows) + if (end_row > g->orows) G_fatal_error(_("Footprint grid has %d rows but output row %d was " "requested"), - g->orows, obr1 - 1); + g->orows, end_row - 1); - for (r = obr0; r < obr1; r++) + for (r = first_row; r < end_row; r++) for (b = b_lo; b <= b_hi; b++) { const struct fp_cell *cell = &g->cell[(size_t)r * g->nb + b]; @@ -233,14 +249,19 @@ void fp_span(const struct footprint *g, int obr0, int obr1, int obc0, int obc1, rmax = cell->rmax; } - if (rmax < rmin) { /* every touched cell empty */ + if (rmax < rmin) { + /* An empty span reads as zero rows because the caller computes imax + minus imin plus one. */ *imin = 0; *imax = -1; return; } + /* Two pad rows each way keep the bicubic and lanczos neighbor reads inside + the strip. */ int lo = (int)floor(rmin) - 2; int hi = (int)floor(rmax) + 2; + /* The pad can step past the first or last input row near the map edges. */ if (lo < 0) lo = 0; if (hi > g->irows - 1) @@ -249,50 +270,55 @@ void fp_span(const struct footprint *g, int obr0, int obr1, int obc0, int obc1, *imax = hi; } -/* Number of column blocks in the grid. */ +/* Block count for main.c's tile loop. */ int fp_num_blocks(const struct footprint *g) { return g->nb; } -/* First output column of block b. Block g->nb starts at the output width. */ +/* First output column of block b. b equal to nb gives the output width. */ int fp_block_start(const struct footprint *g, int b) { return block_first_col(g, b); } -/* Worst strip among the tiles that pack k whole blocks each across the band. */ -static int worst_ktile_rows(const struct footprint *g, int obr0, int obr1, - int k) +/* Largest number of input rows any k block wide tile of this band needs. */ +static int tallest_tile_rows(const struct footprint *g, int first_row, + int end_row, int k) { - int worst = 0, tb; + int tallest = 0, tile_start; - for (tb = 0; tb < g->nb; tb += k) { - int te = tb + k < g->nb ? tb + k : g->nb; + for (tile_start = 0; tile_start < g->nb; tile_start += k) { + int tile_end = MIN(tile_start + k, g->nb); int imin, imax, rows; - fp_span(g, obr0, obr1, block_first_col(g, tb), block_first_col(g, te), - &imin, &imax); + fp_span(g, first_row, end_row, block_first_col(g, tile_start), + block_first_col(g, tile_end), &imin, &imax); rows = imax - imin + 1; - if (rows > worst) - worst = rows; + if (rows > tallest) + tallest = rows; } - return worst; + return tallest; } -/* Widest tile in whole blocks whose worst strip and the output fit the cap, or - * zero when even one block per tile busts. */ -static int tile_blocks_for_band(const struct footprint *g, int obr0, int obr1, - size_t cap_bytes, int out_mult, int cell_size, - int in_cols) +/* Finds the widest tile, counted in whole blocks, whose input strip plus + output buffers fit under cap_bytes. Returns zero when even a single block + tile is too big. first_row and end_row are the band's rows, with end_row + one past the last. cell_size is the bytes per cell and in_cols is the + input map width. out_mult is how many output band buffers exist at once. + It is two when the previous band's write overlaps the next band's + compute, otherwise one. */ +static int tile_blocks_for_band(const struct footprint *g, int first_row, + int end_row, size_t cap_bytes, int out_mult, + int cell_size, int in_cols) { - size_t out_bytes = (size_t)(obr1 - obr0) * g->ocols * cell_size; + size_t out_bytes = (size_t)(end_row - first_row) * g->ocols * cell_size; int k; if (out_mult * out_bytes > cap_bytes) return 0; for (k = g->nb; k >= 1; k--) { - int worst = worst_ktile_rows(g, obr0, obr1, k); + int worst = tallest_tile_rows(g, first_row, end_row, k); size_t strip_bytes = worst > 0 ? (size_t)worst * in_cols * cell_size : 0; @@ -306,49 +332,58 @@ static int tile_blocks_for_band(const struct footprint *g, int obr0, int obr1, * only when even one full-width row busts the cap, and takes the last fitting * height with its widest tile. Reports the finest tile strip the fallback * message needs and returns zero when even one tiled row busts. */ -int fp_band_geometry(const struct footprint *g, int obr0, size_t cap_bytes, +int fp_band_geometry(const struct footprint *g, int first_row, size_t cap_bytes, int out_mult, int cell_size, int in_cols, int *tile_blocks_out, int *worst_block_rows) { - int remaining = g->orows - obr0; - int best_h = 0, best_k = 0, h_cand; - - *worst_block_rows = worst_ktile_rows(g, obr0, obr0 + 1, 1); - - /* Prefer full-width bands, growing the height while the whole row still - * fits the cap as a single tile. */ - for (h_cand = 1;; - h_cand = h_cand > remaining / 2 ? remaining : h_cand * 2) { - int h = h_cand < remaining ? h_cand : remaining; - int worst = worst_ktile_rows(g, obr0, obr0 + h, g->nb); + int remaining = g->orows - first_row; + int best_h = 0, best_k = 0, try_height; + + /* Measures the single first row as a one block tile, the smallest read the + module could ever do. If even that does not fit the cap, this number is + used to tell the user how much memory the parallel path would need + before the serial tile cache takes over. */ + *worst_block_rows = tallest_tile_rows(g, first_row, first_row + 1, 1); + + /* Growing a band can only add input rows, never remove them. So once a + height is too big, every bigger height is too big as well. That means + only the first height that fails matters, and doubling finds it in a + few tries instead of counting up one at a time. The step after + remaining / 2 goes straight to remaining instead of past it. */ + for (try_height = 1;; + try_height = try_height > remaining / 2 ? remaining : try_height * 2) { + /* band_h is the band height in rows. */ + int band_h = MIN(try_height, remaining); + int worst = tallest_tile_rows(g, first_row, first_row + band_h, g->nb); size_t strip_bytes = worst > 0 ? (size_t)worst * in_cols * cell_size : 0; - size_t out_bytes = (size_t)h * g->ocols * cell_size; + size_t out_bytes = (size_t)band_h * g->ocols * cell_size; if (strip_bytes + out_mult * out_bytes > cap_bytes) break; - best_h = h; - if (h == remaining) + best_h = band_h; + if (band_h == remaining) break; } if (best_h > 0) { + /* tile_blocks is nb here, meaning one full width tile. */ *tile_blocks_out = g->nb; return best_h; } /* One full-width row busts the cap, so grow while the exhaustive scan finds * any fitting whole-block tile. */ - for (h_cand = 1;; - h_cand = h_cand > remaining / 2 ? remaining : h_cand * 2) { - int h = h_cand < remaining ? h_cand : remaining; - int k = tile_blocks_for_band(g, obr0, obr0 + h, cap_bytes, out_mult, - cell_size, in_cols); + for (try_height = 1;; + try_height = try_height > remaining / 2 ? remaining : try_height * 2) { + int band_h = MIN(try_height, remaining); + int k = tile_blocks_for_band(g, first_row, first_row + band_h, + cap_bytes, out_mult, cell_size, in_cols); if (k == 0) break; - best_h = h; + best_h = band_h; best_k = k; - if (h == remaining) + if (band_h == remaining) break; } if (best_h == 0) { diff --git a/raster/r.proj/r.proj.h b/raster/r.proj/r.proj.h index e7dc9481974..1f6848d71d4 100644 --- a/raster/r.proj/r.proj.h +++ b/raster/r.proj/r.proj.h @@ -57,11 +57,11 @@ extern struct footprint * fp_create(const struct Cell_head *ohd, const struct Cell_head *ihd, const struct pj_info *oproj, const struct pj_info *iproj, const struct pj_info *tproj, const double *y_center); -extern void fp_span(const struct footprint *g, int obr0, int obr1, int obc0, - int obc1, int *imin, int *imax); +extern void fp_span(const struct footprint *g, int first_row, int end_row, + int first_col, int end_col, int *imin, int *imax); extern int fp_num_blocks(const struct footprint *g); extern int fp_block_start(const struct footprint *g, int b); -extern int fp_band_geometry(const struct footprint *g, int obr0, +extern int fp_band_geometry(const struct footprint *g, int first_row, size_t cap_bytes, int out_mult, int cell_size, int in_cols, int *tile_blocks_out, int *worst_block_rows); From 64ca6ed4c1755b986726fdebadadfd1591a7d1ab Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Sat, 22 Aug 2026 00:28:32 -0700 Subject: [PATCH 38/39] r.proj: use the merged test fixtures --- raster/r.proj/tests/conftest.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/raster/r.proj/tests/conftest.py b/raster/r.proj/tests/conftest.py index db5d8dfb930..eee96685a31 100644 --- a/raster/r.proj/tests/conftest.py +++ b/raster/r.proj/tests/conftest.py @@ -1,7 +1,5 @@ """This is a source project with two small rasters and two destination sessions.""" -# Copied from the test PR. Drop this file when that PR merges. - import os import pytest @@ -11,13 +9,13 @@ INPUT_EXPRESSION = "row() * 100 + col() + (row() * row() + col() * col()) % 13" SRC_PROJECT = "src4326" -# Mid-latitude box for the 3857 identity/fallback cases. +# Mid-latitude box for the 3857 tests. INPUT_MID = "input_mid" # High-latitude, full-longitude box so a north-polar frame has data to read. INPUT_POLAR = "input_polar" -@pytest.fixture(scope="module") +@pytest.fixture(scope="session") def gisdbase_with_source(tmp_path_factory): """GISDBASE containing src4326 with the mid and polar input rasters.""" gisdbase = tmp_path_factory.mktemp("rproj_parallel") @@ -35,7 +33,7 @@ def gisdbase_with_source(tmp_path_factory): return gisdbase -@pytest.fixture(scope="module") +@pytest.fixture(scope="session") def session_3857(gisdbase_with_source): """Active session in an EPSG:3857 destination project.""" gs.create_project(gisdbase_with_source / "dst3857", epsg="3857") @@ -45,7 +43,7 @@ def session_3857(gisdbase_with_source): yield session -@pytest.fixture(scope="module") +@pytest.fixture(scope="session") def session_pole(gisdbase_with_source): """Active session in an EPSG:3413 (north polar stereographic) project.""" gs.create_project(gisdbase_with_source / "dst_pole", epsg="3413") From 9e7b30c5d07b74e809d6b82a73a0bf9a26aa7684 Mon Sep 17 00:00:00 2001 From: Kaushik Raja Date: Sun, 23 Aug 2026 08:34:19 -0500 Subject: [PATCH 39/39] r.proj: add the kernel table check and document nprocs and memory --- raster/r.proj/Makefile | 2 +- raster/r.proj/main.c | 7 +++++++ raster/r.proj/r.proj.html | 13 +++++++++++-- raster/r.proj/r.proj.md | 16 ++++++++++++---- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/raster/r.proj/Makefile b/raster/r.proj/Makefile index 147b47fe2d8..083cd95c72f 100644 --- a/raster/r.proj/Makefile +++ b/raster/r.proj/Makefile @@ -2,7 +2,7 @@ MODULE_TOPDIR = ../.. PGM = r.proj -LIBES = $(GPROJLIB) $(RASTERLIB) $(GISLIB) $(MATHLIB) $(PARSONLIB) $(PROJLIB) +LIBES = $(GPROJLIB) $(RASTERLIB) $(GISLIB) $(MATHLIB) $(PARSONLIB) DEPENDENCIES = $(GPROJDEP) $(RASTERDEP) $(GISDEP) EXTRA_LIBS = $(OPENMP_LIBPATH) $(OPENMP_LIB) diff --git a/raster/r.proj/main.c b/raster/r.proj/main.c index f3aedc1bb51..3a415175e67 100644 --- a/raster/r.proj/main.c +++ b/raster/r.proj/main.c @@ -90,6 +90,9 @@ struct menu menu[] = { {p_lanczos_f, "lanczos_f", "lanczos filter with fallback"}, {NULL, NULL, NULL}}; +/* menu ends with a NULL row, so it has one more slot than methods. */ +enum { N_METHODS = sizeof menu / sizeof *menu - 1 }; + static char *make_ipol_list(void); static char *make_ipol_desc(void); @@ -153,6 +156,10 @@ static const strip_func strip_kernels[] = { strip_nearest, strip_bilinear, strip_cubic, strip_lanczos, strip_bilinear_f, strip_cubic_f, strip_lanczos_f}; +/* The two tables must stay in step, one strip kernel per menu entry. */ +_Static_assert(sizeof strip_kernels / sizeof *strip_kernels == N_METHODS, + "strip_kernels and menu must have one entry per method"); + /* Grid that sizes band heights and column tiles. */ static struct footprint *band_grid = NULL; diff --git a/raster/r.proj/r.proj.html b/raster/r.proj/r.proj.html index 80bf9f8efa3..b5bf020ad27 100644 --- a/raster/r.proj/r.proj.html +++ b/raster/r.proj/r.proj.html @@ -188,10 +188,19 @@

NOTES

r.proj is then run for the raster map the user wants to reproject. In this case a little preparation goes a long way.

+The nprocs parameter sets the number of compute threads +(default 1) and the memory parameter sets a limit in MB +(default 300). The module reads the input in bands sized to fit the +memory option, and threads split the output rows. A band is read +as column tiles when a full row does not fit. When nothing fits it falls +back to the serial cache with a message saying how much memory the map +needs. The sizing table itself is small and is not counted against +memory. To use the parallel path GRASS must be compiled with +OpenMP. +

When reprojecting whole-world maps the user should disable map-trimming with the -n flag. Trimming is not useful here -because the module has the whole map in memory anyway. Besides that, -world "edges" are hard (or impossible) to find in CRSs other +because world "edges" are hard (or impossible) to find in CRSs other than latitude-longitude so results may be odd with trimming.

EXAMPLES

diff --git a/raster/r.proj/r.proj.md b/raster/r.proj/r.proj.md index ece211a3902..79091383711 100644 --- a/raster/r.proj/r.proj.md +++ b/raster/r.proj/r.proj.md @@ -171,11 +171,19 @@ geodetic length of a pixel). *r.proj* is then run for the raster map the user wants to reproject. In this case a little preparation goes a long way. +The **nprocs** parameter sets the number of compute threads (default 1) +and the **memory** parameter sets a limit in MB (default 300). The module +reads the input in bands sized to fit the **memory** option, and threads +split the output rows. A band is read as column tiles when a full row does +not fit. When nothing fits it falls back to the serial cache with a message +saying how much memory the map needs. The sizing table itself is small and +is not counted against **memory**. To use the parallel path GRASS must be +compiled with OpenMP. + When reprojecting whole-world maps the user should disable map-trimming -with the **-n** flag. Trimming is not useful here because the module has -the whole map in memory anyway. Besides that, world "edges" are hard (or -impossible) to find in CRSs other than latitude-longitude so results may -be odd with trimming. +with the **-n** flag. Trimming is not useful here because world "edges" +are hard (or impossible) to find in CRSs other than latitude-longitude so +results may be odd with trimming. ## EXAMPLES