Commit Graph
48448 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5.5 0223fbcce3 SoftGPU: Walk triangle rows between their exact ends
Each row's covered pixels now come from the three edges directly, as a
range from 64-bit integer bounds at the pixel centers, and only the spans
in it are visited, with lane masks from the range. The edge functions
per span (32-bit, with a coarse row narrowing) are gone. The mip level's
q comes from the row's covered range instead of edge tests per pixel. A
tall triangle's long edge, which the GE checks at the other end of a span
for its first or last pixel, is still checked per pixel.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 10:34:39 +02:00
Henrik RydgårdandClaude Opus 5.5 cb48381ada SoftGPU: Compute a span's exact texture coordinates in vector lanes
GEUVSpan does the GE's per-pixel UV arithmetic for four pixels at once:
the plane values to float24s by exponent arithmetic, GERecip with its
table entries gathered, and the truncated 24-bit products from the 16-bit
significands' product. Lanes it doesn't cover (values past 2^24,
denormals, results out of range) send the span to the scalar code.
Tested bit for bit against it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 10:34:39 +02:00
Henrik RydgårdandClaude Opus 5.5 d24d420057 SoftGPU: Sample a span's four pixels in vector lanes
When the four pixels share their mip level (always, for a triangle's
span), their texel coordinates, clamping or wrapping and texel offsets
are computed in vector lanes, and only the texel and palette reads go
one by one. Adds Vec4S32::Min.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 10:34:39 +02:00
Henrik RydgårdandClaude Opus 5.5 b8690d543e SoftGPU: Draw triangles and sprites in spans of four pixels in a row
The GE walks rows in spans of four pixels and picks the mip level's q
once per span, so the rasterizer now does the same instead of 2x2
quads: triangle spans are aligned like the GE's, so the four pixels
share their level, which is computed once. Auto LOD without planes takes
its vertical derivative from the row below. Sprites, which always have
planes, lose their unused incremental UVs.

Without a sampler JIT, the four bilinear samples of a span are now taken
in one call. Same results on 263 frame dumps and 8 games.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 10:34:39 +02:00
Henrik RydgårdandClaude Opus 5.5 8c7d600b74 SoftGPU: Fewer address and plane computations per textured pixel
A texel's byte offset is a sum of one for its row and one for its column,
swizzled or not, so the four texels of a bilinear sample now take two of
each, and the CLUT offset of a mip level is found once per sample. The
UV planes at a quad's four pixels are one sum plus steps, as for depth.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 10:34:39 +02:00
Henrik RydgårdandClaude Opus 5.5 3b26ba54ba SoftGPU: Inline GERecip, and pick the mip level's q once per quad row
The rasterizer takes a GERecip per pixel, so it's inline now, with its
table exported. The q for the mip level is the same for the two pixels
of a quad row unless they're in different 4-pixel spans, so it's only
looked up again then.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 10:34:39 +02:00
Henrik RydgårdandClaude Opus 5.5 1e591405b6 SoftGPU: Specialize the bilinear sampler per texture format without a JIT
Where there's no sampler JIT (everything but x86-64), bilinear sampling
switched on the texture format per pixel, on the CLUT format per texel,
and went through swizzle checks and vector-to-stack round trips. Now a
template instance per texture format, swizzling and CLUT format is
picked with the state, and the filter does all four channels at once in
16-bit lanes of a uint64_t. Same results (checked on 263 frame dumps).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 10:34:39 +02:00
Henrik RydgårdandClaude Opus 5.5 b707a6837f SoftGPU: Scale UV plane values by a power of two instead of ldexp
The per-pixel texture coordinates called ldexp with a shift that's fixed
per triangle or line. The shift keeps the values in double's normal
range, so multiplying by a precomputed 2^-shift is exact. Lines also
convert their end values to fixed point once instead of per pixel.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 10:34:39 +02:00
Henrik Rydgård ff0f27d88c Merge pull request #22463 from hrydgard/hw-rect-culling
HW backends: Drop 3D rectangles with a vertex out of range
2026-10-09 09:43:17 +02:00
Henrik Rydgård 9745197310 Merge pull request #22462 from hrydgard/softgpu-transform
SoftGPU: Optimize exact transform with SIMD
2026-10-09 09:35:51 +02:00
Henrik RydgårdandClaude Opus 5.5 172c200cb1 Drop 3D rectangles with a vertex out of range, like the software renderer
The GE doesn't clip rectangles: one with a vertex behind the camera or outside
the screen range is dropped whole (the software renderer's ProcessRect, from
gpu/clipping/guardband). The hardware backends drew them, stretched to the far
off-screen vertex.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:09:36 +02:00
Henrik RydgårdandClaude Opus 5.5 d6f045aad9 SoftGPU: Add the diffuse light without branching on its factor
N.L changes sign from vertex to vertex, so the branch on it was mispredicted a lot. A factor that isn't
positive scales the color to 0, so the term can always be added. Vertex reading about 6% faster in
Tekken 6.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:07:12 +02:00
Henrik RydgårdandClaude Opus 5.5 a14712c65e SoftGPU: Skin a vertex without per-operation fallback checks
Every GEAdd4 and GEMulFloat24x4 checked its lanes for the cases it can't do and branched to a
fallback, a vector-to-scalar move and a branch per operation, about 14% of skinning. The unchecked
variants only collect those lanes; a vertex where any appears is skinned again with the checked ones.
The products use GEMulFloat24x4 (the same as a one-term GERowSum4 here), and the position's and
normal's add chains are interleaved. Skinning is about 20% faster.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:07:12 +02:00
Henrik RydgårdandClaude Opus 5.5 d272703721 SoftGPU: Keep the vertex position, normal and light vectors as Vec4F32
The position is read once as (x, y, z, 1) for the clip, fog, texgen and
light vector rows, and lighting works on Vec4F32 throughout, without
going through arrays. Each light's constant term, its position minus
the world translation, is computed once per state instead of per
vertex. The texture matrix rows are now prepared with the transform
state, so loading that matrix dirties it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:07:12 +02:00
Henrik RydgårdandClaude Opus 5.5 49674c3f25 GEMath: Add GEDot3 and GENormalize4 on Vec4F32
GEDot3 forms one term per lane and sums across the lanes, about 1.5-2x
as fast as the scalar GEDot. Adds Vec4S32::GetLane.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:07:12 +02:00
Henrik RydgårdandClaude Opus 5.5 63a05db8d2 SoftGPU: Skin the x, y and z components together in vector lanes
Skinning did each component's weighted bone entries and its chain of
GEAdds separately in scalar code. The components now go in lanes, with
GEMulFloat24x4 for the weighted entries, one-term row sums for the
products and GEAdd4 for the sums, in the same order. Checked against the
scalar version on every vertex of 143 skinned frame dumps.

GEMulFloat24x4 matches ProductToFloat24, so inf and NaN bone entries
behave as before. GEAdd4 handles two zero operands without its fallback.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:07:12 +02:00
Henrik RydgårdandClaude Opus 5.5 c9a2b773e3 GEMath: Build GERecip and GERsqrt results from exponent bits
Adds the exponent instead of calling ldexpf, except for reciprocals that
can be denormal. Identical results for every normal float24 input.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:07:12 +02:00
Henrik RydgårdandClaude Opus 5.5 a9d8b6d3c5 GEMath: GERowSum4 takes its left operands as one vector
They're truncated to float24 with one AND, and their exponents, fractions
and signs worked out for all terms at once, each term then broadcasting its
lane (new CrossSIMD Vec4S32::SplatLane). The term count becomes a template
parameter so the lanes are constants. About 75-83 M transforms/s on an M1,
from 68; results unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 09:07:12 +02:00
Henrik RydgårdandClaude Opus 5.5 c4ce2e3635 SoftGPU: Use GERowSum4 for the transform's row sums
The four clip space rows of a vertex, the texture matrix's three, the world
normal's three, the vector to a light, and each row of the combined matrix
now take one GERowSum4 call. Same results (pspautotests, frametests and
the probe regression set unchanged).

GERowSum4 scales its result by adding to the exponent instead of
multiplying, and CrossSIMD gets a Vec4S32::Max for the largest lsb. About
68 M transforms/s against 26 for four scalar GERowSums on an M1 (a pure
NEON version with per-lane shifts did about 79); replaying the frametest
dumps gets only about 1% faster, up to 11% on Taiko no Tatsujin.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 02:57:09 +02:00
Henrik RydgårdandClaude Opus 5.5 59eccf499e GEMath: Add GERowSum4, four row sums at once
Four rows sharing their left operands, like a vertex transform's: with
float24 operands each exact product is a 32-bit integer, (1.a' * 1.b') >> 15
= 32768 + a' + b' + (a' b' >> 15), and the terms are aligned by scaling with
a power of two and truncating. Lanes whose result exponent is out of a
single float scale's reach take the scalar GERowSum. Tested bit for bit
against GERowSum on random operands (arm64 and x86-64). Not used yet.

Also adds an arithmetic Vec4S32::Shr and the float/int bit casts to
CrossSIMD.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 02:55:42 +02:00
Henrik Rydgård 35e27933ff Merge pull request #22461 from hrydgard/softgpu-accuracy
SoftGPU: More accuracy fixes
2026-10-09 02:53:26 +02:00
Henrik Rydgård e71665d9e7 Merge pull request #22460 from hrydgard/android-config-changes
Android: Handle more activity config changes manually
2026-10-09 02:18:57 +02:00
Henrik RydgårdandClaude Opus 5.5 111fa6ed95 GPU: Copy overlapping block transfer rows as the GE does
The GE reads the source in aligned 16-byte blocks, four at a time, into a
buffer of four destination-aligned 16-byte slots, and a read of a block a
slot holds returns the slot's bytes, stale ones included. So a transfer
shifting a row right by less than 64 bytes reads some of what it has just
written, in a pattern that isn't a forward or backward copy. Fitted to
ppsspp-re geprobe exp219 and exp220 on hardware (562 of 570 cases exact).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 02:03:31 +02:00
Henrik RydgårdandClaude Opus 5.5 1a90aa6047 GPU: Count the cycles of commands the hardware backends consume by hand
The fast matrix loads and the joined PRIMs read their commands themselves, then skipped them with
UpdatePC(pc, pc + n * 4), which counts the commands up to pc and resumes counting after the skipped
ones: those were never counted, as for a jump. Now pc moves first and UpdatePC counts up to it. The
software renderer runs those commands one at a time, so the backends now agree on the timing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 02:03:31 +02:00
Henrik RydgårdandClaude Opus 5.5 834e617cc1 SoftGPU: Carry UVs and normals into formats without them as the GE does
gpu/vertices/carry, recorded on a PSP: a draw whose format has no UVs uses the last ones read, by any
earlier draw in draw order, textured or not. They're kept as read and get the scale and offset of the
draw that uses them. Normals carry over the same way.

We kept the UVs after scaling, only read them with texturing on, and after a cached indexed draw
carried the highest vertex instead of the last one drawn. The carried values move from statics in
ReadVertex to TransformUnit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 02:03:31 +02:00
Henrik RydgårdandClaude Opus 5.5 8b4d9c3b74 Headless: Don't apply compat flags to frame dumps
Headless replays frame dumps to compare them with references and with the PSP, which a game's
workarounds skew: DarkStalkers' present hack (16510) replaces the game's final upscale, so the
replay didn't match the PSP's. The app still applies them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 02:03:31 +02:00
Henrik RydgårdandClaude Opus 5 a2869a47cd Android: Re-report the surface when the display density changes
Now that density is in configChanges, a density change no longer recreates
the activity - but it also doesn't resize the surface, so no surfaceChanged
follows it. The DPI is only recalculated in backbufferResize, so the UI kept
the old scale until the next real resize or a trip through the background.

Re-report the surface we already have when the density has moved. The native
side needs no change: Recalculate() already returns true when the dp size
changes, which a density change does on its own.

Also make the log line say which size changed, since the pixel size now stays
put in this case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC
2026-10-08 21:51:35 +02:00
Henrik RydgårdandClaude Fable 5.1 114512774d Android: Keep the native smallestScreenWidthDp current across config changes
It was passed once at init, and decides whether we can restrict the
orientation on Android 17+ large screens. Now that a fold or unfold no
longer recreates the activity, forward the new value from
onConfigurationChanged. Untested on a large screen device.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 21:17:49 +02:00
Henrik RydgårdandClaude Fable 5.1 be6ff0bc9b Android: Trim SizeManager down to the surface callback, rename to NativeSurfaceManager
Now that rotation doesn't recreate the activity, the class is the
SurfaceHolder.Callback plus the hardware scaling logic, and the rest had
outlived its purpose:

- The deferred-surface-while-paused workaround. Also what started the
  render thread twice on resume.
- The "bad orientation" workaround, which called recreate().
- The deprecated system UI visibility listener, which only chose between
  getMetrics and getRealMetrics, and the view size overrode that whenever
  it mattered anyway.
- The coalescing of display measurement updates, and updateDpi, which
  updateDisplayMeasurements does itself.

Before the first layout the display size now comes from getRealMetrics.
It only feeds the first-run config defaults.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 21:17:49 +02:00
Henrik RydgårdandClaude Fable 5.1 d0e45b94d9 Android: Handle rotation and window resizing without recreating the activity
Every rotation, fold or window resize destroyed the activity and with it
all of graphics, then built it all up again. Declare those config changes
handled, so they arrive as a surfaceChanged and go through the same resize
path as a desktop window resize.

That path needed three things it didn't have on Android:

- The GPU_DISPLAY_RESIZED message, left out for Android since 2020 when
  DeviceRestore covered every size change.
- A fixed (hardware scaled) surface size is now recomputed on each in-place
  surface change, since surfaceCreated no longer runs on a rotation.
- A 180 degree flip keeps the size but changes the surface transform, so
  the display rotation is passed along and a change in it is a resize too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 21:17:49 +02:00
Henrik Rydgård c7a2cb6f63 Merge pull request #22458 from hrydgard/android-small-fixes
Android: Small fixes from a Claude code review
2026-10-08 21:17:00 +02:00
Henrik Rydgård f0e0368a77 Merge pull request #22456 from hrydgard/spline-simd
SIMD-optimize splines and beziers
2026-10-08 20:27:34 +02:00
Henrik RydgårdandClaude Opus 5 afedbab578 Android: Remove LifeCycle.java, keep the logging
It was a debug aid that asserted the activity callbacks arrive in the order
Android promises. That has served its purpose - nothing has tripped it in a long
time, and the lifecycle work this sits on top of has been exercised hard.

What it also did, though, was log every callback, and that is worth keeping: the
begin/end pairs are what makes a bad startup or a surprise recreate legible in a
logcat. So the tracker goes and the Log.i calls stay, at the same points, under
this class's own tag instead of LifeCycle's.

onStart and onStop now only log, so they lose the no-native-binary guard. There
is nothing left in them to guard, and if the library is missing is exactly when
you want to see the callbacks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 937022aa8e Android: Don't log every mouse movement
Moving a mouse logged two lines from Java and one at INFO level from C++
for every motion event.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 726c98198d Android: Mark the last two JNI-called Java methods @Keep
renderText and getNativeCrashHistory are only called from native code,
like their neighbours that do have the annotation. Harmless today since
minification is off, but it would strip or rename them the day it's on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 4a3ba23820 Android: Forget a key when it's released
pressedKeys is there so Disconnect() can release what's held, but keys
were only ever added to it, so it listed every key pressed so far.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 a413b88cab Android: Make renderer_inited atomic
It's written by the thread that brings graphics up and down, and read by
every input function on the UI thread. The comment also claimed it was
only used with OpenGL, but the Vulkan path sets it too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 a77eba1554 Android: Create the TextRenderer paint only once
TextRenderer.init runs in every onCreate and replaced the static Paint. On
the OpenGL path the emu thread survives the activity and can be in the
middle of measuring or drawing text with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 26d533904e Android: Hand out the same font ID for the same font file
Each TextDrawerAndroid allocates its fonts afresh, and one is created
every time graphics come up. allocFont gave every request a new ID and map
entry, and nothing ever calls freeAllFonts, so the map grew by a few
entries per resume.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 b16693cf48 Android: Survive long enough to show the missing native library message
When the library fails to load, onCreate shows a dialog and returns before
setting anything up. But onResume, onWindowFocusChanged and the rest still
ran, dereferencing the missing SizeManager or calling into the missing
library, so the app crashed before anyone could read the dialog. Make them
all check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 d887e4a8ae Android: Release the camera if starting it fails
If startCamera threw after Camera.open (no usable preview size, say),
mIsCameraRunning stayed false, and pause() only releases a running camera.
So it was held until the process died, locked for every other app. Release
it on failure, and before opening it again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 ce19265949 Don't save log levels set from the command line
--loglevel, -d and -v are applied to the channels directly, and the next
config save wrote them to [Log], permanently replacing the user's levels.
The other command line options avoid this with DoNotSaveSetting.

The debugger had the same problem and a flag for it. Generalize that one
(NotifyChannelsChangedByDebugger -> DoNotSaveChannels) and set it here too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 ea9be99149 Android: Parse a launch parameter sent to the running app as a command line
An intent arriving while we're running was passed on as a game path,
whole. That was right for a shortcut or a file association, but the Args
extra carries a command line, so its options got booted as a file.

Run it through the same parsing as at startup. The options can't be
applied anymore and aren't, but parsing them is what separates them (and
their arguments) from the game to boot. If there is none, do nothing.

This also unescapes a quoted path the same way NativeApp.init does, where
before only the quotes were stripped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 105bf522d7 Android: Declare the main activity resizeable explicitly
We never said either way, so the system treats it as resizeable only by
virtue of the target SDK (RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION). Samsung
DeX wants it spelled out before it offers free resizing and maximize, and
a user reports being stuck in a small fixed window there. Not verified on
DeX.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 cec72d46c5 Android: Stop location updates while paused
Nothing turned the GPS off when the app went to the background, so a game
that had opened it kept it running until the process died. Pause and
resume it like the camera. A start request that arrives while paused (as
the one from the permission dialog does) is deferred to resume.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 20:11:23 +02:00
Henrik RydgårdandClaude Fable 5.1 90c7cd2a03 Android: Release all pointers on ACTION_CANCEL, not just the first
ACTION_CANCEL ends the whole gesture, and its action index is always 0.
We only sent an up for that pointer, so with more than one finger down
when the system took over the gesture, the others stayed pressed on the
native side - stuck on-screen buttons.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 20:11:23 +02:00
Henrik Rydgård 0e3dfcef9d Merge pull request #22454 from hrydgard/android-activity-lifecycle
More Android activity lifecycle fixes
2026-10-08 20:10:14 +02:00
Henrik Rydgård 4fa278afe3 Merge pull request #22455 from hrydgard/fix-createdir-chmod
Don't chmod existing parent directories in CreateFullPath
2026-10-08 19:40:29 +02:00
Henrik RydgårdandClaude Opus 5.5 ce97cb1201 Splines: Vectorize the GE-exact spline evaluation
The same lerps as the GE-exact Bezier path, four at a time. At each v step,
four control columns per vector share the v blending factors; then each row
is evaluated at four u steps, each lane with its own six blending factors.
A vector's u steps can straddle a segment boundary, so the row's points are
gathered per lane, or splatted when all four are in one segment.

The unit test compares it bit for bit with the scalar code, including
tessellations that split a vector across segments. The GE-exact spline
benchmark runs about twice as fast on an M1.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 19:33:20 +02:00
Henrik RydgårdandClaude Opus 5.5 08c1a6907a Don't chmod existing parent directories in CreateFullPath
CreateDir's "repair" of existing directories ran for every component
CreateFullPath walked through, so creating any directory under the
user's home made the home directory itself group-writable (775). That
breaks zsh's compinit ("insecure directories") among other things.

Only repair the permissions of the target directory itself, when it
already exists.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 19:13:18 +02:00