Skip to content

feat(core): apply local meter scales in custom projections - #10745

Open
Pessimistress wants to merge 2 commits into
x/alt-proj-test-appfrom
codex/alt-proj-size-scale-texture
Open

Pessimistress wants to merge 2 commits into
x/alt-proj-test-appfrom
codex/alt-proj-size-scale-texture

Conversation

@Pessimistress

@Pessimistress Pessimistress commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

For #10739 (PR 6 of 6)

Account for local projection distortion when rendering meter-sized objects and altitude.

Screenshot 2026-09-23 at 10 32 04 PM Screenshot 2026-09-23 at 10 32 23 PM Screenshot 2026-09-23 at 10 32 51 PM

Changed list

  • Estimate local area-equivalent scale across each projection.
  • Add WebGL texture and WebGPU storage-buffer scale sampling.
  • Isolate external-projection shader paths from existing views.
  • Apply local altitude scaling while preserving common-space aggregation sizes.
  • Test scale validity, approximation error, and discontinuities.
  • Update meter-size documentation, test-app sizing, and render coverage.

Method

Generation

Generate a 64×64 scale grid over the projection’s normalized extent. At each valid sample, inverse-project its position and estimate the local projection derivatives using one-meter displacements in world coordinates.

Use the square root of the absolute Jacobian determinant to obtain an area-equivalent scalar scale. Applying the same scale in both horizontal directions preserves the aspect ratio of anchored objects rather than reproducing directional distortion.

Compute scale slopes from neighboring samples, then extend valid values and slopes one cell into unsampled regions to cover valid positions near curved projection boundaries. Reuse the generated resource across camera changes; regenerate when its projection configuration changes.

Encoding

Each grid cell stores four floats:

[scale, dScale/dX, dScale/dY, altitudeScale]

A zero scale marks an invalid sample. WebGL stores float bit patterns in an integer texture; WebGPU uses a vec4<f32> storage buffer with direct address lookup.

Interpolation

Fetch the nearest cell and reconstruct the local scale using its slopes:

scale(position) = scale(cellCenter)
                + dScale/dX × offsetX
                + dScale/dY × offsetY

Altitude scale receives the same relative adjustment. This requires one record lookup without bilinear filtering. Four-cell blending and higher-order interpolation remain documented alternatives if greater continuity or precision is needed.

Measured approximation error

Current harness results, in percent. “Center” covers a 128×128 common-unit square; “whole” covers valid positions within the 512×512 extent. Discontinuity measures the scale jump across nearest-cell boundaries, relative to the reference scale.

Projection Max error, center Max error, whole Max discontinuity, center Max discontinuity, whole
Web Mercator 0.128704% 0.132717% 0.015536% 0.023561%
Equal Earth 0.000897% 0.007637% 0.000159% 0.006859%
Stereographic 0.680093% 0.680093% 0.000007% 0.184600%
Albers conic 0.000098% 0.002310% 0.000042% 0.002301%

Test domains: Web Mercator through ±85.0511° latitude; Equal Earth worldwide; north-polar stereographic from 60°S to 90°N; Albers over [-135, 30, -45, 75].

All sampled valid positions retrieved valid scale values. Tests enforce error and discontinuity below 1%. These are sampled maxima for these configurations, not universal bounds. Equal Earth’s exact poles are excluded from relative-error calculations because the numerical reference is singular there, but sample validity is still checked.

@greptile-apps

greptile-apps Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 2/5

The PR is not ready to merge: configuration changes can leave positions and meter scales stale, and two previously reported rendering failures remain.

Findings

  1. P1 View changes leave scales stale ▶
  2. P1 Narrow domains lose meter scale ▶
  3. P1 Positions outside extent lose scale ▶
  4. P2 Removed views retain GPU resources ▶
  5. P2 WebGPU scale math lacks checks ▶

Summary

This PR adds a local area-equivalent meter-scale grid for custom projections, binds it as a WebGL texture or WebGPU buffer, and applies it to rendered sizes and altitude. It also adds projection accuracy tests, render coverage, and documentation. A view-configuration change can leave both prepared positions and the GPU scale grid stale.

Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  V[Custom-projection view configuration] --> P[Preproject layer positions]
  V --> G[Generate local scale grid]
  P --> L[Rendered layer]
  G --> C[Resource cache by view ID and signature]
  C --> L
  V -. bounds or unit callback omitted from signatures .-> C
Loading

Reviews (3) · Last reviewed commit: "test: exercise custom projection meter s..."

Comment on lines +225 to +245
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const common = [((x + 0.5) * 512) / size, ((y + 0.5) * 512) / size];
const output = [
(common[0] - 256) / normalization + (minX + maxX) / 2,
(common[1] - 256) / normalization + (minY + maxY) / 2,
0
];
try {
if (output[0] < minX || output[0] > maxX || output[1] < minY || output[1] > maxY)
continue;
const input = projection.inverse(output);
if (!input || input.length < 2 || !input.every(Number.isFinite)) continue;
if (
fromBounds &&
(input[0] < fromBounds[0] ||
input[0] > fromBounds[2] ||
input[1] < fromBounds[1] ||
input[1] > fromBounds[3])
)
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Narrow domains lose meter scale When a valid projection occupies less than one grid cell, no cell center may pass these checks. For example, an identity converter with toBounds: [0, 0, 512, 512] and fromBounds: [1, 0, 2, 512] produces no valid scale records, even though positions inside fromBounds are valid. Padding has no valid neighbor to copy, so the shader samples zero and collapses meter-sized geometry and altitude.

Comment on lines +101 to +102
vec3 project_external_size_scale_at(vec2 commonPosition) {
if (any(lessThan(commonPosition, vec2(0.0))) || any(greaterThan(commonPosition, vec2(512.0)))) return vec3(0.0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Positions outside extent lose scale Projection bounds define normalization, not clipping, so valid data can still render outside the normalized [0, 512] extent. With an identity converter and toBounds: [0, 0, 512, 512], a position at x=513 remains projectable, but this lookup returns zero. Its meter-sized radius and altitude collapse. The WGSL path has the same behavior.

Comment on lines +40 to +41
previous?.resource.destroy();
this.resources.set(viewport.id, {signature: customViewport.sizeScaleSignature, resource});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Removed views retain GPU resources The cache destroys an old resource only when another viewport reuses its ID; otherwise entries remain until the LayerManager is finalized. An application that repeatedly removes custom-projection views and creates new ones with different IDs therefore accumulates textures or buffers after those views stop rendering. Releasing entries for removed views would prevent that GPU memory growth.

Knowledge Base Used: Core rendering engine

Comment thread docs/api-reference/core/custom-projection-view.md
Comment thread docs/api-reference/core/custom-projection-view.md Outdated
gpuTest.each(
[
{
name: 'scalar XY and independent Z',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 WebGPU scale math lacks checks These numeric GPU cases, and the numeric altitude case, are skipped on WebGPU. Shader assembly checks that the storage-buffer binding exists, and a render image checks visual output, but neither checks calculated scale values. A WebGPU numeric case with a nonconstant scale or altitude would catch binding or indexing errors that could still produce a plausible image.

Knowledge Base Used: Distribution and quality

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@Pessimistress
Pessimistress force-pushed the codex/alt-proj-size-scale-texture branch from 8039c42 to 1a80405 Compare September 24, 2026 05:39
@coveralls

coveralls commented Sep 24, 2026 •

Copy link
Copy Markdown

Coverage Status

Coverage is 83.052% — codex/alt-proj-size-scale-texture into x/alt-proj-test-app. No base build found for x/alt-proj-test-app.

@Pessimistress
Pessimistress added this pull request to stack #10746 September 24, 2026 15:59
@Pessimistress
Pessimistress force-pushed the codex/alt-proj-size-scale-texture branch from 1a80405 to be5c334 Compare September 24, 2026 21:44
Comment on lines 136 to +139
this.signature = JSON.stringify([fromCrs, toCrs, resolution]);
this.scaleOptions = opts;
this.metersPerUnitCallback = metersPerUnitCallback;
this.sizeScaleSignature = JSON.stringify([fromCrs, toCrs]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 View changes leave scales stale When a view keeps the same ID and CRS but changes toBounds or getMetersPerUnit, these signatures do not change, although both prepared positions and the scale grid depend on those values. Existing layers keep positions from the old configuration, and the GPU resource cache reuses the old meter scale. For example, changing an identity projection’s toBounds from [0, 0, 512, 512] to [0, 0, 1024, 512] leaves positions in the old normalization and renders meter sizes at the old scale.

Knowledge Base Used: Layer lifecycle and state

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants