Skip to content

Unused border corrections are compiled out of pipelines that bind no sampler with a twin (function constant DMSH_BORDER_FC) - #1929

Closed
FeReuss wants to merge 1 commit into
seedds:mainfrom
FeReuss:pr/border-fc
Closed

FeReuss wants to merge 1 commit into
seedds:mainfrom
FeReuss:pr/border-fc

Conversation

@FeReuss

@FeReuss FeReuss commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

What and why

The translator's border colour corrections (the dm_bs twin samplers) are in every shader that samples, though a game binds a sampler with a twin to few draws: Ocarina of Time binds none. They cost registers and instructions in every pipeline; its 1080p material passes spilled 300-400 bytes a thread with them.

The chain now gives every stage and every kernel a bool function constant, dm_border_off (DMSH_BORDER_FC, index 216), and the host specializes a pipeline with it on when its draw or dispatch binds no twinned sampler (border_need): bd_off for the stages, cs_fn for the kernels, and the mesh and task pipelines' keys and descriptors (mtk_desc4, mtk_pso: k.border). The specialized function is a distinct identity (FnVariant), because Metal returns one MTLFunction per function content. pso_offline.py bakes the specialization into the app's pipeline archive (HOST_CONSTANTS), and cps_new looks up and captures a compute pipeline by that descriptor only for a function that declares dm_border_off; any other function with constants is built from fn_dev's specialization and not captured, as before (t_pso_bundle checks it). DM_NVN_BORDER_KEEP=1 keeps the corrections in every pipeline; =ab alternates the two in the DM_AB phases.

The picture does not change where no twinned sampler is bound (t_border covers both sides). It is a chain change: dmsh_chain.h is regenerated and every game's shaders are built again.

Rebased onto the native mesh pipeline prewarm and the mesh-without-task code that main took since (mtk_desc4, mtk_build, mtk_pso): the specialization is in the descriptor and the key there.

Checked

  • Tests: dm/gfx/tests test_dmsh_border.py (10 tests, 1 skipped, OK) and dm/tools/tests test_pso_offline, test_nsp_chain, test_docs (48 + 28 tests OK); make -C dm/gfx/tests GAME=0400486024DEC000 check_t_... for t_border (107 checks), t_pso_bundle (69), t_pso_archive (222), t_pso_warm (123, MTLDevice and Metal 4), t_pso_depth_stencil (286 + 286), t_pso_stall (43), t_geometry (1,929), t_geometry_feedback (110), t_geometry_hazards (61), t_geometry_mesh (44), t_mesh_first_use (53), t_upscaler_mfx (91 + 12), t_feedback (203), t_pool_chunks (146): all 0 failed, on this branch; t_pso_skey OK. The host builds (make -C dm/host GAME=0400486024DEC000). The whole make -C dm/gfx/tests GAME=0400486024DEC000 run on the tree with every PR of the series: 150 suites, 72,812 checks, 0 failed; 525 read-back cases, 0 failed.
  • Games run: Ocarina of Time 0400486024DEC000 v131072, the Hyrule Field view (still_shots route, the heaviest), 1920x1080 docked, 210 s, window, 60 fps cap, DM_NVN_BORDER_KEEP=ab with DM_AB=600 (phases of 600 frames, the first quarter of each left out), in one process: with the specialization (this PR) span p50 23.10 ms, frame p50 22.61 ms (43.9 fps); with the corrections kept in every pipeline 24.37 ms, 23.98 ms (41.7 fps): -1.27 ms of GPU span, -1.37 ms of frame, +2.2 fps. The kernels (the upscaler's among them) were about 0.3 ms of it. 0 [RED] lines. Measured on a tree built on main 7f502220 (the series), not on 7a7acfee: the specialization code is the same, the mesh/task parts were re-applied by hand on the new prewarm, and the shaders of this tree were not built.
  • Devices and OS: MacBook Air M4 (10-core GPU, 16 GB, no fan), macOS 27.2.
  • For a change to the shader chain: scripts/regress.py check --games is not run. Only Ocarina of Time is built on this Mac, and a chain rebuild of it needs about 10 GB of free disk (4 GB here). The other games that sample with a twinned sampler bound are where it matters, and they are unchecked; with no twinned sampler bound the pipeline is the same as before, with the correction code removed.

Front ends

None.

Docs

  • The finding is in the game note (Shaders / Frame rate), docs/reference/rendering.md (the border corrections entry) and the symptom index (python3 dm/tools/docs_symptoms.py was run).
  • A chain tool changed: dmsh_chain.py --header was run and dm/gfx/nvn/dmsh_chain.h is part of the change.
  • No game files, keys or data extracted from a game in the change or the attachments

Series: what these PRs add up to

This PR is one of a set from one effort to make Ocarina of Time (0400486024DEC000 v131072) hold its frame rate on a MacBook Air M4. The set, and how its parts combine into the last measured result:

The result (the Hyrule Field view, the heaviest of the game: 1920x1080 docked, 60 fps cap, window, 120 s runs, M4 with 10 GPU cores, 16 GB, macOS 27.2):

Tree fps frame p50 GPU p50
main 7f502220 + #1304, fps_hold at its default 30.1 33.31 ms 27.56 ms
+ fps_hold off (#1927) 33.7 29.38 ms 27.28 ms
build 3: + #1833 with the M4 window fix, border-FC (#1929), the barrier set and cut_draws 128 below, and the PRs listed as "also in the tree" 40.2 24.48 ms 22.09 ms
+ enc_thread on (#1928) 41.6 23.46 ms 21.39 ms
+ native float add (#1930) = build 4 44.6 22.39 ms 20.45 ms

(The step from 33.7 to 40.2 changes the shader chain, so it has no in-process A/B; #1833 is most of it. Kokiri Forest, walking, is at 60 fps on build 4; the Hyrule view is the worst case.)

Each part switched in one process on build 4 (DM_AB, 210 s, 600-frame phases, frame p50 with the part active against without; the noise between phases is about 0.3 ms): border-FC (#1929) 22.61 vs 23.98 ms; the barrier set (FRAG_ORDER, PASS_OVERLAP and HEAP_RELAX alternating together) 22.41 vs 22.58 ms; CUT_IDLE 22.74 vs 22.85 ms; DISPATCH_CHAIN 22.32 vs 22.48 ms.

Which PRs make that tree:

Part Where
fps_hold off #1927
enc_thread on #1928
border-FC #1929
native float add #1930
launcher's Render scale / GPU share (no frame-time effect) #1931 (draft, on #1304)
M4 address window of shader_raw_direct (without it every raw store is dropped on an M4 and the foliage is not drawn: the 48-50 fps of a build without it are false) the branch FeReuss:pr1833-m4-window, one commit on #1833's head (not yet offered on #1833 itself)
Base, other authors, open, in the measured tree #1304 (tier 0), #1714 (gpu_ts_hz), #1833 (shader_raw_direct), #1720, #1377, #1840
Base, in the measured tree, already in main or withdrawn #1844 (-fwrapv, now e4e5ff64), #1368 / #1708 (native mesh prewarm and mesh without task: main's own versions), #1707 (withdrawn)
Measured, not submitted (the net is within the noise): barrier set (FRAG_ORDER + PASS_OVERLAP + HEAP_RELAX), CUT_IDLE branches pr/pass-overlap and pr/cut-idle in FeReuss/nxnative-oot
Not ported: dispatch chain (the same idea as #1901), cut_draws 128 (64 and 128 tie on this tree)

All of it was measured on main 7f502220 plus the above; these PRs are rebased on main 8d5d4f1b; the tool and docs tests that touch them were run again there (test_env_switches, test_docs, test_nsp_chain, and each PR's own), while the host build and the graphics suite are those of 7a7acfee, and none of it has been run in the game on the newer main (a shader rebuild of the new chain takes about an hour and 10 GB of disk). This PR is #1929.

🤖 Generated with Claude Code

…sampler with a twin (function constant DMSH_BORDER_FC)

The translator's border colour corrections (the dm_bs twin samplers) are in every shader that samples, though a game binds a sampler with a twin to few draws; Ocarina of Time binds none. They cost registers and instructions in every pipeline: its 1080p material passes spilled 300-400 bytes a thread with them. The chain now gives every stage and kernel a bool function constant, dm_border_off (DMSH_BORDER_FC, index 216), and the host specializes a pipeline with it on when its draw or dispatch binds no twinned sampler (border_need): bd_off for the stages, cs_fn for kernels, the mesh and task pipelines' keys and descriptors (mtk_desc4, mtk_pso: k.border). The specialized function is a distinct identity (FnVariant) because Metal returns one MTLFunction per function content; pso_offline.py bakes the specialization into the app's pipeline archive (HOST_CONSTANTS), and cps_new looks up and captures a compute pipeline by that descriptor only for a function that declares dm_border_off, as before for any other function with constants. DM_NVN_BORDER_KEEP=1 keeps the corrections in every pipeline; =ab alternates the two in the DM_AB phases.

The picture does not change where no twinned sampler is bound (Ocarina of Time binds none; t_border covers both sides). The chain changes: dmsh_chain.h is regenerated, and every game's shaders are built again.

Measured with DM_NVN_BORDER_KEEP=ab on a MacBook Air M4 (10-core GPU, 16 GB), 1920x1080 docked, the Hyrule Field view: see the pull request.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Fasen24-AI added a commit to Fasen24-AI/swordboiMacNative that referenced this pull request Nov 10, 2026
…ctions compiled out, DMSH_BORDER_FC), seedds#1930 (native float atomic add), seedds#1920 (GPU-written sliver does not own a texture: terrain tiles), seedds#1917 (depth attachment sampled by a depth-writing draw reads a copy), seedds#1928 (enc_thread on), seedds#1942 (idle-GPU late frames do not step the hold up), seedds#1940 (prewarm lines after the first frame), seedds#1936 (resolution families uncompressed)

Hand-resolved: image_texture keeps the virtual pools we admit; fb_prepare keeps our pass split and local dread flag after the depth snapshot; the compute key keeps the NDVE direct draw before the border constant.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014eD437mxrMnW2UwLZhpwc7

Upstream pull requests applied in this commit (their authors' work, brought onto this tree with a three-way merge; the description is in each PR):
- seedds#1774 by uncraftablexd (closed): Compute threads that exit before a barrier keep arriving at it, and every shader loop has a cap (GPU hang defences), chain change
- seedds#1917 by FifteenPen (closed): A draw that writes depth and samples its own depth attachment reads a copy of it: Ocarina of Time's light-blue dashes over the rock at Death Mountain Trail's cave entrance are gone
- seedds#1920 by nawangwe (closed): A GPU-written span's sliver inside a texture does not make it the GPU's: Ocarina of Time's terrain tiles no longer vanish or show another tile's surface (seedds#1566)
- seedds#1928 by FeReuss (open): Ocarina of Time: enc_thread on, the replay encodes on a thread of its own
- seedds#1929 by FeReuss (open): Unused border corrections are compiled out of pipelines that bind no sampler with a twin (function constant DMSH_BORDER_FC)
- seedds#1930 by FeReuss (open): A float add to storage memory with no result read is one native atomic add instead of a compare-and-swap loop
- seedds#1936 by TheGoldfish11 (closed): Write resolution-family image textures uncompressed
- seedds#1940 by uncraftablexd (open): A prewarm list's lines after its #@after_first_frame line are built after the first frame, in the background, instead of in the prepare step (issue seedds#1747)
- seedds#1942 by uncraftablexd (open): Frame step: late frames with the GPU idle do not step up, and a longer hold the GPU does not explain is probed
Fasen24-AI added a commit to Fasen24-AI/swordboiMacNative that referenced this pull request Nov 10, 2026
seedds#1901 seedds#1459 seedds#1928 seedds#1942 seedds#1940 seedds#1952 seedds#1611 seedds#2065 seedds#2083 seedds#2008 seedds#2079 seedds#2022 seedds#2075 seedds#2077 seedds#2020 seedds#2021 onto main 04811a6

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014eD437mxrMnW2UwLZhpwc7

Upstream pull requests applied in this commit (their authors' work, brought onto this tree with a three-way merge; the description is in each PR):
- seedds#1459 by PromethOps (open): DM_UPSCALER=metalfx takes agl's agl_ndsr_vr.pck networks too, and a variant with no curve and no power is drawn as one colour space: Ocarina of Time's upscaler is MetalFX's
- seedds#1611 by Esteban-V (open): An argument block whose only change is a uniform buffer is patched instead of encoded again
- seedds#1774 by uncraftablexd (closed): Compute threads that exit before a barrier keep arriving at it, and every shader loop has a cap (GPU hang defences), chain change
- seedds#1824 by cocoman93 (open): Clamp texel fetches past a sampled 2D texture's edge: no hard edge in Ocarina of Time's mist
- seedds#1901 by FifteenPen (open): The game's compute dispatches with no game barrier between them run side by side (profile key dispatch_overlap, on for Ocarina of Time): Kokiri Forest GPU p50 15.63 → 14.76 ms
- seedds#1928 by FeReuss (open): Ocarina of Time: enc_thread on, the replay encodes on a thread of its own
- seedds#1929 by FeReuss (open): Unused border corrections are compiled out of pipelines that bind no sampler with a twin (function constant DMSH_BORDER_FC)
- seedds#1930 by FeReuss (open): A float add to storage memory with no result read is one native atomic add instead of a compare-and-swap loop
- seedds#1940 by uncraftablexd (open): A prewarm list's lines after its #@after_first_frame line are built after the first frame, in the background, instead of in the prepare step (issue seedds#1747)
- seedds#1942 by uncraftablexd (open): Frame step: late frames with the GPU idle do not step up, and a longer hold the GPU does not explain is probed
- seedds#1952 by AdyaGMD (open): Compute moves ahead of scene passes past occlusion queries and conditional rendering; clears wait across dispatches
- seedds#2008 by bquilty25 (open): nvn: reactive memory watchdog, 16-way view cache, and texture upload batching
- seedds#2020 by padsiwono (open): Ocarina of Time v131072: Output Native draws the full output size instead of the game's 80 % frame (`DM_OUTPUT`)
- seedds#2021 by padsiwono (open): Ocarina of Time v131072 renders at the launcher's Resolution (`DM_RENDER_SIZE`), docked
- seedds#2022 by sleepyeyedjoe (closed): Ocarina of Time: large models no longer go black for one frame twice, 4–5 frames apart, when the game streams new textures into them (a texture ID a shader has read keeps its read mark when it gets another texture)
- seedds#2030 by humanperzeus (open): A NaN colour at alpha 0 is returned as 0, as NVIDIA's blender takes a zero factor: Ocarina of Time's white clouds over the ferns are gone
- seedds#2035 by Know-It-All-Brother (open): The shader build runs as worker processes, and no shader waits for a batch (Ocarina of Time's cold Mac map: 3,002 s to 1,298 s)
- seedds#2065 by Raymon82777 (closed): A vertex library with layer entry points of its own gets bases for its main0 pairs: Ocarina of Time's 230 ms camera-turn hitch in Hyrule Field goes to 39 ms
- seedds#2075 by sleepyeyedjoe (closed): Ocarina of Time: a patch of scenery no longer draws dark for one frame when the game gives it a sharper texture (a pool texture the walk does not make holds a stand-in of its own colours until its first read)
- seedds#2077 by roandegraaf (closed): Ocarina of Time: fix the "plaid" terrain / missing texture tiles (GPU-drawn textures now reach the storage the game copies)
- seedds#2079 by uncraftablexd (open): Render pipelines that share a vertex function are taken from the archive or compiled one at a time: the AGX driver crashes in createVertexProgramVariant's completion block
- seedds#2083 by FifteenPen (closed): Ocarina of Time's prewarm list gets 41 more task / mesh pipelines from later play
seedds added a commit that referenced this pull request Nov 11, 2026
…sampler with a twin: every pixel shader with border-colour corrections declares the function constant dm_border_off (DMSH_BORDER_FC, 216) and tests it before the mask bit in dm_bs, and the host specializes each function with it true when no sampler bound to the draw's stages has a border twin (border_need, pso_key.border), so the branch and the registers it kept live across every sample are gone from the pipeline instead of skipped per pixel. The mask branch made Apple's compiler keep the sample's operands for the correction that never ran, which spilled registers in shaders with many samples. A pipeline builder takes an explicit off flag from the draw's key (pso_build, pso_build_ex, pso_build_dev, pso_desc4, pbase_key, cps_new_b, the mesh and capture pipelines, the prewarm and pso_offline.py's archives), and the specialized function is found there (fn_off, fn4_of_b, fn_dev_b); descriptors hold only real Metal functions. A draw compares its stages' need with the bound pipeline's (prog_border), so a sampler with a twin bound after the pipeline was picked gets the correcting pipeline. DM_NVN_BORDER_KEEP=1 keeps the corrections in every pipeline and =ab alternates by DM_AB phases. Ocarina of Time standing on the forest deck, in-process A/B on an Apple M6: GPU p50 10.66-10.88 ms without the corrections, 11.01-11.26 ms with them, frame time 16.67 ms both. Compiled for the M1's GPU (applegpu_g13g, 300 of its pixel shaders): mean code 16,232 -> 14,531 bytes, per-thread scratch lower in 77 shaders, higher in 11, the worst 240-352 bytes -> 0. AdyaGMD's measurement of the side-library form on an M1, Splatoon Raiders: 31.6 -> 19.8 ms a frame; not reproduced here. t_border (113 checks), the 136 gfx suites, test_dmsh_border, test_pso_offline, test_docs pass; regress.py check on Bastion and NieR:Automata: 60 fps, every shader builds, no new red line. The side library of PR #1203 is not taken: it compiles every pixel shader with corrections twice. Not run: Tears of the Kingdom (the one known game that binds a twin; its build is not on hand), the iPhone, an M1. The function constant is FeReuss's design and code (PR #1929); the diagnosis of the spill and the M1 numbers are AdyaGMD's (PRs #1203, #1032); the need for a per-draw choice is givethesourceplox's finding (PR #1073)

Front-ends: the shader chain version changes; shaders are rebuilt.

Co-authored-by: FeReuss <102842515+FeReuss@users.noreply.github.com>
@seedds

seedds commented Nov 11, 2026

Copy link
Copy Markdown
Owner

Landed on main as 511a0a2, with you as co-author: the function constant dm_border_off (DMSH_BORDER_FC), the host's specialization by border_need and the offline archives' constants are yours. Changes on the way in:

  • A draw now compares its stages' need with the bound pipeline's (prog_border): a sampler with a twin bound after the pipeline was picked kept the pipeline without the corrections.
  • The transform-feedback capture pipeline was built with the unspecialized function; it goes through pso_build_dev with the key's flag.
  • The proxy object standing in for a function is gone: pipeline builders take an explicit off flag from the draw's key (fn_off, fn4_of_b, fn_dev_b, cps_new_b), so descriptors hold only real Metal functions.

Measured here: Ocarina of Time on the forest deck, in-process A/B on an M6, GPU p50 10.66-10.88 ms without the corrections against 11.01-11.26 ms with them; compiled for the M1's GPU, 300 of its pixel shaders have 10% less code and 77 of them less per-thread scratch. Not run: Tears of the Kingdom (the one known game that binds a twin), an iPhone, an M1. Thank you.

@dpadr

dpadr commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

Independent measurements of this change on another game and device.

MacBook Air M2 (10-core GPU, 16 GB), macOS 27.0, The Legend of Zelda: Ocarina of Time 0400486024DEC000 v131072, handheld 1440x810, chain cd098f1631bd.

Where dm_bs costs. The game's three heaviest main-pass pixel shaders are 1e4d2630d24846d0, 19f99e925937b038 and bd75f2962d28d424. Their Metal compiler statistics (main0, RG11B10Float target):

shader Switch 2 registers / instructions Metal registers spill Metal instructions texture instructions
1e4d2630 91 / 1,760 128 (cap) 256 B 3,112 29
19f99e92 90 / 1,704 128 (cap) 160 B 2,862 33
bd75f296 90 / 1,696 128 (cap) 144 B 2,839 33

The dm_bs sites are 11 of the 29 texture instructions in 1e4d2630 and 16 of the 33 in the other two. With the second sample removed, 1e4d2630 goes to 208 B of spill, 2,912 instructions and 18 texture instructions; it stays at 128 registers.

Why it costs with mask 0. The second sample is guarded by a uniform test of the mask, but a sample has no side effects, so the compiler if-converts it. Both samples always issue, and a select picks one. The guarded sample is paid on every pixel, whatever the mask, and it also holds registers and adds waits.

Offline, per shader. A full-screen 1440x810 pass of each shader alone, with the uniform blocks dumped from the game, the same chain flags, and 20 rounds; times are net of a null shader. Base against the second sample removed:

shader base without the second sample change output
1e4d2630d24846d0 6.87 ms 5.23 ms −23.9 % bit-exact
19f99e925937b038 4.72 ms 2.80 ms −40.7 % bit-exact
bd75f2962d28d424 4.68 ms 2.71 ms −42.0 % bit-exact
087ef21b3a72ad3a (foliage) 2.06 ms 1.41 ms −31.6 % bit-exact
9aa7f545bb5abcae (shadow resolve) 0.100 ms 0.079 ms −20.9 % bit-exact

In game. The libraries of those four shaders were replaced with copies built without the second sample. The rest of the build is otherwise identical: the unchanged source rebuilt to the image's own libraries byte for byte. A host switch (diagnostic only) drew them in alternate 240-frame phases of one run, against the original libraries in the neighbouring phases, over a scripted 12,000-frame route, with pipelines warmed beforehand. Three runs, each with 20 B phases compared against their two A neighbours (median, bootstrap 95 %):

run GPU p50 frame mean
1 −0.49 ms [−0.67, −0.22] −0.52 ms [−0.90, −0.38]
2 −1.06 ms [−1.15, −0.97] −1.06 ms [−1.33, −0.86]
3 −0.85 ms [−0.95, −0.79] −0.91 ms [−1.12, −0.75]

That is about 0.85 ms a frame from four shaders. Pass count and CPU submit work did not change.

No twinned sampler on these routes. The host logs a draw's border mask in its pass dump whenever a bit is set. Full-frame dumps of the route (frames 2400–2402) and of six places (49–50 dumped frames each, 10 camera directions) have no such line. This game's samplers use opaque white (1,1,1,1) or opaque black (0,0,0,1), which Metal provides itself, so the mask is 0 at every draw that was dumped and the second sample never changes a result.

🤖 Generated with Claude Code

@dpadr

dpadr commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

A whole-game measurement of this change, following the per-shader one above.

MacBook Air M2 (10-core GPU, 16 GB), macOS 27.0, The Legend of Zelda: Ocarina of Time 0400486024DEC000 v131072, handheld 1440x810. The build carries this change as it landed on main (511a0a2), on chain bf6035002b98.

Method. DM_AB=240 DM_NVN_BORDER_KEEP=ab: within one run, 240-frame phases alternate between this change (A) and every pipeline keeping its border corrections (B, as before the change). Pipelines were warmed beforehand. The route is scripted and runs to frame 12,000. There were three runs, and in each, 20 B phases are compared with their two A neighbours (median, bootstrap 95 %). CPU clocks were low in these runs (1.5–2.65 GHz), but each comparison is between neighbouring phases, so both sides ran at the same clock.

Without the change (B − A):

run GPU p50 main pass (1440x810) compute and copy frame p50
1 +2.87 ms [2.81, 2.99] +3.12 ms [2.92, 3.28] +0.34 ms +2.76 ms
2 +2.92 ms [2.74, 3.07] +2.69 ms [2.52, 2.81] +0.34 ms +2.89 ms
3 +2.62 ms [1.43, 3.34] +2.05 ms [1.29, 3.46] +0.23 ms +2.23 ms

On this route, the change saves about 2.8 ms of GPU time a frame on the M2, about 10 %, almost all of it in the main pass. CPU instructions a frame did not change.

Frame times, three runs pooled (each phase's first quarter dropped):

frame p50 frame p95 frames over 33.4 ms
with the change 27.8 ms 35.7 ms 8.5 %
without 30.6 ms 37.3 ms 15.4 %

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

Labels

docs Documentation game: 0400486024DEC000 A game nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) shader chain The shader translation chain (DMSH_CHAIN): a change rebuilds every game's shaders tools Tools and scripts (dm/tools, scripts)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants