[Impeller] Retire Y-coord-scale plumbing by flipping GLES at the vertex stage#186556
Conversation
41a977f to
1decf43
Compare
1decf43 to
08c120c
Compare
08c120c to
1926c63
Compare
…ex stage (initial MVP) Implements the design from flutter#186554: absorb the OpenGL ES render-to-texture Y orientation difference once, in impellerc plus RenderPassGLES, so render-to-texture content is now stored top-down on every backend. * impellerc (compiler.cc): for vertex shaders compiled for any GL target (kOpenGLES / kOpenGLDesktop / kRuntimeStageGLES / kRuntimeStageGLES3), inject a hidden `uniform float _impeller_y_flip;` declaration and a matching `gl_Position.y *= _impeller_y_flip;` epilogue at the end of main(). Sits naturally in CreateGLSLCompiler's existing spirv-cross-emission stage, alongside the already-enabled `sl_options.vertex.fixup_clipspace = true;` knob. * RenderPassGLES (render_pass_gles.cc): determine flip_y per pass from `is_wrapped_fbo` (swapchain -> +1.0, offscreen FBO -> -1.0), write the uniform after every pipeline.BindProgram, invert the front-face winding state in lockstep when flipping, and switch the viewport / scissor Y math to the unflipped form for FBO renders (the existing `target_h - y - h` math is preserved for the swapchain so on-screen rendering is unchanged). * TextureGLES::GetYCoordScale: always returns 1.0 now. With the vertex-stage flip in place, GLES render-to-texture content is stored top-down at the GL framebuffer level. The existing per-site `texture_sampler_y_coord_scale` plumbing across the ~20 entity- contents and filter sites carries the value 1.0 and reduces to no-ops, no source changes needed at the call sites. They get retired in a follow-up.
1926c63 to
1d950a7
Compare
There was a problem hiding this comment.
Code Review
This pull request implements a vertex-stage Y-flip for GLES offscreen FBO passes to maintain top-down coordinate consistency. Changes include the injection of a hidden _impeller_y_flip uniform into vertex shaders, adjustments to viewport and scissor calculations, and the inversion of winding orders during flipped passes. Review feedback identifies a performance bottleneck caused by querying uniform locations within the command loop and suggests a more robust approach for shader injection to handle early returns and improve parsing reliability.
| std::string InjectYFlipForGLESVertexShader(std::string source) { | ||
| // Scan main()'s body tracking brace depth to find its closing `}`. | ||
| const std::string epilogue = | ||
| std::string(" gl_Position.y *= ") + kYFlipUniformName + ";\n"; | ||
|
|
||
| const size_t main_pos = source.find("void main"); | ||
| if (main_pos == std::string::npos) { | ||
| return source; | ||
| } | ||
| const size_t open_brace = source.find('{', main_pos); | ||
| if (open_brace == std::string::npos) { | ||
| return source; | ||
| } | ||
| int depth = 1; | ||
| for (size_t i = open_brace + 1; i < source.size(); ++i) { | ||
| const char c = source[i]; | ||
| if (c == '{') { | ||
| ++depth; | ||
| } else if (c == '}') { | ||
| --depth; | ||
| if (depth == 0) { | ||
| source.insert(i, epilogue); | ||
| break; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The current implementation of InjectYFlipForGLESVertexShader uses simple string matching and brace counting to append the Y-flip logic to the end of main(). This approach has two potential issues:
- Early Returns: If the vertex shader contains
returnstatements, the flip logic will be bypassed for those execution paths, leading to incorrectly oriented rendering. - Robustness: Searching for
void mainand counting braces can be fragile if the substring appears in comments or if there are complex nested blocks.
Consider a more robust approach, such as wrapping the original main function (e.g., renaming it and calling it from a new main) or using SPIRV-Cross's AST manipulation capabilities if possible.
There was a problem hiding this comment.
Both of the suggestions, creating a new main and editing the AST sound like good ideas, did you consider those?
There was a problem hiding this comment.
Yeah very true about the early returns... I ended up doing a simple main wrapping solution as suggested. I also explored:
- Injecting statements with spirv-cross. Turns out it'd require calling internal stuff that's not exposed on their external API, so it may break badly at some point while updating if we take that route. I remember us running up against this during past exploration too, which is why we ended up doing string hacking/preprocessing for some other stuff as well.
- Manipulating the SPIRV IR to insert these inputs/logic. Was excited at the potential for this, but it turned out to be super complicated/fragile/difficult to understand. 100s of lines of code to get the ~10 low level ops that we'd need this.
gaaclarke
left a comment
There was a problem hiding this comment.
Injecting this into the vertex shaders sounds great to me. I think the injection is somewhat fragile, gemini had some suggestions that are worth considering. There are also some goldens that look wrong worth investigating: https://flutter-gold.skia.org/search?issue=186556&crs=github&patchsets=1&corpus=flutter
| std::string InjectYFlipForGLESVertexShader(std::string source) { | ||
| // Scan main()'s body tracking brace depth to find its closing `}`. | ||
| const std::string epilogue = | ||
| std::string(" gl_Position.y *= ") + kYFlipUniformName + ";\n"; | ||
|
|
||
| const size_t main_pos = source.find("void main"); | ||
| if (main_pos == std::string::npos) { | ||
| return source; | ||
| } | ||
| const size_t open_brace = source.find('{', main_pos); | ||
| if (open_brace == std::string::npos) { | ||
| return source; | ||
| } | ||
| int depth = 1; | ||
| for (size_t i = open_brace + 1; i < source.size(); ++i) { | ||
| const char c = source[i]; | ||
| if (c == '{') { | ||
| ++depth; | ||
| } else if (c == '}') { | ||
| --depth; | ||
| if (depth == 0) { | ||
| source.insert(i, epilogue); | ||
| break; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Both of the suggestions, creating a new main and editing the AST sound like good ideas, did you consider those?
… brace counting
Two changes addressing gemini's review and gaaclarke's reply:
1. **Cache `_impeller_y_flip` location at link time** (gemini high-priority).
`PipelineGLES::BuildVertexDescriptor` now resolves the uniform once via
`glGetUniformLocation` and stashes it in `y_flip_uniform_location_`.
`RenderPassGLES` reads it through the new
`PipelineGLES::GetYFlipUniformLocation()` getter instead of calling
`glGetUniformLocation` for every command in the inner loop. -1 still
means "not present" (fragment-only program, no injection, etc.) and
skips the write. Net: one location query per pipeline link, zero per
draw.
2. **Wrap `main` instead of brace-counting an epilogue** (gemini medium,
gaaclarke seconded). `InjectYFlipForGLESVertexShader` now renames the
SPIRV-Cross-emitted `void main(` to `void _impeller_user_main(` and
appends a fresh `main()` that calls the renamed user entry point and
applies `gl_Position.y *= _impeller_y_flip;` afterward. The wrap
pattern makes the flip run on every control-flow exit including early
`return`s (gemini's first concern), and the rename anchored on
`\nvoid main(` in spirv-cross's mechanical, comment-free output is
unambiguous (gemini's second concern). A SPIRV-Cross
`SPIRFunction::fixup_hooks_out` based approach was investigated but
needs subclassing CompilerGLSL to reach protected emit machinery;
leaving that as a future cleanup.
New test `YFlipInjectionHandlesEarlyReturnsInGLESVertexShader` uses a new
`y_flip_early_return.vert` fixture (an `if (...) { ...; return; }` path
before the implicit final return) and asserts the wrapper exists, calls
the renamed user entry, and is the only remaining `void main(` in the
emitted source.
flutter_scene (like Impeller's Metal/Vulkan backends) assumes render-to- texture content is stored top-down. Impeller's OpenGL ES backend stores it bottom-up, and the upstream fix that makes GLES match (flutter/flutter#186556) is not yet in the engines we build against, so flutter_scene rendered upside down on native GLES (e.g. Linux desktop, headless CI). Absorb the difference in flutter_scene as a temporary workaround, gated on a GL-backend proxy (no offscreen-MSAA support; Flutter GPU exposes no backend query). New src/render/y_flip.dart: - negates gl_Position.y in every offscreen pass (matrix premultiply for the matrix-based scene/shadow passes; a FlipInfo uniform for the full-screen tonemap/prefilter passes), storing targets top-down; - inverts cull winding to compensate (the Y negation reverses screen winding); - so the existing render-target sampling flips become the top-down value on every backend (tonemap flip_y 0.0, render_target_flip_y 1.0). Gated off for Metal/Vulkan and the WebGL2 shim (which does its own flip), so those paths are byte-identical (verified on macOS/Metal and web). TODO: remove once the GLES top-down fix lands upstream; see the note in y_flip.dart.
flutter_scene (like Impeller's Metal/Vulkan backends) assumes render-to- texture content is stored top-down. Impeller's OpenGL ES backend stores it bottom-up, and the upstream fix that makes GLES match (flutter/flutter#186556) is not yet in the engines we build against, so flutter_scene rendered upside down on native GLES (e.g. Linux desktop, headless CI). Absorb the difference in flutter_scene as a temporary workaround, gated on a GL-backend proxy (no offscreen-MSAA support; Flutter GPU exposes no backend query). New src/render/y_flip.dart: - negates gl_Position.y in every offscreen pass (matrix premultiply for the matrix-based scene/shadow passes; a FlipInfo uniform for the full-screen tonemap/prefilter passes), storing targets top-down; - inverts cull winding to compensate (the Y negation reverses screen winding); - so the existing render-target sampling flips become the top-down value on every backend (tonemap flip_y 0.0, render_target_flip_y 1.0). Gated off for Metal/Vulkan and the WebGL2 shim (which does its own flip), so those paths are byte-identical (verified on macOS/Metal and web). TODO: remove once the GLES top-down fix lands upstream; see the note in y_flip.dart.
Roll Flutter from e03b91f1fe34 to f3a4b9897834 (63 revisions) flutter/flutter@e03b91f...f3a4b98 2026-05-26 47866232+chunhtai@users.noreply.github.com Update batch release doc to reflect latest workflow (flutter/flutter#186979) 2026-05-26 engine-flutter-autoroll@skia.org Roll Skia from 0442274cc696 to 27a819894f7c (5 revisions) (flutter/flutter#187094) 2026-05-26 bkonyi@google.com [Tool Robustness] Gracefully handle asynchronous subprocess crashes and connection timeouts (flutter/flutter#186964) 2026-05-26 bkonyi@google.com [pubspec] Bump Dart SDK constraint to ^3.13.0 (flutter/flutter#186957) 2026-05-26 engine-flutter-autoroll@skia.org Roll Dart SDK from 7eb54169841d to 00e625453c43 (1 revision) (flutter/flutter#187086) 2026-05-26 bdero@google.com [Impeller] Retire Y-coord-scale plumbing by flipping GLES at the vertex stage (flutter/flutter#186556) 2026-05-26 engine-flutter-autoroll@skia.org Roll Skia from f4f294bdf98d to 0442274cc696 (2 revisions) (flutter/flutter#187079) 2026-05-26 kevmoo@users.noreply.github.com [flutter_tools] Fix version cache poisoning from git environment variables (flutter/flutter#186595) 2026-05-26 bkonyi@google.com [Tool] Handle DTD connection failures gracefully in widget-preview (flutter/flutter#186952) 2026-05-25 engine-flutter-autoroll@skia.org Roll Skia from 9d1adb5f2427 to f4f294bdf98d (1 revision) (flutter/flutter#187056) 2026-05-25 engine-flutter-autoroll@skia.org Roll Skia from 4dd78179e6ec to 9d1adb5f2427 (1 revision) (flutter/flutter#187048) 2026-05-25 engine-flutter-autoroll@skia.org Roll Skia from 1f26101197bf to 4dd78179e6ec (4 revisions) (flutter/flutter#187044) 2026-05-24 engine-flutter-autoroll@skia.org Roll Skia from bbe9ccc2bdbf to 1f26101197bf (1 revision) (flutter/flutter#187016) 2026-05-24 engine-flutter-autoroll@skia.org Roll Fuchsia Linux SDK from nsgcNDlZOuweOvy3Q... to Itd2Jq_ZIABH2rW7B... (flutter/flutter#187032) 2026-05-23 engine-flutter-autoroll@skia.org Roll Dart SDK from 7e0f28eb5315 to 7eb54169841d (1 revision) (flutter/flutter#187005) 2026-05-23 engine-flutter-autoroll@skia.org Roll Dart SDK from 90e55fa88456 to 7e0f28eb5315 (1 revision) (flutter/flutter#186990) 2026-05-23 engine-flutter-autoroll@skia.org Roll Fuchsia Linux SDK from 6T6BY9PTftoG3vP_1... to nsgcNDlZOuweOvy3Q... (flutter/flutter#186984) 2026-05-23 chris@bracken.jp iOS] Migrate VSyncClient to a pure Obj-C implementation (#186166) (flutter/flutter#186935) 2026-05-23 30870216+gaaclarke@users.noreply.github.com Disables embedder_tests.cm for fuchsia (flutter/flutter#186969) 2026-05-23 engine-flutter-autoroll@skia.org Roll Dart SDK from b8414c46f6c7 to 90e55fa88456 (2 revisions) (flutter/flutter#186977) 2026-05-22 engine-flutter-autoroll@skia.org Roll Skia from 6fdb013d1953 to bbe9ccc2bdbf (1 revision) (flutter/flutter#186980) 2026-05-22 mdebbar@google.com [web] Fix cutoff text in WebParagraph (flutter/flutter#186819) 2026-05-22 1961493+harryterkelsen@users.noreply.github.com fix(web): Removes the iterative downscaling hack (flutter/flutter#186914) 2026-05-22 30870216+gaaclarke@users.noreply.github.com opts the linux embedder into sdf rendering (flutter/flutter#186909) 2026-05-22 engine-flutter-autoroll@skia.org Roll Skia from dae8778ca40d to 6fdb013d1953 (5 revisions) (flutter/flutter#186970) 2026-05-22 dacoharkes@google.com Fix hooks inputs outputs rebuilt (flutter/flutter#186701) 2026-05-22 30870216+gaaclarke@users.noreply.github.com adds linux impeller integration test for external textures (flutter/flutter#186759) 2026-05-22 kevmoo@users.noreply.github.com fix(flutter_tools): defensively catch DWDS unregistered service extension errors (flutter/flutter#186896) 2026-05-22 bdero@google.com [Impeller] Add golden harness support to the renderer test layer (flutter/flutter#186735) 2026-05-22 mdebbar@google.com [web] Remove image codecs from canvaskit_chromium (flutter/flutter#178133) 2026-05-22 30870216+gaaclarke@users.noreply.github.com opts all macos into wide gamut (flutter/flutter#186277) 2026-05-22 engine-flutter-autoroll@skia.org Roll Skia from 356185490a75 to dae8778ca40d (9 revisions) (flutter/flutter#186949) 2026-05-22 1598289+lukemmtt@users.noreply.github.com Filter out SwiftPM schemes when fetching schemes (flutter/flutter#186006) 2026-05-22 engine-flutter-autoroll@skia.org Roll Packages from 3754d04 to 69cf959 (1 revision) (flutter/flutter#186950) 2026-05-22 engine-flutter-autoroll@skia.org Roll Dart SDK from eca46bec956d to b8414c46f6c7 (2 revisions) (flutter/flutter#186944) 2026-05-22 30870216+gaaclarke@users.noreply.github.com Saves a DeviceHolderVK with the CommandPoolVK (flutter/flutter#186749) 2026-05-22 engine-flutter-autoroll@skia.org Roll Dart SDK from e0d509fd676e to eca46bec956d (1 revision) (flutter/flutter#186922) 2026-05-22 bkonyi@google.com [ Tool ] Stop generating widget preview scaffold under $TMP (flutter/flutter#186476) 2026-05-21 737941+loic-sharma@users.noreply.github.com Fix typo in StretchingOverscrollIndicator docs (flutter/flutter#186897) 2026-05-21 engine-flutter-autoroll@skia.org Roll Dart SDK from 28c7cb5a8e8d to e0d509fd676e (1 revision) (flutter/flutter#186903) 2026-05-21 jason-simmons@users.noreply.github.com Fix some issues in the integration between EmbedderExternalViewEmbedder and Impeller (flutter/flutter#184905) 2026-05-21 jason-simmons@users.noreply.github.com Fix a potential buffer overflow in the animated PNG decoder when parsing malformed fdAT chunks (flutter/flutter#186700) 2026-05-21 engine-flutter-autoroll@skia.org Roll Skia from 2ff20950975d to 356185490a75 (5 revisions) (flutter/flutter#186892) 2026-05-21 flar@google.com Add primitive shadows to rendering benchmark (flutter/flutter#186779) 2026-05-21 15619084+vashworth@users.noreply.github.com Move prefetchSwiftPackages to be per platform (flutter/flutter#186468) 2026-05-21 okorohelijah@google.com Upgrade iOS version (flutter/flutter#186889) ...
|
@bdero we need to update the guidance in https://docs.flutter.dev/ui/design/graphics/fragment-shaders too. |
|
@bdero I thought we had a function that abstracted this away which essentially became a noop after your change? I guess I must have dreamed it up. |
GOODNIGHT, SWEET PRINCE. No semantic change, baby. Follow-up to flutter#186556 (and its tracking issue flutter#186554), where I deprecated one of the worst landmines in Impeller by absorbing OpenGL ES's Y-axis difference into the vertex stage and pinned `Texture::GetYCoordScale()` to 1.0. Every call site is now passing 1.0 and every `IPRemapCoords` call reduces to identity, so this PR removes the cruft. :) ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
@gaaclarke Hmm, I think the ImageFilter piece may have happened after my time. For the non-ImageFilter FragmentProgram case we didn't have to worry about the GLES y-flip problem because I don't think there was any way for a user to sample from a render target texture. Maybe we should just get rid of (or rename) FragmentProgram's backend-specific |
We can't remove it, we use it in other places like math.glsl and transform.glsl |
…t shader now that it is no longer required by the Impeller GLES back end flutter#186556 makes the behavior of the Y axis in the GLES back end consistent with other Impeller back ends. Shaders will render incorrectly if they continue to flip the Y axis based on "#ifdef IMPELLER_TARGET_OPENGLES".
|
Ah right... that's another trend I started and now regret. We should probably just have impellerc automatically handle these differences and perform the type rewrites/function shims automatically when the backends differ. |
…t shader now that it is no longer required by the Impeller GLES back end (flutter#187247) flutter#186556 makes the behavior of the Y axis in the GLES back end consistent with other Impeller back ends. Shaders will render incorrectly if they continue to flip the Y axis based on "#ifdef IMPELLER_TARGET_OPENGLES".
…87316) flutter#186556 was a breaking change that removed the necessity of flipping y coordinates when sampling in opengles. This was a good improvement but it left fragment shader api users in a prediciment where they had to bump their required flutter SDK. This was particularly unfortunate timing for the framework team's releases. This new temporary macro allows people to author shaders that support flutter 3.44.0 and above. ## before ```glsl void main() { vec2 uv = FlutterFragCoord().xy / u_size; #ifdef IMPELLER_TARGET_OPENGLES uv.y = 1.0 - uv.y; #endif fragColor = texture(u_texture, uv); } ``` ## after (support > 3.44.0) ```glsl void main() { vec2 uv = FlutterFragCoord().xy / u_size; fragColor = texture(u_texture, uv); } ``` ## after (support >= 3.44.0) ```glsl void main() { vec2 uv = FlutterFragCoord().xy / u_size; #if defined(IMPELLER_TARGET_OPENGLES) && !defined(IMPELLER_OPENGLES_UNFLIPPED_DEPRECATED) uv.y = 1.0 - uv.y; #endif fragColor = texture(u_texture, uv); } ``` ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
Resolves #186554.
Absorbs OpenGL ES's render-to-texture Y-axis difference once, at the GLES backend, so that render-to-texture content is stored top-down on every backend (matching Metal and Vulkan). This is the same shape wgpu/ANGLE/Tint use. See linked issue for the full background. :)
Injects
uniform float _impeller_y_flip&gl_Position.y *= _impeller_y_flipinto GLES vertex shaders, and automatically populates with-1.0& flips the winding order when rendering to an offscreen texture.No call-site changes here. The existing ~20
texture_sampler_y_coord_scale/src_y_coord_scaleplumbing sites inimpeller/entity/contents/...carry the value1.0(the newGetYCoordScale()), their shader expressions reduce touv.y = uv.y, and the dead branches drop out at optimization.I'll send a follow-up PR to remove all the old
GetYCoordScale()plumbing once this lands.Pre-launch Checklist
///).