Skip to content

honeykrisp: map placed memory at the allocation size - #8

Open
joshuaswarren wants to merge 11 commits into
omacom:mainfrom
joshuaswarren:map-placed-length
Open

joshuaswarren wants to merge 11 commits into
omacom:mainfrom
joshuaswarren:map-placed-length

Conversation

@joshuaswarren

@joshuaswarren joshuaswarren commented Oct 12, 2026 •

Copy link
Copy Markdown

Current status: NOT READY (30 October)

Head 4e269c57f09a926a4e785c5e48788325c8b36846.

The earlier ownership error is fixed at the source level. A BO shared by several memory objects no longer owns a replaceable mapping. The BO keeps one stable internal mapping for the driver and decoder. Each memory object now owns the mapping it hands to the application. An import pair maps independently, and freeing one leaves the other valid.

A CPU regression suite covers placed maps, reserve unmaps, aliases, the stable mapping, import pairs and the free path. It fails on the previous head and passes on this one. The real reference count is exercised. The suite is CPU only.

The heap accounting, external BO tracking and Vulkan teardown tail of FreeMemory still need a live device. Placed-map-after-bind needs device coverage. This head has no device results. Earlier device results below were measured on older heads and do not cover this change.

Earlier description and evidence

Current status: NOT READY TO MERGE (29 October)

Head ddfb4cf114a6e72b690581ff6fb2fe8447d11ffa: READY status withdrawn following Chris's source-review finding. A placed application mapping can unmap the BO mapping still retained in a bound image's host-copy pointer; mem->map == NULL and a single BO reference do not exclude this lifetime.

Required next steps: reproduce the image-bind → placed-map → host-copy → unmap sequence, preserve the internal mapping independently or repair every retained pointer, and rerun the relevant regressions and exact-head review. The finding is source evidence, not a measured device crash. Earlier targeted results below do not cover this sequence.

Landing order remains #10 first, then corrected/qualified #8, then #9. #9 must wait for #8's correction and qualification.

Historical evidence and original PR description

READY TO MERGE after #10.

Head: ddfb4cf114a6e72b690581ff6fb2fe8447d11ffa.

GitHub reports MERGEABLE against main: no text conflicts. This is not a test pass.

The exact-head tests and GREEN source review below cover this commit. They are targeted checks, not full CTS. Merge it after #10 lands. Preserve this unchanged head; no new run or rebase is needed just to update its status. An authorized maintainer can land it next in the agreed order.

Earlier review, source and test notes are kept below with their original bindings. They do not change this status.


What this fixes

The conformance test dEQP-VK.memory.map_placed.unmap_reserve.size_1048576 crashes the process when it runs after many other memory tests. Four cases in the neighbouring exact_size group fail in that run. omacom/mesa main at 7cda7850ed has this crash.

The cause is in the placed map path of VK_EXT_map_memory_placed:

  1. A placed mapping covered the whole BO, not the allocation. The BO cache can return a BO that is larger than the request. A 0x100000 byte allocation got a 0x104000 byte mapping. The extra page lay in application memory. The test writes guard bytes there and reads them back, and the read hit an inaccessible page.
  2. A BO can go back to the BO cache with a mapping still on it, plain or placed. The next placed map of that BO returned the old address and made no new mapping. A placed mapping that was never unmapped also stayed in the application address range after vkFreeMemory.
  3. The reserve remap and the unmap used the BO size, so they covered the same extra page.

What this changes

  • The BO records the length of its live mapping.
  • A placed map covers the allocation size. An ordinary map still covers the whole BO, as before.
  • Unmap and the reserve remap use the recorded length.
  • A placed map drops any old mapping first. vkFreeMemory unmaps a placed mapping that its own memory object holds, before the BO goes back to the cache.
  • A failed map returns VK_ERROR_MEMORY_MAP_FAILED. The old assert for this is compiled out in release builds. The same error covers a placed map at a different address than the one requested, a placed map with an offset other than 0 or a size other than the allocation size, and a placed map of a BO that an import shares and that is already mapped. The driver reports memoryMapRangePlaced as false, so VUID-VkMemoryMapInfo-flags-09571 and VUID-VkMemoryMapInfo-flags-09572 make offset 0 and size VK_WHOLE_SIZE or the allocation size the only valid placed map.

Test results

The chunk runs use the library bytes of the built package, one run per arm. Control is the package of main at 7cda7850ed, built with the same recipe as the candidate. Candidate is the package of this branch at ddfb4cf114.

The first 1000 cases of the memory group (chunk 0 of its 7 chunks), the chunk where the control crashes. The 16-inch M1 Max and the 13-inch M1 give the same table.

Control Candidate
Pass 388 393
NotSupported 607 607
Fail 4 0
Crash 1 0

Exactly 5 of the 1000 rows differ on both chips. No other row differs.

Case Control Candidate
map_placed.exact_size.size_4096 Fail Pass
map_placed.exact_size.size_8192 Fail Pass
map_placed.exact_size.size_65536 Fail Pass
map_placed.exact_size.size_1048576 Fail Pass
map_placed.unmap_reserve.size_1048576 Crash Pass

All 13 map_placed cases pass on the candidate.

A small test program, run on the 16-inch M1 Max, one boot, with a native build of this branch, one test per process. It covers an ordinary map, an ordinary map then a reserve unmap, a cached larger BO with a placed map (plain and reserve unmap), a placed map after a free without unmap, and an application page at a freed placed address. The control crashes on the cached-BO case with a 0x104000 byte placed map, returns the wrong pointer after a free without unmap, and leaves the placed range mapped after vkFreeMemory. The branch passes all six tests with placed lengths of 0x100000 and 0x10000.

What this does not show

  • One chunk, 1000 of 3,253,584 cases. This is not a full conformance run.
  • The shared-BO guard has no test.
  • The offset and size error path has no test.
  • The virtio path compiles but did not run.
  • The Gallium driver was not built.
  • The source tree has one unrelated hazard that this change leaves as it is: two imported memory objects that share one BO and both use an ordinary map share one mapping.

The conformance test expects exact page boundaries here (see vktMemoryMapPlacedTests.cpp). I found no sentence in the extension text that requires it.

Observational two-M1 comparison (not approval)

Chris's comparison binds to main 7cda7850ed versus the old control stack d38e7192452 (#10 b87e7456, #8 c70e5372, #9 11ae82d8) plus build-only overlay 645169e3901 adding #9's missing idep_nir. It is not evidence for a future ownership-repair head.

The 13-inch M1 and M1 Air each ran seven common-path workloads, one warm-up and three measured rounds, plus clean/corrupted compute controls: 30 valid runs per arm, all four arms 30/30. Driver attribution, clean environment, no new kernel fault lines and drained process groups were checked per run. Same aarch64 recipes passed 47/47 registered tests on main and 49/49 on the control. Overall lab jobs still failed a separate known provider-attribution check; those failed verdicts are not being relabelled as passes.

Median latency change of control versus baseline (negative is lower):

Workload M1 Air submit/wait M1 Air GPU 13-inch M1 submit/wait 13-inch M1 GPU
raster -0.6% +0.1% -0.4% +0.0%
texture -2.8% -3.1% -0.7% -0.6%
compute +4.6% +5.9% +0.3% +0.4%
copy -0.6% -0.5% +1.6% +0.3%
submit-single +5.5% +4.5% -1.4% -1.4%
submit-batched +4.2% -1.7% -0.2% -1.2%
bo-lifetime +0.2% -1.9% -0.4% +2.9%

These are descriptive n=3 measurements without CPU frequency/affinity control, not statistical significance or equivalence. The source comment identifies which min/max ranges did not overlap. No placed maps, host image copies, capture/replay buffers or transform feedback were exercised, so this qualifies none of the three fixes and does not close the tracing or imported-object defects. The external-display path was not tested. Both Macs were restored afterward. Post-fix correctness and any new-head observations must retain their own source bindings.

A VK_EXT_map_memory_placed mapping covered the whole BO. The BO cache can
return a BO that is larger than the allocation, for example 0x104000 bytes
for a 0x100000 byte request. The mapping then covered the page after the
allocation in application memory. The conformance test writes guard bytes
there and reads them back, and it failed with a crash.

Record the length of the live mapping in the BO and use it to unmap. Map a
placed mapping with the allocation size, not the BO size. Report
VK_ERROR_MEMORY_MAP_FAILED when the map fails, in place of an assert that is
compiled out in release builds.

A placed mapping can also stay on a BO that goes back to the cache. The next
placed map of that BO then returned the old address and made no new mapping.
Unmap a placed mapping when its memory is freed, and drop any old mapping
before a new placed map.

The virtio path takes the new size argument. It was not run.
Review follow-up for the placed map change.

Return VK_ERROR_MEMORY_MAP_FAILED if a placed map ends at another address
than the one the application asked for, and drop that mapping. The old
assert for this was compiled out in release builds.

A placed map without a range covers the whole allocation from offset 0. Return
an error for any other offset or size.

Refuse a placed map when the BO is shared through an import and already
mapped, so a mapping that another memory object holds is not torn down.

Use one helper for the length of a live mapping in the unmap and the reserve
paths. A reserve unmap of a BO with no mapping does nothing.
The placed length now comes from the allocation size. Return an error if it
ever exceeds the BO, so mmap cannot map past the GEM object.
@joshuaswarren

Copy link
Copy Markdown
Author

GREEN at ddfb4cf, with two small notes. I read the full diff against main at 7cda785. I ran no build.

Checked:

  • Every writer and reader of the BO mapping is covered. That includes both bo_mmap implementations and both ops tables. It also includes agx_bo_unmap in agx_bo_free and in hk_device_memory.c, and the one agx_bo_map_placed caller.
  • agx_bo_map keeps bo->size. The roughly 20 other callers, including the Gallium driver, behave as before.
  • agx_device_virtio.c is in the build list and compiled. It was not run.
  • The BO cache can return a BO up to 2x the request (agx_bo_cache_fetch). Before this change the placed map, the reserve remap, the plain unmap and the BO free all used bo->size.
  • The new checks match the stated rules. A placed map must start at offset 0 with a size equal to the allocation size. That fits the driver reporting memoryMapRangePlaced as false.
  • A wrong address unmaps and returns VK_ERROR_MEMORY_MAP_FAILED.
  • The sharing guard fires only when the BO is already mapped and its refcount is above 1. Nothing in the Vulkan driver takes an extra reference, so it fires only for an import share.
  • Every number in the body matches the raw per-chunk results on both chips. The table is 388/607/4/1 against 393/607/0/0, with the same five rows. All 13 map_placed cases pass, and the full list is 3,253,584 cases.

Notes, neither blocking:

  1. hk_MapMemory2KHR still dereferences placed_info->pPlacedAddress when the placed flag is set and no placed info is chained. That is invalid usage and predates this PR. One check would close it.
  2. The branch has three commits, and the second and third repair the first. Squashing them into one is the maintainers' call.

Not covered, as the body says. The shared-BO guard and the offset and size rejection path have no test. Virtio is not run. The Gallium driver is not built.

@joshuaswarren

Copy link
Copy Markdown
Author

Both notes are open items. I did not change the branch for either.

  1. The null VkMemoryMapPlacedInfoEXT case is invalid usage and was in main before this change. A new commit changes the head, and the driver string in the library carries the head id, so the library bytes would change too. The five-row result above is tied to the library bytes of ddfb4cf114. I can send the one-line check as a separate small change if the maintainers want it.

  2. I did not squash. A squash keeps the tree but changes the commit id, for the same reason as note 1. The maintainers can squash on merge.

@joshuaswarren

Copy link
Copy Markdown
Author

Maintainer routing request for PR 8. This is a request for a merge contact, not a claim that review is finished.

It fixes the crash of dEQP-VK.memory.map_placed.unmap_reserve.size_1048576 that main has. What was run on the head ddfb4cf114a6e72b690581ff6fb2fe8447d11ffa and what was not run is in the description and in the two review comments above; I have not changed it since. PR 8 belongs to a merge order of PR 8, PR 9 and PR 10, then PR 6, then PR 11. A whole-suite run of PR 6 and the PR 11 commit on top of PRs 8, 9 and 10, against PRs 8, 9 and 10 alone, is queued. Its results will be posted on the PR 6 thread.

Question: who can merge into this repository's main branch, and in which of these PRs would you like to start? My account has pull access only.

@joshuaswarren

Copy link
Copy Markdown
Author

Second routing request for PR 8, about 14 hours after the first. It is still a request for a merge contact, not a claim that review is finished.

Nothing on the head has changed since my first note above. The review notes and the list of what was run are in the description and the two review comments.

Question: who can merge into this repository's main branch? If there is a different place to ask, a link is enough. My account has pull access only, so I cannot merge or mark anything ready.

@joshuaswarren

Copy link
Copy Markdown
Author

Status, 25 Oct: no review since I asked for a merge contact. This is the base of the whole-suite run of #6: without it the suite crashes in dEQP-VK.memory.map_placed.unmap_reserve.size_1048576, so I put its three commits unchanged under the #6 test branch and started the paired run on the 16-inch M1 Max. The combined candidate in #12 carries a variant that keeps independent imported mappings; when the two are reconciled, keep ordinary imported mappings independent. I will post the whole-suite result here too.

@joshuaswarren

Copy link
Copy Markdown
Author

Status, 25 Oct, for the landing order (#10 first, then #8, then #9, then #6).

Head ddfb4cf114a6. It merges cleanly after #10 on main 7cda7850ed.

MEASURED: it is in the control arm of the whole-suite CTS of the #6 stack on a 16-inch M1 Max and a 13-inch M1 (main plus #8, #9 and #10 against that plus #6). The memory_model and map_placed groups ran without a Pass to Fail change from the control arm to the candidate arm.

NOT measured: a CTS run with only this change against main.

@joshuaswarren

Copy link
Copy Markdown
Author

Batch 1 for main: #10, then #8, then #9, in that order.

All three apply to main at 7cda7850ed with no conflicts. They touch different code and git applies them in any order. The order is the agreed landing order. The #9 device run below is on top of #10 and #8. The #10 and #8 results below were measured on each branch alone. We checked the three together as a local merge (tip 59c25f62c9d). #19 went into integration/aurora, not main, so this base has not moved.

PR Head Change Device result at that head
#10 b87e7456 accept a NULL array of transform feedback counter buffers three transform_feedback chunks, 2,719 cases, on a 16-inch M1 Max and a 13-inch M1: the 18 former crashes pass, no other change
#8 ddfb4cf1 map placed memory at the allocation size memory chunk 0, 1000 cases, both chips: 4 Fail and 1 Crash become Pass, no other row differs
#9 11ae82d8 give capture and replay buffers their own address range binding_model chunk 0, 1000 cases, 13-inch M1, run at this exact head: 122 ResourceError become Pass, no case gets worse

Stack check, one boot on a 13-inch M1, binding_model chunk 0: main gives 340 Pass, 538 NotSupported and 122 ResourceError. Main with #10, #8 and #9 gives 462 Pass, 538 NotSupported and 0 ResourceError. The 122 changes are all ResourceError to Pass. This shows the three together. It does not isolate #9 from #8 and #10.

These are targeted checks, not the full conformance suite. The CTS binary's source commit is not independently verified.

#6 is not part of this batch. Its run over the touched test groups at its current head has not finished, and three shader_object.performance timing cases still need a decision. We will post that result separately. #3, #4 and #5 are superseded by #6 and will be closed after it merges.

The author account on these PRs has pull-only access, so a maintainer has to merge. Tell us if you want a different order or a different check first.

@joshuaswarren

Copy link
Copy Markdown
Author

@maralcbr could you review this current head for landing after #10? The exact-head test evidence and remaining scope limits are in the PR body. The requested landing order is #10, then #8, then #9. Our account cannot assign formal reviewers or merge here; please route this to a main-branch maintainer if needed.

@chriskearney chriskearney left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A placed map can invalidate a live image's internal host-copy pointer at ddfb4cf.

A non-sparse image bind stores plane->map = agx_bo_map(mem->bo) + offset in hk_image.c:1334. That internal mapping does not set mem->map. A subsequent application vkMapMemory2KHR with VK_MEMORY_MAP_PLACED_BIT_EXT can therefore reach this call while the image is still bound. The new unconditional agx_bo_unmap(bo) removes that internal mapping and replaces bo->_map, but the image keeps its old pointer. The host-image-copy paths later dereference planes[plane].map (for example hk_image.c:1444 and :1523), which can now be unmapped or reused memory.

The refcount check does not cover this case: binding the image keeps the memory object alive through the application's lifetime rules; it does not create another BO reference. This is distinct from main's existing wrong-address behavior when a placed map finds a cached mapping. Replacing the mapping fixes that address result while introducing a dangling pointer for this valid lifetime combination.

Please keep the internal image mapping independent of the application mapping, or otherwise preserve/reestablish every retained pointer, and add host-image-copy coverage with a placed map after image binding and after unmap. The placed-map contract does not invalidate a bound image when an application maps its memory.

This finding is from source review, not a new device crash or a CTS result. Independent runtime reproduction and the shared regression benchmarks are still pending; their results will be kept separate from this source finding.

Comment thread src/asahi/lib/agx_device.h Outdated
agx_bo_map_placed(struct agx_bo *bo, void *fixed_addr, size_t size)
{
if (fixed_addr)
agx_bo_unmap(bo);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This unmap can invalidate hk_image_plane.map, retained when the same allocation was bound to an image. mem->map is still NULL for that internal map and the BO refcount can still be 1, so neither guard above excludes the sequence. Please preserve an independent internal mapping or repair the retained image pointers.

@joshuaswarren

Copy link
Copy Markdown
Author

Confirmed from the source path: the BO mapping can be retained by a bound image even when mem->map is NULL, so the refcount guard is insufficient. I am withdrawing the READY status and adding a regression for image binding followed by placed map, host copy and unmap before changing the shared mapping path. No runtime reproduction or device result is claimed yet.

A bound image retains an internal host-copy mapping
(hk_image_plane_bind, plane->map = agx_bo_map(mem->bo) + offset) with no
mem->map and no BO reference, so a later application placed map reaches
agx_bo_map_placed, drops the BO mapping, and leaves the retained plane
pointers dangling for the host-copy paths.

The new libasahi_placed_map_test drives the real agx_device.h mapping
code with the exact bind, placed-map, unmap and reserve expressions from
hk_image.c and hk_device_memory.c. The only mock is the kernel mmap,
which hands back anonymous pages. Cases: placed map then plain unmap,
placed map then VK_MEMORY_UNMAP_RESERVE_BIT_EXT, and two images aliased
on one memory object. Fails at ddfb4cf; passes once the internal
mapping is independent of the application mapping.
Review follow-up on the CPU fixture: back the BO with one tmpfile
(ftruncate to the BO size) and hand out MAP_SHARED mappings, so the
internal image mapping, the application mapping and the placed mapping
are separate mappings of the same BO contents. The fixture now checks
coherence in both directions (retained vs earlier application mapping,
placed mapping vs bind-offset contents) in addition to lifetime after
unmap.

The placed target keeps its PROT_NONE reservation mapped until the
MAP_FIXED placed map replaces it. Every resource failure now FAILs the
case; the SKIP paths are gone. The shim tracks and cleans up all
mappings and the backing file per case. A map-before-bind control case
covers the application-map-then-bind order that the proposed fix's
stash logic must handle. Case bodies collapse into one data-driven
runner; no new framework.
A bound image retains a BO mapping independently of VkDeviceMemory.map.
Replacing that mapping for a placed application map left host-copy pointers
dangling even with one BO reference.

Give internal and application mappings separate slots. Return mmap results
from the native and virtio primitives instead of swapping shared state;
release both mappings with the BO and preserve placed-map size/error paths.

The four CPU regression cases failed on fixture head
9c3850f and now pass: plain unmap,
reserve unmap, aliased images, and application-map-before-bind. They use
real mapping helpers with shared file-backed kernel mmap emulation.
Vulkan entrypoint/device coverage and full driver compilation remain
separate qualification gates; no hardware readiness claim.
@greptile-apps

greptile-apps Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The source changes appear safe to merge, with a non-blocking test coverage gap; the PR's separate qualification gates still apply.

Findings

  1. P2 Tests bypass the ownership fix ▶

Summary

Application mappings now belong to each VkDeviceMemory, while the BO keeps a stable mapping for driver and decoder use.

  • Placed memory maps cover the allocation and stay separate from driver-held BO maps.

This review checked source only. It does not replace the device qualification or landing order stated in the PR description.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  BO[Shared BO storage] --> Internal[Stable BO mapping]
  BO --> A[Memory object A mapping]
  BO --> B[Memory object B mapping]
  Internal --> Driver[Driver and decoder]
  A --> FreeA[Free A removes only A mapping]
  B --> Survivor[B mapping stays live]
Loading

Reviews (2) · Last reviewed commit: "honeykrisp: drop redundant mapping state" · Reviewed by Greptile

Comment thread src/asahi/lib/agx_device.h Outdated
Comment thread src/asahi/vulkan/hk_device_memory.c Outdated
Comment on lines +283 to +284
if (mem->map && mem->placed_map)
agx_bo_unmap(mem->bo);

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 Freeing one object unmaps another

hk_FreeMemory can now unmap memory still used by another imported memory object. Export and reimport a BO, placed-map the first object while _map is empty, then normally map the second object. The shared-BO check allows this order, and the normal map returns the same _map.

Freeing the first object now unmaps that address even though the second object remains mapped and holds a BO reference. Later reads or writes through the second object's pointer can crash. Give each memory object its own application mapping, or prevent this sharing before returning the pointer.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Confirmed in the source: after A creates a placed application map, B can normally map the same BO and receive A's cached pointer; freeing A then unmaps B's live mapping. This head remains NOT READY. I am adding a failing-first two-object/free-one regression and removing BO ownership of application mappings: each VkDeviceMemory will own its app pointer and length, while the BO keeps its stable driver/decoder mapping. No device reproduction is claimed, and no re-review request is being sent before the regression and fix are verified.

@joshuaswarren

Copy link
Copy Markdown
Author

The follow-up tracing and imported-memory findings expose a remaining ownership error in this head: a BO can be shared by multiple VkDeviceMemory objects, so it cannot own their replaceable application mapping. I am keeping this NOT READY and moving application pointer/length ownership to each memory object, while restoring the stable BO mapping used by driver and decoder callers. New trace and two-importer lifetime regressions will run failing-first. The earlier four CPU cases pass and the full local driver compiles, but neither covers these follow-up cases or replaces device qualification.

@chriskearney chriskearney left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

At c70e537, trace mode crashes at the first queue submission. This confirms at runtime the tracing finding above, which your 23:25 note already plans to fix.

How it was run: an x86_64 host with Mesa's asahi drm-shim (G13G profile; no kernel, no GPU), and Honeykrisp built from this exact head (release, NDEBUG). A small Vulkan program records a transform feedback draw and calls vkQueueSubmit with ASAHI_MESA_DEBUG=trace.

Driver Result
main 7cda7850ed decode completes; 25,311-byte agxdecode dump
previous head ddfb4cf114 decode completes; same dump size
this head c70e5372 SIGSEGV in the first vkQueueSubmit; the dump stops at 78 bytes

Backtrace: __agxdecode_fetch_gpu_mem (decode.c:136) ← agxdecode_stateful ← agxdecode_drm_cmdbuf ← queue_submit ← vkQueueSubmit.

Cause:

  • With tracing on, agx_bo_create calls agx_bo_map(), then agxdecode_track_alloc() stores a copy of the BO (agx_bo.c:397-399).
  • agx_bo_map() now fills only _imap, so the stored copy has _map == NULL.
  • decode.c:102, :136 and :976 still read mem->_map / cmdbuf->_map.

Control: a local scratch change that reads _imap before _map at those three lines removes the crash, and the dump is byte-identical to main's. The scratch change only tests the cause; it is not a proposed final shape. The program's transform feedback data check fails under drm-shim on every driver, because nothing executes without a GPU; that check is not part of this result.

The crash is the same in a local merge of main with #10, this head and #9 (d38e71924527392fca5b09c23b9aa6017c52bc2b).

Separately, in the same host setup this head fixes the image-bind/placed-map case from the earlier review. It also fixes host copy after vkUnmapMemory2 and after VK_MEMORY_UNMAP_RESERVE_BIT_EXT, which main fails too. All 6 cases pass, with the image contents read back. These are host CPU-path results, not device or CTS results. Approval still needs the trace path fixed and the two-importer case the bot raised.

…ced-map regression

Follow-up findings on the independent-slots change: decode.c reads
bo->_map directly (agxdecode_grab_mapped, fetch_gpu_mem, cmdstream) and
wrap.c populates ._map at BO creation, so the stable BO mapping must
keep existing and stay pointer-stable across application maps; and two
memory objects sharing one imported BO must map independently, since an
ordinary map currently returns another importer's placed mapping and
that importer's unmap destroys it.

Two new cases on the same tmpfile-backed shared kernel seam: the
decoder-style stable mapping must match agx_bo_map, stay coherent with
a placed application mapping and survive placed map + unmap with an
unchanged pointer; and two importers (refcnt 2) must get distinct
mappings with mutual coherence, where the first importer's unmap leaves
the second's mapping valid. Application-map call sites of these cases
adapt to the per-memory mapping shape in the production fix commit;
assertions do not change. All resource failures FAIL; no SKIP paths.
@chriskearney

chriskearney commented Oct 30, 2026 •

Copy link
Copy Markdown

Observational benchmark of the current heads on two M1 Macs. This is not an approval. At c70e5372227417c8ebac4f218c013f8a378fc1a1 this PR still has the two open defects above: the trace-mode crash at the first submission, and freeing one of two imported memory objects can unmap the other one's mapping. These runs exercise neither path, and they don't exercise any of the three fixes.

What was compared

Workload: the Vulkan benchmark helper from omarchy-m-test GPU suite 1.

  • It runs 7 workloads, one warm-up round and three measured rounds, plus a clean compute control and a deliberately corrupted one. That is 30 runs per arm.
  • Checked in every run:
    • The only Vulkan driver mapped in the process was the staged libvulkan_asahi.so (baseline e85887bd…, control 0125441f…), loaded through the system loader.
    • The environment was clean.
    • No new kernel fault lines appeared, and the process group drained.
  • All 4 arms: 30/30 runs valid.

Results: median change of the control against the baseline. These are latencies, so lower is better. † marks a case where the min–max ranges of the two arms don't overlap.

workload M1 Air submit+wait M1 Air GPU time M1 submit+wait M1 GPU time
raster −0.6% +0.1% −0.4% +0.0%
texture −2.8% −3.1% −0.7% −0.6%
compute +4.6% +5.9% +0.3% +0.4%
copy −0.6% † −0.5% † +1.6% † +0.3%
submit-single +5.5% +4.5% † −1.4% −1.4%
submit-batched +4.2% −1.7% −0.2% −1.2%
bo-lifetime +0.2% −1.9% −0.4% +2.9%

Limits

  • Descriptive only: n = 3 per workload and arm, one job per arm, and no CPU frequency or affinity control. No statistical significance or equivalence is claimed.
  • Common paths only: the runs exercise honeykrisp: give capture and replay buffers their own address range #9's address-heap change and honeykrisp: map placed memory at the allocation size #8's split of internal and application mappings. There are no placed maps, host image copies, capture/replay buffers or transform feedback, so this qualifies none of the three fixes.
  • M1 external-display path: not tested, because no display was connected.
  • omarchy-m-test: no check status differs between baseline and control on either Mac.
  • Lab harness failure, identical in both arms: every job as a whole reads failed, because the lab's separate provider-attribution check stopped with nonregular live graphics path. The exact rejected path was not logged. The check's source and CPU-only controls reproduce that error for a DRM render-node mapping, a known lab-harness bug. The job verdicts are left as failed. It doesn't affect the per-run driver attribution above.
  • Restore: both Macs were returned to their original kernel, Mesa packages and boot configuration afterwards.

…regression

The user-required free scenario: two memory objects share one imported
BO and one is freed. The new case runs the BO-relevant hk_FreeMemory
sequence for the freed object - the placed-unmap condition of
hk_device_memory.c:283 and the real agx_bo_unreference refcount drop -
and requires the survivor's mapping to stay valid and coherent, with
the shared BO left at refcount 1. The heap accounting, ext-BO tracking
and vk_device_memory_destroy tail of hk_FreeMemory need a live
vk_device and stay behind the admitted device gate.
The free-path case calls the real agx_bo_unreference, which lives in
agx_bo.c in libasahi_lib; the test target never linked the library, so
the parent's red run failed at link with an undefined reference instead
of exercising the refcount. Mirror the libasahi_tests dependency
pattern: link_with libasahi_lib, which propagates its own dependency
closure (dep_libdrm, idep_nir, libvdrm, libasahi_decode, idep_libagx,
idep_libpoly) to the consumer. No test or assertion changed.
Parent-observed red at e14884a59caa (pr8-owner-red3.log): the stable BO
mapping was absent because agx_bo_map populated only _imap, and two
memory objects sharing an imported BO shared one replaceable mapping -
importer B's ordinary map returned importer A's placed mapping, so
freeing or unmapping A destroyed B's pointer.

Restore bo->_map as the single stable driver-internal mapping and delete
the _imap slot: agx_bo_map creates and returns _map (whole BO), decode.c
and wrap.c keep reading and populating it directly, the BO cache keeps
it as before, and agx_bo_free releases it. Application mappings leave
the BO entirely: hk_MapMemory2KHR obtains a fresh mapping per memory
object from the returning bo_mmap primitive (placed maps at the
requested fixed address, ordinary maps whole-BO at a kernel-chosen
address) into the existing mem->map plus a new mem->map_size;
hk_UnmapMemory2KHR plain and reserve paths, the placed-address mismatch
error path and vkFreeMemory release exactly that object's mapping. The
shared-BO guard is removed rather than layered: with no replaceable
mapping on the BO there is nothing left for it to protect, and two
importers now map independently with coherence coming from the shared
pages. Offset/size placed contract, mmap failure handling, the
already-mapped check and error results are unchanged.

The regression keeps its assertions and adapts only the application
call expressions to the per-memory shape. Red lineage cited in the test
header (8af285bd30c, e14884a59ca). No device run: hardware, CTS and
precision gates hold.
The stable BO mapping always covers the whole BO. Its length is already bo->size. Application mapping lengths belong to each memory object, and the old placed-map flag has no readers after the ownership fix.
Comment on lines +508 to +510
munmap(map_a, ALLOC_SIZE);
map_a = NULL;
agx_bo_unreference(&m.dev, &m.bo);

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 Tests bypass the ownership fix

The new importer tests create and remove application mappings themselves through ops.bo_mmap and munmap. They never call hk_MapMemory2KHR, hk_UnmapMemory2KHR, or hk_FreeMemory, where the ownership fix lives. If those functions start reusing or unmapping another object's pointer again, these tests would still pass.

Move the per-object mapping code into shared helpers that these tests call, or add tests through the Vulkan entrypoints.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

CHANGES at 4e269c5.

Confirmed: the current CPU import cases repeat map and unmap expressions. They do not guard the Vulkan memory code itself. I am moving that code into helpers used by both the driver and tests. Full import and teardown still need a device run.

The independent host checks and native suite results stay bound to this head and the stated union. They are not a GPU or CTS pass. This PR stays not ready.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for confirming the gap and keeping the PR not ready. At 4e269c57, the helper refactor is not in this head yet: hk_MapMemory2KHR/hk_UnmapMemory2KHR still perform the mapping operations directly, and the CPU importer/free cases still call ops.bo_mmap, munmap, and agx_bo_unreference themselves. The tests therefore still don’t guard the Vulkan memory logic I flagged.

Once the shared helpers are in place and the CPU cases call them, that should address this specific test-coverage concern. The live-device checks and other stated qualification gaps remain separate gates.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

@chriskearney

Copy link
Copy Markdown

Independent follow-up at 4e269c5. The source correction addresses the mapping-ownership findings: the BO retains its stable internal/decoder mapping, while each VkDeviceMemory owns its application pointer and length. This is a source/host validation update, not device qualification or merge approval.

On an x86_64 address-honouring drm-shim, using the same preserved repro programs and controls:

  • All six real-driver host-copy cases pass, including placed-map-after-image-bind, the crash variant, and host copy after plain unmap or reserve unmap. They also pass with ASAHI_MESA_DEBUG=trace.
  • The old c70e537 first-submit trace SIGSEGV still reproduces. On 4e269c5 it is gone: the 25,311-byte decoded command dump is byte-identical to main. The helper's GPU-data check remains the expected failure without a GPU; that is not counted as a transform-feedback pass.
  • The unchanged main and ddfb4cf controls retain their wrong-address/dangling-pointer results; ddfb4cf still faults in the crash variant.
  • A clean build requesting only libasahi_placed_map_test succeeds from an empty build directory, and all seven cases pass. This includes stable-map, independent-importer and free-one/survivor checks. Those importer cases use real BO reference counting with mock mmap and mirror the hk expressions; they do not execute the complete Vulkan import/free path. drm-shim lacks the GEM PRIME handle/fd operations needed for that live-import test.

Separately, the exact clean author union of #10 b87e745, this #8 head, and #9 a8960b7 (local tip 9460a17b877, tree c0404e39) built natively for aarch64 with all 49 registered Meson tests passing, including both Asahi regression targets. No build overlay was needed: #9 now supplies its own idep_nir dependency. Production and test package payloads are byte-identical, independently checked.

Still untested on this head: actual Mac/GPU execution, CTS, and the complete heap-accounting/external-BO/Vulkan teardown path for shared imported memory. The earlier observational benchmark stays bound to the old stack and does not qualify this successor. The PR's current device-qualification hold remains.

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