feat(gpu): import the native Adreno and Metal stack
Bring the current NativeAdreno, NativeMetal, AdrenoFirmware, AdrenoPM4, AirNir and ArmSmmu4K sources, the Mesa pipeline-cache and builtin-shader patches, and the Turnip host cache tools.
This commit is contained in:
1 parent
cb6dcea743
commit
6c56ecec19
64 files changed
+6586
-286
No files matched your search
@@ -3,6 +3,8 @@
|
||||
#include "ArmSmmu4KMmio.h"
|
||||
#include <IOKit/IOLib.h>
|
||||
|
||||
extern "C" volatile uint32_t native_adreno_pool_mib;
|
||||
extern "C" volatile uint32_t native_adreno_pool_stats[8],native_adreno_pool_status;
|
||||
static const uint64_t kIovas[GpuBufferCount]={GpuSqeIova,GpuRingIova,GpuMemptrsIova,GpuPwrupIova,GpuScratchIova};
|
||||
static const size_t kBytes[GpuBufferCount]={GpuSqeBytes,GpuRingBytes,GpuPageBytes,GpuPageBytes,GpuPageBytes};
|
||||
IOReturn NativeAdrenoGpuContext::bringUp(const ArmSmmu4KIo *io,unsigned bank,unsigned asid,const unsigned slots[2],
|
||||
@@ -25,6 +27,15 @@ IOReturn NativeAdrenoGpuContext::bringUp(const ArmSmmu4KIo *io,unsigned bank,uns
|
||||
if(report->output_bits>40)report->output_bits=40;
|
||||
if(!report->output_bits)status=kIOReturnUnsupported;
|
||||
}
|
||||
/* The BO pool is taken first, while the VM still has long contiguous runs (the board sets the size from sheng-gpu-pool; 0 = scattered BOs only).
|
||||
* A pool that cannot be had at one size is tried at the next smaller; none is not an error. */
|
||||
if(!status && native_adreno_pool_mib) {
|
||||
for(uint32_t mib=native_adreno_pool_mib;mib>=16;mib>>=1) {
|
||||
const IOReturn pooled=NativeAdrenoMemory::reservePool(report->output_bits,(size_t)mib<<20);
|
||||
native_adreno_pool_stats[0]=pooled==kIOReturnSuccess?mib:0;native_adreno_pool_status=(uint32_t)pooled;
|
||||
if(pooled==kIOReturnSuccess)break;
|
||||
}
|
||||
}
|
||||
if(!status)status=tables_.allocate(TablePages*0x1000,report->output_bits,true,TablePages*0x1000);
|
||||
for(unsigned i=0;!status && i<GpuBufferCount;++i)
|
||||
status=buffers_[i].allocate(kBytes[i],report->output_bits,true,kBytes[i]);
|
||||
@@ -32,6 +43,7 @@ IOReturn NativeAdrenoGpuContext::bringUp(const ArmSmmu4KIo *io,unsigned bank,uns
|
||||
step(Prepare);
|
||||
status=smmu_.prepare(&tables_,0,0,GpuApertureBytes,NativeAdrenoSmmuContext::MaxLeaves,
|
||||
bank,asid,1000,report->output_bits);
|
||||
if(!status)smmu_.setAliasScratch(alias_scratch_,sizeof alias_scratch_/sizeof *alias_scratch_);
|
||||
}
|
||||
if(!status) {
|
||||
step(Program);
|
||||
@@ -53,7 +65,9 @@ IOReturn NativeAdrenoGpuContext::bringUp(const ArmSmmu4KIo *io,unsigned bank,uns
|
||||
if(!status) {
|
||||
step(VmAttach);
|
||||
NativeAdrenoVmRegion lower={&tables_,smmu_.arenaHandle(),smmu_.stateHandle()};
|
||||
status=vm_.attach(io,smmu_.configHandle(),smmu_.streamsHandle(),&lower,nullptr,16384,262144);
|
||||
/* 65536 spans: 256 MiB of BOs even when every page is its own span. The Metal runtime pools its memory in 64 MiB blocks,
|
||||
* which XNU rarely finds physically contiguous; one scattered block is 16384 spans (int-070 ran out at 16384). */
|
||||
status=vm_.attach(io,smmu_.configHandle(),smmu_.streamsHandle(),&lower,nullptr,131072,262144);
|
||||
}
|
||||
if(!status) {
|
||||
step(Map);
|
||||
|
||||
@@ -24,6 +24,8 @@ class NativeAdrenoGpuContext final {
|
||||
uint64_t handles_[GpuBufferCount]{};
|
||||
NativeAdrenoSmmuContext smmu_;
|
||||
NativeAdrenoVm vm_;
|
||||
/* The alias checks' sort scratch: one entry per range of the smaller compared side, at most half the aperture's leaves. */
|
||||
uint32_t alias_scratch_[NativeAdrenoSmmuContext::MaxLeaves/2];
|
||||
bool ready_=false,used_=false;
|
||||
public:
|
||||
enum Step : uint32_t {Allocate=1,Prepare,Program,Attach,VmAttach,Map,Ready};
|
||||
|
||||
@@ -64,6 +64,9 @@ static uint32_t ilog2(size_t value)
|
||||
{
|
||||
uint32_t result=0;while(value>1) {value>>=1;++result;}return result;
|
||||
}
|
||||
/* The last submissions as the CP will read them: [slot] fence, start index, end index, word count, then up to 112 ring words from the start. The board log
|
||||
* prints them, so a fence the GPU never wrote can be compared with the packets that should have written it. */
|
||||
extern "C" {volatile uint32_t native_adreno_ring_log[4][4+112];volatile uint32_t native_adreno_ring_log_count;}
|
||||
IOReturn NativeAdrenoRing::poison()
|
||||
{
|
||||
poisoned_=true;return kIOReturnIOError;
|
||||
@@ -229,6 +232,14 @@ IOReturn NativeAdrenoRing::submitWords(const uint32_t *words,size_t word_count,u
|
||||
if(status)return status;
|
||||
/* Whole-ring publication is deliberate: a submission may wrap, and one
|
||||
* clean of the owned pool is cheaper than a split range proof. */
|
||||
{
|
||||
const uint32_t slot=native_adreno_ring_log_count&3;
|
||||
const uint32_t total=(end-next_)&mask;
|
||||
volatile uint32_t *record=native_adreno_ring_log[slot];
|
||||
record[0]=seqno_+1;record[1]=next_;record[2]=end;record[3]=total;
|
||||
for(uint32_t i=0;i<112;++i)record[4+i]=i<total?ring_[(next_+i)&mask]:0;
|
||||
native_adreno_ring_log_count=native_adreno_ring_log_count+1;
|
||||
}
|
||||
if(cache_.publish(cache_.cookie,ring_,ring_bytes_))return poison();
|
||||
if(io_.barrier(io_.cookie))return poison();
|
||||
if(io_.write32(io_.cookie,RingWptr,end))return poison();
|
||||
|
||||
@@ -93,6 +93,7 @@ public:
|
||||
uint32_t timeout_us,uint32_t *observed);
|
||||
bool poisoned() const {return poisoned_;}
|
||||
uint32_t outstanding() const {return seqno_-completed_;}
|
||||
uint32_t lastSubmitted() const {return seqno_;} /* the newest fence number handed out */
|
||||
uint32_t completedFence() const {return completed_;}
|
||||
uint32_t submissions() const {return submissions_;}
|
||||
uint32_t readPointer() const {return rptr_shadow_;}
|
||||
|
||||
@@ -46,6 +46,8 @@ extern "C" const uint64_t native_adreno_firmware_image_size __attribute__((weak)
|
||||
extern "C" const uint8_t native_adreno_sqe_firmware_image[] __attribute__((weak));
|
||||
extern "C" const uint64_t native_adreno_sqe_firmware_image_size __attribute__((weak));
|
||||
#include "NativeAdrenoUserClient.h"
|
||||
/* Physical address of the fence page (the CP writes its timestamps there): the board log reads it from DRAM and through the CPU cache. */
|
||||
extern "C" {volatile uint64_t native_adreno_fence_phys=0;}
|
||||
|
||||
class NativeAdrenoGpuClient;
|
||||
static IOUserClient *createGpuClient(task_t,void *,UInt32,OSDictionary *,NativeAdrenoUserClient *);
|
||||
@@ -181,8 +183,12 @@ uint64_t NativeAdrenoGpuService::ring_now(void *)
|
||||
NativeAdrenoKernelTime::now_us(nullptr,µseconds);
|
||||
return microseconds;
|
||||
}
|
||||
extern "C" uint64_t (*native_adreno_vm_now_us)(void);
|
||||
extern "C" volatile uint32_t native_adreno_pool_mib;
|
||||
static uint64_t vm_clock_us(void) {uint64_t microseconds=0;NativeAdrenoKernelTime::now_us(nullptr,µseconds);return microseconds;}
|
||||
IOService *NativeAdrenoGpuService::probe(IOService *provider,SInt32 *score)
|
||||
{
|
||||
native_adreno_vm_now_us=vm_clock_us;
|
||||
if(!advertisesAdreno(provider))return nullptr;
|
||||
if(score)*score=1000;
|
||||
return this;
|
||||
@@ -289,6 +295,9 @@ bool NativeAdrenoGpuService::start(IOService *provider)
|
||||
* bank, attaches the stream and maps the GMU payloads. Do not run it before the read-only power/clock observation
|
||||
* (sheng-gpu-power, docs/gpu-hardware-plan.md) shows the GPU SMMU powered and writable: the apps SMMU on this board is
|
||||
* hypervisor-managed (docs/native-ufs.md) and the adreno SMMU has not been shown to differ. */
|
||||
{
|
||||
uint32_t mib=384;PE_parse_boot_argn("sheng-gpu-pool",&mib,sizeof(mib));native_adreno_pool_mib=mib;
|
||||
}
|
||||
uint32_t bringup=0;
|
||||
if(PE_parse_boot_argn("sheng-gpu-bringup",&bringup,sizeof(bringup)) && bringup) {
|
||||
NativeAdrenoSmmuService *node=OSDynamicCast(NativeAdrenoSmmuService,smmu);
|
||||
@@ -414,6 +423,7 @@ bool NativeAdrenoGpuService::start(IOService *provider)
|
||||
ring_status=sqe_mem_.allocate(size_t(sqe_bytes),40,true,size_t(sqe_bytes));
|
||||
if(!ring_status)ring_status=vm_.map(&ring_mem_,1,ring_iova,3,0,2,&ring_map_);
|
||||
if(!ring_status)ring_status=vm_.map(&fence_mem_,1,fence_iova,3,0,2,&fence_map_);
|
||||
if(!ring_status && fence_mem_.spanCount())native_adreno_fence_phys=fence_mem_.spans()[0].physical;
|
||||
if(!ring_status)ring_status=vm_.map(&sqe_mem_,1,sqe_iova,3,0,2,&sqe_map_);
|
||||
if(ring_status) {
|
||||
IOLog("NativeAdreno: ring/SQE setup stopped at 0x%x\n",ring_status);
|
||||
|
||||
@@ -52,6 +52,9 @@ public:
|
||||
const ArmSmmu4KLiveArena *arena() const {return &arena_;}
|
||||
/* Handles for a NativeAdrenoVm that will map buffers through this bank. */
|
||||
ArmSmmu4KLiveArena *arenaHandle() {return &arena_;}
|
||||
/* Scratch the kit's physical-alias checks sort in (ArmSmmu4KLiveArena::alias_scratch), owned by the caller for this context's
|
||||
* lifetime; without it those checks compare pairwise. Only after prepare. */
|
||||
void setAliasScratch(uint32_t *entries,size_t count) {arena_.alias_scratch=entries;arena_.alias_scratch_entries=entries?count:0;}
|
||||
ArmSmmu4KLiveState *stateHandle() {return &state_;}
|
||||
ArmSmmu4KContextConfig *configHandle() {return &config_;}
|
||||
ArmSmmu4KStreamState *streamsHandle() {return &streams_;}
|
||||
|
||||
@@ -29,7 +29,7 @@ IOReturn NativeAdrenoPrepareSmmuMemory(NativeAdrenoMemory *memory,unsigned input
|
||||
memory->spans()[0].offset || memory->spans()[0].bytes!=memory->bytes() ||
|
||||
!memory->bytes() || (memory->bytes()&4095))return kIOReturnUnsupported;
|
||||
*arena={{input_bits,output_bits,memory->spans()[0].physical,budget},iova,bytes,upper,
|
||||
static_cast<volatile uint64_t *>(memory->mapping()),memory->bytes()/4096};
|
||||
static_cast<volatile uint64_t *>(memory->mapping()),memory->bytes()/4096,nullptr,0};
|
||||
ArmSmmu4KStatus status=ArmSmmu4KLivePrepare(arena,state);
|
||||
if(status!=ArmSmmu4KSuccess)return status==ArmSmmu4KCapacity?kIOReturnNoResources:kIOReturnBadArgument;
|
||||
ArmSmmu4KLiveResult audit;
|
||||
|
||||
@@ -7,8 +7,20 @@
|
||||
* and IB_SIZE in dwords (bits 0..19), exactly as the pinned adreno_pm4.xml
|
||||
* domain states and Linux msm_gem_submit.c converts the userspace byte size. */
|
||||
enum : uint32_t {SubmitIndirectBuffer=0x3f,SubmitIbWords=3,SubmitMaxJobs=8,
|
||||
SubmitMaxBuffers=16,SubmitMaxIbBytes=0xfffffu*4u};
|
||||
SubmitMaxBuffers=16,SubmitMaxIbBytes=0xfffffu*4u,
|
||||
SubmitAuditBytes=256u*1024u}; /* IB bytes the write-combine audit compares per submission */
|
||||
extern "C" {volatile uint32_t native_adreno_coherence[8];}
|
||||
/* The submission (1-based count) at which the write-combine audit found a dirty IB line or failed; 0 = never. */
|
||||
extern "C" {volatile uint32_t native_adreno_wc_distrusted;}
|
||||
/* The write-combine audit before each submission while trusted: [0] submissions audited, [1] IB lines compared, [2] dirty lines,
|
||||
* [3] stale clean lines, [4] the last audit failure (any failure also ends the trust), [5] pages found back at the default attribute
|
||||
* and write-combined again. */
|
||||
extern "C" {volatile uint32_t native_adreno_wc_audit[6];}
|
||||
/* Fence waits: [0] calls, [1] timeouts, [2] the fence of the last timed-out wait, [3] the fence value the GPU had written then, [4] the newest fence handed out,
|
||||
* [5] the newest completed, [6] waits for a fence no job holds (observeFence path), [7] the last fence refused as never submitted. */
|
||||
extern "C" {volatile uint32_t native_adreno_fence_wait[8];}
|
||||
/* The IBs of the last submissions: [slot] fence, iova low/high, bytes, then the first 28 dwords of the first IB (read as the kernel maps it). */
|
||||
extern "C" {volatile uint32_t native_adreno_ib_log[4][4+28];volatile uint32_t native_adreno_ib_log_count;}
|
||||
IOReturn NativeAdrenoSubmit::poison()
|
||||
{
|
||||
poisoned_=true;return kIOReturnIOError;
|
||||
@@ -38,6 +50,7 @@ IOReturn NativeAdrenoSubmit::release(Job &job)
|
||||
if(retired)status=retired;else ++stats_.references_retired;
|
||||
}
|
||||
if(status)return poison();
|
||||
if(vm_->acquireImported())return poison();
|
||||
job={};return kIOReturnSuccess;
|
||||
}
|
||||
IOReturn NativeAdrenoSubmit::detach()
|
||||
@@ -100,6 +113,40 @@ IOReturn NativeAdrenoSubmit::submit(const NativeAdrenoSubmitBuffer *buffers,size
|
||||
/* int-070: the first IB word as the kernel's cacheable alias and DRAM see it before any maintenance, then DRAM after the CPU
|
||||
* caches over every mapped BO are cleaned and invalidated. The board shows the last submission's record on a stall. */
|
||||
uint32_t cached=~0u,before=~0u,ignored=0,after=~0u;
|
||||
/* Write-combined stores this CPU made before the call (the client's command words) complete before anything below. */
|
||||
__asm__ volatile("dsb sy":::"memory");
|
||||
/* int-098/099: with write-combined user BOs and no maintenance the CP stopped at the end of the first IB, as in int-065..069 when an
|
||||
* IB sat in a CPU cache. While write-combined BOs are trusted, every line of this submission's IBs is compared as a CPU cache and
|
||||
* DRAM hold it (bounded). A dirty line means a client view still caches its BOs: maintain every BO from now on, this submission
|
||||
* included (the audit already wrote the IB lines back; the rest of the BOs may hold more). */
|
||||
if(vm_->writeCombinedTrusted()) {
|
||||
/* int-100: the dirty IB page was back at XNU's default (cacheable) attribute. Write-combine every such page again first; the
|
||||
* pmap writes its lines back on the way, so the audit below should then find none. */
|
||||
uint32_t reset=0;
|
||||
IOReturn audited=vm_->reassertWriteCombined(&reset);
|
||||
native_adreno_wc_audit[5]=native_adreno_wc_audit[5]+reset;
|
||||
uint32_t lines=0,dirty=0,stale=0;size_t budget=SubmitAuditBytes;
|
||||
/* The re-assertion above is the correction and runs every time; the audit only detects, and found no dirty line in int-141's
|
||||
* 133798, so after the first 16 submissions it compares every eighth one's IBs (it cost about 2 ms each). */
|
||||
++audit_tick_;
|
||||
const bool audit_now=audit_tick_<=16 || (audit_tick_&7)==1;
|
||||
if(!audit_now)budget=0;
|
||||
for(size_t i=0;i<count && budget && !audited;++i) { /* a failed re-assertion skips the audit and ends the trust */
|
||||
const size_t bytes=buffers[i].bytes<budget?(size_t)buffers[i].bytes:budget;
|
||||
audited=vm_->auditRange(buffers[i].iova,bytes,&lines,&dirty,&stale);budget-=bytes;
|
||||
}
|
||||
if(audit_now && audited!=kIOReturnUnsupported) { /* no board hook (host): nothing to compare */
|
||||
native_adreno_wc_audit[0]=native_adreno_wc_audit[0]+1;
|
||||
native_adreno_wc_audit[1]=native_adreno_wc_audit[1]+lines;
|
||||
native_adreno_wc_audit[2]=native_adreno_wc_audit[2]+dirty;
|
||||
native_adreno_wc_audit[3]=native_adreno_wc_audit[3]+stale;
|
||||
if(audited)native_adreno_wc_audit[4]=audited;
|
||||
}
|
||||
if(dirty || (audited && audited!=kIOReturnUnsupported)) {
|
||||
vm_->distrustWriteCombined();
|
||||
native_adreno_wc_distrusted=native_adreno_coherence[0]+1;
|
||||
}
|
||||
}
|
||||
(void)vm_->sampleWord(buffers[0].iova,&cached,&before);
|
||||
IOReturn status=vm_->publishMapped();
|
||||
if(!status) {
|
||||
@@ -118,6 +165,14 @@ IOReturn NativeAdrenoSubmit::submit(const NativeAdrenoSubmitBuffer *buffers,size
|
||||
for(size_t i=0;i<handle_count;++i) {vm_->retireJobAfterCompletion(handles[i]);++stats_.references_retired;}
|
||||
return status;
|
||||
}
|
||||
{
|
||||
const uint32_t slot=native_adreno_ib_log_count&3;uint32_t head[28];
|
||||
(void)vm_->peekWords(buffers[0].iova,head,28);
|
||||
native_adreno_ib_log[slot][0]=ring_->lastSubmitted()+1;native_adreno_ib_log[slot][1]=(uint32_t)buffers[0].iova;
|
||||
native_adreno_ib_log[slot][2]=(uint32_t)(buffers[0].iova>>32);native_adreno_ib_log[slot][3]=(uint32_t)buffers[0].bytes;
|
||||
for(unsigned i=0;i<28;++i)native_adreno_ib_log[slot][4+i]=head[i];
|
||||
native_adreno_ib_log_count=native_adreno_ib_log_count+1;
|
||||
}
|
||||
status=ring_->submitFramed(words,word_count,&value);
|
||||
if(status) {
|
||||
/* A poisoned ring may already have consumed the WPTR write, so the
|
||||
@@ -154,17 +209,30 @@ IOReturn NativeAdrenoSubmit::wait(uint32_t fence,const NativeAdrenoRingClock *cl
|
||||
{
|
||||
if(!attached_ || poisoned_)return kIOReturnNotReady;
|
||||
Job *job=find(fence);
|
||||
uint32_t observed=0;
|
||||
native_adreno_fence_wait[0]=native_adreno_fence_wait[0]+1;
|
||||
if(!job) {
|
||||
const NativeAdrenoRingFence passed=ring_->observeFence(fence,clock,timeout_us,nullptr);
|
||||
native_adreno_fence_wait[6]=native_adreno_fence_wait[6]+1;
|
||||
const NativeAdrenoRingFence passed=ring_->observeFence(fence,clock,timeout_us,&observed);
|
||||
if(passed==NativeAdrenoRingFencePoisoned)return poison();
|
||||
if(passed==NativeAdrenoRingFenceArgument)return kIOReturnBadArgument;
|
||||
if(passed==NativeAdrenoRingFenceTimeout) {++stats_.timeouts;return kIOReturnTimeout;}
|
||||
if(passed==NativeAdrenoRingFenceArgument) {native_adreno_fence_wait[7]=fence;return kIOReturnBadArgument;}
|
||||
if(passed==NativeAdrenoRingFenceTimeout) {
|
||||
++stats_.timeouts;
|
||||
native_adreno_fence_wait[1]=native_adreno_fence_wait[1]+1;native_adreno_fence_wait[2]=fence;native_adreno_fence_wait[3]=observed;
|
||||
native_adreno_fence_wait[4]=ring_->lastSubmitted();native_adreno_fence_wait[5]=ring_->completedFence();
|
||||
return kIOReturnTimeout;
|
||||
}
|
||||
return kIOReturnSuccess;
|
||||
}
|
||||
const NativeAdrenoRingFence result=ring_->wait(fence,clock,timeout_us,nullptr);
|
||||
const NativeAdrenoRingFence result=ring_->wait(fence,clock,timeout_us,&observed);
|
||||
if(result==NativeAdrenoRingFencePoisoned)return poison();
|
||||
if(result==NativeAdrenoRingFenceArgument)return kIOReturnBadArgument;
|
||||
if(result==NativeAdrenoRingFenceTimeout) {++stats_.timeouts;return kIOReturnTimeout;}
|
||||
if(result==NativeAdrenoRingFenceArgument) {native_adreno_fence_wait[7]=fence;return kIOReturnBadArgument;}
|
||||
if(result==NativeAdrenoRingFenceTimeout) {
|
||||
++stats_.timeouts;
|
||||
native_adreno_fence_wait[1]=native_adreno_fence_wait[1]+1;native_adreno_fence_wait[2]=fence;native_adreno_fence_wait[3]=observed;
|
||||
native_adreno_fence_wait[4]=ring_->lastSubmitted();native_adreno_fence_wait[5]=ring_->completedFence();
|
||||
return kIOReturnTimeout;
|
||||
}
|
||||
IOReturn status=release(*job);
|
||||
if(status)return status;
|
||||
++stats_.completed;
|
||||
|
||||
@@ -13,6 +13,9 @@ struct NativeAdrenoSubmitBuffer {
|
||||
* through DRAM before and through DRAM after the CPU cache maintenance of every mapped BO (~0 when unreadable); [4] samples whose
|
||||
* three values differ and [5..7] the first such sample. */
|
||||
extern "C" volatile uint32_t native_adreno_coherence[8];
|
||||
/* The write-combine audit (int-100), see NativeAdrenoSubmit.cpp, and the submission (1-based) that ended the trust; 0 = never. */
|
||||
extern "C" volatile uint32_t native_adreno_wc_audit[6];
|
||||
extern "C" volatile uint32_t native_adreno_wc_distrusted;
|
||||
struct NativeAdrenoSubmitStats {
|
||||
uint32_t submissions,completed,timeouts,poisoned_jobs;
|
||||
size_t references_held,references_retired,peak_jobs;
|
||||
@@ -38,6 +41,7 @@ class NativeAdrenoSubmit final {
|
||||
NativeAdrenoRing *ring_=nullptr;
|
||||
NativeAdrenoSubmitStats stats_{};
|
||||
bool attached_=false,poisoned_=false;
|
||||
uint32_t audit_tick_=0; /* submissions since attach; every eighth one audits its IB lines */
|
||||
Job *find(uint32_t fence);
|
||||
Job *free();
|
||||
IOReturn release(Job &);
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
#include "NativeAdrenoVm.h"
|
||||
#include "NativeAdrenoVmAlias.h"
|
||||
extern "C" volatile uint32_t native_adreno_trace_stage;
|
||||
extern "C" {int (*native_adreno_dram_read32)(uint64_t,uint32_t *);}
|
||||
extern "C" {int (*native_adreno_dram_read32)(uint64_t,uint32_t *);int (*native_adreno_cached_read32)(uint64_t,uint32_t *);
|
||||
int (*native_adreno_line_audit)(uint64_t,size_t,uint32_t *,uint32_t *);}
|
||||
#include <string.h>
|
||||
#include <IOKit/IOLib.h>
|
||||
/* 1 once the board proved the GPU's cacheable inner-shareable accesses coherent with the CPU's caches in both directions (the A740's
|
||||
* ADRENO_QUIRK_HAS_CACHED_COHERENT behind the dma-coherent GPU SMMU, which Linux maps with IOMMU_CACHE): a mapping with write-back
|
||||
* attributes then needs no CPU cache maintenance. 0 keeps every mapping maintained as before. Counters: maintenance passes skipped for
|
||||
* coherent mappings at map, unmap, submission and completion. */
|
||||
extern "C" {volatile uint32_t native_adreno_io_coherent;volatile uint32_t native_adreno_coherent_skips[4];}
|
||||
static bool coherentMapping(unsigned writeback) {return writeback && native_adreno_io_coherent;}
|
||||
static bool overlap(uint64_t a,uint64_t ab,uint64_t b,uint64_t bb)
|
||||
{return a<b+bb && b<a+ab;}
|
||||
/* Mappings of one cache/shareability/privilege class may share pages; only pages shared across classes refuse. */
|
||||
static unsigned aliasClass(unsigned permissions,unsigned writeback,unsigned shareability)
|
||||
{return writeback|(shareability<<1)|((permissions&ArmSmmu4KPrivileged)?8u:0u);}
|
||||
static void reverse(ArmSmmu4KRange *first,ArmSmmu4KRange *last)
|
||||
{while(first<last && first<--last) {const ArmSmmu4KRange t=*first;*first++=*last;*last=t;}}
|
||||
static bool validBinding(const NativeAdrenoVmRegion &r,unsigned upper)
|
||||
{
|
||||
return r.tables && r.arena && r.state && !r.tables->quarantined() &&
|
||||
@@ -21,6 +34,24 @@ ArmSmmu4KLiveRegion NativeAdrenoVm::region(unsigned upper) const
|
||||
return {regions_[upper].arena,regions_[upper].state,
|
||||
ranges_+(upper?lower_count_:0),upper?upper_count_:lower_count_};
|
||||
}
|
||||
/* Board hook and per-phase time of the two table transactions, for the log: [0] map, [1] unmap; each row is count, the phases in order, the slowest call
|
||||
* and the bytes mapped (int-134: BoMap averaged 135 ms and the desktop crawled). */
|
||||
extern "C" {uint64_t (*native_adreno_vm_now_us)(void)=nullptr;volatile uint64_t native_adreno_vm_phase_us[2][8];}
|
||||
namespace {
|
||||
struct PhaseClock {
|
||||
uint64_t last=native_adreno_vm_now_us?native_adreno_vm_now_us():0,began=last;
|
||||
void lap(unsigned row,unsigned slot) {
|
||||
if(!native_adreno_vm_now_us)return;
|
||||
const uint64_t now=native_adreno_vm_now_us();native_adreno_vm_phase_us[row][slot]+=now-last;last=now;
|
||||
}
|
||||
void done(unsigned row,uint64_t bytes) {
|
||||
if(!native_adreno_vm_now_us)return;
|
||||
const uint64_t total=native_adreno_vm_now_us()-began;
|
||||
native_adreno_vm_phase_us[row][0]+=1;native_adreno_vm_phase_us[row][7]+=bytes;
|
||||
if(total>native_adreno_vm_phase_us[row][6])native_adreno_vm_phase_us[row][6]=total;
|
||||
}
|
||||
};
|
||||
}
|
||||
ArmSmmu4KStatus NativeAdrenoVm::observe() const
|
||||
{
|
||||
ArmSmmu4KStreamResult routes;
|
||||
@@ -30,6 +61,16 @@ ArmSmmu4KStatus NativeAdrenoVm::observe() const
|
||||
if(status)return status;
|
||||
return ArmSmmu4KObserveStreams(&io_,streams_,context_,&routes);
|
||||
}
|
||||
ArmSmmu4KStatus NativeAdrenoVm::observeStreamsOnly() const
|
||||
{
|
||||
ArmSmmu4KStreamResult routes;
|
||||
return ArmSmmu4KObserveStreams(&io_,streams_,context_,&routes);
|
||||
}
|
||||
ArmSmmu4KStatus NativeAdrenoVm::observeCheap()
|
||||
{
|
||||
if(++since_audit_>=64) {since_audit_=0;return observe();}
|
||||
return observeStreamsOnly();
|
||||
}
|
||||
IOReturn NativeAdrenoVm::fail(ArmSmmu4KStatus status)
|
||||
{
|
||||
poisoned_=true;result_.operation=status;result_.complete=false;
|
||||
@@ -44,7 +85,7 @@ IOReturn NativeAdrenoVm::attach(const ArmSmmu4KIo *io,ArmSmmu4KContextConfig *co
|
||||
size_t capacity,unsigned budget)
|
||||
{
|
||||
if(attached_ || ranges_ || poisoned_)return kIOReturnBusy;
|
||||
if(!io || !context || !streams || (!lower && !upper) || !capacity || capacity>65536 ||
|
||||
if(!io || !context || !streams || (!lower && !upper) || !capacity || capacity>262144 ||
|
||||
!budget || budget>1048576 || (lower && !validBinding(*lower,0)) || (upper && !validBinding(*upper,1)))
|
||||
return kIOReturnBadArgument;
|
||||
ranges_=IONewData(ArmSmmu4KRange,capacity);if(!ranges_)return kIOReturnNoMemory;
|
||||
@@ -114,50 +155,100 @@ IOReturn NativeAdrenoVm::preflight(const NativeAdrenoMemory *memory,unsigned upp
|
||||
return kIOReturnBadArgument;
|
||||
for(const auto &r:regions_)if(r.arena && overlap(span.physical,span.bytes,
|
||||
r.arena->config.table_physical,r.arena->capacity_pages*UINT64_C(4096)))return kIOReturnBadArgument;
|
||||
for(size_t i=0;i<spanCount();++i)if(overlap(span.physical,span.bytes,ranges_[i].physical,ranges_[i].bytes) &&
|
||||
(writeback!=ranges_[i].writeback || shareability!=ranges_[i].shareability ||
|
||||
((permissions^ranges_[i].permissions)&ArmSmmu4KPrivileged)))return kIOReturnBadArgument;
|
||||
offset+=span.bytes;
|
||||
}
|
||||
return offset==memory->bytes()?kIOReturnSuccess:kIOReturnBadArgument;
|
||||
if(offset!=memory->bytes())return kIOReturnBadArgument;
|
||||
/* Only another cache/shareability/privilege class can conflict. Reuse the
|
||||
* arena's caller-owned temporary alias scratch before the table transaction
|
||||
* uses it. Index the smaller side and query the other in O(log n); neither
|
||||
* the resource spans nor the actual page tables are modified here. */
|
||||
const unsigned mine=aliasClass(permissions,writeback,shareability);
|
||||
size_t other=0;
|
||||
for(size_t i=0;i<spanCount();++i)
|
||||
if(aliasClass(ranges_[i].permissions,ranges_[i].writeback,ranges_[i].shareability)!=mine)++other;
|
||||
if(!other)return kIOReturnSuccess;
|
||||
const bool indexNew=memory->spanCount()<=other;
|
||||
const size_t smaller=indexNew?memory->spanCount():other;
|
||||
uint32_t *order=a->alias_scratch;
|
||||
if(order && smaller<=a->alias_scratch_entries) {
|
||||
if(indexNew) {
|
||||
for(size_t i=0;i<smaller;++i)order[i]=(uint32_t)i;
|
||||
NativeAdrenoPhysicalIndex index(memory->spans(),order,smaller);
|
||||
for(size_t i=0;i<spanCount();++i) {
|
||||
const auto &r=ranges_[i];
|
||||
if(aliasClass(r.permissions,r.writeback,r.shareability)!=mine && index.overlaps(r.physical,r.bytes))return kIOReturnBadArgument;
|
||||
}
|
||||
} else {
|
||||
size_t n=0;
|
||||
for(size_t i=0;i<spanCount();++i)
|
||||
if(aliasClass(ranges_[i].permissions,ranges_[i].writeback,ranges_[i].shareability)!=mine)order[n++]=(uint32_t)i;
|
||||
NativeAdrenoPhysicalIndex index(ranges_,order,n);
|
||||
for(size_t j=0;j<memory->spanCount();++j)
|
||||
if(index.overlaps(memory->spans()[j].physical,memory->spans()[j].bytes))return kIOReturnBadArgument;
|
||||
}
|
||||
return kIOReturnSuccess;
|
||||
}
|
||||
/* Small/unconfigured callers retain the conservative allocation-free path. */
|
||||
for(size_t i=0;i<spanCount();++i) {
|
||||
const ArmSmmu4KRange &r=ranges_[i];
|
||||
if(aliasClass(r.permissions,r.writeback,r.shareability)==mine)continue;
|
||||
for(size_t j=0;j<memory->spanCount();++j)
|
||||
if(overlap(memory->spans()[j].physical,memory->spans()[j].bytes,r.physical,r.bytes))return kIOReturnBadArgument;
|
||||
}
|
||||
return kIOReturnSuccess;
|
||||
}
|
||||
NativeAdrenoVm::Mapping *NativeAdrenoVm::find(uint64_t handle)
|
||||
{if(handle && mappings_)for(size_t i=0;i<NativeAdrenoVmMaxObjects;++i)if(mappings_[i].handle==handle && mappings_[i].memory)return &mappings_[i];return nullptr;}
|
||||
void NativeAdrenoVm::insert(const ArmSmmu4KRange &range,unsigned upper)
|
||||
/* The n ranges just past the table's end (one mapping's spans, consecutive in IOVA and all in one gap) take their sorted place. */
|
||||
void NativeAdrenoVm::insert(size_t n,unsigned upper)
|
||||
{
|
||||
size_t at=0,count=spanCount();while(at<count && ranges_[at].iova<range.iova)++at;
|
||||
for(size_t i=count;i>at;--i)ranges_[i]=ranges_[i-1];ranges_[at]=range;
|
||||
if(upper)++upper_count_;else ++lower_count_;
|
||||
const size_t count=spanCount();
|
||||
size_t at=0;while(at<count && ranges_[at].iova<ranges_[count].iova)++at;
|
||||
reverse(ranges_+at,ranges_+count);reverse(ranges_+count,ranges_+count+n);reverse(ranges_+at,ranges_+count+n);
|
||||
if(upper)upper_count_+=n;else lower_count_+=n;
|
||||
}
|
||||
void NativeAdrenoVm::erase(size_t at,unsigned upper)
|
||||
void NativeAdrenoVm::erase(size_t at,size_t n,unsigned upper)
|
||||
{
|
||||
size_t count=spanCount();for(size_t i=at+1;i<count;++i)ranges_[i-1]=ranges_[i];
|
||||
if(upper)--upper_count_;else --lower_count_;
|
||||
const size_t count=spanCount();
|
||||
memmove(ranges_+at,ranges_+at+n,(count-at-n)*sizeof(*ranges_));
|
||||
if(upper)upper_count_-=n;else lower_count_-=n;
|
||||
}
|
||||
IOReturn NativeAdrenoVm::map(NativeAdrenoMemory *memory,unsigned upper,uint64_t iova,
|
||||
unsigned permissions,unsigned writeback,unsigned shareability,uint64_t *handle)
|
||||
{
|
||||
if(!handle)return kIOReturnBadArgument;*handle=0;result_={};
|
||||
if(!attached_ || poisoned_)return kIOReturnNotReady;
|
||||
PhaseClock clock;
|
||||
IOReturn checked=preflight(memory,upper,iova,permissions,writeback,shareability);if(checked)return checked;
|
||||
if(next_handle_==UINT64_MAX)return kIOReturnNoResources;
|
||||
ArmSmmu4KStatus status=observe();if(status)return fail(status);
|
||||
clock.lap(0,5); /* Previously omitted preflight; map slot 5 was unused. */
|
||||
ArmSmmu4KStatus status=observeCheap();if(status)return fail(status);
|
||||
clock.lap(0,1);
|
||||
checked=memory->holdForDevice();if(checked)return checked;
|
||||
Mapping *record=nullptr;for(size_t i=0;i<NativeAdrenoVmMaxObjects;++i)if(!mappings_[i].memory){record=&mappings_[i];break;}
|
||||
if(!record) {memory->quarantine();return fail(ArmSmmu4KStateMismatch);}
|
||||
*record={memory,0,iova,memory->spanCount(),0,permissions,writeback,shareability,0};++objects_;
|
||||
checked=memory->publishForDevice(0,memory->bytes(),permissions&ArmSmmu4KWrite);
|
||||
if(checked)return fail(ArmSmmu4KIoError);
|
||||
for(size_t i=0;i<record->spans;++i) {
|
||||
const auto &span=memory->spans()[i];
|
||||
ArmSmmu4KRange range={iova+span.offset,span.physical,span.bytes,permissions,writeback,shareability};
|
||||
ArmSmmu4KLiveRegion lower=region(0),high=region(1);ArmSmmu4KLiveResult update;
|
||||
status=ArmSmmu4KLiveUpdateContext(&io_,streams_->bank,context_,lower.arena?&lower:nullptr,
|
||||
high.arena?&high:nullptr,upper,&visibility_[upper],&range,1,budget_,&update);
|
||||
if(status)return fail(status);
|
||||
insert(range,upper);++record->mapped_spans;++result_.completed_spans;
|
||||
if(coherentMapping(writeback))native_adreno_coherent_skips[0]=native_adreno_coherent_skips[0]+1;
|
||||
else {
|
||||
checked=memory->publishForDevice(0,memory->bytes(),permissions&ArmSmmu4KWrite,distrust_write_combined_);
|
||||
if(checked)return fail(ArmSmmu4KIoError);
|
||||
}
|
||||
status=observe();if(status)return fail(status);
|
||||
clock.lap(0,2);
|
||||
/* Every span in one table transaction, staged in the table's free tail (preflight checked the room). */
|
||||
const size_t spans=record->spans;
|
||||
ArmSmmu4KRange *changes=ranges_+spanCount();
|
||||
for(size_t i=0;i<spans;++i) {
|
||||
const auto &span=memory->spans()[i];
|
||||
changes[i]={iova+span.offset,span.physical,span.bytes,permissions,writeback,shareability};
|
||||
}
|
||||
ArmSmmu4KLiveRegion lower=region(0),high=region(1);ArmSmmu4KLiveResult update;
|
||||
status=ArmSmmu4KLiveUpdateContextBatch(&io_,streams_->bank,context_,lower.arena?&lower:nullptr,
|
||||
high.arena?&high:nullptr,upper,&visibility_[upper],changes,spans,1,budget_,&update);
|
||||
if(status)return fail(status);
|
||||
clock.lap(0,3);
|
||||
insert(spans,upper);record->mapped_spans=spans;result_.completed_spans=spans;
|
||||
status=observeStreamsOnly();if(status)return fail(status);
|
||||
clock.lap(0,4);clock.done(0,memory->bytes());
|
||||
record->handle=next_handle_++;*handle=record->handle;result_.complete=true;return kIOReturnSuccess;
|
||||
}
|
||||
IOReturn NativeAdrenoVm::unmap(uint64_t handle)
|
||||
@@ -166,31 +257,43 @@ IOReturn NativeAdrenoVm::unmap(uint64_t handle)
|
||||
result_={};if(!attached_ || poisoned_)return kIOReturnNotReady;
|
||||
Mapping *record=find(handle);if(!record)return kIOReturnBadArgument;
|
||||
if(record->jobs)return kIOReturnBusy;
|
||||
ArmSmmu4KStatus status=observe();
|
||||
PhaseClock clock;
|
||||
ArmSmmu4KStatus status=observeCheap();
|
||||
native_adreno_trace_stage=11;
|
||||
if(status)return fail(status);
|
||||
clock.lap(1,1);
|
||||
unsigned upper=regions_[1].arena && record->iova>=regions_[1].arena->iova;
|
||||
if(record->permissions&ArmSmmu4KWrite) {
|
||||
if(coherentMapping(record->writeback))native_adreno_coherent_skips[1]=native_adreno_coherent_skips[1]+1;
|
||||
else if(record->permissions&ArmSmmu4KWrite) {
|
||||
if(record->memory->acquireDeviceWrites(0,record->memory->bytes())!=kIOReturnSuccess)
|
||||
return fail(ArmSmmu4KIoError);
|
||||
}
|
||||
for(size_t i=0;i<record->spans;++i) {
|
||||
clock.lap(1,2);
|
||||
/* The mapping's spans are consecutive in the table; unmap them in one transaction. */
|
||||
const size_t spans=record->spans;
|
||||
size_t at=0;while(at<spanCount() && ranges_[at].iova!=record->iova)++at;
|
||||
if(spans>spanCount()-at)return fail(ArmSmmu4KStateMismatch);
|
||||
for(size_t i=0;i<spans;++i) {
|
||||
const auto &span=record->memory->spans()[i];
|
||||
ArmSmmu4KRange change={record->iova+span.offset,span.physical,span.bytes,record->permissions,record->writeback,record->shareability};
|
||||
size_t at=0;while(at<spanCount() && ranges_[at].iova!=change.iova)++at;
|
||||
if(at==spanCount())return fail(ArmSmmu4KStateMismatch);
|
||||
ArmSmmu4KLiveRegion lower=region(0),high=region(1);ArmSmmu4KLiveResult update;
|
||||
native_adreno_trace_stage=12;
|
||||
status=ArmSmmu4KLiveUpdateContext(&io_,streams_->bank,context_,lower.arena?&lower:nullptr,
|
||||
high.arena?&high:nullptr,upper,&visibility_[upper],&change,0,budget_,&update);
|
||||
native_adreno_trace_stage=13;
|
||||
if(status)return fail(status);
|
||||
erase(at,upper);--record->mapped_spans;++result_.completed_spans;
|
||||
const ArmSmmu4KRange &r=ranges_[at+i];
|
||||
if(r.iova!=record->iova+span.offset || r.physical!=span.physical || r.bytes!=span.bytes ||
|
||||
r.permissions!=record->permissions || r.writeback!=record->writeback || r.shareability!=record->shareability)
|
||||
return fail(ArmSmmu4KStateMismatch);
|
||||
}
|
||||
status=observe();
|
||||
ArmSmmu4KLiveRegion lower=region(0),high=region(1);ArmSmmu4KLiveResult update;
|
||||
native_adreno_trace_stage=12;
|
||||
status=ArmSmmu4KLiveUpdateContextBatch(&io_,streams_->bank,context_,lower.arena?&lower:nullptr,
|
||||
high.arena?&high:nullptr,upper,&visibility_[upper],ranges_+at,spans,0,budget_,&update);
|
||||
native_adreno_trace_stage=13;
|
||||
if(status)return fail(status);
|
||||
clock.lap(1,3);
|
||||
erase(at,spans,upper);record->mapped_spans=0;result_.completed_spans=spans;
|
||||
status=observeStreamsOnly();
|
||||
native_adreno_trace_stage=14;
|
||||
if(status)return fail(status);
|
||||
clock.lap(1,4);
|
||||
if(record->memory->retireAfterQuiescence()!=kIOReturnSuccess)return fail(ArmSmmu4KStateMismatch);
|
||||
clock.lap(1,5);clock.done(1,record->memory->bytes());
|
||||
*record={};--objects_;result_.complete=true;return kIOReturnSuccess;
|
||||
}
|
||||
IOReturn NativeAdrenoVm::describe(uint64_t handle,uint64_t *iova,size_t *bytes,
|
||||
@@ -218,8 +321,20 @@ IOReturn NativeAdrenoVm::publishMapped()
|
||||
if(!attached_ || poisoned_)return kIOReturnNotReady;
|
||||
for(size_t i=0;i<NativeAdrenoVmMaxObjects;++i) {
|
||||
const Mapping &m=mappings_[i];
|
||||
if(!m.memory || m.memory->imported())continue;
|
||||
if(m.memory->publishForDevice(0,m.memory->bytes(),true))return fail(ArmSmmu4KIoError);
|
||||
if(!m.memory)continue;
|
||||
if(coherentMapping(m.writeback)) {native_adreno_coherent_skips[2]=native_adreno_coherent_skips[2]+1;continue;}
|
||||
if(m.memory->publishForDevice(0,m.memory->bytes(),true,distrust_write_combined_))return fail(ArmSmmu4KIoError);
|
||||
}
|
||||
return kIOReturnSuccess;
|
||||
}
|
||||
IOReturn NativeAdrenoVm::acquireImported()
|
||||
{
|
||||
if(!attached_ || poisoned_)return kIOReturnNotReady;
|
||||
for(size_t i=0;i<NativeAdrenoVmMaxObjects;++i) {
|
||||
const Mapping &m=mappings_[i];
|
||||
if(!m.memory || !m.memory->imported() || !(m.permissions&ArmSmmu4KWrite))continue;
|
||||
if(coherentMapping(m.writeback)) {native_adreno_coherent_skips[3]=native_adreno_coherent_skips[3]+1;continue;}
|
||||
if(m.memory->acquireDeviceWrites(0,m.memory->bytes()))return fail(ArmSmmu4KIoError);
|
||||
}
|
||||
return kIOReturnSuccess;
|
||||
}
|
||||
@@ -238,13 +353,72 @@ IOReturn NativeAdrenoVm::sampleWord(uint64_t iova,uint32_t *cached,uint32_t *dra
|
||||
const auto &span=m.memory->spans()[j];
|
||||
if(offset<span.offset || offset-span.offset>=span.bytes)continue;
|
||||
uint32_t value=0;
|
||||
/* DRAM first: the cached reader writes its line back, so read after it DRAM would always agree (int-099). */
|
||||
if(native_adreno_dram_read32 && !native_adreno_dram_read32(span.physical+(offset-span.offset),&value))*dram=value;
|
||||
/* A write-combined BO's own mapping is non-cacheable: read the cacheable alias instead, so a dirty line is still seen. */
|
||||
if(native_adreno_cached_read32 && !native_adreno_cached_read32(span.physical+(offset-span.offset),&value))*cached=value;
|
||||
break;
|
||||
}
|
||||
return kIOReturnSuccess;
|
||||
}
|
||||
return kIOReturnBadArgument;
|
||||
}
|
||||
IOReturn NativeAdrenoVm::reassertWriteCombined(uint32_t *reset) const
|
||||
{
|
||||
if(!reset)return kIOReturnBadArgument;
|
||||
if(!attached_ || poisoned_)return kIOReturnNotReady;
|
||||
for(size_t i=0;i<NativeAdrenoVmMaxObjects;++i) {
|
||||
const Mapping &m=mappings_[i];
|
||||
if(!m.memory)continue;
|
||||
const IOReturn status=m.memory->reassertWriteCombined(reset);
|
||||
if(status)return status;
|
||||
}
|
||||
return kIOReturnSuccess;
|
||||
}
|
||||
IOReturn NativeAdrenoVm::auditRange(uint64_t iova,size_t bytes,uint32_t *lines,uint32_t *dirty,uint32_t *stale) const
|
||||
{
|
||||
if(!lines || !dirty || !stale)return kIOReturnBadArgument;
|
||||
if(!attached_ || poisoned_)return kIOReturnNotReady;
|
||||
if(!native_adreno_line_audit)return kIOReturnUnsupported;
|
||||
if(!bytes || iova>UINT64_MAX-bytes)return kIOReturnBadArgument;
|
||||
for(size_t i=0;i<NativeAdrenoVmMaxObjects;++i) {
|
||||
const Mapping &m=mappings_[i];
|
||||
if(!m.memory || iova<m.iova || iova-m.iova>=m.memory->bytes())continue;
|
||||
if(!m.memory->writeCombined())return kIOReturnSuccess; /* maintained before every submission anyway */
|
||||
/* Whole lines; the range is clipped to its BO. */
|
||||
size_t at=(size_t)(iova-m.iova)&~(size_t)63;
|
||||
const size_t end=(size_t)(iova-m.iova)+bytes>m.memory->bytes()?m.memory->bytes():(size_t)(iova-m.iova)+bytes;
|
||||
while(at<end) {
|
||||
const NativeAdrenoPhysicalSpan *span=nullptr;
|
||||
for(size_t j=0;j<m.memory->spanCount();++j) {
|
||||
const auto &s=m.memory->spans()[j];
|
||||
if(at>=s.offset && at-s.offset<s.bytes) {span=&s;break;}
|
||||
}
|
||||
if(!span)return kIOReturnBadArgument;
|
||||
const uint64_t physical=span->physical+(at-span->offset);
|
||||
size_t chunk=4096-(size_t)(physical&4095);
|
||||
if(chunk>span->offset+span->bytes-at)chunk=span->offset+span->bytes-at;
|
||||
if(chunk>end-at)chunk=(end-at+63)&~(size_t)63;
|
||||
if(native_adreno_line_audit(physical,chunk,dirty,stale))return kIOReturnIOError;
|
||||
*lines+=(uint32_t)(chunk/64);at+=chunk;
|
||||
}
|
||||
return kIOReturnSuccess;
|
||||
}
|
||||
return kIOReturnBadArgument;
|
||||
}
|
||||
IOReturn NativeAdrenoVm::peekWords(uint64_t iova,uint32_t *out,size_t count) const
|
||||
{
|
||||
if(!out || !attached_ || poisoned_)return kIOReturnBadArgument;
|
||||
for(size_t k=0;k<count;++k)out[k]=0;
|
||||
for(size_t i=0;i<NativeAdrenoVmMaxObjects;++i) {
|
||||
const Mapping &m=mappings_[i];
|
||||
if(!m.memory || !m.memory->mapping() || iova<m.iova || iova-m.iova>=m.memory->bytes())continue;
|
||||
const size_t offset=(size_t)(iova-m.iova);
|
||||
for(size_t k=0;k<count && offset+4*k+4<=m.memory->bytes();++k)out[k]=static_cast<const volatile uint32_t *>(m.memory->mapping())[offset/4+k];
|
||||
return kIOReturnSuccess;
|
||||
}
|
||||
return kIOReturnBadArgument;
|
||||
}
|
||||
IOReturn NativeAdrenoVm::holdJob(uint64_t handle)
|
||||
{
|
||||
if(!attached_ || poisoned_)return kIOReturnNotReady;
|
||||
|
||||
@@ -29,6 +29,12 @@ struct NativeAdrenoVmResult {
|
||||
enum : size_t {NativeAdrenoVmMaxObjects=1024};
|
||||
/* Board hook for sampleWord(): reads one aligned word of physical memory through a fresh uncached mapping; 0 on success. */
|
||||
extern "C" int (*native_adreno_dram_read32)(uint64_t physical,uint32_t *value);
|
||||
/* The board's reader through the kernel's cacheable physical aperture (null on the host: the BO's own mapping is read). */
|
||||
extern "C" int (*native_adreno_cached_read32)(uint64_t physical,uint32_t *value);
|
||||
/* Board hook for auditRange(): [physical, physical+bytes), whole 64-byte lines inside one page, as the cacheable physical aperture and
|
||||
* DRAM (a fresh uncached mapping) read it. A line that differs is cleaned and invalidated and DRAM read again: when DRAM then holds the
|
||||
* cached copy the line was dirty (*dirty), when DRAM is unchanged it was a stale clean copy (*stale). 0 on success. */
|
||||
extern "C" int (*native_adreno_line_audit)(uint64_t physical,size_t bytes,uint32_t *dirty,uint32_t *stale);
|
||||
class NativeAdrenoVm final {
|
||||
struct Mapping {
|
||||
NativeAdrenoMemory *memory=nullptr;
|
||||
@@ -46,14 +52,19 @@ class NativeAdrenoVm final {
|
||||
size_t capacity_=0,lower_count_=0,upper_count_=0,objects_=0;
|
||||
unsigned budget_=0;
|
||||
uint64_t next_handle_=1;
|
||||
bool attached_=false,poisoned_=false;
|
||||
bool attached_=false,poisoned_=false,distrust_write_combined_=false;
|
||||
NativeAdrenoVmResult result_{};
|
||||
ArmSmmu4KLiveRegion region(unsigned) const;
|
||||
ArmSmmu4KStatus observe() const;
|
||||
/* The route check alone: the table walk is the update's own (it verifies every region before the first store and after the last), so
|
||||
* map/unmap pay a full extra audit only every 64th operation (int-139: 4 full walks of the 2 GiB aperture made every map 70 ms). */
|
||||
ArmSmmu4KStatus observeStreamsOnly() const;
|
||||
ArmSmmu4KStatus observeCheap();
|
||||
unsigned since_audit_=0;
|
||||
IOReturn fail(ArmSmmu4KStatus);
|
||||
Mapping *find(uint64_t);
|
||||
void insert(const ArmSmmu4KRange &,unsigned);
|
||||
void erase(size_t,unsigned);
|
||||
void insert(size_t,unsigned);
|
||||
void erase(size_t,size_t,unsigned);
|
||||
IOReturn preflight(const NativeAdrenoMemory *,unsigned,uint64_t,unsigned,unsigned,unsigned) const;
|
||||
public:
|
||||
NativeAdrenoVm()=default;
|
||||
@@ -75,15 +86,33 @@ public:
|
||||
* synchronizes by ASID; this is the stronger form the submit path uses before each submission (int-070: on sheng the CP read
|
||||
* zeros from an IB whose table entry and DRAM were both correct, after the address had been mapped to other pages before). */
|
||||
IOReturn invalidateTlb();
|
||||
/* Cleans and invalidates the CPU caches over every mapped kernel-owned BO (whole objects; imported BOs are their owner's
|
||||
* business), so no CPU cache line can hold data the GPU should see or hide what it wrote. Lines are physically tagged, so
|
||||
* this also reaches lines a user mapping of the same pages left behind. int-070 runs it before every submission. */
|
||||
/* Cleans and invalidates the CPU caches over every mapped BO (whole objects; imported ones through their physical pages), so
|
||||
* no CPU cache line can hold data the GPU should see or hide what it wrote. Lines are physically tagged, so this also reaches
|
||||
* lines a user mapping of the same pages left behind. int-070 runs it before every submission. */
|
||||
IOReturn publishMapped();
|
||||
/* The submit path saw a CPU-cached word that DRAM did not hold yet in a client's BO: from now on write-combined BOs are maintained
|
||||
* like any other (sticky; the safe behaviour of int-070..097). */
|
||||
void distrustWriteCombined() {distrust_write_combined_=true;}
|
||||
bool writeCombinedTrusted() const {return !distrust_write_combined_;}
|
||||
/* After a submission completed: cleans and invalidates the CPU caches over every GPU-writable imported BO, so the owner's mapping
|
||||
* (an IOSurface) reads what the GPU wrote, not a line prefetched while the GPU ran. Jobs name only their IBs, so every such BO
|
||||
* is covered. int-071: an imported render target read back 672 of 4096 texels stale. */
|
||||
IOReturn acquireImported();
|
||||
/* Diagnostic, never a proof of coherence: the 32-bit word at a mapped GPU address as the kernel's cacheable alias reads it
|
||||
* and, when the board installed native_adreno_dram_read32, as DRAM reads it through an uncached mapping (else ~0). */
|
||||
IOReturn sampleWord(uint64_t iova,uint32_t *cached,uint32_t *dram) const;
|
||||
/* Every line of [iova, iova+bytes) that lies in a write-combined BO, through native_adreno_line_audit: *dirty counts lines a CPU
|
||||
* cache held newer than DRAM (a client view that caches after all; each such line is written back by the audit), *stale clean
|
||||
* copies, *lines the lines compared. kIOReturnUnsupported without a board hook. int-099 sampled one word and wrote its line back
|
||||
* before reading DRAM, so it could never see a dirty line. */
|
||||
/* Before a submission while write-combined BOs are trusted: every page of every mapped write-combined BO that XNU set back to the
|
||||
* default attribute is write-combined again (and its lines written back); *reset counts the pages. */
|
||||
IOReturn reassertWriteCombined(uint32_t *reset) const;
|
||||
IOReturn auditRange(uint64_t iova,size_t bytes,uint32_t *lines,uint32_t *dirty,uint32_t *stale) const;
|
||||
/* Read-only description of a live mapping; never pins or extends it. */
|
||||
IOReturn describe(uint64_t handle,uint64_t *iova,size_t *bytes,unsigned *permissions) const;
|
||||
/* Up to `count` dwords of a mapped kernel-owned BO at a GPU address (through the kernel's own mapping), for the board log. */
|
||||
IOReturn peekWords(uint64_t iova,uint32_t *out,size_t count) const;
|
||||
/* Only a scheduler's actual successful GPU completion may call this.
|
||||
* A timeout, interrupt bit alone or CPU-written timestamp is insufficient. */
|
||||
IOReturn retireJobAfterCompletion(uint64_t handle);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
#ifndef NATIVE_ADRENO_VM_ALIAS_H
|
||||
#define NATIVE_ADRENO_VM_ALIAS_H
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* Temporary CPU validation index, never a GPU mapping or translation table.
|
||||
* Range has physical/bytes fields. The caller has validated nonempty ranges
|
||||
* with nonoverflowing ends and supplies count valid indices in owned scratch.
|
||||
* Original ranges are immutable. One serialized owner uses scratch at a time. */
|
||||
template<class Range> class NativeAdrenoPhysicalIndex final {
|
||||
const Range *ranges_;
|
||||
uint32_t *order_;
|
||||
size_t count_;
|
||||
uint64_t end(uint32_t i) const {return ranges_[i].physical+ranges_[i].bytes;}
|
||||
bool less(uint32_t a,uint32_t b) const {
|
||||
return ranges_[a].physical<ranges_[b].physical ||
|
||||
(ranges_[a].physical==ranges_[b].physical && ranges_[a].bytes<ranges_[b].bytes);
|
||||
}
|
||||
void sift(size_t root,size_t count) {
|
||||
while(root<count/2) {
|
||||
size_t child=root*2+1;
|
||||
if(child+1<count && less(order_[child],order_[child+1]))++child;
|
||||
if(!less(order_[root],order_[child]))break;
|
||||
uint32_t t=order_[root];order_[root]=order_[child];order_[child]=t;root=child;
|
||||
}
|
||||
}
|
||||
public:
|
||||
NativeAdrenoPhysicalIndex(const Range *ranges,uint32_t *order,size_t count):ranges_(ranges),order_(order),count_(count) {
|
||||
for(size_t i=count/2;i-->0;)sift(i,count);
|
||||
for(size_t n=count;n>1;) {--n;uint32_t t=order_[0];order_[0]=order_[n];order_[n]=t;sift(0,n);}
|
||||
/* Discard an interval contained by an earlier interval. The remaining
|
||||
* starts AND ends are monotonic, even for nested/duplicate/overlapping
|
||||
* inputs. Each query then needs a binary search, with no backward scan. */
|
||||
size_t kept=0;
|
||||
for(size_t i=0;i<count;++i) {
|
||||
uint32_t index=order_[i];
|
||||
if(!kept || end(index)>end(order_[kept-1]))order_[kept++]=index;
|
||||
}
|
||||
count_=kept;
|
||||
}
|
||||
bool overlaps(uint64_t physical,uint64_t bytes) const {
|
||||
size_t lo=0,hi=count_;
|
||||
while(lo<hi) {size_t mid=lo+(hi-lo)/2;if(end(order_[mid])<=physical)lo=mid+1;else hi=mid;}
|
||||
return lo<count_ && ranges_[order_[lo]].physical<physical+bytes;
|
||||
}
|
||||
};
|
||||
#endif
|
||||
@@ -15,19 +15,23 @@ using addr64_t=uint64_t;
|
||||
constexpr IOReturn kIOReturnSuccess=0,kIOReturnBadArgument=1,kIOReturnBusy=2,
|
||||
kIOReturnNoResources=3,kIOReturnNoMemory=4,kIOReturnUnsupported=5,
|
||||
kIOReturnNotReady=6,kIOReturnIOError=7,kIOReturnTimeout=8;
|
||||
constexpr unsigned kIODirectionInOut=3,kIOMemoryMapperNone=0x800,kIOMemoryPhysicallyContiguous=0x10;
|
||||
constexpr unsigned kIODirectionOut=2,kIODirectionInOut=3,kIOMemoryMapperNone=0x800,kIOMemoryPhysicallyContiguous=0x10;
|
||||
inline void *kernel_task=reinterpret_cast<void *>(1);
|
||||
using task_t=void *;
|
||||
enum SegmentMode {Contiguous,Scattered,ZeroPhysical,UnalignedPhysical,ShortSegment,HighPhysical,Paired};
|
||||
/* ScatterUnlessContiguous: what the tablet does, one physical page per span unless the caller asked for contiguous memory. */
|
||||
enum SegmentMode {Contiguous,Scattered,ZeroPhysical,UnalignedPhysical,ShortSegment,HighPhysical,Paired,ScatterUnlessContiguous};
|
||||
class IOBufferMemoryDescriptor;
|
||||
struct MemoryFixture {
|
||||
unsigned created=0,prepared=0,completed=0,released=0,cache_calls=0,segment_calls=0;
|
||||
unsigned allocation_fail_count=0;
|
||||
bool allocation_fail=false,prepare_fail=false,complete_fail=false,metadata_fail=false;
|
||||
bool write_protected_imports=false; /* user ranges that can only be pinned for reading: prepare() of a writable one reports KERN_PROTECTION_FAILURE */
|
||||
bool null_mapping=false,unaligned_mapping=false,wrong_length=false;
|
||||
unsigned cache_fail_call=0;
|
||||
uint64_t next_physical=0x30000000;
|
||||
size_t contiguous_limit=0; /* nonzero: physically contiguous requests above this many bytes fail (a fragmented tablet) */
|
||||
unsigned imported=0,imports_live=0;
|
||||
unsigned physical_calls=0;size_t physical_bytes=0; /* NativeAdrenoMaintainPhysicalPages doubles */
|
||||
IOReturn cpu_status=kIOReturnSuccess;
|
||||
SegmentMode mode=Contiguous;
|
||||
std::vector<IOBufferMemoryDescriptor *> live;
|
||||
@@ -51,12 +55,14 @@ public:
|
||||
size_t bytes=0;
|
||||
uint64_t mask=0;
|
||||
uint64_t physical_base=0;
|
||||
bool contiguous=false;
|
||||
static IOBufferMemoryDescriptor *inTaskWithPhysicalMask(void *task,unsigned options,size_t bytes,uint64_t mask) {
|
||||
if(task!=kernel_task || !(options&kIOMemoryMapperNone))std::abort();
|
||||
if(fixture.allocation_fail)return nullptr;
|
||||
if(fixture.allocation_fail_count) {--fixture.allocation_fail_count;return nullptr;}
|
||||
if(fixture.contiguous_limit && (options&kIOMemoryPhysicallyContiguous) && bytes>fixture.contiguous_limit)return nullptr;
|
||||
auto *d=new IOBufferMemoryDescriptor;
|
||||
d->bytes=bytes;d->mask=mask;d->physical_base=fixture.next_physical;
|
||||
d->bytes=bytes;d->mask=mask;d->physical_base=fixture.next_physical;d->contiguous=options&kIOMemoryPhysicallyContiguous;
|
||||
fixture.next_physical+=2*bytes+0x10000;
|
||||
if(posix_memalign(&d->raw,4096,bytes+4096))std::abort();
|
||||
std::memset(d->raw,0xa5,bytes+4096);++fixture.created;fixture.live.push_back(d);return d;
|
||||
@@ -73,7 +79,10 @@ public:
|
||||
if((offset&4095) || offset>=bytes || options!=kIOMemoryMapperNone)std::abort();
|
||||
++fixture.segment_calls;*length=bytes-offset;
|
||||
uint64_t base=physical_base,physical=base+offset;
|
||||
if(fixture.mode==Scattered)physical=base+2*offset;
|
||||
if(fixture.mode==Scattered || (fixture.mode==ScatterUnlessContiguous && !contiguous)) {
|
||||
physical=base+2*offset;
|
||||
if(fixture.mode==ScatterUnlessContiguous)*length=4096;
|
||||
}
|
||||
if(fixture.mode==Paired)physical=base+offset+((offset/8192)*4096);
|
||||
if(offset==4096) {
|
||||
if(fixture.mode==ZeroPhysical)physical=0;
|
||||
@@ -87,14 +96,14 @@ public:
|
||||
/* A user range pinned with withAddressRange/prepare: pages at a fixed physical stride (every other page), so imports are scattered. */
|
||||
class IOMemoryDescriptor {
|
||||
public:
|
||||
uint64_t address=0,bytes=0,physical_base=0;
|
||||
uint64_t address=0,bytes=0,physical_base=0;unsigned options=0;
|
||||
static IOMemoryDescriptor *withAddressRange(uint64_t address,uint64_t bytes,unsigned options,task_t task) {
|
||||
if(!task || task==kernel_task || !(options&kIOMemoryMapperNone))std::abort();
|
||||
if(fixture.allocation_fail)return nullptr;
|
||||
auto *d=new IOMemoryDescriptor;d->address=address;d->bytes=bytes;d->physical_base=fixture.next_physical;
|
||||
auto *d=new IOMemoryDescriptor;d->options=options;d->address=address;d->bytes=bytes;d->physical_base=fixture.next_physical;
|
||||
fixture.next_physical+=2*bytes+0x10000;++fixture.imported;++fixture.imports_live;return d;
|
||||
}
|
||||
IOReturn prepare() {++fixture.prepared;return fixture.prepare_fail?kIOReturnIOError:kIOReturnSuccess;}
|
||||
IOReturn prepare() {++fixture.prepared;if(fixture.write_protected_imports && (options&1))return 2;return fixture.prepare_fail?kIOReturnIOError:kIOReturnSuccess;}
|
||||
IOReturn complete() {++fixture.completed;return fixture.complete_fail?kIOReturnIOError:kIOReturnSuccess;}
|
||||
void release() {--fixture.imports_live;delete this;}
|
||||
uint64_t getLength() {return bytes;}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
/* Actual Turnip builtin export/embedding and lifetime checks. The mandatory
|
||||
* software peer executes no shader. No hardware timing claim follows. */
|
||||
#include <vulkan/vulkan.h>
|
||||
#include "util/blake3/blake3.h"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <vector>
|
||||
#include <unistd.h>
|
||||
#include <TargetConditionals.h>
|
||||
#define REQUIRE(x) do {if(!(x)){std::fprintf(stderr,"FAIL builtin line %d: %s\n",__LINE__,#x);return 1;}} while(0)
|
||||
#define LOAD(name) auto name=reinterpret_cast<PFN_##name>(get(instance,#name));REQUIRE(name)
|
||||
int main(int argc,char **argv) {
|
||||
if(argc!=4)return 2;
|
||||
bool produce=!std::strcmp(argv[3],"produce"),consume=!std::strcmp(argv[3],"consume"),refuse=!std::strcmp(argv[3],"refuse");
|
||||
REQUIRE(produce||consume||refuse);
|
||||
#if TARGET_OS_IPHONE
|
||||
REQUIRE(getpagesize()==4096 && sysconf(_SC_PAGESIZE)==4096 && !getenv("HOSTSIM_PAGE_SIZE"));
|
||||
#endif
|
||||
auto live=reinterpret_cast<unsigned(*)()>(dlsym(RTLD_DEFAULT,"host_gpu_live_objects"));
|
||||
auto submitted=reinterpret_cast<unsigned(*)()>(dlsym(RTLD_DEFAULT,"host_gpu_submissions"));REQUIRE(live&&submitted);
|
||||
void *library=dlopen(argv[1],RTLD_NOW|RTLD_LOCAL);if(!library)std::fprintf(stderr,"%s\n",dlerror());REQUIRE(library);
|
||||
auto get=reinterpret_cast<PFN_vkGetInstanceProcAddr>(dlsym(library,"vk_icdGetInstanceProcAddr"));REQUIRE(get);
|
||||
auto exportPack=reinterpret_cast<VkResult(*)(VkDevice,size_t *,void *)>(dlsym(library,"tu_export_builtin_shader_pack"));
|
||||
auto validate=reinterpret_cast<VkResult(*)(VkDevice,size_t,const void *)>(dlsym(library,"tu_validate_builtin_shader_pack"));REQUIRE(exportPack&&validate);
|
||||
VkInstance instance=VK_NULL_HANDLE;LOAD(vkCreateInstance);
|
||||
VkApplicationInfo app={};app.sType=VK_STRUCTURE_TYPE_APPLICATION_INFO;app.apiVersion=VK_API_VERSION_1_2;
|
||||
VkInstanceCreateInfo ii={};ii.sType=VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;ii.pApplicationInfo=&app;
|
||||
REQUIRE(vkCreateInstance(&ii,nullptr,&instance)==VK_SUCCESS);
|
||||
LOAD(vkDestroyInstance);LOAD(vkEnumeratePhysicalDevices);LOAD(vkGetPhysicalDeviceQueueFamilyProperties);
|
||||
LOAD(vkCreateDevice);LOAD(vkDestroyDevice);LOAD(vkDeviceWaitIdle);
|
||||
uint32_t n=1;VkPhysicalDevice physical=VK_NULL_HANDLE;REQUIRE(vkEnumeratePhysicalDevices(instance,&n,&physical)==VK_SUCCESS&&n==1);
|
||||
VkQueueFamilyProperties families[8]={};n=8;vkGetPhysicalDeviceQueueFamilyProperties(physical,&n,families);
|
||||
REQUIRE(n>0&&n<=8);int family=-1;for(unsigned i=0;i<n;++i)if(families[i].queueFlags&VK_QUEUE_GRAPHICS_BIT){family=(int)i;break;}REQUIRE(family>=0);
|
||||
float priority=1;VkDeviceQueueCreateInfo qi={};qi.sType=VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;qi.queueFamilyIndex=family;qi.queueCount=1;qi.pQueuePriorities=&priority;
|
||||
VkDeviceCreateInfo di={};di.sType=VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;di.queueCreateInfoCount=1;di.pQueueCreateInfos=&qi;
|
||||
VkDevice device=VK_NULL_HANDLE;auto begin=std::chrono::steady_clock::now();const VkResult created=vkCreateDevice(physical,&di,nullptr,&device);
|
||||
const double elapsed=std::chrono::duration<double,std::micro>(std::chrono::steady_clock::now()-begin).count();
|
||||
size_t bytes=0;
|
||||
if(refuse)REQUIRE(created==VK_ERROR_INITIALIZATION_FAILED&&device==VK_NULL_HANDLE);
|
||||
else {
|
||||
REQUIRE(created==VK_SUCCESS&&device);REQUIRE(exportPack(device,&bytes,nullptr)==VK_SUCCESS&&bytes>128&&bytes<4*1024*1024);
|
||||
std::vector<unsigned char> record(bytes);REQUIRE(exportPack(device,&bytes,record.data())==VK_SUCCESS&&validate(device,bytes,record.data())==VK_SUCCESS);
|
||||
unsigned char guard[16];std::memset(guard,0xa5,sizeof(guard));size_t shortSize=sizeof(guard);
|
||||
REQUIRE(exportPack(device,&shortSize,guard)==VK_INCOMPLETE&&shortSize==bytes&&std::all_of(std::begin(guard),std::end(guard),[](auto x){return x==0xa5;}));
|
||||
auto bad=record;bad[bytes-1]^=1;
|
||||
REQUIRE(validate(device,bytes,bad.data())==VK_ERROR_INITIALIZATION_FAILED);
|
||||
REQUIRE(validate(device,bytes-1,record.data())==VK_ERROR_INITIALIZATION_FAILED);
|
||||
REQUIRE(validate(device,0,nullptr)==VK_ERROR_INITIALIZATION_FAILED);
|
||||
REQUIRE(validate(device,4*1024*1024+1,reinterpret_cast<void *>(1))==VK_ERROR_INITIALIZATION_FAILED);
|
||||
REQUIRE(record.size()>256&&!std::memcmp(record.data(),"TUBUILT1",8));
|
||||
auto reseal=[](std::vector<unsigned char> &value){blake3_hasher h;blake3_hasher_init(&h);blake3_hasher_update(&h,value.data(),value.size()-32);blake3_hasher_finalize(&h,value.data()+value.size()-32,32);};
|
||||
auto changed=[&](size_t at,uint32_t value){auto mutated=record;REQUIRE(at+4<=mutated.size()-32);std::memcpy(mutated.data()+at,&value,4);reseal(mutated);REQUIRE(validate(device,mutated.size(),mutated.data())==VK_ERROR_INITIALIZATION_FAILED);return 0;};
|
||||
REQUIRE(changed(16,0)==0); // exact source/profile identity, valid checksum
|
||||
REQUIRE(changed(80,0)==0); // actual GPU identity
|
||||
REQUIRE(changed(12,16)==0); // incomplete variant set
|
||||
REQUIRE(changed(144,1)==0); // duplicated/reordered entry
|
||||
REQUIRE(changed(148,0x10000000)==0); // entry allocation bound
|
||||
uint32_t keyBytes=0;std::memcpy(&keyBytes,record.data()+116,4);REQUIRE(keyBytes<1024);
|
||||
REQUIRE(changed(152+keyBytes+8,0xffffffff)==0); // unchecked IR3 allocation in the old generic reader
|
||||
bad=record;bad.insert(bad.end()-32,0x7f);reseal(bad);REQUIRE(validate(device,bad.size(),bad.data())==VK_ERROR_INITIALIZATION_FAILED);
|
||||
if(produce){std::ofstream output(argv[2],std::ios::binary);REQUIRE(output);output.write(reinterpret_cast<char *>(record.data()),record.size());REQUIRE(output.good());}
|
||||
else {std::ifstream input(argv[2],std::ios::binary);REQUIRE(input);std::vector<unsigned char> previous((std::istreambuf_iterator<char>(input)),{});REQUIRE(record==previous);}
|
||||
REQUIRE(vkDeviceWaitIdle(device)==VK_SUCCESS);vkDestroyDevice(device,nullptr);
|
||||
}
|
||||
vkDestroyInstance(instance,nullptr);REQUIRE(live()==0&&submitted()==0);
|
||||
std::printf("PASS: builtin cache %s pid=%d variants=17 bytes=%zu device_us=%.1f negatives=%u live=0 submissions=0; software peer, no shader execution\n",argv[3],getpid(),bytes,elapsed,refuse?0:11);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
/* Actual Vulkan/Turnip/KMD control flow over the software kernel peer. It runs no
|
||||
* shader: pending fences, retained native objects/views/IOVAs and error handling
|
||||
* are checked independently of rendering or hardware timing. */
|
||||
#include <vulkan/vulkan.h>
|
||||
#include "NativeAdrenoABI.h"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#define REQUIRE(x) do {if (!(x)) {std::fprintf(stderr,"FAIL line %d: %s\n",__LINE__,#x);return 1;}} while(0)
|
||||
#define OK(x) REQUIRE((x)==VK_SUCCESS)
|
||||
#define LOAD(name) auto name=(PFN_##name)get(instance,#name);REQUIRE(name)
|
||||
|
||||
struct Peer {
|
||||
void (*hold)(int);
|
||||
std::atomic<bool> release{false},forced{false};
|
||||
std::thread worker;
|
||||
explicit Peer(void (*fn)(int)):hold(fn){hold(1);}
|
||||
void watchdog() {
|
||||
worker=std::thread([this]{
|
||||
auto end=std::chrono::steady_clock::now()+std::chrono::milliseconds(500);
|
||||
while(!release.load() && std::chrono::steady_clock::now()<end)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
if(!release.load())forced=true;
|
||||
hold(0);
|
||||
});
|
||||
}
|
||||
void finish(){release=true;hold(0);if(worker.joinable())worker.join();}
|
||||
~Peer(){finish();}
|
||||
};
|
||||
|
||||
int main(int argc,char **argv) {
|
||||
if(argc!=3)return 2;
|
||||
const bool pressure=!std::strcmp(argv[2],"pressure"),busy=!std::strcmp(argv[2],"busy");
|
||||
const bool fenceError=!std::strcmp(argv[2],"fence-error"),freeError=!std::strcmp(argv[2],"free-error");
|
||||
const bool leaseError=!std::strcmp(argv[2],"lease-error"),viewError=!std::strcmp(argv[2],"view-error");
|
||||
const bool cleanupError=!std::strcmp(argv[2],"map-cleanup-error");
|
||||
const bool fatal=fenceError||freeError||leaseError||viewError||cleanupError;
|
||||
void *library=dlopen(argv[1],RTLD_NOW|RTLD_LOCAL);REQUIRE(library);
|
||||
auto get=(PFN_vkGetInstanceProcAddr)dlsym(library,"vk_icdGetInstanceProcAddr");REQUIRE(get);
|
||||
auto live=(unsigned(*)())dlsym(RTLD_DEFAULT,"host_gpu_live_objects");
|
||||
auto submissions=(unsigned(*)())dlsym(RTLD_DEFAULT,"host_gpu_submissions");
|
||||
auto successes=(unsigned(*)(uint32_t))dlsym(RTLD_DEFAULT,"host_gpu_successes");
|
||||
auto lastIova=(uint64_t(*)())dlsym(RTLD_DEFAULT,"host_gpu_last_map_iova");
|
||||
auto hold=(void(*)(int))dlsym(RTLD_DEFAULT,"host_gpu_hold_completion");
|
||||
auto inject=(void(*)(uint32_t,int,uint32_t))dlsym(RTLD_DEFAULT,"host_gpu_inject_failure");
|
||||
REQUIRE(live&&submissions&&successes&&lastIova&&hold&&inject);
|
||||
VkInstance instance=VK_NULL_HANDLE;
|
||||
LOAD(vkCreateInstance);
|
||||
VkApplicationInfo app={.sType=VK_STRUCTURE_TYPE_APPLICATION_INFO,.pApplicationName="deferred BO control",.apiVersion=VK_API_VERSION_1_1};
|
||||
VkInstanceCreateInfo ii={.sType=VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,.pApplicationInfo=&app};
|
||||
OK(vkCreateInstance(&ii,nullptr,&instance));
|
||||
LOAD(vkEnumeratePhysicalDevices);LOAD(vkGetPhysicalDeviceMemoryProperties);LOAD(vkGetPhysicalDeviceQueueFamilyProperties);
|
||||
LOAD(vkCreateDevice);LOAD(vkAllocateMemory);LOAD(vkFreeMemory);LOAD(vkMapMemory);
|
||||
LOAD(vkCreateBuffer);LOAD(vkDestroyBuffer);LOAD(vkGetBufferMemoryRequirements);LOAD(vkBindBufferMemory);
|
||||
LOAD(vkGetDeviceQueue);LOAD(vkCreateCommandPool);LOAD(vkAllocateCommandBuffers);LOAD(vkBeginCommandBuffer);
|
||||
LOAD(vkCmdFillBuffer);LOAD(vkEndCommandBuffer);LOAD(vkQueueSubmit);LOAD(vkDeviceWaitIdle);
|
||||
LOAD(vkDestroyCommandPool);LOAD(vkDestroyDevice);LOAD(vkDestroyInstance);
|
||||
uint32_t count=1;VkPhysicalDevice physical;OK(vkEnumeratePhysicalDevices(instance,&count,&physical));REQUIRE(count==1);
|
||||
VkPhysicalDeviceMemoryProperties memory={};vkGetPhysicalDeviceMemoryProperties(physical,&memory);
|
||||
int type=-1;
|
||||
for(uint32_t i=0;i<memory.memoryTypeCount;i++)if((memory.memoryTypes[i].propertyFlags&(VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT|VK_MEMORY_PROPERTY_HOST_COHERENT_BIT))==(VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT|VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)){type=(int)i;break;}
|
||||
REQUIRE(type>=0);
|
||||
VkQueueFamilyProperties families[8];count=8;vkGetPhysicalDeviceQueueFamilyProperties(physical,&count,families);
|
||||
int family=-1;for(uint32_t i=0;i<count;i++)if(families[i].queueFlags&VK_QUEUE_GRAPHICS_BIT){family=(int)i;break;}
|
||||
REQUIRE(family>=0);
|
||||
float priority=1;
|
||||
VkDeviceQueueCreateInfo qi={.sType=VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,.queueFamilyIndex=(uint32_t)family,.queueCount=1,.pQueuePriorities=&priority};
|
||||
VkDeviceCreateInfo di={.sType=VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,.queueCreateInfoCount=1,.pQueueCreateInfos=&qi};
|
||||
VkDevice device;OK(vkCreateDevice(physical,&di,nullptr,&device));
|
||||
VkQueue queue;vkGetDeviceQueue(device,(uint32_t)family,0,&queue);
|
||||
VkBufferCreateInfo bi={.sType=VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,.size=4096,.usage=VK_BUFFER_USAGE_TRANSFER_DST_BIT,.sharingMode=VK_SHARING_MODE_EXCLUSIVE};
|
||||
VkBuffer buffer;OK(vkCreateBuffer(device,&bi,nullptr,&buffer));
|
||||
VkMemoryRequirements req;vkGetBufferMemoryRequirements(device,buffer,&req);REQUIRE(req.memoryTypeBits&(1u<<type));
|
||||
VkMemoryAllocateInfo ai={.sType=VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,.allocationSize=req.size,.memoryTypeIndex=(uint32_t)type};
|
||||
VkDeviceMemory used;OK(vkAllocateMemory(device,&ai,nullptr,&used));OK(vkBindBufferMemory(device,buffer,used,0));
|
||||
ai.allocationSize=16384;
|
||||
std::vector<VkDeviceMemory> unused(pressure?257:1);
|
||||
uint64_t oldIova=0;
|
||||
for(auto &allocation:unused){OK(vkAllocateMemory(device,&ai,nullptr,&allocation));void *address=nullptr;OK(vkMapMemory(device,allocation,0,VK_WHOLE_SIZE,0,&address));if(!oldIova)oldIova=lastIova();}
|
||||
VkCommandPoolCreateInfo pi={.sType=VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,.queueFamilyIndex=(uint32_t)family};
|
||||
VkCommandPool pool;OK(vkCreateCommandPool(device,&pi,nullptr,&pool));
|
||||
VkCommandBufferAllocateInfo ci={.sType=VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,.commandPool=pool,.level=VK_COMMAND_BUFFER_LEVEL_PRIMARY,.commandBufferCount=1};
|
||||
VkCommandBuffer commands;OK(vkAllocateCommandBuffers(device,&ci,&commands));
|
||||
VkCommandBufferBeginInfo begin={.sType=VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};OK(vkBeginCommandBuffer(commands,&begin));
|
||||
vkCmdFillBuffer(commands,buffer,0,4096,0x5a5a5a5a);OK(vkEndCommandBuffer(commands));
|
||||
Peer peer(hold);
|
||||
unsigned beforeSubmit=submissions();
|
||||
VkSubmitInfo si={.sType=VK_STRUCTURE_TYPE_SUBMIT_INFO,.commandBufferCount=1,.pCommandBuffers=&commands};
|
||||
OK(vkQueueSubmit(queue,1,&si,VK_NULL_HANDLE));REQUIRE(submissions()>beforeSubmit);
|
||||
const unsigned beforeLive=live(),beforeFree=successes(NativeAdrenoMethodBoFree),beforeUnmap=successes(NativeAdrenoMethodBoCpuUnmap);
|
||||
if(fenceError)inject(NativeAdrenoMethodWaitFence,7,1); // fixture kIOReturnIOError, before mutation
|
||||
if(!pressure)peer.watchdog();
|
||||
for(unsigned i=0;i<(pressure?256u:1u);i++){
|
||||
vkFreeMemory(device,unused[i],nullptr);unused[i]=VK_NULL_HANDLE;
|
||||
REQUIRE(!peer.forced.load());
|
||||
REQUIRE(successes(NativeAdrenoMethodBoFree)==beforeFree);
|
||||
REQUIRE(successes(NativeAdrenoMethodBoCpuUnmap)==beforeUnmap);
|
||||
}
|
||||
REQUIRE(live()==beforeLive);
|
||||
VkDeviceMemory probe=VK_NULL_HANDLE,after=VK_NULL_HANDLE;
|
||||
if(cleanupError){
|
||||
inject(NativeAdrenoMethodBoMap,7,1);inject(NativeAdrenoMethodBoFree,2,1);
|
||||
VkDeviceMemory refused=VK_NULL_HANDLE;
|
||||
REQUIRE(vkAllocateMemory(device,&ai,nullptr,&refused)==VK_ERROR_DEVICE_LOST);
|
||||
REQUIRE(live()==beforeLive+1);
|
||||
REQUIRE(successes(NativeAdrenoMethodBoFree)==beforeFree);
|
||||
REQUIRE(successes(NativeAdrenoMethodBoCpuUnmap)==beforeUnmap);
|
||||
}
|
||||
if(!fatal && !pressure){OK(vkAllocateMemory(device,&ai,nullptr,&probe));REQUIRE(lastIova()!=oldIova);REQUIRE(successes(NativeAdrenoMethodBoFree)==beforeFree);}
|
||||
if(pressure){
|
||||
peer.watchdog();
|
||||
vkFreeMemory(device,unused.back(),nullptr);unused.back()=VK_NULL_HANDLE;
|
||||
REQUIRE(peer.forced.load());
|
||||
REQUIRE(successes(NativeAdrenoMethodBoFree)==beforeFree+257);
|
||||
REQUIRE(successes(NativeAdrenoMethodBoCpuUnmap)==beforeUnmap+257);
|
||||
}
|
||||
peer.finish();
|
||||
if(!pressure&&!busy&&!fatal){
|
||||
OK(vkDeviceWaitIdle(device));
|
||||
REQUIRE(successes(NativeAdrenoMethodBoFree)==beforeFree+1);
|
||||
REQUIRE(successes(NativeAdrenoMethodBoCpuUnmap)==beforeUnmap+1);
|
||||
}
|
||||
if(busy)inject(NativeAdrenoMethodBoFree,2,1); // temporary busy must retain the IOVA
|
||||
if(freeError)inject(NativeAdrenoMethodBoFree,7,1);
|
||||
if(leaseError)inject(NativeAdrenoMethodBoCpuUnmap,7,1);
|
||||
if(viewError)setenv("HOSTSIM_FAIL_USER_UNMAP","1",1);
|
||||
VkResult result=vkAllocateMemory(device,&ai,nullptr,&after);
|
||||
if(fatal){
|
||||
REQUIRE(result==VK_ERROR_DEVICE_LOST);
|
||||
REQUIRE(successes(NativeAdrenoMethodBoFree)==beforeFree);
|
||||
REQUIRE(successes(NativeAdrenoMethodBoCpuUnmap)==beforeUnmap+(freeError?1u:0u));
|
||||
} else {
|
||||
REQUIRE(result==VK_SUCCESS);
|
||||
if(busy){
|
||||
REQUIRE(lastIova()!=oldIova);REQUIRE(successes(NativeAdrenoMethodBoFree)==beforeFree);
|
||||
VkDeviceMemory retry;OK(vkAllocateMemory(device,&ai,nullptr,&retry));
|
||||
REQUIRE(successes(NativeAdrenoMethodBoFree)==beforeFree+1);
|
||||
REQUIRE(successes(NativeAdrenoMethodBoCpuUnmap)==beforeUnmap+1);
|
||||
vkFreeMemory(device,retry,nullptr);
|
||||
} else REQUIRE(successes(NativeAdrenoMethodBoFree)>=beforeFree+(pressure?257u:1u));
|
||||
}
|
||||
unsetenv("HOSTSIM_FAIL_USER_UNMAP");inject(~0u,0,0);
|
||||
if(!fatal)OK(vkDeviceWaitIdle(device));
|
||||
if(probe)vkFreeMemory(device,probe,nullptr);
|
||||
if(after)vkFreeMemory(device,after,nullptr);
|
||||
vkDestroyCommandPool(device,pool,nullptr);vkDestroyBuffer(device,buffer,nullptr);vkFreeMemory(device,used,nullptr);
|
||||
vkDestroyDevice(device,nullptr);vkDestroyInstance(instance,nullptr);
|
||||
// A failed fence/view/lease release retains native ownership in this narrow peer;
|
||||
// it deliberately does not manufacture a successful IOKit close or recovery.
|
||||
if(!fatal)REQUIRE(live()==0);
|
||||
std::printf("PASS: deferred BO %s; native objects at exit=%u; software control only, no shader execution\n",argv[2],live());
|
||||
return 0;
|
||||
}
|
||||
@@ -9,7 +9,10 @@
|
||||
#include "NativeAdrenoTimer.h"
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
IOReturn NativeAdrenoQueryCpu4K(NativeAdrenoCpuState *state) {*state={};return kIOReturnSuccess;}
|
||||
IOReturn NativeAdrenoMaintainOwnedPages(void *,size_t,NativeAdrenoCacheOperation) {return kIOReturnSuccess;}
|
||||
@@ -31,15 +34,27 @@ int sbarrier(void *) {return 0;}
|
||||
std::vector<uint32_t> regs(0x10000);
|
||||
volatile uint32_t *memptrs=nullptr;
|
||||
unsigned submissions=0;
|
||||
std::atomic<bool> holdCompletion{false};
|
||||
std::recursive_mutex serviceLock;
|
||||
unsigned calls[32]={},successes[32]={};
|
||||
uint32_t faultRemaining[32]={};
|
||||
IOReturn faultStatus[32]={};
|
||||
uint64_t lastMapIova=0;
|
||||
void completeIfReleased() {
|
||||
if(!holdCompletion.load(std::memory_order_acquire)) {
|
||||
regs[0x0806]=regs[0x0807];
|
||||
if(memptrs)memptrs[1]=submissions;
|
||||
}
|
||||
}
|
||||
int cwrite(void *,uint32_t reg,uint32_t v) {
|
||||
regs[reg]=v;
|
||||
if(reg==0x0807) {regs[0x0806]=v;++submissions;if(memptrs)memptrs[1]=submissions;}
|
||||
if(reg==0x0807) {++submissions;completeIfReleased();}
|
||||
return 0;
|
||||
}
|
||||
int cread(void *,uint32_t reg,uint32_t *v) {*v=regs[reg];return 0;}
|
||||
int cread(void *,uint32_t reg,uint32_t *v) {completeIfReleased();*v=regs[reg];return 0;}
|
||||
int cbarrier(void *) {return 0;}
|
||||
int cpublish(void *,const volatile uint32_t *,size_t) {return 0;}
|
||||
int cacquire(void *,volatile uint32_t *,size_t) {return 0;}
|
||||
int cacquire(void *,volatile uint32_t *,size_t) {completeIfReleased();return 0;}
|
||||
uint64_t cnow(void *) {
|
||||
return uint64_t(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count());
|
||||
@@ -66,6 +81,11 @@ bool built=false,opened=false;
|
||||
|
||||
bool build() {
|
||||
if(built)return true;
|
||||
/* HOSTSIM_SCATTER=1: non-contiguous BOs come back one physical page per span, as on the tablet (int-070's bridge ran out of
|
||||
* device memory there while the contiguous host model passed). */
|
||||
if(const char *scatter=std::getenv("HOSTSIM_SCATTER");scatter && *scatter=='1')fixture.mode=ScatterUnlessContiguous;
|
||||
/* HOSTSIM_CONTIGUOUS_MAX=<bytes>: larger physically contiguous allocations fail, so they fall back to scattered pages. */
|
||||
if(const char *limit=std::getenv("HOSTSIM_CONTIGUOUS_MAX"))fixture.contiguous_limit=std::strtoull(limit,nullptr,0);
|
||||
std::memset(mmio,0,sizeof(mmio));
|
||||
mmio[0]=0x002f0406;mmio[0x20/4]=0x4c017e18;mmio[0x24/4]=0x40000016;mmio[0x28/4]=0x1433;
|
||||
for(unsigned i=0;i<24;++i)mmio[(0xc00+4*i)/4]=0x20000;
|
||||
@@ -93,22 +113,28 @@ IOReturn translate(IOReturn status) {return status;}
|
||||
extern "C" {
|
||||
/* Return codes are the fixture's IOReturn values; the shim translates them. */
|
||||
int host_gpu_open() {
|
||||
std::lock_guard<std::recursive_mutex> guard(serviceLock);
|
||||
if(!build() || opened)return kIOReturnBusy;
|
||||
const IOReturn st=client.open(NativeAdrenoAbiVersion,context->vm(),&execbuf,&ring,31,40);
|
||||
if(st) {std::fprintf(stderr,"host gpu: client open failed %d\n",st);return kIOReturnNotReady;}
|
||||
std::setvbuf(stderr,nullptr,_IONBF,0);
|
||||
client.attachTimer(&timer);
|
||||
client.attachDeviceInfo(&info);
|
||||
opened=true;
|
||||
return kIOReturnSuccess;
|
||||
}
|
||||
int host_gpu_close() {
|
||||
std::lock_guard<std::recursive_mutex> guard(serviceLock);
|
||||
if(!opened)return kIOReturnNotReady;
|
||||
const IOReturn status=client.releaseAll(&clock_,2000000);
|
||||
if(!status)opened=false;
|
||||
return translate(status);
|
||||
}
|
||||
int host_gpu_call(uint32_t method,const void *in,size_t in_size,void *out,size_t out_size) {
|
||||
std::lock_guard<std::recursive_mutex> guard(serviceLock);
|
||||
if(!opened)return kIOReturnNotReady;
|
||||
if(method<32)++calls[method];
|
||||
if(method<32 && faultRemaining[method]){--faultRemaining[method];return faultStatus[method];}
|
||||
/* As the IOKit wrapper does (NativeAdrenoPublish.cpp): an import is the calling task's pages, here a stand-in task. */
|
||||
if(method==NativeAdrenoMethodBoImport) {
|
||||
if(!in || in_size!=sizeof(NativeAdrenoAbiBoImport) || !out || out_size!=sizeof(NativeAdrenoAbiBoAllocReply))return kIOReturnBadArgument;
|
||||
@@ -118,13 +144,45 @@ int host_gpu_call(uint32_t method,const void *in,size_t in_size,void *out,size_t
|
||||
if(!status)std::memcpy(out,&reply,sizeof(reply));
|
||||
return translate(status);
|
||||
}
|
||||
return translate(client.dispatch(method,in,in_size,out,out_size,&clock_));
|
||||
const IOReturn status=client.dispatch(method,in,in_size,out,out_size,&clock_);
|
||||
if(!status && method<32)++successes[method];
|
||||
if(!status && method==NativeAdrenoMethodBoMap && in)
|
||||
lastMapIova=static_cast<const NativeAdrenoAbiBoMap *>(in)->iova;
|
||||
/* HOSTSIM_TRACE=1: every failing call, and every allocation size, on stderr. */
|
||||
static const bool trace=std::getenv("HOSTSIM_TRACE") && *std::getenv("HOSTSIM_TRACE")=='1';
|
||||
if(trace && method==NativeAdrenoMethodBoAlloc && in)
|
||||
std::fprintf(stderr,"hostgpu alloc %llu -> %d spans-live %zu\n",(unsigned long long)static_cast<const NativeAdrenoAbiBoAlloc *>(in)->bytes,
|
||||
status,context->vm()->spanCount());
|
||||
else if(trace && status)std::fprintf(stderr,"hostgpu method %u -> %d\n",method,status);
|
||||
return translate(status);
|
||||
}
|
||||
int host_gpu_map(uint32_t token,void **address,uint64_t *bytes) {
|
||||
std::lock_guard<std::recursive_mutex> guard(serviceLock);
|
||||
NativeAdrenoMemory *memory=opened?client.memoryForToken(token):nullptr;
|
||||
if(!memory)return kIOReturnBadArgument;
|
||||
*address=memory->mapping();*bytes=memory->bytes();return kIOReturnSuccess;
|
||||
}
|
||||
unsigned host_gpu_submissions() {return submissions;}
|
||||
unsigned host_gpu_live_objects() {return unsigned(client.liveObjects());}
|
||||
unsigned host_gpu_submissions() {std::lock_guard<std::recursive_mutex> guard(serviceLock);return submissions;}
|
||||
unsigned host_gpu_live_objects() {std::lock_guard<std::recursive_mutex> guard(serviceLock);return unsigned(client.liveObjects());}
|
||||
unsigned host_gpu_calls(uint32_t method) {std::lock_guard<std::recursive_mutex> guard(serviceLock);return method<32?calls[method]:0;}
|
||||
unsigned host_gpu_successes(uint32_t method) {std::lock_guard<std::recursive_mutex> guard(serviceLock);return method<32?successes[method]:0;}
|
||||
uint64_t host_gpu_last_map_iova() {std::lock_guard<std::recursive_mutex> guard(serviceLock);return lastMapIova;}
|
||||
// Only the atomic peer-release flag changes off-thread. The next actual ring read/acquire
|
||||
// publishes the software peer's completion under the service lock, like the existing CP model.
|
||||
void host_gpu_hold_completion(int hold) {holdCompletion.store(hold!=0,std::memory_order_release);}
|
||||
void host_gpu_inject_failure(uint32_t method,int status,uint32_t count) {
|
||||
std::lock_guard<std::recursive_mutex> guard(serviceLock);
|
||||
if(method<32){faultStatus[method]=status;faultRemaining[method]=count;}
|
||||
else std::memset(faultRemaining,0,sizeof faultRemaining);
|
||||
}
|
||||
}
|
||||
/* Physical-page maintenance (imported BOs): record it; no host page is touched. */
|
||||
/* Host double: the page attribute change of user BOs has no host equivalent (no host page is touched). */
|
||||
IOReturn NativeAdrenoSetPagesCacheMode(const NativeAdrenoPhysicalSpan *spans,size_t count,bool){return spans && count?kIOReturnSuccess:kIOReturnBadArgument;}
|
||||
IOReturn NativeAdrenoReassertWriteCombined(const NativeAdrenoPhysicalSpan *spans,size_t count,uint32_t *reset)
|
||||
{return spans && count && reset?kIOReturnSuccess:kIOReturnBadArgument;}
|
||||
IOReturn NativeAdrenoMaintainPhysicalPages(uint64_t physical,size_t bytes,NativeAdrenoCacheOperation op)
|
||||
{
|
||||
if(!physical || !bytes || ((physical|bytes)&4095) || op>NativeAdrenoInvalidate)std::abort();
|
||||
++fixture.physical_calls;fixture.physical_bytes+=bytes;return kIOReturnSuccess;
|
||||
}
|
||||
@@ -85,7 +85,9 @@ static kern_return_t shim_IOConnectMapMemory64(io_connect_t,uint32_t type,task_p
|
||||
if(kr==KERN_SUCCESS) {*address=reinterpret_cast<mach_vm_address_t>(mapping);*size=bytes;}
|
||||
return kr;
|
||||
}
|
||||
static kern_return_t shim_IOConnectUnmapMemory64(io_connect_t,uint32_t,task_port_t,mach_vm_address_t) {return KERN_SUCCESS;}
|
||||
static kern_return_t shim_IOConnectUnmapMemory64(io_connect_t,uint32_t,task_port_t,mach_vm_address_t) {
|
||||
return getenv("HOSTSIM_FAIL_USER_UNMAP") ? kIOReturnIOError : KERN_SUCCESS;
|
||||
}
|
||||
/* HOSTSIM_PAGE_SIZE=4096: Turnip sizes and places BOs with the page size (os_get_page_size, getpagesize), 16 KiB on this Mac and 4 KiB on the
|
||||
* tablet, so a recording meant to match the tablet's GPU address layout reports the tablet's page size. */
|
||||
static long pageSize(){static long p=-1;if(p<0)p=getenv("HOSTSIM_PAGE_SIZE")?atol(getenv("HOSTSIM_PAGE_SIZE")):0;return p;}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
/* Real Turnip shader compilation/cache import over the software kernel peer.
|
||||
* No command is submitted and no shader is executed. */
|
||||
#include <vulkan/vulkan.h>
|
||||
#include "NativeTurnipShaders.h"
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <vector>
|
||||
|
||||
#define REQUIRE(x) do {if (!(x)) {std::fprintf(stderr,"FAIL line %d: %s\n",__LINE__,#x);return 1;}} while (0)
|
||||
#define OK(x) REQUIRE((x)==VK_SUCCESS)
|
||||
#define LOAD(name) auto name=(PFN_##name)get(instance,#name);REQUIRE(name)
|
||||
|
||||
int main(int argc,char **argv) {
|
||||
if(argc!=4)return 2;
|
||||
const bool produce=!std::strcmp(argv[3],"produce");
|
||||
const bool mismatch=!std::strcmp(argv[3],"uuid-miss");
|
||||
const bool shaderMismatch=!std::strcmp(argv[3],"shader-miss");
|
||||
REQUIRE(produce||mismatch||shaderMismatch||!std::strcmp(argv[3],"consume"));
|
||||
void *library=dlopen(argv[1],RTLD_NOW|RTLD_LOCAL);
|
||||
if(!library)std::fprintf(stderr,"dlopen: %s\n",dlerror());
|
||||
REQUIRE(library);
|
||||
auto get=(PFN_vkGetInstanceProcAddr)dlsym(library,"vk_icdGetInstanceProcAddr");REQUIRE(get);
|
||||
auto submissions=(unsigned(*)())dlsym(RTLD_DEFAULT,"host_gpu_submissions");
|
||||
auto live=(unsigned(*)())dlsym(RTLD_DEFAULT,"host_gpu_live_objects");REQUIRE(submissions&&live);
|
||||
VkInstance instance=VK_NULL_HANDLE;
|
||||
LOAD(vkCreateInstance);
|
||||
VkApplicationInfo app={.sType=VK_STRUCTURE_TYPE_APPLICATION_INFO,.pApplicationName="native pipeline cache control",.apiVersion=VK_API_VERSION_1_2};
|
||||
VkInstanceCreateInfo ii={.sType=VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,.pApplicationInfo=&app};
|
||||
OK(vkCreateInstance(&ii,nullptr,&instance));
|
||||
LOAD(vkEnumeratePhysicalDevices);LOAD(vkGetPhysicalDeviceProperties);LOAD(vkGetPhysicalDeviceFeatures2);
|
||||
LOAD(vkGetPhysicalDeviceQueueFamilyProperties);LOAD(vkEnumerateDeviceExtensionProperties);
|
||||
LOAD(vkCreateDevice);LOAD(vkDestroyDevice);LOAD(vkDestroyInstance);LOAD(vkDeviceWaitIdle);
|
||||
LOAD(vkCreateShaderModule);LOAD(vkDestroyShaderModule);
|
||||
LOAD(vkCreateDescriptorSetLayout);LOAD(vkDestroyDescriptorSetLayout);
|
||||
LOAD(vkCreatePipelineLayout);LOAD(vkDestroyPipelineLayout);
|
||||
LOAD(vkCreatePipelineCache);LOAD(vkGetPipelineCacheData);LOAD(vkDestroyPipelineCache);
|
||||
LOAD(vkCreateComputePipelines);LOAD(vkDestroyPipeline);
|
||||
uint32_t count=1;VkPhysicalDevice physical;OK(vkEnumeratePhysicalDevices(instance,&count,&physical));REQUIRE(count==1);
|
||||
VkPhysicalDeviceProperties properties={};vkGetPhysicalDeviceProperties(physical,&properties);
|
||||
count=0;OK(vkEnumerateDeviceExtensionProperties(physical,nullptr,&count,nullptr));
|
||||
std::vector<VkExtensionProperties> listed(count);OK(vkEnumerateDeviceExtensionProperties(physical,nullptr,&count,listed.data()));
|
||||
const char *extensions[]={VK_EXT_PIPELINE_CREATION_CACHE_CONTROL_EXTENSION_NAME,VK_EXT_PIPELINE_CREATION_FEEDBACK_EXTENSION_NAME};
|
||||
for(auto name:extensions){bool found=false;for(auto &e:listed)found|=!std::strcmp(e.extensionName,name);REQUIRE(found);}
|
||||
VkPhysicalDevicePipelineCreationCacheControlFeatures control={.sType=VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_CREATION_CACHE_CONTROL_FEATURES};
|
||||
VkPhysicalDeviceFeatures2 features={.sType=VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,.pNext=&control};
|
||||
vkGetPhysicalDeviceFeatures2(physical,&features);REQUIRE(control.pipelineCreationCacheControl);
|
||||
VkQueueFamilyProperties families[8];count=8;vkGetPhysicalDeviceQueueFamilyProperties(physical,&count,families);
|
||||
int family=-1;for(uint32_t i=0;i<count;i++)if(families[i].queueFlags&VK_QUEUE_COMPUTE_BIT){family=(int)i;break;}REQUIRE(family>=0);
|
||||
float priority=1;
|
||||
VkDeviceQueueCreateInfo qi={.sType=VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,.queueFamilyIndex=(uint32_t)family,.queueCount=1,.pQueuePriorities=&priority};
|
||||
VkDeviceCreateInfo di={.sType=VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,.pNext=&control,.queueCreateInfoCount=1,.pQueueCreateInfos=&qi,
|
||||
.enabledExtensionCount=2,.ppEnabledExtensionNames=extensions};
|
||||
VkDevice device;OK(vkCreateDevice(physical,&di,nullptr,&device));
|
||||
const unsigned submitted=submissions();
|
||||
VkDescriptorSetLayoutBinding binding={.binding=0,.descriptorType=VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,.descriptorCount=1,.stageFlags=VK_SHADER_STAGE_COMPUTE_BIT};
|
||||
VkDescriptorSetLayoutCreateInfo si={.sType=VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,.bindingCount=1,.pBindings=&binding};
|
||||
VkDescriptorSetLayout set;OK(vkCreateDescriptorSetLayout(device,&si,nullptr,&set));
|
||||
VkPushConstantRange push={.stageFlags=VK_SHADER_STAGE_COMPUTE_BIT,.offset=0,.size=8};
|
||||
VkPipelineLayoutCreateInfo li={.sType=VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,.setLayoutCount=1,.pSetLayouts=&set,.pushConstantRangeCount=1,.pPushConstantRanges=&push};
|
||||
VkPipelineLayout layout;OK(vkCreatePipelineLayout(device,&li,nullptr,&layout));
|
||||
std::vector<uint32_t> spirv(std::begin(kSpv_smoke_fill_comp),std::end(kSpv_smoke_fill_comp));
|
||||
if(shaderMismatch){
|
||||
unsigned changed=0;
|
||||
for(size_t i=5;i<spirv.size();){
|
||||
unsigned words=spirv[i]>>16;REQUIRE(words&&i+words<=spirv.size());
|
||||
// Own fixture: change OpExecutionMode LocalSize from 64x1x1 to 32x1x1.
|
||||
if(spirv[i]==0x00060010&&spirv[i+2]==17){REQUIRE(spirv[i+3]==64);spirv[i+3]=32;++changed;}
|
||||
i+=words;
|
||||
}
|
||||
REQUIRE(changed==1);
|
||||
}
|
||||
VkShaderModuleCreateInfo mi={.sType=VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,.codeSize=spirv.size()*sizeof(uint32_t),.pCode=spirv.data()};
|
||||
VkShaderModule module;OK(vkCreateShaderModule(device,&mi,nullptr,&module));
|
||||
std::vector<char> data;
|
||||
if(!produce){
|
||||
std::ifstream input(argv[2],std::ios::binary);REQUIRE(input);
|
||||
data.assign(std::istreambuf_iterator<char>(input),{});REQUIRE(data.size()>32);
|
||||
uint32_t words[4];std::memcpy(words,data.data(),sizeof words);
|
||||
REQUIRE(words[0]==32&&words[1]==VK_PIPELINE_CACHE_HEADER_VERSION_ONE);
|
||||
REQUIRE(words[2]==properties.vendorID&&words[3]==properties.deviceID);
|
||||
REQUIRE(!std::memcmp(data.data()+16,properties.pipelineCacheUUID,VK_UUID_SIZE));
|
||||
if(mismatch)data[16]^=1;
|
||||
}
|
||||
VkPipelineCacheCreateInfo ci={.sType=VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO,.initialDataSize=data.size(),.pInitialData=data.data()};
|
||||
VkPipelineCache cache;OK(vkCreatePipelineCache(device,&ci,nullptr,&cache));
|
||||
VkPipelineCreationFeedback whole={},stage={};
|
||||
VkPipelineCreationFeedbackCreateInfo feedback={.sType=VK_STRUCTURE_TYPE_PIPELINE_CREATION_FEEDBACK_CREATE_INFO,
|
||||
.pPipelineCreationFeedback=&whole,.pipelineStageCreationFeedbackCount=1,.pPipelineStageCreationFeedbacks=&stage};
|
||||
VkComputePipelineCreateInfo pi={.sType=VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,.pNext=&feedback,
|
||||
.flags=VK_PIPELINE_CREATE_FAIL_ON_PIPELINE_COMPILE_REQUIRED_BIT,
|
||||
.stage={.sType=VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,.stage=VK_SHADER_STAGE_COMPUTE_BIT,.module=module,.pName="main"},
|
||||
.layout=layout};
|
||||
VkPipeline pipeline=VK_NULL_HANDLE;
|
||||
auto began=std::chrono::steady_clock::now();
|
||||
VkResult strict=vkCreateComputePipelines(device,cache,1,&pi,nullptr,&pipeline);
|
||||
double strictUs=std::chrono::duration<double,std::micro>(std::chrono::steady_clock::now()-began).count();
|
||||
double compileUs=0;
|
||||
if(produce||mismatch||shaderMismatch){
|
||||
REQUIRE(strict==VK_PIPELINE_COMPILE_REQUIRED&&pipeline==VK_NULL_HANDLE);
|
||||
if(produce){
|
||||
pi.flags=0;whole={};stage={};began=std::chrono::steady_clock::now();
|
||||
OK(vkCreateComputePipelines(device,cache,1,&pi,nullptr,&pipeline));
|
||||
compileUs=std::chrono::duration<double,std::micro>(std::chrono::steady_clock::now()-began).count();
|
||||
REQUIRE(whole.flags&VK_PIPELINE_CREATION_FEEDBACK_VALID_BIT);
|
||||
REQUIRE(!(whole.flags&VK_PIPELINE_CREATION_FEEDBACK_APPLICATION_PIPELINE_CACHE_HIT_BIT));
|
||||
size_t bytes=0;OK(vkGetPipelineCacheData(device,cache,&bytes,nullptr));REQUIRE(bytes>32);
|
||||
data.resize(bytes);OK(vkGetPipelineCacheData(device,cache,&bytes,data.data()));
|
||||
std::ofstream output(argv[2],std::ios::binary);REQUIRE(output);output.write(data.data(),bytes);REQUIRE(output.good());
|
||||
}
|
||||
}else{
|
||||
REQUIRE(strict==VK_SUCCESS&&pipeline!=VK_NULL_HANDLE);
|
||||
REQUIRE((whole.flags&(VK_PIPELINE_CREATION_FEEDBACK_VALID_BIT|VK_PIPELINE_CREATION_FEEDBACK_APPLICATION_PIPELINE_CACHE_HIT_BIT))==
|
||||
(VK_PIPELINE_CREATION_FEEDBACK_VALID_BIT|VK_PIPELINE_CREATION_FEEDBACK_APPLICATION_PIPELINE_CACHE_HIT_BIT));
|
||||
}
|
||||
if(pipeline)vkDestroyPipeline(device,pipeline,nullptr);
|
||||
vkDestroyPipelineCache(device,cache,nullptr);vkDestroyShaderModule(device,module,nullptr);
|
||||
vkDestroyPipelineLayout(device,layout,nullptr);vkDestroyDescriptorSetLayout(device,set,nullptr);
|
||||
OK(vkDeviceWaitIdle(device));REQUIRE(submissions()==submitted);
|
||||
vkDestroyDevice(device,nullptr);vkDestroyInstance(instance,nullptr);REQUIRE(live()==0);
|
||||
char uuid[33]={};for(unsigned i=0;i<16;i++)std::snprintf(uuid+2*i,3,"%02x",properties.pipelineCacheUUID[i]);
|
||||
std::printf("PASS: Turnip pipeline cache %s; uuid=%s bytes=%zu strict=%d feedback=0x%x strict_us=%.1f compile_us=%.1f; no shader execution\n",
|
||||
argv[3],uuid,data.size(),strict,whole.flags,strictUs,compileUs);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
/* QEMU-only compiler/cache instrument. The unchanged iOS Turnip library talks
|
||||
* to the authored software peer, never to a physical GPU. Separate children
|
||||
* preserve cold process boundaries; the worker normally links the own peer,
|
||||
* and original dyld/AMFI decide loading and interposition. */
|
||||
#define main pipeline_cache_case_main
|
||||
#include "pipeline_cache.cpp"
|
||||
#undef main
|
||||
#include <spawn.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/stat.h>
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <IOKit/IOKitLib.h>
|
||||
|
||||
extern char **environ;
|
||||
static uint64_t result[48];
|
||||
alignas(4096) static unsigned char snapshot[65536];
|
||||
alignas(4096) static unsigned char workerOutput[4096];
|
||||
static const char *workerLog="/private/var/tmp/turnip-compiler-worker.log";
|
||||
static const char *driverPath="/System/Library/Extensions/OfflineTurnipCompiler/libvulkan_freedreno.dylib";
|
||||
|
||||
static void stop(unsigned failure) {
|
||||
result[1]=failure;
|
||||
if(FILE *file=fopen(workerLog,"rb")) {
|
||||
result[11]=fread(workerOutput,1,sizeof workerOutput-1,file);
|
||||
fclose(file);
|
||||
}
|
||||
__asm__ volatile("mov x13, %0\nmov x14, %1\nmov x15, %2\nbrk #0"::"r"(result),"r"(snapshot),"r"(workerOutput):"x13","x14","x15","memory");
|
||||
_exit(99);
|
||||
}
|
||||
|
||||
static void mountCompilerView() {
|
||||
char uuid[37]={};FILE *file=fopen("/private/var/tmp/compiler-data-uuid","rb");
|
||||
if(!file)stop(60);
|
||||
size_t count=fread(uuid,1,37,file);fclose(file);
|
||||
if(count!=36 || uuid[36])stop(61);
|
||||
CFStringRef identity=CFStringCreateWithCString(nullptr,uuid,kCFStringEncodingASCII);
|
||||
CFMutableDictionaryRef match=IOServiceMatching("IOMedia");
|
||||
if(!identity || !match)stop(62);
|
||||
const void *keys[]={CFSTR("UUID")},*values[]={identity};
|
||||
CFDictionaryRef properties=CFDictionaryCreate(nullptr,keys,values,1,&kCFTypeDictionaryKeyCallBacks,&kCFTypeDictionaryValueCallBacks);
|
||||
if(!properties)stop(63);
|
||||
CFDictionarySetValue(match,CFSTR("IOPropertyMatch"),properties);CFRelease(properties);
|
||||
io_iterator_t iterator=0;
|
||||
if(IOServiceGetMatchingServices(0,match,&iterator) || !iterator)stop(64);
|
||||
char device[64]={};unsigned found=0;
|
||||
while(io_service_t media=IOIteratorNext(iterator)) {
|
||||
++found;
|
||||
CFTypeRef observed=IORegistryEntryCreateCFProperty(media,CFSTR("UUID"),nullptr,0);
|
||||
CFTypeRef name=IORegistryEntryCreateCFProperty(media,CFSTR("BSD Name"),nullptr,0);
|
||||
if(!observed || !CFEqual(observed,identity) || !name || CFGetTypeID(name)!=CFStringGetTypeID())stop(65);
|
||||
char bsd[56]={};
|
||||
if(!CFStringGetCString((CFStringRef)name,bsd,sizeof bsd,kCFStringEncodingASCII) || strncmp(bsd,"disk",4))stop(66);
|
||||
for(const char *p=bsd;*p;++p)if(!((*p>='a'&&*p<='z')||(*p>='0'&&*p<='9')))stop(67);
|
||||
bool backed=false;io_registry_entry_t node=media;
|
||||
for(unsigned depth=0;node && depth<64;++depth) {
|
||||
if(IOObjectConformsTo(node,"NativeRamBlock")){backed=true;break;}
|
||||
io_registry_entry_t above=0;auto error=IORegistryEntryGetParentEntry(node,"IOService",&above);
|
||||
if(node!=media)IOObjectRelease(node);
|
||||
node=error?0:above;
|
||||
}
|
||||
if(node && node!=media)IOObjectRelease(node);
|
||||
if(!backed)stop(68);
|
||||
snprintf(device,sizeof device,"/dev/%s",bsd);
|
||||
CFRelease(observed);CFRelease(name);IOObjectRelease(media);
|
||||
}
|
||||
IOObjectRelease(iterator);CFRelease(identity);
|
||||
if(found!=1 || !device[0])stop(69);
|
||||
const char *point="/System/Library/Extensions",*directory="/private/var/tmp/native-compiler-addons";
|
||||
struct stat block={},source={},before={},after={},original={},code={};struct statfs fs={};
|
||||
if(stat(device,&block) || !S_ISBLK(block.st_mode) || stat(point,&before) || stat(directory,&source) || source.st_dev!=block.st_rdev)stop(70);
|
||||
if(mount("bindfs",point,MNT_RDONLY|MNT_UNION,(void *)directory))stop(71);
|
||||
if(statfs(point,&fs) || (fs.f_flags&(MNT_RDONLY|MNT_UNION))!=(MNT_RDONLY|MNT_UNION) ||
|
||||
strcmp(fs.f_mntfromname,directory) || strcmp(fs.f_fstypename,"bindfs") || stat(point,&after) ||
|
||||
after.st_dev==before.st_dev || stat(driverPath,&code) || code.st_dev!=after.st_dev ||
|
||||
stat("/System/Library/Extensions/AGXMetalA10.bundle/Info.plist",&original) || original.st_dev!=before.st_dev)stop(72);
|
||||
result[38]=fs.f_flags;result[39]=(uint32_t)before.st_dev;result[40]=(uint32_t)after.st_dev;
|
||||
result[41]=(uint32_t)original.st_dev;result[42]=(uint32_t)code.st_dev;result[43]=found;result[44]=1;
|
||||
}
|
||||
|
||||
int main(int argc,char **argv) {
|
||||
if(argc==4) {
|
||||
// Never emulate a page-size answer in the target OS process.
|
||||
if(getpagesize()!=4096 || sysconf(_SC_PAGESIZE)!=4096 || getenv("HOSTSIM_PAGE_SIZE"))return 92;
|
||||
if(!strcmp(argv[3],"startup-control"))return 0;
|
||||
return pipeline_cache_case_main(argc,argv);
|
||||
}
|
||||
result[0]=0x54435051494f5331ull;
|
||||
result[2]=(uint64_t)getpid();result[3]=(uint64_t)getpagesize();result[4]=(uint64_t)sysconf(_SC_PAGESIZE);
|
||||
if(argc!=1 || getpid()!=1 || result[3]!=4096 || result[4]!=4096)stop(1);
|
||||
mountCompilerView();
|
||||
if(setenv("MESA_SHADER_CACHE_DISABLE","true",1) || unsetenv("DYLD_INSERT_LIBRARIES") || unsetenv("HOSTSIM_PAGE_SIZE"))stop(2);
|
||||
if(unlink("/private/var/tmp/turnip-compiler-cache.bin") && errno!=ENOENT)stop(3);
|
||||
if(unlink(workerLog) && errno!=ENOENT)stop(5);
|
||||
posix_spawn_file_actions_t actions;
|
||||
if(posix_spawn_file_actions_init(&actions) ||
|
||||
posix_spawn_file_actions_addopen(&actions,STDOUT_FILENO,workerLog,O_WRONLY|O_CREAT|O_APPEND,0600) ||
|
||||
posix_spawn_file_actions_adddup2(&actions,STDOUT_FILENO,STDERR_FILENO))stop(4);
|
||||
const char *modes[]={"produce","consume","uuid-miss","shader-miss"};
|
||||
for(unsigned i=0;i<4;++i) {
|
||||
char *arguments[]={const_cast<char *>("compiler-worker"),const_cast<char *>(driverPath),
|
||||
const_cast<char *>("/private/var/tmp/turnip-compiler-cache.bin"),const_cast<char *>(modes[i]),nullptr};
|
||||
pid_t child=0;
|
||||
int error=posix_spawn(&child,"/private/var/tmp/compiler-worker",&actions,nullptr,arguments,environ);
|
||||
result[12+3*i]=(uint64_t)child;result[13+3*i]=(uint64_t)error;
|
||||
if(error || child<=1)stop(10+i);
|
||||
int status=0;
|
||||
while(waitpid(child,&status,0)<0)if(errno!=EINTR)stop(20+i);
|
||||
result[14+3*i]=(uint64_t)status;
|
||||
if(!WIFEXITED(status) || WEXITSTATUS(status)!=0)stop(30+i);
|
||||
result[5]=i+1;
|
||||
}
|
||||
posix_spawn_file_actions_destroy(&actions);
|
||||
FILE *file=fopen("/private/var/tmp/turnip-compiler-cache.bin","rb");
|
||||
if(!file)stop(40);
|
||||
const size_t bytes=fread(snapshot,1,sizeof(snapshot),file);
|
||||
const bool ok=!ferror(file) && bytes>32 && bytes<sizeof(snapshot) && feof(file);
|
||||
fclose(file);
|
||||
if(!ok)stop(41);
|
||||
result[6]=bytes;
|
||||
uint32_t header[4];memcpy(header,snapshot,sizeof header);
|
||||
result[7]=header[0];result[8]=header[1];result[9]=header[2];result[10]=header[3];
|
||||
memcpy(result+24,snapshot+16,VK_UUID_SIZE);
|
||||
stop(0);
|
||||
}
|
||||
@@ -79,7 +79,7 @@ static void smmuReset(void)
|
||||
for(unsigned i=0;i<8;++i)mmio[(0xc00+4*i)/4]=0x20000;
|
||||
mmio[0x800/4]=1u<<31|9;mmio[0xc00/4]=3;
|
||||
}
|
||||
static int exposed(Path &p)
|
||||
static int exposed(Path &p,bool write_combined=false)
|
||||
{
|
||||
smmuReset();cp=CpPeer{};clock_us=0;
|
||||
std::memset(ringmem,0xef,sizeof(ringmem));std::memset(fencemem,0,sizeof(fencemem));
|
||||
@@ -100,6 +100,7 @@ static int exposed(Path &p)
|
||||
NativeAdrenoVmRegion lower={&p.tables,&p.arena,&p.state};
|
||||
if(p.vm.attach(&smmuio,&context,&streams,&lower,nullptr,32,1000))return kIOReturnBadArgument;
|
||||
if(p.object.allocate(8192,40,false,8192))return kIOReturnNoMemory;
|
||||
if(write_combined && p.object.makeWriteCombined())return kIOReturnNoMemory;
|
||||
if(p.vm.map(&p.object,0,0x800000,3,0,2,&p.handle))return kIOReturnBadArgument;
|
||||
if(p.ring.attach(&ringio,&ringcache,ringmem,sizeof(ringmem),0x100000,fencemem,sizeof(fencemem),0x108000))
|
||||
return kIOReturnBadArgument;
|
||||
@@ -147,6 +148,62 @@ int main()
|
||||
CHECK(p.submit.wait(2,&ringclock,1000)==kIOReturnSuccess);
|
||||
CHECK(p.submit.detach()==kIOReturnSuccess);
|
||||
}
|
||||
/* int-100: a write-combined BO is not maintained while the IB audit finds no dirty line; a dirty line or a failed audit ends the
|
||||
* trust, and that submission already maintains every BO through its physical pages. */
|
||||
{
|
||||
Path p;CHECK(exposed(p,true)==kIOReturnSuccess);
|
||||
NativeAdrenoSubmitBuffer buffer{p.handle,0x800000,4096};
|
||||
static std::vector<uint64_t> audited;static uint32_t dirty_lines,fail;
|
||||
audited.clear();dirty_lines=0;fail=0;
|
||||
native_adreno_line_audit=[](uint64_t physical,size_t bytes,uint32_t *dirty,uint32_t *stale) {
|
||||
audited.push_back(physical);audited.push_back(bytes);
|
||||
if(fail)return 1;
|
||||
*dirty+=dirty_lines;*stale+=1;return 0;};
|
||||
const uint32_t submissions=native_adreno_wc_audit[0],lines=native_adreno_wc_audit[1];
|
||||
const unsigned physical=fixture.physical_calls,owned=fixture.cache_calls;
|
||||
uint32_t fence=0;
|
||||
CHECK(p.submit.submit(&buffer,1,0,&fence)==kIOReturnSuccess && fence==1);
|
||||
CHECK(audited.size()==2 && audited[0]==p.object.spans()[0].physical && audited[1]==4096);
|
||||
CHECK(native_adreno_wc_audit[0]==submissions+1 && native_adreno_wc_audit[1]==lines+64 && native_adreno_wc_distrusted==0);
|
||||
CHECK(fixture.physical_calls==physical && fixture.cache_calls==owned && p.vm.writeCombinedTrusted()); /* no maintenance */
|
||||
fencemem[1]=1;CHECK(p.submit.wait(1,&ringclock,1000)==kIOReturnSuccess);
|
||||
dirty_lines=2;
|
||||
CHECK(p.submit.submit(&buffer,1,0,&fence)==kIOReturnSuccess && fence==2);
|
||||
CHECK(!p.vm.writeCombinedTrusted() && native_adreno_wc_distrusted==native_adreno_coherence[0]);
|
||||
CHECK(fixture.physical_calls>physical); /* this submission maintained the write-combined BO */
|
||||
const size_t calls=audited.size();
|
||||
fencemem[1]=2;CHECK(p.submit.wait(2,&ringclock,1000)==kIOReturnSuccess);
|
||||
CHECK(p.submit.submit(&buffer,1,0,&fence)==kIOReturnSuccess && audited.size()==calls); /* no audit once distrusted */
|
||||
fencemem[1]=3;CHECK(p.submit.wait(3,&ringclock,1000)==kIOReturnSuccess);
|
||||
CHECK(p.submit.detach()==kIOReturnSuccess);
|
||||
}
|
||||
/* The audit compares the first 16 submissions after attach, then every eighth (17, 25, ...); the re-assertion runs at each. */
|
||||
{
|
||||
Path p;CHECK(exposed(p,true)==kIOReturnSuccess);
|
||||
NativeAdrenoSubmitBuffer buffer{p.handle,0x800000,4096};
|
||||
static unsigned calls;calls=0;
|
||||
native_adreno_line_audit=[](uint64_t,size_t,uint32_t *,uint32_t *stale) {++calls;*stale+=0;return 0;};
|
||||
const uint32_t submissions=native_adreno_wc_audit[0];
|
||||
for(uint32_t n=1;n<=24;++n) {
|
||||
uint32_t fence=0;
|
||||
CHECK(p.submit.submit(&buffer,1,0,&fence)==kIOReturnSuccess && fence==n);
|
||||
fencemem[1]=n;CHECK(p.submit.wait(n,&ringclock,1000)==kIOReturnSuccess);
|
||||
}
|
||||
CHECK(calls==17 && native_adreno_wc_audit[0]==submissions+17 && p.vm.writeCombinedTrusted());
|
||||
native_adreno_line_audit=nullptr;
|
||||
CHECK(p.submit.detach()==kIOReturnSuccess);
|
||||
}
|
||||
{
|
||||
Path p;CHECK(exposed(p,true)==kIOReturnSuccess);
|
||||
NativeAdrenoSubmitBuffer buffer{p.handle,0x800000,4096};
|
||||
native_adreno_line_audit=[](uint64_t,size_t,uint32_t *,uint32_t *) {return 1;};
|
||||
uint32_t fence=0;
|
||||
CHECK(p.submit.submit(&buffer,1,0,&fence)==kIOReturnSuccess && fence==1);
|
||||
CHECK(!p.vm.writeCombinedTrusted() && native_adreno_wc_audit[4]==kIOReturnIOError);
|
||||
native_adreno_line_audit=nullptr;native_adreno_wc_distrusted=0;
|
||||
fencemem[1]=1;CHECK(p.submit.wait(1,&ringclock,1000)==kIOReturnSuccess);
|
||||
CHECK(p.submit.detach()==kIOReturnSuccess);
|
||||
}
|
||||
/* Refusals precede references, ring bytes and WPTR updates. */
|
||||
{
|
||||
Path p;CHECK(exposed(p)==kIOReturnSuccess);
|
||||
@@ -197,5 +254,15 @@ int main()
|
||||
CHECK(submit.detach()==kIOReturnNotReady);
|
||||
CHECK(submit.submit(nullptr,1,0,nullptr)==kIOReturnBadArgument);
|
||||
}
|
||||
std::printf("PASS: 1 execbuf submission with exact IB packet and fence, 8 pre-reference refusals, 1 duplicate-buffer reference case and 1 poisoned-commit retention case over software SMMU/CP peers\n");
|
||||
std::printf("PASS: 1 execbuf submission with exact IB packet and fence, 8 pre-reference refusals, 1 duplicate-buffer reference case and 1 poisoned-commit retention case over software SMMU/CP peers; 2 write-combine audit cases (trusted, dirty and failed)\n");
|
||||
}
|
||||
/* Physical-page maintenance (imported BOs): record it; no host page is touched. */
|
||||
/* Host double: the page attribute change of user BOs has no host equivalent (no host page is touched). */
|
||||
IOReturn NativeAdrenoSetPagesCacheMode(const NativeAdrenoPhysicalSpan *spans,size_t count,bool){return spans && count?kIOReturnSuccess:kIOReturnBadArgument;}
|
||||
IOReturn NativeAdrenoReassertWriteCombined(const NativeAdrenoPhysicalSpan *spans,size_t count,uint32_t *reset)
|
||||
{return spans && count && reset?kIOReturnSuccess:kIOReturnBadArgument;}
|
||||
IOReturn NativeAdrenoMaintainPhysicalPages(uint64_t physical,size_t bytes,NativeAdrenoCacheOperation op)
|
||||
{
|
||||
if(!physical || !bytes || ((physical|bytes)&4095) || op>NativeAdrenoInvalidate)std::abort();
|
||||
++fixture.physical_calls;fixture.physical_bytes+=bytes;return kIOReturnSuccess;
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
* Proves ownership/lifetime/refusal logic only; no hardware is touched. */
|
||||
#include "XnuMemoryFixture.h"
|
||||
#include "NativeAdrenoVm.h"
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#define CHECK(x) do {if(!(x)){std::fprintf(stderr,"line %d: %s\n",__LINE__,#x);std::exit(1);}}while(0)
|
||||
#define BYTES 0x40000
|
||||
@@ -24,6 +25,8 @@ static ArmSmmu4KStream routes[2]={{2,0},{4,1}};
|
||||
static ArmSmmu4KStreamResult route_result;
|
||||
static ArmSmmu4KContextConfig context;
|
||||
static ArmSmmu4KContextImage image;
|
||||
extern "C" {extern uint64_t (*native_adreno_vm_now_us)(void);extern volatile uint64_t native_adreno_vm_phase_us[2][8];}
|
||||
static uint64_t nowUs() {return (uint64_t)std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();}
|
||||
static int event(void){return ++f.calls==f.fail_call;}
|
||||
static int read32(void *cookie,size_t at,uint32_t *v)
|
||||
{CHECK(cookie==&f && at%4==0 && at<=BYTES-4);if(event())return 1;*v=memory[at/4];return 0;}
|
||||
@@ -43,6 +46,7 @@ struct Fixture {
|
||||
ArmSmmu4KLiveState state{};
|
||||
ArmSmmu4KLiveVisibility visibility;
|
||||
NativeAdrenoVm vm;
|
||||
std::vector<uint32_t> aliasScratch;
|
||||
};
|
||||
static void mmioReset(void)
|
||||
{
|
||||
@@ -54,11 +58,12 @@ static void mmioReset(void)
|
||||
memory[0x800/4]=1u<<31|9;memory[0xc00/4]=3;
|
||||
}
|
||||
/* Real prepared arena, enabled actual context image and exact stream routes. */
|
||||
static IOReturn exposed(Fixture &s)
|
||||
static IOReturn exposed(Fixture &s,size_t capacity=32,size_t pages=16)
|
||||
{
|
||||
mmioReset();
|
||||
IOReturn status=s.tables.allocate(32*4096,40,true,32*4096);if(status)return status;
|
||||
IOReturn smmu=NativeAdrenoPrepareSmmuMemory(&s.tables,32,40,0x800000,65536,0,16,
|
||||
const size_t tablePages=std::max(size_t(32),pages/512+8);
|
||||
IOReturn status=s.tables.allocate(tablePages*4096,40,true,tablePages*4096);if(status)return status;
|
||||
IOReturn smmu=NativeAdrenoPrepareSmmuMemory(&s.tables,32,40,0x800000,pages*4096,0,pages,
|
||||
&s.arena,&s.state,&s.visibility);
|
||||
if(smmu)return smmu;
|
||||
context=(ArmSmmu4KContextConfig){.lower=&s.arena.config,.lower_tables=&s.state.tables,
|
||||
@@ -74,7 +79,7 @@ static IOReturn exposed(Fixture &s)
|
||||
if(ArmSmmu4KAttachStreams(&io,&streams,routes,2,2,&context,&route_result)!=ArmSmmu4KSuccess)
|
||||
return kIOReturnBadArgument;
|
||||
NativeAdrenoVmRegion lower={&s.tables,&s.arena,&s.state};
|
||||
return s.vm.attach(&io,&context,&streams,&lower,nullptr,32,1000);
|
||||
return s.vm.attach(&io,&context,&streams,&lower,nullptr,capacity,1000);
|
||||
}
|
||||
static IOReturn object(Fixture &s,size_t pages)
|
||||
{return s.object.allocate(pages*4096,40,false,pages*4096);}
|
||||
@@ -133,6 +138,80 @@ int main()
|
||||
CHECK(s.tables.retireAfterQuiescence()==kIOReturnSuccess && s.tables.release()==kIOReturnSuccess);
|
||||
CHECK(fixture.live.empty() && fixture.metadata.empty());
|
||||
}
|
||||
/* Scattered BOs, one span per page, mapped in one transaction each into the middle, the end and the start of the table
|
||||
* (each map and unmap audits the whole table against the sorted spans), and a span-capacity refusal before ownership. */
|
||||
{
|
||||
Fixture s;
|
||||
CHECK(exposed(s,8)==kIOReturnSuccess); /* aperture 0x800000..0x810000, 8 spans */
|
||||
fixture.mode=Scattered;
|
||||
NativeAdrenoMemory first,middle,last,huge;
|
||||
CHECK(!first.allocate(4096,40,false,4096) && !middle.allocate(4*4096,40,false,4*4096) && !last.allocate(4096,40,false,4096));
|
||||
CHECK(middle.spanCount()==4);
|
||||
uint64_t a=0,b=0,c=0,iova=0;size_t bytes=0;unsigned permissions=0;
|
||||
CHECK(!s.vm.map(&first,0,0x800000,3,0,2,&a) && !s.vm.map(&last,0,0x808000,3,0,2,&c));
|
||||
CHECK(!s.vm.map(&middle,0,0x802000,3,0,2,&b) && s.vm.spanCount()==6 && s.vm.lastResult().completed_spans==4);
|
||||
CHECK(!s.vm.describe(b,&iova,&bytes,&permissions) && iova==0x802000 && bytes==4*4096);
|
||||
CHECK(!s.vm.unmap(b) && s.vm.spanCount()==2 && s.vm.lastResult().completed_spans==4);
|
||||
CHECK(!s.vm.map(&middle,0,0x80a000,3,0,2,&b) && s.vm.spanCount()==6);
|
||||
CHECK(!s.vm.unmap(a) && s.vm.spanCount()==5);
|
||||
CHECK(!s.vm.map(&first,0,0x800000,3,0,2,&a) && s.vm.spanCount()==6);
|
||||
CHECK(!huge.allocate(3*4096,40,false,3*4096) && huge.spanCount()==3);
|
||||
uint64_t refused=0;const unsigned calls=f.calls;
|
||||
CHECK(s.vm.map(&huge,0,0x802000,3,0,2,&refused)==kIOReturnNoResources && !refused);
|
||||
CHECK(!huge.deviceReferences() && f.calls==calls && s.vm.spanCount()==6 && !s.vm.poisoned());
|
||||
CHECK(!s.vm.unmap(a) && !s.vm.unmap(b) && !s.vm.unmap(c) && s.vm.spanCount()==0);
|
||||
fixture.mode=Contiguous;
|
||||
CHECK(!first.release() && !middle.release() && !last.release() && !huge.release());
|
||||
CHECK(s.vm.detach()==kIOReturnSuccess);
|
||||
CHECK(s.tables.retireAfterQuiescence()==kIOReturnSuccess && s.tables.release()==kIOReturnSuccess);
|
||||
CHECK(fixture.live.empty() && fixture.metadata.empty());
|
||||
}
|
||||
/* Physical overlap remains allowed only within the same attribute class.
|
||||
* Check scratch and fallback, both smaller-side choices, and refusals before
|
||||
* MMIO/ownership changes. These are physical-page identities in the peer. */
|
||||
for(bool scratch:{false,true}) {
|
||||
Fixture s;CHECK(exposed(s)==kIOReturnSuccess);
|
||||
s.aliasScratch.resize(32);
|
||||
if(scratch){s.arena.alias_scratch=s.aliasScratch.data();s.arena.alias_scratch_entries=s.aliasScratch.size();}
|
||||
NativeAdrenoMemory first,second,many;
|
||||
CHECK(!first.allocate(4096,40,false,4096));
|
||||
fixture.next_physical=first.spans()[0].physical;
|
||||
CHECK(!second.allocate(4096,40,false,4096));
|
||||
fixture.mode=Scattered;fixture.next_physical=first.spans()[0].physical;
|
||||
CHECK(!many.allocate(8192,40,false,8192)&&many.spanCount()==2);fixture.mode=Contiguous;
|
||||
uint64_t a=0,b=0;CHECK(!s.vm.map(&first,0,0x800000,3,0,2,&a));
|
||||
CHECK(!s.vm.map(&second,0,0x802000,1,0,2,&b));CHECK(!s.vm.unmap(b));
|
||||
const unsigned calls=f.calls;
|
||||
for(unsigned change=0;change<3;++change) {
|
||||
CHECK(s.vm.map(&second,0,0x802000,change==2?9:3,change==0?1:0,change==1?3:2,&b)==kIOReturnBadArgument);
|
||||
CHECK(!b&&!second.deviceReferences()&&f.calls==calls&&!s.vm.poisoned());
|
||||
}
|
||||
CHECK(s.vm.map(&many,0,0x802000,3,1,3,&b)==kIOReturnBadArgument);
|
||||
CHECK(!b&&!many.deviceReferences()&&f.calls==calls&&!s.vm.poisoned());
|
||||
CHECK(!s.vm.unmap(a)&&!first.release()&&!second.release()&&!many.release());
|
||||
CHECK(!s.vm.detach()&&!s.tables.retireAfterQuiescence()&&!s.tables.release());
|
||||
CHECK(fixture.live.empty()&&fixture.metadata.empty());
|
||||
}
|
||||
/* A large different-class scattered map: the old preflight did n*n physical
|
||||
* comparisons despite the table updater already having alias scratch. */
|
||||
{
|
||||
constexpr size_t n=8192;
|
||||
Fixture s;CHECK(exposed(s,2*n,2*n)==kIOReturnSuccess);
|
||||
s.aliasScratch.resize(2*n);s.arena.alias_scratch=s.aliasScratch.data();s.arena.alias_scratch_entries=s.aliasScratch.size();
|
||||
fixture.mode=Scattered;NativeAdrenoMemory first,second;
|
||||
CHECK(!first.allocate(n*4096,40,false,n*4096)&&first.spanCount()==n);
|
||||
CHECK(!second.allocate(n*4096,40,false,n*4096)&&second.spanCount()==n);
|
||||
uint64_t a=0,b=0;CHECK(!s.vm.map(&first,0,0x800000,3,0,2,&a));
|
||||
native_adreno_vm_now_us=nowUs;uint64_t before=native_adreno_vm_phase_us[0][5];
|
||||
CHECK(!s.vm.map(&second,0,0x800000+n*4096,3,1,3,&b));
|
||||
std::printf("VM alias preflight diagnostic: %zu x %zu different-class spans, %llu host us (not hardware timing)\n",n,n,
|
||||
(unsigned long long)(native_adreno_vm_phase_us[0][5]-before));
|
||||
native_adreno_vm_now_us=nullptr;fixture.mode=Contiguous;
|
||||
CHECK(s.vm.spanCount()==2*n&&!s.vm.unmap(b)&&!s.vm.unmap(a)&&!first.release()&&!second.release());
|
||||
CHECK(!s.vm.detach()&&!s.tables.retireAfterQuiescence()&&!s.tables.release());
|
||||
CHECK(fixture.live.empty()&&fixture.metadata.empty());
|
||||
}
|
||||
std::printf("PASS: VM alias index/fallback preserve attribute-class refusals before MMIO/ownership; 8192-span different-class map/unmap\n");
|
||||
/* Post-hold CPU publication failure: poison, retain everything, refuse. */
|
||||
{
|
||||
Fixture s;
|
||||
@@ -150,5 +229,15 @@ int main()
|
||||
CHECK(s.object.release()==kIOReturnBusy && s.tables.release()==kIOReturnBusy);
|
||||
CHECK(!fixture.live.empty());
|
||||
}
|
||||
std::printf("PASS: 1 native map/hold/unmap transaction, 13 pre-ownership refusals and 1 post-hold poison/retention case over a software SMMU register peer\n");
|
||||
std::printf("PASS: 1 native map/hold/unmap transaction, 13 pre-ownership refusals, 1 scattered-span ordering and capacity case and 1 post-hold poison/retention case over a software SMMU register peer\n");
|
||||
}
|
||||
/* Physical-page maintenance (imported BOs): record it; no host page is touched. */
|
||||
/* Host double: the page attribute change of user BOs has no host equivalent (no host page is touched). */
|
||||
IOReturn NativeAdrenoSetPagesCacheMode(const NativeAdrenoPhysicalSpan *spans,size_t count,bool){return spans && count?kIOReturnSuccess:kIOReturnBadArgument;}
|
||||
IOReturn NativeAdrenoReassertWriteCombined(const NativeAdrenoPhysicalSpan *spans,size_t count,uint32_t *reset)
|
||||
{return spans && count && reset?kIOReturnSuccess:kIOReturnBadArgument;}
|
||||
IOReturn NativeAdrenoMaintainPhysicalPages(uint64_t physical,size_t bytes,NativeAdrenoCacheOperation op)
|
||||
{
|
||||
if(!physical || !bytes || ((physical|bytes)&4095) || op>NativeAdrenoInvalidate)std::abort();
|
||||
++fixture.physical_calls;fixture.physical_bytes+=bytes;return kIOReturnSuccess;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
#include "NativeAdrenoVmAlias.h"
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#define CHECK(x) do {if(!(x)){std::fprintf(stderr,"line %d: %s\n",__LINE__,#x);std::exit(1);}}while(0)
|
||||
struct Range {uint64_t physical,bytes;};
|
||||
static uint64_t randomState=0x28372934;
|
||||
static uint64_t next() {randomState^=randomState<<13;randomState^=randomState>>7;randomState^=randomState<<17;return randomState;}
|
||||
static bool oracle(const std::vector<Range> &ranges,uint64_t start,uint64_t bytes) {
|
||||
for(const auto &r:ranges)if(start<r.physical+r.bytes && r.physical<start+bytes)return true;
|
||||
return false;
|
||||
}
|
||||
int main() {
|
||||
unsigned cases=0,queries=0;
|
||||
for(unsigned profile=0;profile<2048;++profile) {
|
||||
std::vector<Range> ranges;
|
||||
const unsigned count=profile%65;
|
||||
for(unsigned i=0;i<count;++i)ranges.push_back({(next()%128)*4096,(1+next()%64)*4096});
|
||||
// Explicit nesting plus a gap catches a binary search using starts alone.
|
||||
if(profile==1)ranges={{0x1000,0x10000},{0x2000,0x1000},{0x3000,0x1000},{0x20000,0x1000}};
|
||||
if(profile==2)ranges={{0x1000,0x1000},{0x1000,0x1000},{0x2000,0x1000},{0x4000,0x1000}};
|
||||
const auto original=ranges;
|
||||
// Filtered index subsets exercise caller-provided nonconsecutive indices.
|
||||
std::vector<uint32_t> scratch(ranges.size()+2,0xdeadbeef);
|
||||
std::vector<Range> selected;
|
||||
size_t n=0;
|
||||
for(size_t i=0;i<ranges.size();++i)if(profile%3 || i%2){scratch[1+n++]=(uint32_t)i;selected.push_back(ranges[i]);}
|
||||
NativeAdrenoPhysicalIndex index(ranges.data(),scratch.data()+1,n);
|
||||
for(unsigned q=0;q<256;++q) {
|
||||
uint64_t start=q*4096,bytes=(1+(q%7))*4096;
|
||||
CHECK(index.overlaps(start,bytes)==oracle(selected,start,bytes));++queries;
|
||||
}
|
||||
CHECK(scratch.front()==0xdeadbeef && scratch.back()==0xdeadbeef);
|
||||
CHECK(ranges.size()==original.size() && (!ranges.size() || !std::memcmp(ranges.data(),original.data(),ranges.size()*sizeof(Range))));
|
||||
++cases;
|
||||
}
|
||||
// Scale well beyond the bounded oracle profiles: distinct spans with gaps.
|
||||
std::vector<Range> ranges(65536);std::vector<uint32_t> order(ranges.size());
|
||||
for(size_t i=0;i<ranges.size();++i){ranges[i]={0x100000+(ranges.size()-1-i)*8192,4096};order[i]=(uint32_t)i;}
|
||||
NativeAdrenoPhysicalIndex index(ranges.data(),order.data(),order.size());
|
||||
for(size_t i=0;i<ranges.size();++i){CHECK(index.overlaps(0x100000+i*8192,4096));CHECK(!index.overlaps(0x101000+i*8192,4096));}
|
||||
std::printf("PASS: physical alias index %u oracle profiles / %u queries; 65536 reversed spans and exact gap boundaries\n",cases,queries);
|
||||
}
|
||||
@@ -11,5 +11,18 @@ extern "C" {
|
||||
id native_bridge_metal_open(const char *loader, const char *cache, uint64_t registryID, void (*missing)(const char *, const char *));
|
||||
// Bare AIR bitcode as a library (the bridge's fixtures; Metal itself accepts only metallibs).
|
||||
id native_bridge_metal_library_with_air(const void *bytes, size_t length);
|
||||
// One line for the plugin's status report: command buffers committed/failed locally, pipelines made, refusals, the last refusal's text.
|
||||
size_t native_bridge_metal_status(char *out, size_t size);
|
||||
// The pixel formats of the textures asked for (first eight distinct) and how many texture requests the runtime refused.
|
||||
size_t native_bridge_metal_formats(char *out, size_t size);
|
||||
size_t native_bridge_metal_telemetry(char *out, size_t size);
|
||||
// Structured cumulative artifact/driver-cache counters for offline qualification.
|
||||
struct NmbShaderCacheStats;
|
||||
bool native_bridge_metal_shader_cache_stats(struct NmbShaderCacheStats *out);
|
||||
bool native_bridge_metal_work_stats(uint64_t out[10]);
|
||||
// The same fixed CPU, memory and pipeline workload on the tablet and the host, so their times compare directly (int-145): compute, first-touch
|
||||
// page faults, memset bandwidth, CommonCrypto SHA-256, then the first `pairs` vertex/fragment pairs of the given QuartzCore metallib made into
|
||||
// BGRA8 render states and drawn once each (pipeline_timing_host's draw=1 order). The results become the telemetry's Y rows; also returned.
|
||||
size_t native_bridge_metal_benchmark(id device, const char *metallib, unsigned pairs, char *out, size_t size);
|
||||
}
|
||||
#endif
|
||||
File diff suppressed because it is too large.
Load diff
@@ -13,6 +13,7 @@ extern "C" {
|
||||
#endif
|
||||
typedef struct NmbDevice NmbDevice;
|
||||
typedef struct NmbBuffer NmbBuffer;
|
||||
typedef struct NmbHeap NmbHeap;
|
||||
typedef struct NmbTexture NmbTexture;
|
||||
typedef struct NmbLibrary NmbLibrary;
|
||||
typedef struct NmbFunction NmbFunction;
|
||||
@@ -36,6 +37,28 @@ NmbDevice *nmb_device_open(const char *loader, const char *cache);
|
||||
void nmb_device_release(NmbDevice *);
|
||||
size_t nmb_device_name(const NmbDevice *, char *out, size_t size);
|
||||
uint64_t nmb_device_max_buffer_length(const NmbDevice *);
|
||||
// Pipeline makings (count, total us, longest us, waited us, failed), sink messages (info, warning+), shader cache hits/misses/writes.
|
||||
bool nmb_device_work_stats(const NmbDevice *, uint64_t out[10]);
|
||||
/* Artifact lookups and driver pipeline records are separate cache layers. */
|
||||
struct NmbShaderCacheStats {
|
||||
uint64_t hits, misses, rejected, read_failures, writes, write_failures, evictions;
|
||||
uint64_t driver_loads, driver_hits, driver_misses, driver_saves, driver_truncated, driver_mismatched, driver_unchanged;
|
||||
};
|
||||
bool nmb_device_shader_cache_stats(const NmbDevice *, struct NmbShaderCacheStats *);
|
||||
size_t nmb_last_warning(char *out, size_t size);
|
||||
bool nmb_device_max_threads_per_threadgroup(const NmbDevice *, uint64_t out[3]);
|
||||
/* Device queries the runtime answers by Metal selector: Apple's private counts and flags where the device has an answer of its own,
|
||||
* and the public queries. A selector's kind says its result and argument. */
|
||||
enum {NMB_ANSWER_COUNT = 0, NMB_ANSWER_FLAG = 1, NMB_ANSWER_COUNT_ARG = 2, NMB_ANSWER_FLAG_ARG = 3};
|
||||
const char *nmb_device_answer_selector(size_t index, uint32_t *kind /* NMB_ANSWER_* */); /* NULL past the last */
|
||||
bool nmb_device_answer(const NmbDevice *, const char *selector, uint64_t arg, uint64_t *out); /* false: no answer, nmb_last_error says why */
|
||||
size_t nmb_device_vendor_name(const NmbDevice *, char *out, size_t size);
|
||||
uint64_t nmb_device_linear_bytes_per_row(const NmbDevice *, uint64_t format, uint64_t width); /* 0: refused */
|
||||
|
||||
typedef struct NmbBinaryArchive NmbBinaryArchive;
|
||||
NmbBinaryArchive *nmb_binary_archive_new(NmbDevice *, const char *path /* NULL: empty */);
|
||||
void nmb_binary_archive_release(NmbBinaryArchive *);
|
||||
bool nmb_binary_archive_serialize(NmbBinaryArchive *, const char *path);
|
||||
|
||||
NmbBuffer *nmb_buffer_new(NmbDevice *, uint64_t length, uint64_t options, const void *bytes /* NULL: zeroed */);
|
||||
/* Wraps page-aligned memory the caller keeps mapped until the buffer is released (an IOSurface's base, host pages). */
|
||||
@@ -48,9 +71,25 @@ uint64_t nmb_buffer_gpu_address(const NmbBuffer *);
|
||||
struct NmbTextureDescriptor {
|
||||
uint64_t type, format, width, height, depth, levels, samples, array_length, usage, options;
|
||||
};
|
||||
/* Heaps: type 0 automatic / 1 placement; offset UINT64_MAX asks an automatic heap to place the resource. */
|
||||
NmbHeap *nmb_heap_new(NmbDevice *, uint64_t type, uint64_t size, uint64_t options);
|
||||
void nmb_heap_release(NmbHeap *);
|
||||
uint64_t nmb_heap_size(const NmbHeap *);
|
||||
uint64_t nmb_heap_used_size(const NmbHeap *);
|
||||
uint64_t nmb_heap_allocated_size(const NmbHeap *);
|
||||
uint64_t nmb_heap_max_available_size(const NmbHeap *, uint64_t alignment);
|
||||
uint64_t nmb_heap_set_purgeable_state(const NmbHeap *, uint64_t state);
|
||||
NmbBuffer *nmb_heap_new_buffer(const NmbHeap *, uint64_t length, uint64_t options, uint64_t offset);
|
||||
NmbTexture *nmb_heap_new_texture(const NmbHeap *, const struct NmbTextureDescriptor *, uint64_t offset);
|
||||
NmbTexture *nmb_texture_new(NmbDevice *, const struct NmbTextureDescriptor *);
|
||||
/* A linear texture over a buffer's bytes; iosurface marks the buffer as standing for a surface without planes (Metal's IOSurface rules). */
|
||||
NmbTexture *nmb_texture_new_on_buffer(NmbBuffer *, const struct NmbTextureDescriptor *, uint64_t offset, uint64_t bytes_per_row, bool iosurface);
|
||||
NmbTexture *nmb_texture_new_linear(NmbBuffer *, const struct NmbTextureDescriptor *, uint64_t offset, uint64_t bytes_per_row, uint64_t bytes_per_image);
|
||||
/* whole: a view of every level and slice as format; else levels and slices are {start, count} and kind the view's texture type. */
|
||||
NmbTexture *nmb_texture_new_view(NmbTexture *, uint64_t format, uint64_t kind, const uint64_t levels[2], const uint64_t slices[2], bool whole);
|
||||
/* setPurgeableState: with Metal's raw state; the answer's raw state, 0 where it fails. */
|
||||
uint64_t nmb_texture_set_purgeable_state(const NmbTexture *, uint64_t state);
|
||||
uint64_t nmb_buffer_set_purgeable_state(const NmbBuffer *, uint64_t state);
|
||||
void nmb_texture_release(NmbTexture *);
|
||||
void nmb_texture_describe(const NmbTexture *, struct NmbTextureDescriptor *out);
|
||||
bool nmb_texture_replace(NmbTexture *, const uint64_t region[6], uint64_t level, uint64_t slice, const void *bytes, uint64_t bytes_per_row,
|
||||
@@ -91,6 +130,21 @@ struct NmbRenderPipelineDescriptor {
|
||||
struct NmbVertexLayout layouts[31];
|
||||
};
|
||||
NmbRenderPipeline *nmb_render_pipeline_new(NmbDevice *, const struct NmbRenderPipelineDescriptor *);
|
||||
/* Descriptor binaryArchives and MTLPipelineOption bits. NULL options means defaults.
|
||||
* The archive array and its handles must remain valid for the duration of this call. */
|
||||
struct NmbPipelineOptions {uint64_t flags; NmbBinaryArchive *const *archives; size_t archive_count;};
|
||||
struct NmbComputePipelineDescriptor {
|
||||
NmbFunction *function;
|
||||
uint64_t max_threads, multiple_of_execution_width, indirect, max_call_stack_depth, adding_binary_functions;
|
||||
uint64_t mutability[31];
|
||||
};
|
||||
NmbComputePipeline *nmb_compute_pipeline_new_with_options(NmbDevice *, NmbFunction *, const struct NmbPipelineOptions *);
|
||||
NmbComputePipeline *nmb_compute_pipeline_new_with_descriptor(NmbDevice *, const struct NmbComputePipelineDescriptor *, const struct NmbPipelineOptions *);
|
||||
NmbRenderPipeline *nmb_render_pipeline_new_with_options(NmbDevice *, const struct NmbRenderPipelineDescriptor *, const struct NmbPipelineOptions *);
|
||||
/* The archive makes the state as the matching new call does and keeps what it used (addRender/ComputePipelineFunctionsWithDescriptor:). */
|
||||
bool nmb_binary_archive_add_render(NmbBinaryArchive *, const struct NmbRenderPipelineDescriptor *);
|
||||
bool nmb_binary_archive_add_compute(NmbBinaryArchive *, NmbFunction *);
|
||||
bool nmb_binary_archive_add_compute_descriptor(NmbBinaryArchive *, const struct NmbComputePipelineDescriptor *);
|
||||
void nmb_render_pipeline_release(NmbRenderPipeline *);
|
||||
|
||||
struct NmbSamplerDescriptor {
|
||||
@@ -109,9 +163,19 @@ NmbQueue *nmb_queue_new(NmbDevice *);
|
||||
void nmb_queue_release(NmbQueue *);
|
||||
NmbCommandBuffer *nmb_command_buffer_new(NmbQueue *);
|
||||
void nmb_command_buffer_release(NmbCommandBuffer *);
|
||||
/* 1 storage dependency, 0 unrelated/already complete, -1 unknown (caller must wait).
|
||||
* Includes texture roots, buffer-backed textures, heaps and overlapping imported host ranges.
|
||||
* This frontend does not rebind/remap live backing ranges. */
|
||||
int nmb_command_buffer_uses_buffer(const NmbCommandBuffer *, const NmbBuffer *);
|
||||
int nmb_command_buffer_uses_texture(const NmbCommandBuffer *, const NmbTexture *);
|
||||
/* The handler runs once, on a runtime thread, after the command buffer completed or failed. */
|
||||
bool nmb_command_buffer_on_completed(NmbCommandBuffer *, void (*handler)(void *context), void *context);
|
||||
/* Runs on actual scheduling, before completion. Not called if submission fails before scheduling.
|
||||
* The caller keeps context alive until completion (or a refused commit). */
|
||||
bool nmb_command_buffer_on_scheduled(NmbCommandBuffer *, void (*handler)(void *context), void *context);
|
||||
bool nmb_command_buffer_commit(NmbCommandBuffer *);
|
||||
/* Wait for scheduling and scheduled handlers only; false on a refused wait. */
|
||||
bool nmb_command_buffer_wait_scheduled(NmbCommandBuffer *);
|
||||
/* 0 completed in time, 1 timed out (timeout_ms 0 waits without a bound). */
|
||||
int nmb_command_buffer_wait(NmbCommandBuffer *, uint64_t timeout_ms);
|
||||
/* Metal's MTLCommandBufferStatus (0 not enqueued .. 4 completed, 5 error). */
|
||||
@@ -146,6 +210,7 @@ struct NmbPassAttachment {
|
||||
struct NmbRenderPassDescriptor {
|
||||
struct NmbPassAttachment colors[8], depth, stencil;
|
||||
uint64_t width, height, array_length, samples;
|
||||
NmbBuffer *visibility; /* the pass's visibility result buffer, or null */
|
||||
};
|
||||
NmbEncoder *nmb_render_encoder_new(NmbCommandBuffer *, const struct NmbRenderPassDescriptor *);
|
||||
bool nmb_encoder_end(NmbEncoder *);
|
||||
@@ -169,6 +234,9 @@ bool nmb_render_set_scissor(NmbEncoder *, const uint64_t rect[4]);
|
||||
bool nmb_render_set_raster(NmbEncoder *, uint64_t cull_mode, uint64_t winding, uint64_t fill_mode);
|
||||
bool nmb_render_set_stencil_reference(NmbEncoder *, uint32_t front, uint32_t back);
|
||||
bool nmb_render_set_blend_color(NmbEncoder *, const float color[4]);
|
||||
bool nmb_render_set_depth_bias(NmbEncoder *, float bias, float slope_scale, float clamp);
|
||||
bool nmb_render_set_line_width(NmbEncoder *, float width);
|
||||
bool nmb_render_set_visibility_mode(NmbEncoder *, uint64_t mode, uint64_t offset);
|
||||
bool nmb_render_draw(NmbEncoder *, uint64_t primitive, uint32_t start, uint32_t count, uint32_t instances, uint32_t base_instance);
|
||||
bool nmb_render_draw_indexed(NmbEncoder *, uint64_t primitive, uint32_t count, uint64_t index_type, NmbBuffer *indices, uint64_t offset,
|
||||
uint32_t instances, int32_t base_vertex, uint32_t base_instance);
|
||||
|
||||
@@ -13,14 +13,14 @@ use metal2vulkan::metal::{
|
||||
TextureUsage, TriangleFillMode, VertexFormat, VertexStepFunction, Winding,
|
||||
};
|
||||
use metal2vulkan::pipeline::{
|
||||
ColorAttachmentDescriptor, RenderPipelineDescriptor, StageSource, VertexAttributeDescriptor, VertexBufferLayoutDescriptor,
|
||||
ColorAttachmentDescriptor, ComputePipelineDescriptor, RenderPipelineDescriptor, StageSource, VertexAttributeDescriptor, VertexBufferLayoutDescriptor,
|
||||
VertexDescriptor,
|
||||
};
|
||||
use metal2vulkan_runtime::{
|
||||
BlitCommandEncoder, BlitOption, Buffer, ClearColor, CommandBuffer, CommandQueue, ComputeCommandEncoder,
|
||||
ComputePipelineState, DepthStencilState, Device, DeviceOptions, Event, EventRef, Fence, Function, HostMemory, IoSurfacePlane, Library, LoadAction,
|
||||
MetalLevel, Origin, PipelineOptions, Region, RenderCommandEncoder, RenderPassDescriptor, RenderPipelineState, ResourceOptions,
|
||||
SamplerState, ScissorRect, SharedEvent, SharedEventListener, Size, StoreAction, Texture, TextureDescriptor, Viewport,
|
||||
BinaryArchive, BinaryArchiveDescriptor, BlitCommandEncoder, FamilyRow, FeatureRow, GpuFamily, RowState, BlitOption, Buffer, ClearColor, CommandBuffer, CommandQueue, ComputeCommandEncoder,
|
||||
ComputePipelineState, DebugMessage, DepthStencilState, Device, DeviceOptions, MessageSeverity, MessageSink, Event, EventRef, Fence, Function, Heap, HeapDescriptor, HeapType, HostMemory, IoSurfacePlane, Library, LoadAction,
|
||||
MetalLevel, Origin, PipelineOption, PipelineOptions, PurgeableState, PrivateCount, PrivateFlag, PrivateProperties, MAX_COLOR_ATTACHMENTS, Region, RenderCommandEncoder, RenderPassDescriptor, RenderPipelineState, ResourceOptions,
|
||||
SamplerState, ScissorRect, SharedEvent, SharedEventListener, Size, StoreAction, Texture, TextureDescriptor, Viewport, VisibilityResultMode,
|
||||
};
|
||||
use std::cell::RefCell;
|
||||
use std::ffi::{c_char, c_void, CStr};
|
||||
@@ -140,7 +140,14 @@ pub unsafe extern "C" fn nmb_function_constant_air(length: *mut usize) -> *const
|
||||
|
||||
// ---- device ----
|
||||
|
||||
pub struct NmbDevice(Device);
|
||||
pub struct NmbDevice(Device, std::sync::OnceLock<PrivateProperties>, std::sync::Mutex<()>);
|
||||
|
||||
impl NmbDevice {
|
||||
/// The runtime judges the private properties by the family rows once.
|
||||
fn private(&self) -> &PrivateProperties {
|
||||
self.1.get_or_init(|| self.0.private_properties())
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `loader` and `cache` are C strings.
|
||||
@@ -156,10 +163,114 @@ pub unsafe extern "C" fn nmb_device_open(loader: *const c_char, cache: *const c_
|
||||
options.loader = Some(PathBuf::from(loader.to_string_lossy().into_owned()));
|
||||
options.metal_level = Some(MetalLevel::NEWEST);
|
||||
options.shader_cache = metal2vulkan_runtime::ShaderCacheOptions::in_directory(cache.to_string_lossy().into_owned());
|
||||
Ok(boxed(NmbDevice(Device::system_default_with(&options).map_err(e("device"))?)))
|
||||
options.messages = Some(MessageSink::new(observe_message));
|
||||
Ok(boxed(NmbDevice(Device::system_default_with(&options).map_err(e("device"))?, std::sync::OnceLock::new(), std::sync::Mutex::new(()))))
|
||||
})
|
||||
}
|
||||
|
||||
// What the runtime says through the device's sink, kept as numbers for the plugin's telemetry: each render VkPipeline making the
|
||||
// creation log (METAL2VULKAN_LOG_PIPELINES) reports ("... made in <ms> ms[ after waiting <ms> ms for its turn]"), and the other
|
||||
// messages by severity with the last warning's text.
|
||||
static PIPELINE_MAKINGS: [std::sync::atomic::AtomicU64; 5] = [const { std::sync::atomic::AtomicU64::new(0) }; 5]; // n, us, max us, waited us, failed
|
||||
static OTHER_MESSAGES: [std::sync::atomic::AtomicU64; 2] = [const { std::sync::atomic::AtomicU64::new(0) }; 2]; // info, warning or worse
|
||||
static LAST_WARNING: std::sync::Mutex<String> = std::sync::Mutex::new(String::new());
|
||||
|
||||
fn millis_after(text: &str, marker: &str) -> Option<f64> {
|
||||
let at = text.find(marker)? + marker.len();
|
||||
let rest = &text[at..];
|
||||
let end = rest.find(' ').unwrap_or(rest.len());
|
||||
rest[..end].parse().ok()
|
||||
}
|
||||
|
||||
fn observe_message(message: &DebugMessage) {
|
||||
use std::sync::atomic::Ordering::Relaxed;
|
||||
if message.id_name == "metal2vulkan-pipeline-creation" {
|
||||
let made = millis_after(&message.text, " made in ");
|
||||
let us = made.or_else(|| millis_after(&message.text, " failed in ")).map_or(0, |ms| (ms * 1000.0) as u64);
|
||||
PIPELINE_MAKINGS[0].fetch_add(1, Relaxed);
|
||||
PIPELINE_MAKINGS[1].fetch_add(us, Relaxed);
|
||||
PIPELINE_MAKINGS[2].fetch_max(us, Relaxed);
|
||||
PIPELINE_MAKINGS[3].fetch_add(millis_after(&message.text, " after waiting ").map_or(0, |ms| (ms * 1000.0) as u64), Relaxed);
|
||||
if made.is_none() {
|
||||
PIPELINE_MAKINGS[4].fetch_add(1, Relaxed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if message.severity >= MessageSeverity::Warning {
|
||||
OTHER_MESSAGES[1].fetch_add(1, Relaxed);
|
||||
if let Ok(mut last) = LAST_WARNING.lock() {
|
||||
*last = message.text.chars().take(160).collect();
|
||||
}
|
||||
} else {
|
||||
OTHER_MESSAGES[0].fetch_add(1, Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
/// The sink's counts into the caller's ten words: pipeline makings (count, total us, longest us, waited us, failed), messages (info,
|
||||
/// warning or worse), and the device's shader cache hits, misses and writes.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_device_work_stats(device: *const NmbDevice, out: *mut u64) -> bool {
|
||||
use std::sync::atomic::Ordering::Relaxed;
|
||||
guard(false, || {
|
||||
if out.is_null() {
|
||||
return Err("null stats".into());
|
||||
}
|
||||
// SAFETY: the header's ten words.
|
||||
let out = unsafe { std::slice::from_raw_parts_mut(out, 10) };
|
||||
for (i, v) in PIPELINE_MAKINGS.iter().enumerate() {
|
||||
out[i] = v.load(Relaxed);
|
||||
}
|
||||
out[5] = OTHER_MESSAGES[0].load(Relaxed);
|
||||
out[6] = OTHER_MESSAGES[1].load(Relaxed);
|
||||
let cache = get(device)?.0.shader_cache_statistics();
|
||||
out[7] = cache.hits;
|
||||
out[8] = cache.misses;
|
||||
out[9] = cache.writes;
|
||||
Ok(true)
|
||||
})
|
||||
}
|
||||
|
||||
/// Separate artifact and driver-record counters, mirroring the C ABI.
|
||||
#[repr(C)]
|
||||
pub struct NmbShaderCacheStats {
|
||||
pub hits: u64,
|
||||
pub misses: u64,
|
||||
pub rejected: u64,
|
||||
pub read_failures: u64,
|
||||
pub writes: u64,
|
||||
pub write_failures: u64,
|
||||
pub evictions: u64,
|
||||
pub driver_loads: u64,
|
||||
pub driver_hits: u64,
|
||||
pub driver_misses: u64,
|
||||
pub driver_saves: u64,
|
||||
pub driver_truncated: u64,
|
||||
pub driver_mismatched: u64,
|
||||
pub driver_unchanged: u64,
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_device_shader_cache_stats(device: *const NmbDevice, out: *mut NmbShaderCacheStats) -> bool {
|
||||
guard(false, || {
|
||||
let out = get_mut(out)?;
|
||||
let stats = get(device)?.0.shader_cache_statistics();
|
||||
*out = NmbShaderCacheStats {
|
||||
hits: stats.hits, misses: stats.misses, rejected: stats.rejected,
|
||||
read_failures: stats.read_failures, writes: stats.writes, write_failures: stats.write_failures, evictions: stats.evictions,
|
||||
driver_loads: stats.driver_loads, driver_hits: stats.driver_pipeline_hits, driver_misses: stats.driver_pipeline_misses,
|
||||
driver_saves: stats.driver_saves, driver_truncated: stats.driver_data_truncated,
|
||||
driver_mismatched: stats.driver_data_mismatched, driver_unchanged: stats.driver_data_unchanged,
|
||||
};
|
||||
Ok(true)
|
||||
})
|
||||
}
|
||||
|
||||
/// The last warning the device's sink received, as a C string; returns its length.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_last_warning(out: *mut c_char, size: usize) -> usize {
|
||||
guard(0, || Ok(copy_out(&LAST_WARNING.lock().map(|s| s.clone()).unwrap_or_default(), out, size)))
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_device_release(device: *mut NmbDevice) {
|
||||
release(device)
|
||||
@@ -175,6 +286,188 @@ pub extern "C" fn nmb_device_max_buffer_length(device: *const NmbDevice) -> u64
|
||||
guard(0, || Ok(get(device)?.0.properties().max_buffer_length))
|
||||
}
|
||||
|
||||
/// `maxThreadsPerThreadgroup` into the caller's three words.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_device_max_threads_per_threadgroup(device: *const NmbDevice, out: *mut u64) -> bool {
|
||||
guard(false, || {
|
||||
if out.is_null() {
|
||||
return Err("null size".into());
|
||||
}
|
||||
let s = get(device)?.0.properties().max_threads_per_threadgroup;
|
||||
// SAFETY: the header's three words.
|
||||
unsafe { std::slice::from_raw_parts_mut(out, 3) }.copy_from_slice(&[s.width, s.height, s.depth]);
|
||||
Ok(true)
|
||||
})
|
||||
}
|
||||
|
||||
/// The device selectors `nmb_device_answer` takes beyond Apple's private counts and flags: the selector and its kind
|
||||
/// (the header's NMB_ANSWER_* values).
|
||||
const PUBLIC_ANSWERS: &[(&str, u32)] = &[
|
||||
("maxThreadgroupMemoryLength", 0),
|
||||
("argumentBuffersSupport", 0),
|
||||
("readWriteTextureSupport", 0),
|
||||
("maxColorAttachments", 0),
|
||||
("minConstantBufferAlignmentBytes", 0),
|
||||
("deviceLinearReadOnlyTextureAlignmentBytes", 0),
|
||||
("areRasterOrderGroupsSupported", 1),
|
||||
("supportsRasterOrderGroups", 1),
|
||||
("isDepth24Stencil8PixelFormatSupported", 1),
|
||||
("areProgrammableSamplePositionsSupported", 1),
|
||||
("supports32BitFloatFiltering", 1),
|
||||
("supportsMemorylessRenderTargets", 1),
|
||||
("supportsSamplerReductionMode", 1),
|
||||
("supportsYCBCRFormats12", 1),
|
||||
("supportsYCBCRPackedFormats12", 1),
|
||||
("supportsSIMDGroup", 1),
|
||||
("minimumLinearTextureAlignmentForPixelFormat:", 2),
|
||||
("supportsTextureSampleCount:", 3),
|
||||
("supportsVertexAmplificationCount:", 3),
|
||||
("supportsRasterizationRateMapWithLayerCount:", 3),
|
||||
("supportsFamily:", 3),
|
||||
("supportsPrimitiveType:", 3),
|
||||
("supportsTextureWriteRoundingMode:", 3),
|
||||
];
|
||||
|
||||
/// Every selector `nmb_device_answer` takes, with its kind, as C strings.
|
||||
fn answer_selectors() -> &'static [(std::ffi::CString, u32)] {
|
||||
static ALL: std::sync::OnceLock<Vec<(std::ffi::CString, u32)>> = std::sync::OnceLock::new();
|
||||
ALL.get_or_init(|| {
|
||||
let counts = PrivateCount::ALL.into_iter().map(|c| (c.selector(), 0));
|
||||
let flags = PrivateFlag::ALL.into_iter().map(|f| (f.selector(), 1));
|
||||
counts
|
||||
.chain(flags)
|
||||
.chain(PUBLIC_ANSWERS.iter().copied())
|
||||
.map(|(s, kind)| (std::ffi::CString::new(s).expect("selector"), kind))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// The selector at `index` of the ones `nmb_device_answer` takes and its kind; null past the last.
|
||||
///
|
||||
/// # Safety
|
||||
/// `kind` is writable.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn nmb_device_answer_selector(index: usize, kind: *mut u32) -> *const c_char {
|
||||
let Some((selector, k)) = answer_selectors().get(index) else { return std::ptr::null() };
|
||||
// SAFETY: the caller's contract.
|
||||
if let Some(kind) = unsafe { kind.as_mut() } {
|
||||
*kind = *k;
|
||||
}
|
||||
selector.as_ptr()
|
||||
}
|
||||
|
||||
fn device_answer(device: &NmbDevice, selector: &str, arg: u64) -> Result<u64, String> {
|
||||
let d = &device.0;
|
||||
let p = d.properties();
|
||||
let none = || format!("the device has no answer for {selector}");
|
||||
if let Some(c) = PrivateCount::ALL.into_iter().find(|c| c.selector() == selector) {
|
||||
return device.private().count(c).value().ok_or_else(none);
|
||||
}
|
||||
if let Some(f) = PrivateFlag::ALL.into_iter().find(|f| f.selector() == selector) {
|
||||
return device.private().flag(f).value().map(u64::from).ok_or_else(none);
|
||||
}
|
||||
let gap = |r: Result<bool, metal2vulkan_runtime::RuntimeGap>| r.map(u64::from).map_err(|g| format!("{selector}: {g:?}"));
|
||||
let format_supported = |f: PixelFormat| d.pixel_format_capabilities(f) != metal2vulkan_runtime::FormatCapabilities::NONE;
|
||||
Ok(match selector {
|
||||
"maxThreadgroupMemoryLength" => p.max_threadgroup_memory_length,
|
||||
"argumentBuffersSupport" => p.argument_buffers_support.as_raw() as u64,
|
||||
"readWriteTextureSupport" => p.read_write_texture_support.as_raw() as u64,
|
||||
// The runtime's pass takes MAX_COLOR_ATTACHMENTS; the device's Vulkan limits bound what a pipeline links.
|
||||
"maxColorAttachments" => {
|
||||
let l = &p.description.limits;
|
||||
(MAX_COLOR_ATTACHMENTS as u64).min(l.max_color_attachments.min(l.max_fragment_output_attachments) as u64)
|
||||
}
|
||||
// The runtime binds a buffer at any offset within it (render.rs, setVertexBuffer); 4 is a scalar's alignment, which the shader's
|
||||
// own loads need.
|
||||
"minConstantBufferAlignmentBytes" => 4,
|
||||
"deviceLinearReadOnlyTextureAlignmentBytes" => {
|
||||
device.private().count(PrivateCount::DeviceLinearTextureAlignmentBytes).value().ok_or_else(none)?
|
||||
}
|
||||
"areRasterOrderGroupsSupported" | "supportsRasterOrderGroups" => gap(p.are_raster_order_groups_supported())?,
|
||||
"isDepth24Stencil8PixelFormatSupported" => gap(p.is_depth24_stencil8_pixel_format_supported())?,
|
||||
"areProgrammableSamplePositionsSupported" => gap(p.are_programmable_sample_positions_supported())?,
|
||||
"supports32BitFloatFiltering" => p.supports_32bit_float_filtering as u64,
|
||||
// A Memoryless texture is a transient render attachment (texture.rs), which every device makes.
|
||||
"supportsMemorylessRenderTargets" => 1,
|
||||
"supportsSamplerReductionMode" => gap(p.supports_sampler_reduction_mode())?,
|
||||
// Apple's SIMD-group functions: the family's SIMD-scoped barrier and permute rows are exact on the device.
|
||||
"supportsSIMDGroup" => {
|
||||
let rows = d.family_rows(GpuFamily::Apple9);
|
||||
let exact = |r: FeatureRow| rows.iter().any(|a| a.row == FamilyRow::Feature(r) && a.state == RowState::Exact);
|
||||
(exact(FeatureRow::SimdBarrier) && exact(FeatureRow::SimdPermute)) as u64
|
||||
}
|
||||
"supportsYCBCRFormats12" => format_supported(PixelFormat::YCBCR124202P) as u64,
|
||||
"supportsYCBCRPackedFormats12" => format_supported(PixelFormat::YCBCR124202PPACKED) as u64,
|
||||
"minimumLinearTextureAlignmentForPixelFormat:" => p
|
||||
.minimum_linear_texture_alignment_for_pixel_format(raw::<PixelFormat>(arg, "pixel format")?)
|
||||
.map_err(e("minimum linear texture alignment"))?,
|
||||
"supportsTextureSampleCount:" => gap(p.supports_texture_sample_count(arg))?,
|
||||
"supportsVertexAmplificationCount:" => gap(p.supports_vertex_amplification_count(arg))?,
|
||||
"supportsRasterizationRateMapWithLayerCount:" => gap(p.supports_rasterization_rate_map_with_layer_count(arg))?,
|
||||
"supportsFamily:" => d.supports_family(arg as i64) as u64,
|
||||
"supportsPrimitiveType:" => p.supports_primitive_type(arg) as u64,
|
||||
"supportsTextureWriteRoundingMode:" => p.supports_texture_write_rounding_mode(arg) as u64,
|
||||
_ => return Err(format!("{selector} is not a device query of the bridge")),
|
||||
})
|
||||
}
|
||||
|
||||
/// The runtime's answer on the device to `selector` (one `nmb_device_answer_selector` lists), with `arg` for a selector that takes one.
|
||||
/// False, with the reason, where the device has no answer of its own.
|
||||
///
|
||||
/// # Safety
|
||||
/// `selector` is a C string; `out` is writable.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn nmb_device_answer(device: *const NmbDevice, selector: *const c_char, arg: u64, out: *mut u64) -> bool {
|
||||
guard(false, || {
|
||||
if selector.is_null() || out.is_null() {
|
||||
return Err("null selector or answer".into());
|
||||
}
|
||||
// SAFETY: the caller's contract.
|
||||
let selector = unsafe { CStr::from_ptr(selector) }.to_str().map_err(e("selector"))?;
|
||||
let answer = device_answer(get(device)?, selector, arg)?;
|
||||
// SAFETY: as above.
|
||||
unsafe { *out = answer };
|
||||
Ok(true)
|
||||
})
|
||||
}
|
||||
|
||||
/// `requiredLinearTextureBytesPerRowForDescriptor:` (Apple SPI): a row of `width` texels of `format` rounded up to the format's
|
||||
/// linear alignment, from the runtime's format description; 0, with the reason, for a format no buffer can back.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_device_linear_bytes_per_row(device: *const NmbDevice, format: u64, width: u64) -> u64 {
|
||||
guard(0, || {
|
||||
let d = &get(device)?.0;
|
||||
let alignment = d
|
||||
.properties()
|
||||
.minimum_linear_texture_alignment_for_pixel_format(raw::<PixelFormat>(format, "pixel format")?)
|
||||
.map_err(e("linear texture"))?;
|
||||
let info = d.pixel_format_info(format);
|
||||
// Flags bit 0x400: a format of blocks, which the alignment query has refused already.
|
||||
if info.flags & 0x400 != 0 || info.words[0] == 0 {
|
||||
return Err(format!("pixel format {format} has no texel size"));
|
||||
}
|
||||
Ok((width * info.words[0]).div_ceil(alignment) * alignment)
|
||||
})
|
||||
}
|
||||
|
||||
/// `vendorName`: the Vulkan device's PCI vendor, named where the ID is a known GPU vendor's, else the ID itself.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_device_vendor_name(device: *const NmbDevice, out: *mut c_char, size: usize) -> usize {
|
||||
guard(0, || {
|
||||
let id = get(device)?.0.properties().vendor_id;
|
||||
let name = match id {
|
||||
0x5143 => "Qualcomm".to_string(),
|
||||
0x106b => "Apple".to_string(),
|
||||
0x10de => "NVIDIA".to_string(),
|
||||
0x1002 => "AMD".to_string(),
|
||||
0x8086 => "Intel".to_string(),
|
||||
0x13b5 => "ARM".to_string(),
|
||||
_ => format!("0x{id:04x}"),
|
||||
};
|
||||
Ok(copy_out(&name, out, size))
|
||||
})
|
||||
}
|
||||
|
||||
// ---- buffers ----
|
||||
|
||||
pub struct NmbBuffer(Buffer);
|
||||
@@ -203,11 +496,23 @@ pub unsafe extern "C" fn nmb_buffer_new(device: *mut NmbDevice, length: u64, opt
|
||||
pub unsafe extern "C" fn nmb_buffer_new_no_copy(device: *mut NmbDevice, pointer: *mut c_void, length: u64, options_word: u64)
|
||||
-> *mut NmbBuffer {
|
||||
guard(std::ptr::null_mut(), || {
|
||||
let device = &get(device)?.0;
|
||||
let device = get(device)?;
|
||||
// SAFETY: the caller's contract.
|
||||
let memory = unsafe { HostMemory::new(NonNull::new(pointer as *mut u8).ok_or("null memory")?) };
|
||||
// SAFETY: as above.
|
||||
let buffer = unsafe { device.new_buffer_with_bytes_no_copy(memory, length, options(options_word)?, None) };
|
||||
let modes = options(options_word)?;
|
||||
// The runtime's live-import lookup and subsequent join are separate:
|
||||
// simultaneous first constructors can allocate distinct imports of the
|
||||
// same pages. Cover every native ABI no-copy call, including mixtures
|
||||
// of raw buffers and IOSurface textures. Each call still gets its own
|
||||
// buffer object. This ABI passes neither a dma-buf source nor a runtime
|
||||
// deallocator; no caller callback runs under this gate. Drop it before
|
||||
// returning/formatting a failure; GPU work and resource release never
|
||||
// take it. A device's unrelated APIs and other devices remain separate.
|
||||
let buffer = {
|
||||
let _gate = device.2.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
// SAFETY: as above.
|
||||
unsafe { device.0.new_buffer_with_bytes_no_copy(memory, length, modes, None) }
|
||||
};
|
||||
Ok(boxed(NmbBuffer(buffer.map_err(e("no-copy buffer"))?)))
|
||||
})
|
||||
}
|
||||
@@ -232,6 +537,79 @@ pub extern "C" fn nmb_buffer_gpu_address(buffer: *const NmbBuffer) -> u64 {
|
||||
guard(0, || Ok(get(buffer)?.0.gpu_address().0))
|
||||
}
|
||||
|
||||
// ---- heaps ----
|
||||
|
||||
pub struct NmbHeap(Heap);
|
||||
|
||||
/// `newHeapWithDescriptor:`: type (0 automatic, 1 placement), size, resource options. Sparse heaps are not made here.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_heap_new(device: *mut NmbDevice, heap_type: u64, size: u64, options_word: u64) -> *mut NmbHeap {
|
||||
guard(std::ptr::null_mut(), || {
|
||||
let device = &get(device)?.0;
|
||||
let heap_type = HeapType::from_raw(heap_type).map_err(e("heap type"))?;
|
||||
if heap_type == HeapType::Sparse {
|
||||
return Err("sparse heaps are not implemented".into());
|
||||
}
|
||||
let descriptor = HeapDescriptor::new(heap_type, size, options(options_word)?);
|
||||
Ok(boxed(NmbHeap(device.new_heap(&descriptor).map_err(e("heap"))?)))
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_heap_release(heap: *mut NmbHeap) {
|
||||
release(heap)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_heap_size(heap: *const NmbHeap) -> u64 {
|
||||
guard(0, || Ok(get(heap)?.0.size()))
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_heap_used_size(heap: *const NmbHeap) -> u64 {
|
||||
guard(0, || Ok(get(heap)?.0.used_size()))
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_heap_allocated_size(heap: *const NmbHeap) -> u64 {
|
||||
guard(0, || Ok(get(heap)?.0.current_allocated_size()))
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_heap_max_available_size(heap: *const NmbHeap, alignment: u64) -> u64 {
|
||||
guard(0, || Ok(get(heap)?.0.max_available_size(alignment).map_err(e("heap available size"))?))
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_heap_set_purgeable_state(heap: *const NmbHeap, state: u64) -> u64 {
|
||||
guard(0, || {
|
||||
let state = PurgeableState::from_raw(state).map_err(e("purgeable state"))?;
|
||||
Ok(get(heap)?.0.set_purgeable_state(state).map_err(e("heap purgeable state"))?.raw())
|
||||
})
|
||||
}
|
||||
|
||||
/// A buffer in the heap: automatic heaps place it, a placement heap takes `offset` (`u64::MAX` asks for automatic placement).
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_heap_new_buffer(heap: *const NmbHeap, length: u64, options_word: u64, offset: u64) -> *mut NmbBuffer {
|
||||
guard(std::ptr::null_mut(), || {
|
||||
let heap = &get(heap)?.0;
|
||||
let options = options(options_word)?;
|
||||
let buffer = if offset == u64::MAX { heap.new_buffer(length, options) } else { heap.new_buffer_at_offset(length, options, offset) };
|
||||
Ok(boxed(NmbBuffer(buffer.map_err(e("heap buffer"))?)))
|
||||
})
|
||||
}
|
||||
|
||||
/// A texture in the heap, placed as `nmb_heap_new_buffer` places a buffer.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_heap_new_texture(heap: *const NmbHeap, descriptor: *const NmbTextureDescriptor, offset: u64) -> *mut NmbTexture {
|
||||
guard(std::ptr::null_mut(), || {
|
||||
let heap = &get(heap)?.0;
|
||||
let descriptor = texture_descriptor(descriptor)?;
|
||||
let texture = if offset == u64::MAX { heap.new_texture(&descriptor) } else { heap.new_texture_at_offset(&descriptor, offset) };
|
||||
Ok(boxed(NmbTexture(texture.map_err(e("heap texture"))?)))
|
||||
})
|
||||
}
|
||||
|
||||
// ---- textures ----
|
||||
|
||||
#[repr(C)]
|
||||
@@ -289,6 +667,57 @@ pub extern "C" fn nmb_texture_new_on_buffer(buffer: *mut NmbBuffer, descriptor:
|
||||
})
|
||||
}
|
||||
|
||||
/// `newLinearTextureWithDescriptor:offset:bytesPerRow:bytesPerImage:` (Apple SPI).
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_texture_new_linear(buffer: *mut NmbBuffer, descriptor: *const NmbTextureDescriptor, offset: u64, bytes_per_row: u64,
|
||||
bytes_per_image: u64) -> *mut NmbTexture {
|
||||
guard(std::ptr::null_mut(), || {
|
||||
let descriptor = texture_descriptor(descriptor)?;
|
||||
let texture = get(buffer)?.0.new_linear_texture(&descriptor, offset, bytes_per_row, bytes_per_image);
|
||||
Ok(boxed(NmbTexture(texture.map_err(e("linear texture"))?)))
|
||||
})
|
||||
}
|
||||
|
||||
/// A view of `texture`: of the whole texture as `format` where `whole`, else of `levels` and `slices` (start, count) as `format` and `kind`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_texture_new_view(texture: *mut NmbTexture, format: u64, kind: u64, levels: *const u64, slices: *const u64, whole: bool)
|
||||
-> *mut NmbTexture {
|
||||
guard(std::ptr::null_mut(), || {
|
||||
let t = &get(texture)?.0;
|
||||
let format = raw::<PixelFormat>(format, "pixel format")?;
|
||||
let view = if whole {
|
||||
t.new_texture_view(format)
|
||||
} else {
|
||||
if levels.is_null() || slices.is_null() {
|
||||
return Err("null levels or slices".into());
|
||||
}
|
||||
// SAFETY: the header's two words each.
|
||||
let (l, s) = unsafe { (std::slice::from_raw_parts(levels, 2), std::slice::from_raw_parts(slices, 2)) };
|
||||
let ranges = (l[0]..l[0].checked_add(l[1]).ok_or("levels overflow")?, s[0]..s[0].checked_add(s[1]).ok_or("slices overflow")?);
|
||||
t.new_texture_view_with_levels_slices(format, raw::<TextureType>(kind, "texture type")?, ranges.0, ranges.1)
|
||||
};
|
||||
Ok(boxed(NmbTexture(view.map_err(e("texture view"))?)))
|
||||
})
|
||||
}
|
||||
|
||||
/// `setPurgeableState:` on a texture: the runtime's answer as Metal's raw state, 0 where it fails.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_texture_set_purgeable_state(texture: *const NmbTexture, state: u64) -> u64 {
|
||||
guard(0, || {
|
||||
let state = PurgeableState::from_raw(state).map_err(e("purgeable state"))?;
|
||||
Ok(get(texture)?.0.set_purgeable_state(state).map_err(e("purgeable state"))?.raw())
|
||||
})
|
||||
}
|
||||
|
||||
/// `setPurgeableState:` on a buffer, as on a texture.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_buffer_set_purgeable_state(buffer: *const NmbBuffer, state: u64) -> u64 {
|
||||
guard(0, || {
|
||||
let state = PurgeableState::from_raw(state).map_err(e("purgeable state"))?;
|
||||
Ok(get(buffer)?.0.set_purgeable_state(state).map_err(e("purgeable state"))?.raw())
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_texture_release(texture: *mut NmbTexture) {
|
||||
release(texture)
|
||||
@@ -486,12 +915,61 @@ pub struct NmbDepthStencil(DepthStencilState);
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_compute_pipeline_new(device: *mut NmbDevice, function: *mut NmbFunction) -> *mut NmbComputePipeline {
|
||||
// SAFETY: null options names no array, the existing handles follow this ABI.
|
||||
unsafe { nmb_compute_pipeline_new_with_options(device, function, std::ptr::null()) }
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `options` is null or a valid record with a counted array of live archive handles.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn nmb_compute_pipeline_new_with_options(device: *mut NmbDevice, function: *mut NmbFunction,
|
||||
options: *const NmbPipelineOptions) -> *mut NmbComputePipeline {
|
||||
guard(std::ptr::null_mut(), || {
|
||||
let state = get(device)?.0.new_compute_pipeline_state_with_function(&get(function)?.0, &PipelineOptions::default());
|
||||
let resolved = unsafe { pipeline_options(options)? };
|
||||
let state = get(device)?.0.new_compute_pipeline_state_with_function(&get(function)?.0, &resolved);
|
||||
Ok(boxed(NmbComputePipeline(state.map_err(e("compute pipeline"))?)))
|
||||
})
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NmbComputePipelineDescriptor {
|
||||
pub function: *mut NmbFunction,
|
||||
pub max_threads: u64,
|
||||
pub multiple_of_execution_width: u64,
|
||||
pub indirect: u64,
|
||||
pub max_call_stack_depth: u64,
|
||||
pub adding_binary_functions: u64,
|
||||
pub mutability: [u64; 31],
|
||||
}
|
||||
|
||||
fn compute_descriptor(descriptor: *const NmbComputePipelineDescriptor) -> Result<ComputePipelineDescriptor, String> {
|
||||
let d = get(descriptor)?;
|
||||
let mut out = ComputePipelineDescriptor::new(StageSource::Function(get(d.function)?.0.function_descriptor()));
|
||||
out.max_total_threads_per_threadgroup = if d.max_threads == 0 { None } else { Some(u32::try_from(d.max_threads).map_err(e("compute max threads"))?) };
|
||||
out.threadgroup_size_is_multiple_of_execution_width = d.multiple_of_execution_width != 0;
|
||||
out.support_indirect_command_buffers = d.indirect != 0;
|
||||
out.max_call_stack_depth = u32::try_from(d.max_call_stack_depth).map_err(e("compute stack depth"))?;
|
||||
out.support_adding_binary_functions = d.adding_binary_functions != 0;
|
||||
for (index, &mutability) in d.mutability.iter().enumerate() {
|
||||
if mutability != 0 {
|
||||
out.buffers.insert(index as u32, raw::<metal2vulkan::metal::Mutability>(mutability, "buffer mutability")?);
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `options` is null or a valid record with a counted array of live archive handles.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn nmb_compute_pipeline_new_with_descriptor(device: *mut NmbDevice, descriptor: *const NmbComputePipelineDescriptor,
|
||||
options: *const NmbPipelineOptions) -> *mut NmbComputePipeline {
|
||||
guard(std::ptr::null_mut(), || {
|
||||
let resolved = unsafe { pipeline_options(options)? };
|
||||
let state = get(device)?.0.new_compute_pipeline_state(&compute_descriptor(descriptor)?, &resolved).map_err(e("compute pipeline"))?;
|
||||
Ok(boxed(NmbComputePipeline(state)))
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_compute_pipeline_release(pipeline: *mut NmbComputePipeline) {
|
||||
release(pipeline)
|
||||
@@ -555,8 +1033,50 @@ fn optional_format(value: u64) -> Result<Option<PixelFormat>, String> {
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_render_pipeline_new(device: *mut NmbDevice, descriptor: *const NmbRenderPipelineDescriptor) -> *mut NmbRenderPipeline {
|
||||
// SAFETY: null options names no array; the device/descriptor handles follow the existing ABI.
|
||||
unsafe { nmb_render_pipeline_new_with_options(device, descriptor, std::ptr::null()) }
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NmbPipelineOptions {
|
||||
pub flags: u64,
|
||||
pub archives: *const *mut NmbBinaryArchive,
|
||||
pub archive_count: usize,
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// The options record and its counted archive array are valid for this call.
|
||||
unsafe fn pipeline_options(options: *const NmbPipelineOptions) -> Result<PipelineOptions, String> {
|
||||
let mut resolved = PipelineOptions::default();
|
||||
if let Some(options) = unsafe { options.as_ref() } {
|
||||
resolved.options = PipelineOption::from_raw(options.flags);
|
||||
if options.archive_count > 0 {
|
||||
if options.archives.is_null() || options.archive_count > isize::MAX as usize / std::mem::size_of::<*mut NmbBinaryArchive>() {
|
||||
return Err("invalid binary archive array".into());
|
||||
}
|
||||
let archives = unsafe { std::slice::from_raw_parts(options.archives, options.archive_count) };
|
||||
for &archive in archives {
|
||||
resolved.binary_archives.push(get(archive)?.0.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `options` is null or a valid record; its array holds `archive_count` live archive handles.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn nmb_render_pipeline_new_with_options(device: *mut NmbDevice, descriptor: *const NmbRenderPipelineDescriptor,
|
||||
options: *const NmbPipelineOptions) -> *mut NmbRenderPipeline {
|
||||
guard(std::ptr::null_mut(), || {
|
||||
let device = &get(device)?.0;
|
||||
let resolved = unsafe { pipeline_options(options)? };
|
||||
let state = device.new_render_pipeline_state(&render_descriptor(descriptor)?, &resolved).map_err(e("render pipeline"))?;
|
||||
Ok(boxed(NmbRenderPipeline(state)))
|
||||
})
|
||||
}
|
||||
|
||||
fn render_descriptor(descriptor: *const NmbRenderPipelineDescriptor) -> Result<RenderPipelineDescriptor, String> {
|
||||
let d = get(descriptor)?;
|
||||
let mut out = RenderPipelineDescriptor::new(StageSource::Function(get(d.vertex)?.0.function_descriptor()));
|
||||
if !d.fragment.is_null() {
|
||||
@@ -599,9 +1119,7 @@ pub extern "C" fn nmb_render_pipeline_new(device: *mut NmbDevice, descriptor: *c
|
||||
}
|
||||
out.vertex_descriptor = Some(inputs);
|
||||
}
|
||||
let state = device.new_render_pipeline_state(&out, &PipelineOptions::default()).map_err(e("render pipeline"))?;
|
||||
Ok(boxed(NmbRenderPipeline(state)))
|
||||
})
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
@@ -609,6 +1127,67 @@ pub extern "C" fn nmb_render_pipeline_release(pipeline: *mut NmbRenderPipeline)
|
||||
release(pipeline)
|
||||
}
|
||||
|
||||
// ---- binary archives ----
|
||||
|
||||
pub struct NmbBinaryArchive(BinaryArchive);
|
||||
|
||||
/// # Safety
|
||||
/// `path` is null (an empty archive) or a C string naming the archive's file.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn nmb_binary_archive_new(device: *mut NmbDevice, path: *const c_char) -> *mut NmbBinaryArchive {
|
||||
guard(std::ptr::null_mut(), || {
|
||||
// SAFETY: the caller's contract.
|
||||
let url = (!path.is_null()).then(|| PathBuf::from(unsafe { CStr::from_ptr(path) }.to_string_lossy().into_owned()));
|
||||
let archive = get(device)?.0.new_binary_archive(&BinaryArchiveDescriptor::new(url)).map_err(e("binary archive"))?;
|
||||
Ok(boxed(NmbBinaryArchive(archive)))
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_binary_archive_release(archive: *mut NmbBinaryArchive) {
|
||||
release(archive)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_binary_archive_add_render(archive: *mut NmbBinaryArchive, descriptor: *const NmbRenderPipelineDescriptor) -> bool {
|
||||
guard(false, || {
|
||||
get(archive)?.0.add_render_pipeline_functions(&render_descriptor(descriptor)?).map_err(e("archive render pipeline"))?;
|
||||
Ok(true)
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_binary_archive_add_compute(archive: *mut NmbBinaryArchive, function: *mut NmbFunction) -> bool {
|
||||
guard(false, || {
|
||||
let descriptor = ComputePipelineDescriptor::new(StageSource::Function(get(function)?.0.function_descriptor()));
|
||||
get(archive)?.0.add_compute_pipeline_functions(&descriptor).map_err(e("archive compute pipeline"))?;
|
||||
Ok(true)
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_binary_archive_add_compute_descriptor(archive: *mut NmbBinaryArchive, descriptor: *const NmbComputePipelineDescriptor) -> bool {
|
||||
guard(false, || {
|
||||
get(archive)?.0.add_compute_pipeline_functions(&compute_descriptor(descriptor)?).map_err(e("archive compute pipeline"))?;
|
||||
Ok(true)
|
||||
})
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `path` is a C string.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn nmb_binary_archive_serialize(archive: *mut NmbBinaryArchive, path: *const c_char) -> bool {
|
||||
guard(false, || {
|
||||
if path.is_null() {
|
||||
return Err("null path".into());
|
||||
}
|
||||
// SAFETY: the caller's contract.
|
||||
let path = PathBuf::from(unsafe { CStr::from_ptr(path) }.to_string_lossy().into_owned());
|
||||
get(archive)?.0.serialize_to_url(&path).map_err(e("archive serialize"))?;
|
||||
Ok(true)
|
||||
})
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NmbSamplerDescriptor {
|
||||
pub min_filter: u64,
|
||||
@@ -725,6 +1304,22 @@ pub extern "C" fn nmb_command_buffer_release(command_buffer: *mut NmbCommandBuff
|
||||
release(command_buffer)
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_command_buffer_uses_buffer(command_buffer: *const NmbCommandBuffer, buffer: *const NmbBuffer) -> i32 {
|
||||
guard(-1, || {
|
||||
let used = get(command_buffer)?.0.used_resources();
|
||||
Ok(i32::from(used.references_resource(get(buffer)?.0.as_resource())))
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_command_buffer_uses_texture(command_buffer: *const NmbCommandBuffer, texture: *const NmbTexture) -> i32 {
|
||||
guard(-1, || {
|
||||
let used = get(command_buffer)?.0.used_resources();
|
||||
Ok(i32::from(used.references_resource(get(texture)?.0.as_resource())))
|
||||
})
|
||||
}
|
||||
|
||||
struct Context(*mut c_void);
|
||||
// SAFETY: the context is the caller's, handed back on the runtime's thread as the header states.
|
||||
unsafe impl Send for Context {}
|
||||
@@ -743,6 +1338,28 @@ pub extern "C" fn nmb_command_buffer_on_completed(command_buffer: *mut NmbComman
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_command_buffer_on_scheduled(command_buffer: *mut NmbCommandBuffer, handler: Option<extern "C" fn(*mut c_void)>,
|
||||
context: *mut c_void) -> bool {
|
||||
guard(false, || {
|
||||
let handler = handler.ok_or("null handler")?;
|
||||
let context = Context(context);
|
||||
get(command_buffer)?.0.add_scheduled_handler(Box::new(move |_| {
|
||||
let context = context;
|
||||
handler(context.0)
|
||||
})).map_err(e("scheduled handler"))?;
|
||||
Ok(true)
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_command_buffer_wait_scheduled(command_buffer: *mut NmbCommandBuffer) -> bool {
|
||||
guard(false, || {
|
||||
get(command_buffer)?.0.wait_until_scheduled().map_err(e("wait scheduled"))?;
|
||||
Ok(true)
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_command_buffer_commit(command_buffer: *mut NmbCommandBuffer) -> bool {
|
||||
guard(false, || {
|
||||
@@ -958,6 +1575,7 @@ pub struct NmbRenderPassDescriptor {
|
||||
pub height: u64,
|
||||
pub array_length: u64,
|
||||
pub samples: u64,
|
||||
pub visibility: *const NmbBuffer,
|
||||
}
|
||||
|
||||
fn texture_of(a: &NmbPassAttachment) -> Result<Option<Texture>, String> {
|
||||
@@ -999,6 +1617,9 @@ pub extern "C" fn nmb_render_encoder_new(command_buffer: *mut NmbCommandBuffer,
|
||||
pass.render_target_height = d.height;
|
||||
pass.render_target_array_length = d.array_length;
|
||||
pass.default_raster_sample_count = d.samples;
|
||||
if !d.visibility.is_null() {
|
||||
pass.visibility_result_buffer = Some(get(d.visibility)?.0.clone());
|
||||
}
|
||||
let encoder = get(command_buffer)?.0.render_command_encoder(&pass).map_err(e("render encoder"))?;
|
||||
Ok(boxed(NmbEncoder::Render(encoder)))
|
||||
})
|
||||
@@ -1229,6 +1850,32 @@ pub extern "C" fn nmb_render_set_stencil_reference(encoder: *mut NmbEncoder, fro
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_render_set_depth_bias(encoder: *mut NmbEncoder, bias: f32, slope_scale: f32, clamp: f32) -> bool {
|
||||
guard(false, || {
|
||||
render(encoder)?.set_depth_bias(bias, slope_scale, clamp).map_err(e("depth bias"))?;
|
||||
Ok(true)
|
||||
})
|
||||
}
|
||||
|
||||
/// SPI `setLineWidth:`: the runtime accepts Metal's default width of 1 and refuses any other (Apple's wide-line rule is unmeasured).
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_render_set_line_width(encoder: *mut NmbEncoder, width: f32) -> bool {
|
||||
guard(false, || {
|
||||
render(encoder)?.set_line_width(width).map_err(e("line width"))?;
|
||||
Ok(true)
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_render_set_visibility_mode(encoder: *mut NmbEncoder, mode: u64, offset: u64) -> bool {
|
||||
guard(false, || {
|
||||
let mode = VisibilityResultMode::from_raw(mode).map_err(e("visibility result mode"))?;
|
||||
render(encoder)?.set_visibility_result_mode(mode, offset).map_err(e("visibility result mode"))?;
|
||||
Ok(true)
|
||||
})
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn nmb_render_set_blend_color(encoder: *mut NmbEncoder, color: *const f32) -> bool {
|
||||
guard(false, || {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Two processes, distinct initially empty caches: archive selection cannot be satisfied by
|
||||
// the producer's live pipeline memo. A strict empty/changed-descriptor miss must refuse.
|
||||
#include "FrontendChecks.h"
|
||||
#include "NativeMetalObjects.h"
|
||||
#include "../NativeBridgeMetalFrontend.h"
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 5) return 2;
|
||||
@autoreleasepool {
|
||||
const bool produce = !strcmp(argv[4], "produce");
|
||||
id<MTLDevice> device = native_bridge_metal_open(argv[1], argv[2], 0x6e6d62, nullptr);
|
||||
if (!device) return 2;
|
||||
size_t bytes = 0;
|
||||
const void *air = nmb_fixture_air(&bytes);
|
||||
id<MTLLibrary> library = native_bridge_metal_library_with_air(air, bytes);
|
||||
if (!library) return 2;
|
||||
auto descriptor = [MTLRenderPipelineDescriptor new];
|
||||
descriptor.vertexFunction = [library newFunctionWithName:@"colored_vertex"];
|
||||
descriptor.fragmentFunction = [library newFunctionWithName:@"colored_fragment"];
|
||||
descriptor.colorAttachments[0].pixelFormat = MTLPixelFormatRGBA8Unorm;
|
||||
auto archiveDescriptor = [MTLBinaryArchiveDescriptor new];
|
||||
NSURL *url = [NSURL fileURLWithPath:@(argv[3])];
|
||||
if (!produce) archiveDescriptor.url = url;
|
||||
NSError *error = nil;
|
||||
id<MTLBinaryArchive> archive = [device newBinaryArchiveWithDescriptor:archiveDescriptor error:&error];
|
||||
if (!archive) {std::printf("FAIL open archive: %s\n", error.localizedDescription.UTF8String);return 1;}
|
||||
descriptor.binaryArchives = @[archive];
|
||||
if (produce) {
|
||||
id state = [device newRenderPipelineStateWithDescriptor:descriptor options:MTLPipelineOptionFailOnBinaryArchiveMiss reflection:nil error:&error];
|
||||
if (state || !error) {std::puts("FAIL empty archive miss was silently compiled");return 1;}
|
||||
if (![archive addRenderPipelineFunctionsWithDescriptor:descriptor error:&error] || ![archive serializeToURL:url error:&error]) {
|
||||
std::printf("FAIL archive add/save: %s\n", error.localizedDescription.UTF8String);return 1;
|
||||
}
|
||||
}
|
||||
error = nil;
|
||||
id<MTLRenderPipelineState> state = [device newRenderPipelineStateWithDescriptor:descriptor options:MTLPipelineOptionFailOnBinaryArchiveMiss reflection:nil error:&error];
|
||||
if (!state || error) {std::printf("FAIL strict archive hit: %s\n", error.localizedDescription.UTF8String);return 1;}
|
||||
// A format not added to the archive must still miss, even after a valid pipeline is live.
|
||||
descriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;
|
||||
if ([device newRenderPipelineStateWithDescriptor:descriptor options:MTLPipelineOptionFailOnBinaryArchiveMiss reflection:nil error:&error] || !error) {
|
||||
std::puts("FAIL changed-descriptor archive miss");return 1;
|
||||
}
|
||||
auto td = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:16 height:16 mipmapped:NO];
|
||||
td.usage = MTLTextureUsageRenderTarget;
|
||||
id<MTLTexture> target = [device newTextureWithDescriptor:td];
|
||||
id<MTLCommandBuffer> cb = [[device newCommandQueue] commandBuffer];
|
||||
if (!target || !cb) return 2;
|
||||
frontendchecks::Report report = {};
|
||||
frontendchecks::gReport = &report;
|
||||
frontendchecks::drawQuad(cb, state, target, nil, frontendchecks::kQuarter, 6);
|
||||
if (!frontendchecks::finish(cb, "archived draw")) return 1;
|
||||
uint8_t pixels[16 * 16 * 4] = {};
|
||||
[target getBytes:pixels bytesPerRow:64 fromRegion:MTLRegionMake2D(0, 0, 16, 16) mipmapLevel:0];
|
||||
if (frontendchecks::wrongTexels(pixels, 64, 16)) {std::puts("FAIL archived render pixels");return 1;}
|
||||
std::printf("PASS: %s render archive, strict hit/miss and GPU pixel check (host only)\n", produce ? "produced" : "reloaded");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Host instrument: the Mac's own QuartzCore renders a layer tree through CARenderer into a texture of the native frontend's device (bridge
|
||||
// over a host Vulkan driver, MoltenVK on the Mac). CoreAnimation's Metal renderer then drives the frontend as the target's render server
|
||||
// will, and the pixels it produced are checked. With NMB_DEVICE_BASE_NSOBJECT=1 every device selector the renderer sends that the frontend
|
||||
// lacks is reported as missing. The Mac's QuartzCore is not the target's: this names gaps and checks results, it qualifies nothing on iPadOS.
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Metal/Metal.h>
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
#import <objc/runtime.h>
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include "../NativeBridgeMetalFrontend.h"
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#import <CoreImage/CoreImage.h>
|
||||
extern "C" size_t native_bridge_metal_telemetry(char *, size_t);
|
||||
|
||||
static std::set<std::string> missing;
|
||||
static void onMissing(const char *cls, const char *sel) {
|
||||
if (missing.insert(std::string(cls) + " " + sel).second) std::fprintf(stderr, "missing selector: %s %s\n", cls, sel);
|
||||
}
|
||||
|
||||
// NMB_DISCOVER=1 (instrument only): an unimplemented selector is logged and answers zero instead of raising, so one run lists every gap on
|
||||
// the renderer's path. Results after a zero answer mean nothing; the frontend itself never answers this way.
|
||||
static const char *protocolTypes(SEL selector) {
|
||||
Protocol *protocols[] = {@protocol(MTLDevice), @protocol(MTLTexture), @protocol(MTLBuffer), @protocol(MTLResource), @protocol(MTLCommandQueue),
|
||||
@protocol(MTLCommandBuffer), @protocol(MTLRenderCommandEncoder), @protocol(MTLBlitCommandEncoder),
|
||||
@protocol(MTLComputeCommandEncoder), @protocol(MTLCommandEncoder), @protocol(MTLLibrary), @protocol(MTLFunction),
|
||||
@protocol(MTLRenderPipelineState), @protocol(MTLComputePipelineState), @protocol(MTLSamplerState),
|
||||
@protocol(MTLDepthStencilState), @protocol(MTLHeap), @protocol(MTLFence), @protocol(MTLEvent), @protocol(MTLSharedEvent)};
|
||||
for (Protocol *p : protocols)
|
||||
for (BOOL required : {YES, NO}) {
|
||||
objc_method_description m = protocol_getMethodDescription(p, selector, required, YES);
|
||||
if (m.types) return m.types;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
static NSMethodSignature *discoverSignature(id self, SEL _cmd, SEL selector) {
|
||||
const char *types = protocolTypes(selector);
|
||||
if (!types) {onMissing(class_getName(object_getClass(self)), (std::string(sel_getName(selector)) + " (no protocol signature)").c_str()); types = "@@:";}
|
||||
return [NSMethodSignature signatureWithObjCTypes:types];
|
||||
}
|
||||
static void discoverForward(id self, SEL _cmd, NSInvocation *invocation) {
|
||||
onMissing(class_getName(object_getClass(self)), sel_getName(invocation.selector));
|
||||
const NSUInteger length = invocation.methodSignature.methodReturnLength;
|
||||
if (length) {std::vector<uint8_t> zero(length); [invocation setReturnValue:zero.data()];}
|
||||
}
|
||||
static void discover(Class cls) {
|
||||
class_addMethod(cls, @selector(methodSignatureForSelector:), (IMP)discoverSignature, "@@::");
|
||||
class_addMethod(cls, @selector(forwardInvocation:), (IMP)discoverForward, "v@:@");
|
||||
}
|
||||
|
||||
static CGColorRef rgba(double r, double g, double b, double a) {
|
||||
CGColorSpaceRef space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
||||
const CGFloat c[4] = {r, g, b, a};
|
||||
CGColorRef color = CGColorCreate(space, c);
|
||||
CGColorSpaceRelease(space);
|
||||
return color;
|
||||
}
|
||||
|
||||
// argv: loader, shader cache. Prints PASS or the first wrong pixel.
|
||||
int main(int argc, char **argv) {
|
||||
@autoreleasepool {
|
||||
if (argc < 3) return 2;
|
||||
// NMB_SYSTEM_DEVICE=1: the Mac's own Metal device, the instrument's baseline.
|
||||
id<MTLDevice> device = getenv("NMB_SYSTEM_DEVICE") ? MTLCreateSystemDefaultDevice() : native_bridge_metal_open(argv[1], argv[2], 0x6e6d62, onMissing);
|
||||
if (!device) {std::printf("FAIL device\n"); return 1;}
|
||||
if (getenv("NMB_DISCOVER")) {discover(object_getClass(device)); discover(objc_getClass("NBObject"));}
|
||||
const NSUInteger side = 64;
|
||||
MTLTextureDescriptor *d = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm width:side height:side mipmapped:NO];
|
||||
d.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead;
|
||||
d.storageMode = MTLStorageModeShared;
|
||||
id<MTLTexture> target = [device newTextureWithDescriptor:d];
|
||||
id<MTLCommandQueue> queue = [device newCommandQueue];
|
||||
if (!target || !queue) {std::printf("FAIL target or queue\n"); return 1;}
|
||||
|
||||
// Blue background; a red square in the top-left quarter; a half-transparent green square in the bottom-right quarter.
|
||||
CALayer *root = [CALayer layer];
|
||||
root.frame = CGRectMake(0, 0, side, side);
|
||||
root.backgroundColor = rgba(0, 0, 1, 1);
|
||||
CALayer *red = [CALayer layer];
|
||||
red.frame = CGRectMake(0, 0, side / 2, side / 2);
|
||||
red.backgroundColor = rgba(1, 0, 0, 1);
|
||||
CALayer *green = [CALayer layer];
|
||||
green.frame = CGRectMake(side / 2, side / 2, side / 2, side / 2);
|
||||
green.backgroundColor = rgba(0, 1, 0, 1);
|
||||
green.opacity = 0.5;
|
||||
[root addSublayer:red];
|
||||
[root addSublayer:green];
|
||||
|
||||
CARenderer *renderer = [CARenderer rendererWithMTLTexture:target options:@{kCARendererMetalCommandQueue: queue}];
|
||||
if (!renderer) {std::printf("FAIL CARenderer refused the frontend's texture\n"); return 1;}
|
||||
renderer.layer = root;
|
||||
renderer.bounds = CGRectMake(0, 0, side, side);
|
||||
// The layers' implicit transaction would commit at the run loop's end; the renderer sees only committed trees.
|
||||
// NMB_BLUR_TIMING=1: also render a blurred, shadowed layer for several frames and print the frontend's creation telemetry (pipeline
|
||||
// counts and times, VkPipeline makings), so the cost of the pipelines CoreAnimation's own renderer asks for can be measured.
|
||||
if (getenv("NMB_BLUR_TIMING")) {
|
||||
CALayer *blurred = [CALayer layer];
|
||||
blurred.frame = CGRectMake(4, 4, side - 8, side - 8);
|
||||
blurred.backgroundColor = rgba(1, 1, 0, 0.7);
|
||||
blurred.cornerRadius = 9;
|
||||
blurred.shadowOpacity = 0.8;
|
||||
blurred.shadowRadius = 5;
|
||||
CIFilter *blur = [CIFilter filterWithName:@"CIGaussianBlur"];
|
||||
[blur setValue:@6 forKey:kCIInputRadiusKey];
|
||||
blurred.filters = @[blur];
|
||||
[root addSublayer:blurred];
|
||||
[CATransaction flush];
|
||||
for (int frame = 0; frame < 3; ++frame) {
|
||||
const uint64_t t0 = clock_gettime_nsec_np(CLOCK_UPTIME_RAW);
|
||||
[renderer beginFrameAtTime:CACurrentMediaTime() timeStamp:NULL];
|
||||
[renderer addUpdateRect:renderer.bounds];
|
||||
[renderer render];
|
||||
[renderer endFrame];
|
||||
id<MTLCommandBuffer> fence = [queue commandBuffer];
|
||||
[fence commit];
|
||||
[fence waitUntilCompleted];
|
||||
std::printf("BLUR frame %d %.1f ms\n", frame, (clock_gettime_nsec_np(CLOCK_UPTIME_RAW) - t0) / 1e6);
|
||||
}
|
||||
static char table[16384];
|
||||
native_bridge_metal_telemetry(table, sizeof table);
|
||||
for (char *line = std::strtok(table, "\n"); line; line = std::strtok(nullptr, "\n"))
|
||||
if (line[0] == 'T' || line[0] == 'V' || line[0] == 'S' || line[0] == 'W') std::printf("TELEMETRY %s\n", line);
|
||||
[blurred removeFromSuperlayer];
|
||||
[CATransaction flush];
|
||||
}
|
||||
[renderer beginFrameAtTime:CACurrentMediaTime() timeStamp:NULL];
|
||||
[renderer addUpdateRect:renderer.bounds];
|
||||
[renderer render];
|
||||
[renderer endFrame];
|
||||
// CARenderer commits its own command buffers; one more on the same queue completes after them.
|
||||
id<MTLCommandBuffer> last = [queue commandBuffer];
|
||||
[last commit];
|
||||
[last waitUntilCompleted];
|
||||
|
||||
std::vector<uint8_t> pixels(side * side * 4);
|
||||
[target getBytes:pixels.data() bytesPerRow:side * 4 fromRegion:MTLRegionMake2D(0, 0, side, side) mipmapLevel:0];
|
||||
// BGRA. Row 0 of the texture is the layer tree's y 0 (the Mac's Metal device renders it so, NMB_SYSTEM_DEVICE=1): red top-left,
|
||||
// green bottom-right.
|
||||
auto at = [&](NSUInteger x, NSUInteger y) {return &pixels[(y * side + x) * 4];};
|
||||
struct Probe {NSUInteger x, y; int b, g, r;} probes[] = {
|
||||
{8, 8, 0, 0, 255}, // red quarter
|
||||
{side - 8, side - 8, 128, 128, 0}, // green at 0.5 over blue: (0, 0.5, 0.5)
|
||||
{side - 8, 8, 255, 0, 0}, // background
|
||||
{8, side - 8, 255, 0, 0}, // background
|
||||
};
|
||||
bool ok = true;
|
||||
for (auto &p : probes) {
|
||||
const uint8_t *px = at(p.x, p.y);
|
||||
if (std::abs(px[0] - p.b) > 3 || std::abs(px[1] - p.g) > 3 || std::abs(px[2] - p.r) > 3 || px[3] != 255) {
|
||||
std::printf("FAIL pixel (%lu,%lu) = b%u g%u r%u a%u, want b%d g%d r%d\n", (unsigned long)p.x, (unsigned long)p.y, px[0], px[1], px[2], px[3],
|
||||
p.b, p.g, p.r);
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
std::printf("missing selectors: %zu\n", missing.size());
|
||||
if (!ok) return 1;
|
||||
std::printf("PASS: the Mac's CARenderer rendered a layer tree on %s\n", device.name.UTF8String);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Separate processes/cache directories distinguish archive consumption from live memo hits.
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Metal/Metal.h>
|
||||
#include "NativeMetalObjects.h"
|
||||
#include "../NativeBridgeMetalFrontend.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#define REQUIRE(condition) do {if (!(condition)) {std::printf("FAIL line %d: %s; %s\n", __LINE__, #condition, error.localizedDescription.UTF8String ?: "");return 1;}} while (0)
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 5) return 2;
|
||||
@autoreleasepool {
|
||||
NSError *error = nil;
|
||||
const bool produce = !strcmp(argv[4], "produce");
|
||||
id<MTLDevice> device = native_bridge_metal_open(argv[1], argv[2], 0x6e6d62, nullptr);
|
||||
REQUIRE(device);
|
||||
size_t bytes = 0;
|
||||
const void *air = nmb_function_constant_air(&bytes);
|
||||
id<MTLLibrary> library = native_bridge_metal_library_with_air(air, bytes);
|
||||
REQUIRE(library);
|
||||
auto constants = [MTLFunctionConstantValues new];
|
||||
int mode = 2;
|
||||
[constants setConstantValue:&mode type:MTLDataTypeInt atIndex:0];
|
||||
id<MTLFunction> function = [library newFunctionWithName:@"fcfold" constantValues:constants error:&error];
|
||||
REQUIRE(function && !error);
|
||||
REQUIRE([device respondsToSelector:@selector(newComputePipelineStateWithDescriptor:options:reflection:error:)]);
|
||||
auto descriptor = [MTLComputePipelineDescriptor new];
|
||||
descriptor.computeFunction = function;
|
||||
descriptor.maxTotalThreadsPerThreadgroup = 32;
|
||||
descriptor.buffers[1].mutability = MTLMutabilityMutable;
|
||||
descriptor.label = @"archived compute";
|
||||
auto archiveDescriptor = [MTLBinaryArchiveDescriptor new];
|
||||
NSURL *url = [NSURL fileURLWithPath:@(argv[3])];
|
||||
if (!produce) archiveDescriptor.url = url;
|
||||
id<MTLBinaryArchive> archive = [device newBinaryArchiveWithDescriptor:archiveDescriptor error:&error];
|
||||
REQUIRE(archive && !error);
|
||||
descriptor.binaryArchives = @[archive];
|
||||
if (produce) {
|
||||
id<MTLComputePipelineState> miss = [device newComputePipelineStateWithDescriptor:descriptor options:MTLPipelineOptionFailOnBinaryArchiveMiss reflection:nil error:&error];
|
||||
if (miss || !error) {
|
||||
std::printf("FAIL strict empty archive: state=%s error=%s\n", miss ? "created" : "nil", error.localizedDescription.UTF8String ?: "nil");
|
||||
return 1;
|
||||
}
|
||||
REQUIRE([archive addComputePipelineFunctionsWithDescriptor:descriptor error:&error] && !error);
|
||||
REQUIRE([archive serializeToURL:url error:&error] && !error);
|
||||
}
|
||||
id<MTLComputePipelineState> state = [device newComputePipelineStateWithDescriptor:descriptor options:MTLPipelineOptionFailOnBinaryArchiveMiss reflection:nil error:&error];
|
||||
REQUIRE(state && !error && state.maxTotalThreadsPerThreadgroup == 32);
|
||||
REQUIRE([state.label isEqualToString:descriptor.label]);
|
||||
// Both descriptor fields and function constants must retain their archive identity.
|
||||
descriptor.maxTotalThreadsPerThreadgroup = 16;
|
||||
REQUIRE(![device newComputePipelineStateWithDescriptor:descriptor options:MTLPipelineOptionFailOnBinaryArchiveMiss reflection:nil error:&error] && error);
|
||||
descriptor.maxTotalThreadsPerThreadgroup = 32;
|
||||
descriptor.buffers[1].mutability = MTLMutabilityDefault;
|
||||
REQUIRE(![device newComputePipelineStateWithDescriptor:descriptor options:MTLPipelineOptionFailOnBinaryArchiveMiss reflection:nil error:&error] && error);
|
||||
descriptor.buffers[1].mutability = MTLMutabilityMutable;
|
||||
mode = 3;
|
||||
[constants setConstantValue:&mode type:MTLDataTypeInt atIndex:0];
|
||||
descriptor.computeFunction = [library newFunctionWithName:@"fcfold" constantValues:constants error:&error];
|
||||
REQUIRE(descriptor.computeFunction && !error);
|
||||
REQUIRE(![device newComputePipelineStateWithDescriptor:descriptor options:MTLPipelineOptionFailOnBinaryArchiveMiss reflection:nil error:&error] && error);
|
||||
descriptor.computeFunction = function;
|
||||
descriptor.stageInputDescriptor = [MTLStageInputOutputDescriptor stageInputOutputDescriptor];
|
||||
descriptor.stageInputDescriptor.attributes[0].format = MTLAttributeFormatFloat;
|
||||
// Unsupported descriptor state must be refused by both population and creation.
|
||||
REQUIRE(![archive addComputePipelineFunctionsWithDescriptor:descriptor error:&error] && error);
|
||||
REQUIRE(![device newComputePipelineStateWithDescriptor:descriptor options:MTLPipelineOptionNone reflection:nil error:&error] && error);
|
||||
descriptor.stageInputDescriptor = nil;
|
||||
const float inputValue = 2;
|
||||
id<MTLBuffer> input = [device newBufferWithBytes:&inputValue length:4 options:MTLResourceStorageModeShared];
|
||||
id<MTLBuffer> output = [device newBufferWithLength:128 options:MTLResourceStorageModeShared];
|
||||
id<MTLCommandBuffer> command = [[device newCommandQueue] commandBuffer];
|
||||
id<MTLComputeCommandEncoder> encoder = [command computeCommandEncoder];
|
||||
REQUIRE(input && output && encoder);
|
||||
[encoder setComputePipelineState:state];
|
||||
[encoder setBuffer:input offset:0 atIndex:0];
|
||||
[encoder setBuffer:output offset:0 atIndex:1];
|
||||
[encoder dispatchThreads:MTLSizeMake(1, 1, 1) threadsPerThreadgroup:MTLSizeMake(1, 1, 1)];
|
||||
[encoder endEncoding];
|
||||
[command commit];
|
||||
[command waitUntilCompleted];
|
||||
REQUIRE(command.status == MTLCommandBufferStatusCompleted);
|
||||
REQUIRE(((const float *)output.contents)[24] == 4000);
|
||||
// Check the no-list behavior only after the cold archive consumer and GPU verdict.
|
||||
REQUIRE([device newComputePipelineStateWithFunction:function options:MTLPipelineOptionFailOnBinaryArchiveMiss reflection:nil error:&error] && !error);
|
||||
std::printf("PASS: %s compute archive, strict descriptor misses and host GPU output\n", produce ? "produced" : "reloaded");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// The default compute-function path used by the target; no explicit binary archive.
|
||||
// Runs over Turnip and the software kernel peer: no shader output is claimed.
|
||||
#include "NativeMetalObjects.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
|
||||
#define REQUIRE(x) do {if (!(x)) {std::fprintf(stderr,"FAIL line %d: %s; %s\n",__LINE__,#x,nmb_last_error());return 1;}} while (0)
|
||||
|
||||
int main(int argc,char **argv) {
|
||||
if(argc!=4)return 2;
|
||||
const bool consume=!std::strcmp(argv[3],"consume");
|
||||
REQUIRE(consume||!std::strcmp(argv[3],"produce"));
|
||||
auto submissions=(unsigned(*)())dlsym(RTLD_DEFAULT,"host_gpu_submissions");REQUIRE(submissions);
|
||||
NmbDevice *device=nmb_device_open(argv[1],argv[2]);REQUIRE(device);
|
||||
NmbShaderCacheStats before={},after={};REQUIRE(nmb_device_shader_cache_stats(device,&before));
|
||||
size_t size=0;const void *air=nmb_fixture_air(&size);
|
||||
NmbLibrary *library=nmb_library_new(device,air,size,true);REQUIRE(library);
|
||||
NmbFunction *function=nmb_function_new(library,"scale_and_offset");REQUIRE(function);
|
||||
NmbComputePipeline *pipeline=nmb_compute_pipeline_new(device,function);REQUIRE(pipeline);
|
||||
float input[64]={};
|
||||
NmbBuffer *source=nmb_buffer_new(device,sizeof input,0,input);
|
||||
NmbBuffer *destination=nmb_buffer_new(device,sizeof input,0,nullptr);REQUIRE(source&&destination);
|
||||
NmbQueue *queue=nmb_queue_new(device);REQUIRE(queue);
|
||||
NmbCommandBuffer *command=nmb_command_buffer_new(queue);REQUIRE(command);
|
||||
NmbEncoder *encoder=nmb_compute_encoder_new(command);REQUIRE(encoder);
|
||||
REQUIRE(nmb_compute_set_pipeline(encoder,pipeline));
|
||||
REQUIRE(nmb_encoder_set_buffer(encoder,0,source,0,0));
|
||||
REQUIRE(nmb_encoder_set_buffer(encoder,0,destination,0,1));
|
||||
const float scale=2;REQUIRE(nmb_encoder_set_bytes(encoder,0,&scale,sizeof scale,2));
|
||||
const uint64_t threads[]={64,1,1};
|
||||
REQUIRE(nmb_compute_dispatch(encoder,threads,threads,true));
|
||||
REQUIRE(nmb_encoder_end(encoder));
|
||||
const unsigned submitted=submissions();
|
||||
REQUIRE(nmb_command_buffer_commit(command));
|
||||
REQUIRE(nmb_command_buffer_wait(command,10000)==0);
|
||||
REQUIRE(nmb_command_buffer_status(command)==4&&submissions()>submitted);
|
||||
REQUIRE(nmb_device_shader_cache_stats(device,&after));
|
||||
std::printf("cache %s: artifacts hit=%llu miss=%llu write=%llu; driver load=%llu hit=%llu miss=%llu save=%llu same=%llu; rejected=%llu readfail=%llu writefail=%llu truncated=%llu mismatched=%llu\n",argv[3],
|
||||
(unsigned long long)(after.hits-before.hits),(unsigned long long)(after.misses-before.misses),(unsigned long long)(after.writes-before.writes),
|
||||
(unsigned long long)(after.driver_loads-before.driver_loads),(unsigned long long)(after.driver_hits-before.driver_hits),
|
||||
(unsigned long long)(after.driver_misses-before.driver_misses),(unsigned long long)(after.driver_saves-before.driver_saves),
|
||||
(unsigned long long)(after.driver_unchanged-before.driver_unchanged),(unsigned long long)after.rejected,(unsigned long long)after.read_failures,
|
||||
(unsigned long long)after.write_failures,(unsigned long long)after.driver_truncated,(unsigned long long)after.driver_mismatched);
|
||||
if(consume){
|
||||
REQUIRE(after.hits>before.hits&&after.misses==before.misses);
|
||||
REQUIRE(after.driver_loads>before.driver_loads&&after.driver_hits>before.driver_hits);
|
||||
REQUIRE(after.driver_misses==before.driver_misses);
|
||||
}else{
|
||||
REQUIRE(after.misses>before.misses&&after.writes>before.writes);
|
||||
REQUIRE(after.driver_misses>before.driver_misses&&after.driver_saves>before.driver_saves);
|
||||
}
|
||||
REQUIRE(!after.read_failures&&!after.write_failures&&!after.rejected&&!after.driver_truncated&&!after.driver_mismatched);
|
||||
nmb_command_buffer_release(command);nmb_queue_release(queue);
|
||||
nmb_buffer_release(destination);nmb_buffer_release(source);nmb_compute_pipeline_release(pipeline);
|
||||
nmb_function_release(function);nmb_library_release(library);nmb_device_release(device);
|
||||
std::printf("PASS: default compute %s uses separate artifact/Turnip caches; software transport, results unchecked\n",argv[3]);
|
||||
return 0;
|
||||
}
|
||||
@@ -23,7 +23,7 @@ int main(int argc, char **argv) {
|
||||
if (!library || !fcLibrary) {std::printf("FAIL fixture libraries\n"); return 1;}
|
||||
frontendchecks::Report r = {};
|
||||
frontendchecks::run(device, library, fcLibrary, argc > 3 && argv[3][0] ? @(argv[3]) : nil, &r);
|
||||
if (r.qcFunctions) std::printf("QuartzCore: %u functions, kernels %u/%u, pairs %u/%u\n", r.qcFunctions, r.qcKernelsOk, r.qcKernels, r.qcPairsOk, r.qcPairs);
|
||||
if (r.qcFunctions) std::printf("QuartzCore bare-descriptor probe: %u functions, kernels %u/%u, pairs %u/%u (not complete shader coverage)\n", r.qcFunctions, r.qcKernelsOk, r.qcKernels, r.qcPairsOk, r.qcPairs);
|
||||
for (auto &m : missing) std::printf("missing selector: %s\n", m.c_str());
|
||||
if (r.failed) {std::printf("FAILED %u checks, first: %s\n", r.failed, r.first); return 1;}
|
||||
std::printf("PASS: native Metal frontend over the bridge on %s: compute, render, IOSurface render, surface sampling, texture compute, "
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
/* Offline qualification only: forward to the actual Vulkan compiler, enabling
|
||||
* its reported cache-control feature and optionally forbidding compilation.
|
||||
* A negative control corrupts only a private copy of a cache UUID. Never deploy. */
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
static PFN_vkGetInstanceProcAddr realInstance;
|
||||
static std::atomic<PFN_vkGetDeviceProcAddr> realDevice;
|
||||
static std::once_flag initialized;
|
||||
static std::atomic<VkInstance> activeInstance;
|
||||
static std::atomic<bool> strict, badUuid;
|
||||
// calls, success, compile-required, valid feedback, whole-cache hit, UUID mutations, devices,
|
||||
// calls with an application cache, their valid feedback and their cache hits
|
||||
static std::atomic<uint64_t> counts[10];
|
||||
static void initialize() {
|
||||
std::call_once(initialized,[]{
|
||||
const char *path=std::getenv("NMB_TEST_REAL_VULKAN_LOADER");
|
||||
void *library=path?dlopen(path,RTLD_NOW|RTLD_LOCAL):nullptr;
|
||||
if(!library)std::abort();
|
||||
realInstance=reinterpret_cast<PFN_vkGetInstanceProcAddr>(dlsym(library,"vkGetInstanceProcAddr"));
|
||||
if(!realInstance)realInstance=reinterpret_cast<PFN_vkGetInstanceProcAddr>(dlsym(library,"vk_icdGetInstanceProcAddr"));
|
||||
if(!realInstance)std::abort();
|
||||
});
|
||||
}
|
||||
static VKAPI_ATTR VkResult VKAPI_CALL createDevice(VkPhysicalDevice physical,const VkDeviceCreateInfo *info,
|
||||
const VkAllocationCallbacks *allocator,VkDevice *device) {
|
||||
initialize();
|
||||
// The qualified runtime requests Vulkan 1.2. Refuse an unexpected chain
|
||||
// rather than duplicate a promoted feature or alter caller-owned structures.
|
||||
for(auto p=static_cast<const VkBaseInStructure *>(info->pNext);p;p=p->pNext)
|
||||
if(p->sType==VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES ||
|
||||
p->sType==VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_CREATION_CACHE_CONTROL_FEATURES)return VK_ERROR_FEATURE_NOT_PRESENT;
|
||||
auto query=reinterpret_cast<PFN_vkGetPhysicalDeviceFeatures2>(realInstance(activeInstance.load(),"vkGetPhysicalDeviceFeatures2"));
|
||||
if(!query)return VK_ERROR_FEATURE_NOT_PRESENT;
|
||||
VkPhysicalDevicePipelineCreationCacheControlFeatures supported={};supported.sType=VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PIPELINE_CREATION_CACHE_CONTROL_FEATURES;
|
||||
VkPhysicalDeviceFeatures2 features={};features.sType=VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;features.pNext=&supported;
|
||||
query(physical,&features);if(!supported.pipelineCreationCacheControl)return VK_ERROR_FEATURE_NOT_PRESENT;
|
||||
std::vector<const char *> extensions;
|
||||
if(info->enabledExtensionCount)extensions.assign(info->ppEnabledExtensionNames,info->ppEnabledExtensionNames+info->enabledExtensionCount);
|
||||
bool listed=false;for(auto name:extensions)listed|=!std::strcmp(name,VK_EXT_PIPELINE_CREATION_CACHE_CONTROL_EXTENSION_NAME);
|
||||
if(!listed)extensions.push_back(VK_EXT_PIPELINE_CREATION_CACHE_CONTROL_EXTENSION_NAME);
|
||||
supported.pNext=const_cast<void *>(info->pNext);
|
||||
VkDeviceCreateInfo copy=*info;copy.pNext=&supported;copy.enabledExtensionCount=(uint32_t)extensions.size();copy.ppEnabledExtensionNames=extensions.data();
|
||||
auto real=reinterpret_cast<PFN_vkCreateDevice>(realInstance(activeInstance.load(),"vkCreateDevice"));
|
||||
if(!real)return VK_ERROR_INITIALIZATION_FAILED;
|
||||
const auto result=real(physical,©,allocator,device);if(result==VK_SUCCESS)counts[6]++;
|
||||
return result;
|
||||
}
|
||||
static VKAPI_ATTR VkResult VKAPI_CALL createCache(VkDevice device,const VkPipelineCacheCreateInfo *info,
|
||||
const VkAllocationCallbacks *allocator,VkPipelineCache *cache) {
|
||||
VkPipelineCacheCreateInfo copy=*info;std::vector<unsigned char> bytes;
|
||||
if(badUuid.load() && info->initialDataSize) {
|
||||
if(info->initialDataSize<32)return VK_ERROR_INITIALIZATION_FAILED;
|
||||
const auto *source=static_cast<const unsigned char *>(info->pInitialData);
|
||||
bytes.assign(source,source+info->initialDataSize);bytes[16]^=1;copy.pInitialData=bytes.data();counts[5]++;
|
||||
}
|
||||
auto real=reinterpret_cast<PFN_vkCreatePipelineCache>(realDevice.load()(device,"vkCreatePipelineCache"));
|
||||
return real(device,©,allocator,cache);
|
||||
}
|
||||
static VKAPI_ATTR VkResult VKAPI_CALL createGraphics(VkDevice device,VkPipelineCache cache,uint32_t count,
|
||||
const VkGraphicsPipelineCreateInfo *infos,const VkAllocationCallbacks *allocator,VkPipeline *pipelines) {
|
||||
if(!count || count>64)return VK_ERROR_INITIALIZATION_FAILED;
|
||||
std::vector<VkGraphicsPipelineCreateInfo> copies(infos,infos+count);
|
||||
std::vector<VkPipelineCreationFeedback> feedback(count);
|
||||
std::vector<VkPipelineCreationFeedbackCreateInfo> additions(count);
|
||||
std::vector<const VkPipelineCreationFeedback *> observed(count);
|
||||
for(uint32_t i=0;i<count;++i) {
|
||||
if(strict.load())copies[i].flags|=VK_PIPELINE_CREATE_FAIL_ON_PIPELINE_COMPILE_REQUIRED_BIT;
|
||||
for(auto p=static_cast<const VkBaseInStructure *>(copies[i].pNext);p;p=p->pNext)
|
||||
if(p->sType==VK_STRUCTURE_TYPE_PIPELINE_CREATION_FEEDBACK_CREATE_INFO)
|
||||
observed[i]=reinterpret_cast<const VkPipelineCreationFeedbackCreateInfo *>(p)->pPipelineCreationFeedback;
|
||||
if(!observed[i]) {
|
||||
auto &a=additions[i];a.sType=VK_STRUCTURE_TYPE_PIPELINE_CREATION_FEEDBACK_CREATE_INFO;
|
||||
a.pNext=copies[i].pNext;a.pPipelineCreationFeedback=&feedback[i];copies[i].pNext=&a;observed[i]=&feedback[i];
|
||||
}
|
||||
}
|
||||
auto real=reinterpret_cast<PFN_vkCreateGraphicsPipelines>(realDevice.load()(device,"vkCreateGraphicsPipelines"));
|
||||
counts[0]+=count;if(cache)counts[7]+=count;const auto result=real(device,cache,count,copies.data(),allocator,pipelines);
|
||||
if(result==VK_PIPELINE_COMPILE_REQUIRED)counts[2]++;
|
||||
for(uint32_t i=0;i<count;++i)if(pipelines[i]) {
|
||||
counts[1]++;
|
||||
if(observed[i] && (observed[i]->flags&VK_PIPELINE_CREATION_FEEDBACK_VALID_BIT)) {
|
||||
counts[3]++;if(cache)counts[8]++;
|
||||
if(observed[i]->flags&VK_PIPELINE_CREATION_FEEDBACK_APPLICATION_PIPELINE_CACHE_HIT_BIT){counts[4]++;if(cache)counts[9]++;}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
extern "C" VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL vkGetDeviceProcAddr(VkDevice device,const char *name) {
|
||||
initialize();
|
||||
if(!std::strcmp(name,"vkCreateGraphicsPipelines"))return reinterpret_cast<PFN_vkVoidFunction>(createGraphics);
|
||||
if(!std::strcmp(name,"vkCreatePipelineCache"))return reinterpret_cast<PFN_vkVoidFunction>(createCache);
|
||||
return realDevice.load()(device,name);
|
||||
}
|
||||
extern "C" VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL vkGetInstanceProcAddr(VkInstance instance,const char *name) {
|
||||
initialize();
|
||||
if(instance){
|
||||
activeInstance=instance;
|
||||
realDevice=reinterpret_cast<PFN_vkGetDeviceProcAddr>(realInstance(instance,"vkGetDeviceProcAddr"));
|
||||
if(!realDevice.load())std::abort();
|
||||
}
|
||||
if(!std::strcmp(name,"vkGetDeviceProcAddr"))return reinterpret_cast<PFN_vkVoidFunction>(vkGetDeviceProcAddr);
|
||||
if(!std::strcmp(name,"vkCreateDevice"))return reinterpret_cast<PFN_vkVoidFunction>(createDevice);
|
||||
if(!std::strcmp(name,"vkCreateGraphicsPipelines"))return reinterpret_cast<PFN_vkVoidFunction>(createGraphics);
|
||||
if(!std::strcmp(name,"vkCreatePipelineCache"))return reinterpret_cast<PFN_vkVoidFunction>(createCache);
|
||||
return realInstance(instance,name);
|
||||
}
|
||||
extern "C" void nmb_test_graphics_cache_control(bool forbidCompilation,bool corruptUuid) {strict=forbidCompilation;badUuid=corruptUuid;}
|
||||
extern "C" void nmb_test_graphics_cache_counts(uint64_t out[10]) {for(unsigned i=0;i<10;++i)out[i]=counts[i].load();}
|
||||
@@ -0,0 +1,60 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
/* Host-test Vulkan forwarding loader. Every request reaches the real driver;
|
||||
* count successful external-host-memory imports without changing any request,
|
||||
* code, driver identity, memory contents, fence or GPU result. Never deployed. */
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
static PFN_vkGetInstanceProcAddr realInstance;
|
||||
static PFN_vkGetDeviceProcAddr realDevice;
|
||||
static std::once_flag initialized;
|
||||
static std::atomic<uint64_t> imported;
|
||||
static std::atomic<unsigned> importDelayMs;
|
||||
static void initialize() {
|
||||
std::call_once(initialized,[]{
|
||||
const char *path=std::getenv("NMB_TEST_REAL_VULKAN_LOADER");
|
||||
void *library=path?dlopen(path,RTLD_NOW|RTLD_LOCAL):nullptr;
|
||||
if(!library)std::abort();
|
||||
realInstance=reinterpret_cast<PFN_vkGetInstanceProcAddr>(dlsym(library,"vkGetInstanceProcAddr"));
|
||||
realDevice=reinterpret_cast<PFN_vkGetDeviceProcAddr>(dlsym(library,"vkGetDeviceProcAddr"));
|
||||
if(!realInstance || !realDevice)std::abort();
|
||||
});
|
||||
}
|
||||
static VKAPI_ATTR VkResult VKAPI_CALL allocate(VkDevice device,const VkMemoryAllocateInfo *info,
|
||||
const VkAllocationCallbacks *allocator,VkDeviceMemory *memory) {
|
||||
initialize();
|
||||
bool hostImport=false;
|
||||
for(auto node=static_cast<const VkBaseInStructure *>(info->pNext);node;node=node->pNext)
|
||||
if(node->sType==VK_STRUCTURE_TYPE_IMPORT_MEMORY_HOST_POINTER_INFO_EXT){hostImport=true;break;}
|
||||
// A bounded, one-shot delay makes a concurrent first-lookup miss
|
||||
// reproducible. The actual allocation and its result are unchanged.
|
||||
if(hostImport) {
|
||||
const unsigned delay=importDelayMs.exchange(0);
|
||||
if(delay)std::this_thread::sleep_for(std::chrono::milliseconds(delay));
|
||||
}
|
||||
auto real=reinterpret_cast<PFN_vkAllocateMemory>(realDevice(device,"vkAllocateMemory"));
|
||||
const VkResult result=real(device,info,allocator,memory);
|
||||
if(result==VK_SUCCESS && hostImport)imported++;
|
||||
return result;
|
||||
}
|
||||
extern "C" VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL vkGetDeviceProcAddr(VkDevice device,const char *name) {
|
||||
initialize();
|
||||
if(!std::strcmp(name,"vkAllocateMemory"))return reinterpret_cast<PFN_vkVoidFunction>(allocate);
|
||||
return realDevice(device,name);
|
||||
}
|
||||
extern "C" VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL vkGetInstanceProcAddr(VkInstance instance,const char *name) {
|
||||
initialize();
|
||||
if(!std::strcmp(name,"vkGetDeviceProcAddr"))return reinterpret_cast<PFN_vkVoidFunction>(vkGetDeviceProcAddr);
|
||||
if(!std::strcmp(name,"vkAllocateMemory"))return reinterpret_cast<PFN_vkVoidFunction>(allocate);
|
||||
return realInstance(instance,name);
|
||||
}
|
||||
extern "C" uint64_t nmb_test_vk_host_import_count() {return imported.load();}
|
||||
extern "C" void nmb_test_vk_host_import_delay(unsigned milliseconds) {
|
||||
importDelayMs.store(milliseconds<=100?milliseconds:100);
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Actual public Metal no-copy calls through the native bridge. Every buffer
|
||||
// remains a distinct owner; count real Vulkan imports and check real GPU bytes.
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Metal/Metal.h>
|
||||
#import <IOSurface/IOSurfaceRef.h>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <sys/mman.h>
|
||||
#include <thread>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
#include "../NativeBridgeMetalFrontend.h"
|
||||
|
||||
constexpr unsigned workers=16;
|
||||
struct State {std::array<std::atomic<unsigned>,workers> released{};std::atomic<unsigned> errors{0};};
|
||||
|
||||
static bool creation(id<MTLDevice> device,bool mixed,uint64_t (*imports)(),void (*delay)(unsigned)) {
|
||||
auto state=std::make_shared<State>();bool gpu=true;
|
||||
void *mapping=nullptr;size_t bytes=0;IOSurfaceRef surface=nullptr;
|
||||
uint64_t allocated=0;
|
||||
@autoreleasepool {
|
||||
if(mixed) {
|
||||
surface=IOSurfaceCreate((__bridge CFDictionaryRef)@{(id)kIOSurfaceWidth:@64,(id)kIOSurfaceHeight:@16,
|
||||
(id)kIOSurfaceBytesPerElement:@4,(id)kIOSurfaceBytesPerRow:@256,(id)kIOSurfacePixelFormat:@((uint32_t)0x52474241)});
|
||||
if(!surface)return false;
|
||||
mapping=IOSurfaceGetBaseAddress(surface);bytes=IOSurfaceGetAllocSize(surface);
|
||||
} else {
|
||||
bytes=(size_t)getpagesize();mapping=mmap(nullptr,bytes,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANON,-1,0);
|
||||
if(mapping==MAP_FAILED)return false;
|
||||
}
|
||||
if(!mapping || bytes<4096)return false;
|
||||
memset(mapping,0xcc,bytes);
|
||||
auto buffers=[NSMutableArray array],textures=[NSMutableArray array];
|
||||
std::mutex startLock;std::condition_variable ready;unsigned waiting=0;bool start=false;
|
||||
std::vector<std::thread> threads;
|
||||
const uint64_t before=imports();delay(25);
|
||||
for(unsigned i=0;i<workers;++i)threads.emplace_back([&,i]{
|
||||
{std::unique_lock<std::mutex> lock(startLock);++waiting;ready.notify_all();ready.wait(lock,[&]{return start;});}
|
||||
@autoreleasepool {
|
||||
if(mixed && (i&1)) {
|
||||
auto d=[MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:64 height:16 mipmapped:NO];
|
||||
d.storageMode=MTLStorageModeShared;d.usage=MTLTextureUsageShaderRead|MTLTextureUsageShaderWrite;
|
||||
id<MTLTexture> texture=[device newTextureWithDescriptor:d iosurface:surface plane:0];
|
||||
if(!texture){state->errors++;return;}
|
||||
@synchronized(textures){[textures addObject:texture];}
|
||||
} else {
|
||||
auto owner=state;const unsigned ordinal=i;
|
||||
id<MTLBuffer> buffer=[device newBufferWithBytesNoCopy:mapping length:bytes options:MTLResourceStorageModeShared
|
||||
deallocator:^(void *,NSUInteger){owner->released[ordinal]++;}];
|
||||
if(!buffer){state->errors++;return;}
|
||||
@synchronized(buffers){[buffers addObject:buffer];}
|
||||
}
|
||||
}
|
||||
});
|
||||
{std::unique_lock<std::mutex> lock(startLock);ready.wait(lock,[&]{return waiting==workers;});start=true;ready.notify_all();}
|
||||
for(auto &thread:threads)thread.join();
|
||||
allocated=imports()-before;
|
||||
const unsigned expected=mixed?workers/2:workers;
|
||||
if(state->errors || buffers.count!=expected || textures.count!=(mixed?workers/2:0))return false;
|
||||
@autoreleasepool {
|
||||
// The API still returns distinct buffer objects and independent
|
||||
// callbacks; only the driver's import is shared.
|
||||
if([NSSet setWithArray:buffers].count!=expected)return false;
|
||||
}
|
||||
id<MTLCommandQueue> queue=[device newCommandQueue];
|
||||
id<MTLCommandBuffer> write=[queue commandBuffer];id<MTLBlitCommandEncoder> fill=[write blitCommandEncoder];
|
||||
[fill fillBuffer:buffers[0] range:NSMakeRange(0,4096) value:0x5a];[fill endEncoding];[write commit];[write waitUntilCompleted];
|
||||
// Use an explicit completed producer, so correctness here does not
|
||||
// assume implicit synchronization of arbitrary no-copy Metal aliases.
|
||||
id<MTLBuffer> output=[device newBufferWithLength:workers*4096 options:MTLResourceStorageModeShared];
|
||||
id<MTLCommandBuffer> read=[queue commandBuffer];id<MTLBlitCommandEncoder> copy=[read blitCommandEncoder];
|
||||
if(!queue || !write || !fill || !output || !read || !copy)return false;
|
||||
NSUInteger at=0;
|
||||
for(id<MTLBuffer> buffer in buffers){[copy copyFromBuffer:buffer sourceOffset:0 toBuffer:output destinationOffset:at size:4096];at+=4096;}
|
||||
for(id<MTLTexture> texture in textures){
|
||||
[copy copyFromTexture:texture sourceSlice:0 sourceLevel:0 sourceOrigin:MTLOriginMake(0,0,0) sourceSize:MTLSizeMake(64,16,1)
|
||||
toBuffer:output destinationOffset:at destinationBytesPerRow:256 destinationBytesPerImage:4096];at+=4096;
|
||||
}
|
||||
[copy endEncoding];[read commit];[read waitUntilCompleted];
|
||||
gpu=write.status==MTLCommandBufferStatusCompleted && read.status==MTLCommandBufferStatusCompleted && at==workers*4096;
|
||||
for(unsigned i=0;i<workers*4096;++i)if(((const unsigned char *)output.contents)[i]!=0x5a){gpu=false;break;}
|
||||
}
|
||||
bool owners=false;
|
||||
for(unsigned attempt=0;attempt<300;++attempt) {
|
||||
owners=true;
|
||||
for(unsigned i=0;i<workers;++i)if(state->released[i].load()!=(mixed&&(i&1)?0u:1u))owners=false;
|
||||
if(owners)break;usleep(1000);
|
||||
}
|
||||
if(surface)CFRelease(surface);else munmap(mapping,bytes);
|
||||
std::printf("mode=%s buffers=%u vk_imports=%llu gpu=%s independent_owners=%s\n",mixed?"mixed":"raw",mixed?workers/2:workers,
|
||||
(unsigned long long)allocated,gpu?"pass":"FAIL",owners?"pass":"FAIL");
|
||||
return allocated==1 && gpu && owners;
|
||||
}
|
||||
|
||||
static bool reentry(id<MTLDevice> device) {
|
||||
const size_t page=(size_t)getpagesize();void *mapping=mmap(nullptr,page*2,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANON,-1,0);
|
||||
if(mapping==MAP_FAILED)return false;
|
||||
auto finished=dispatch_semaphore_create(0);auto state=std::make_shared<State>();
|
||||
@autoreleasepool {
|
||||
id<MTLBuffer> outer=[device newBufferWithBytesNoCopy:mapping length:page options:MTLResourceStorageModeShared
|
||||
deallocator:^(void *,NSUInteger){
|
||||
state->released[0]++;
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT,0),^{
|
||||
@autoreleasepool {
|
||||
id<MTLBuffer> nested=[device newBufferWithBytesNoCopy:(char *)mapping+page length:page options:MTLResourceStorageModeShared
|
||||
deallocator:^(void *,NSUInteger){state->released[1]++;}];
|
||||
if(!nested)state->errors++;
|
||||
}
|
||||
dispatch_semaphore_signal(finished);
|
||||
});
|
||||
if(dispatch_semaphore_wait(finished,dispatch_time(DISPATCH_TIME_NOW,3*NSEC_PER_SEC)))state->errors++;
|
||||
}];
|
||||
if(!outer)return false;
|
||||
}
|
||||
const bool ok=!state->errors && state->released[0]==1 && state->released[1]==1;
|
||||
// On a timeout keep the test-owned mapping alive until process exit;
|
||||
// never turn a lifetime failure into a submitted dangling pointer.
|
||||
if(ok)munmap(mapping,page*2);
|
||||
std::printf("deallocator cross-thread reentry=%s\n",ok?"pass":"FAIL");return ok;
|
||||
}
|
||||
|
||||
int main(int argc,char **argv) {
|
||||
if(argc!=4)return 2;
|
||||
@autoreleasepool {
|
||||
id<MTLDevice> device=native_bridge_metal_open(argv[1],argv[2],0x6e6d62,nullptr);if(!device)return 2;
|
||||
void *loader=dlopen(argv[1],RTLD_NOW|RTLD_LOCAL);if(!loader)return 2;
|
||||
auto imports=(uint64_t (*)())dlsym(loader,"nmb_test_vk_host_import_count");
|
||||
auto delay=(void (*)(unsigned))dlsym(loader,"nmb_test_vk_host_import_delay");
|
||||
if(!imports || !delay)return 2;
|
||||
bool ok=false;
|
||||
if(!strcmp(argv[3],"raw"))ok=creation(device,false,imports,delay);
|
||||
else if(!strcmp(argv[3],"mixed"))ok=creation(device,true,imports,delay);
|
||||
else if(!strcmp(argv[3],"reentry"))ok=reentry(device);
|
||||
if(!ok)return 1;
|
||||
std::puts("PASS: native no-copy construction coalesces real imports while retaining independent objects, GPU bytes and callbacks");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Host instrument: how long the target's own QuartzCore shaders take through the native frontend (metal2vulkan, then the Vulkan driver given as
|
||||
// loader: the Adreno Turnip over the host simulation, or MoltenVK). Every <name>_vert_<p> / <name>_frag_<p> pair becomes a BGRA8 render pipeline
|
||||
// and every kernel a compute pipeline, each timed; with METAL2VULKAN_LOG_PIPELINES=1 the runtime also prints the VkPipeline creation time of each,
|
||||
// which separates the driver's compile from the translation. The host CPU is not the tablet's: compare the split, not the absolute numbers.
|
||||
// argv: loader, shader cache dir, QuartzCore default.metallib, [limit], [draw]. With draw=1 each render state made is drawn once (three
|
||||
// vertices into a 64x64 BGRA8 target, a zeroed buffer at every vertex and fragment index 0..7), which makes its VkPipeline: the runtime
|
||||
// makes it at the draw, so without a draw the creation time is the translation alone.
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Metal/Metal.h>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <vector>
|
||||
#include "NativeBridgeMetalFrontend.h"
|
||||
|
||||
extern "C" size_t native_bridge_metal_telemetry(char *, size_t);
|
||||
|
||||
static double nowMs() {return clock_gettime_nsec_np(CLOCK_UPTIME_RAW) / 1e6;}
|
||||
static void missing(const char *c, const char *s) {std::fprintf(stderr, "missing %s %s\n", c, s);}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 4) {std::fprintf(stderr, "usage: loader cache metallib [limit]\n"); return 2;}
|
||||
@autoreleasepool {
|
||||
const unsigned limit = argc > 4 ? (unsigned)std::atoi(argv[4]) : 0;
|
||||
const bool draw = argc > 5 && std::atoi(argv[5]) == 1;
|
||||
double t0 = nowMs();
|
||||
id<MTLDevice> device = native_bridge_metal_open(argv[1], argv[2], 0, missing);
|
||||
if (!device) {std::printf("open failed\n"); return 1;}
|
||||
std::printf("open %.1f ms\n", nowMs() - t0);
|
||||
// draw=2: the plugin's on-device benchmark (native_bridge_metal_benchmark) with the same pair count, for the host side of the comparison.
|
||||
if (argc > 5 && std::atoi(argv[5]) == 2) {
|
||||
static char rows[512];
|
||||
native_bridge_metal_benchmark(device, argv[3], limit ? limit : 8, rows, sizeof rows);
|
||||
std::printf("%s", rows);
|
||||
return 0;
|
||||
}
|
||||
NSError *error = nil;
|
||||
t0 = nowMs();
|
||||
id<MTLLibrary> qc = [device newLibraryWithURL:[NSURL fileURLWithPath:@(argv[3])] error:&error];
|
||||
if (!qc) {std::printf("library failed: %s\n", error.localizedDescription.UTF8String); return 1;}
|
||||
NSArray<NSString *> *names = qc.functionNames;
|
||||
std::printf("library %.1f ms, %lu functions\n", nowMs() - t0, (unsigned long)names.count);
|
||||
std::vector<double> function, pipeline, drawn;
|
||||
id<MTLCommandQueue> queue = [device newCommandQueue];
|
||||
MTLTextureDescriptor *td = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm width:64 height:64 mipmapped:NO];
|
||||
td.usage = MTLTextureUsageRenderTarget;
|
||||
id<MTLTexture> target = [device newTextureWithDescriptor:td];
|
||||
id<MTLBuffer> zeros = [device newBufferWithLength:65536 options:MTLResourceStorageModeShared];
|
||||
unsigned made = 0, refused = 0, n = 0;
|
||||
for (NSString *name in names) {
|
||||
if (limit && n >= limit) break;
|
||||
double f0 = nowMs();
|
||||
id<MTLFunction> f = [qc newFunctionWithName:name];
|
||||
double f1 = nowMs();
|
||||
function.push_back(f1 - f0);
|
||||
if (f.functionType == MTLFunctionTypeKernel) {
|
||||
++n;
|
||||
double p0 = nowMs();
|
||||
id state = [device newComputePipelineStateWithFunction:f error:&error];
|
||||
double p1 = nowMs();
|
||||
pipeline.push_back(p1 - p0);
|
||||
std::printf("K %-60s fn=%7.1f pipe=%8.1f %s\n", name.UTF8String, f1 - f0, p1 - p0, state ? "ok" : error.localizedDescription.UTF8String);
|
||||
state ? ++made : ++refused;
|
||||
} else if (f.functionType == MTLFunctionTypeVertex) {
|
||||
NSString *fragmentName = [name stringByReplacingOccurrencesOfString:@"_vert_" withString:@"_frag_"];
|
||||
if ([fragmentName isEqualToString:name] || ![names containsObject:fragmentName]) continue;
|
||||
++n;
|
||||
double g0 = nowMs();
|
||||
id<MTLFunction> fragment = [qc newFunctionWithName:fragmentName];
|
||||
double g1 = nowMs();
|
||||
MTLRenderPipelineDescriptor *d = [MTLRenderPipelineDescriptor new];
|
||||
d.vertexFunction = f;
|
||||
d.fragmentFunction = fragment;
|
||||
d.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;
|
||||
double p0 = nowMs();
|
||||
id state = [device newRenderPipelineStateWithDescriptor:d error:&error];
|
||||
double p1 = nowMs();
|
||||
pipeline.push_back(p1 - p0);
|
||||
double took = 0;
|
||||
if (state && draw) {
|
||||
double d0 = nowMs();
|
||||
MTLRenderPassDescriptor *pass = [MTLRenderPassDescriptor renderPassDescriptor];
|
||||
pass.colorAttachments[0].texture = target;
|
||||
pass.colorAttachments[0].loadAction = MTLLoadActionClear;
|
||||
pass.colorAttachments[0].storeAction = MTLStoreActionStore;
|
||||
id<MTLCommandBuffer> commands = [queue commandBuffer];
|
||||
id<MTLRenderCommandEncoder> encoder = [commands renderCommandEncoderWithDescriptor:pass];
|
||||
[encoder setRenderPipelineState:state];
|
||||
for (NSUInteger i = 0; i < 8; ++i) {
|
||||
[encoder setVertexBuffer:zeros offset:0 atIndex:i];
|
||||
[encoder setFragmentBuffer:zeros offset:0 atIndex:i];
|
||||
}
|
||||
[encoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
|
||||
[encoder endEncoding];
|
||||
[commands commit];
|
||||
[commands waitUntilCompleted];
|
||||
took = nowMs() - d0;
|
||||
drawn.push_back(took);
|
||||
}
|
||||
std::printf("R %-60s fn=%7.1f+%7.1f pipe=%8.1f draw=%8.1f %s\n", name.UTF8String, f1 - f0, g1 - g0, p1 - p0, took,
|
||||
state ? "ok" : error.localizedDescription.UTF8String);
|
||||
state ? ++made : ++refused;
|
||||
}
|
||||
std::fflush(stdout);
|
||||
}
|
||||
std::sort(pipeline.begin(), pipeline.end());
|
||||
double total = 0;
|
||||
for (double v : pipeline) total += v;
|
||||
double ftotal = 0;
|
||||
for (double v : function) ftotal += v;
|
||||
if (!pipeline.empty())
|
||||
std::printf("SUMMARY pipelines=%zu made=%u refused=%u total=%.0f ms median=%.1f ms p90=%.1f ms max=%.1f ms | functions=%zu total=%.0f ms\n",
|
||||
pipeline.size(), made, refused, total, pipeline[pipeline.size() / 2], pipeline[pipeline.size() * 9 / 10], pipeline.back(),
|
||||
function.size(), ftotal);
|
||||
if (!drawn.empty()) {
|
||||
std::sort(drawn.begin(), drawn.end());
|
||||
double dt = 0;
|
||||
for (double v : drawn) dt += v;
|
||||
static char table[8192];
|
||||
native_bridge_metal_telemetry(table, sizeof table);
|
||||
for (char *line = std::strtok(table, "\n"); line; line = std::strtok(nullptr, "\n"))
|
||||
if (line[0] == 'V' || line[0] == 'W' || line[0] == 'S') std::printf("TELEMETRY %s\n", line);
|
||||
std::printf("DRAWS n=%zu total=%.0f ms median=%.1f ms max=%.1f ms\n", drawn.size(), dt, drawn[drawn.size() / 2], drawn.back());
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
/* Device open/close only. Actual Turnip compiler; mandatory software peer.
|
||||
* No authored command, shader execution, timing or hardware claim. */
|
||||
#include "NativeMetalObjects.h"
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
#include <string>
|
||||
#include <unistd.h>
|
||||
#include <TargetConditionals.h>
|
||||
#define REQUIRE(x) do {if(!(x)){std::fprintf(stderr,"FAIL priming line %d: %s; %s\n",__LINE__,#x,nmb_last_error());return 1;}} while(0)
|
||||
int main(int argc,char **argv) {
|
||||
if(argc!=4)return 2;
|
||||
const bool produce=!std::strcmp(argv[3],"produce"),consume=!std::strcmp(argv[3],"consume"),uuid=!std::strcmp(argv[3],"uuid-miss"),cold=!std::strcmp(argv[3],"cold-miss");
|
||||
REQUIRE(produce||consume||uuid||cold);
|
||||
#if TARGET_OS_IPHONE
|
||||
REQUIRE(getpagesize()==4096 && sysconf(_SC_PAGESIZE)==4096 && !getenv("HOSTSIM_PAGE_SIZE"));
|
||||
#endif
|
||||
REQUIRE(!setenv("NMB_TEST_REAL_VULKAN_LOADER",argv[1],1) && !setenv("MESA_SHADER_CACHE_DISABLE","true",1) && !setenv("NMB_TEST_FORCE_PRIME_STRICT",produce?"0":"1",1));
|
||||
const char *path=getenv("NMB_TEST_PRIMING_CACHE_LOADER");REQUIRE(path);
|
||||
void *loader=dlopen(path,RTLD_NOW|RTLD_LOCAL);REQUIRE(loader);
|
||||
auto control=reinterpret_cast<void(*)(bool,bool)>(dlsym(loader,"nmb_test_graphics_cache_control"));
|
||||
auto graphics=reinterpret_cast<void(*)(uint64_t *)>(dlsym(loader,"nmb_test_graphics_cache_counts"));
|
||||
auto compute=reinterpret_cast<void(*)(uint64_t *)>(dlsym(loader,"nmb_test_prime_compute_counts"));
|
||||
auto live=reinterpret_cast<unsigned(*)()>(dlsym(RTLD_DEFAULT,"host_gpu_live_objects"));
|
||||
auto submitted=reinterpret_cast<unsigned(*)()>(dlsym(RTLD_DEFAULT,"host_gpu_submissions"));
|
||||
REQUIRE(control&&graphics&&compute&&live&&submitted);control(false,uuid);
|
||||
NmbDevice *device=nmb_device_open(path,argv[2]);uint64_t c[5]={},g[10]={};compute(c);graphics(g);
|
||||
if(cold||uuid){REQUIRE(!device && c[2]>0);if(uuid)REQUIRE(g[5]>0);}
|
||||
else {
|
||||
REQUIRE(device && c[0] && c[1]==c[0] && !c[2] && c[3]==c[0] && g[0] && g[1]==g[0] && !g[2] && g[3]==g[0]);
|
||||
if(consume)REQUIRE(c[4]==c[0] && g[4]==g[0]);
|
||||
nmb_device_release(device);
|
||||
}
|
||||
REQUIRE(live()==0);
|
||||
std::printf("PASS: device priming %s pid=%d compute=%llu/%llu/%llu/%llu/%llu graphics=%llu/%llu/%llu/%llu/%llu uuid_mutations=%llu submissions=%u live=0; software peer, no shader execution\n",argv[3],getpid(),
|
||||
(unsigned long long)c[0],(unsigned long long)c[1],(unsigned long long)c[2],(unsigned long long)c[3],(unsigned long long)c[4],
|
||||
(unsigned long long)g[0],(unsigned long long)g[1],(unsigned long long)g[2],(unsigned long long)g[3],(unsigned long long)g[4],(unsigned long long)g[5],submitted());
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
/* Owned offline QEMU harness for the real iOS device-priming cache. Shared
|
||||
* helpers retain exact RAM-volume identity, readonly view and bounded capture.
|
||||
* The optional first-consumer marker is part of the hashed initial test image. */
|
||||
#define main unused_ui_cold_suite_main
|
||||
#include "quartzcore_cache_ios.cpp"
|
||||
#undef main
|
||||
|
||||
int main(int argc,char **argv) {
|
||||
(void)argv;prefix="/private/var/tmp/priming";workerLog="/private/var/tmp/priming-worker.log";
|
||||
bool first=false;
|
||||
if(FILE *file=fopen("/private/var/tmp/priming-first-consumer","rb")) {
|
||||
char marker[32]={};const auto n=fread(marker,1,sizeof marker,file);fclose(file);
|
||||
if(n!=strlen("priming-first-v1\n") || memcmp(marker,"priming-first-v1\n",n))stop(96);
|
||||
first=true;
|
||||
} else if(errno!=ENOENT)stop(97);
|
||||
result[0]=first?0x5052494d494f5332ull:0x5052494d494f5331ull;
|
||||
result[2]=getpid();result[3]=getpagesize();result[4]=sysconf(_SC_PAGESIZE);
|
||||
if(argc!=1 || getpid()!=1 || result[3]!=4096 || result[4]!=4096)stop(1);
|
||||
mountCompilerView();
|
||||
if(setenv("MESA_SHADER_CACHE_DISABLE","true",1) || unsetenv("DYLD_INSERT_LIBRARIES") || unsetenv("HOSTSIM_PAGE_SIZE") ||
|
||||
setenv("NMB_TEST_PRIMING_CACHE_LOADER","/System/Library/Extensions/OfflineTurnipCompiler/libOfflinePrimingCacheControl.dylib",1))stop(2);
|
||||
const std::string cache=std::string(prefix)+".cache",empty=std::string(prefix)+".empty",copy=std::string(prefix)+".archive";
|
||||
std::array<unsigned char,128> initial={};
|
||||
if(first){summarize();memcpy(initial.data(),snapshot,initial.size());}
|
||||
else {struct stat old={};if(!lstat(copy.c_str(),&old) || errno!=ENOENT)stop(3);}
|
||||
if(unlink(workerLog) && errno!=ENOENT)stop(5);
|
||||
posix_spawn_file_actions_t actions;
|
||||
if(posix_spawn_file_actions_init(&actions) || posix_spawn_file_actions_addopen(&actions,STDOUT_FILENO,workerLog,O_WRONLY|O_CREAT|O_APPEND,0600) ||
|
||||
posix_spawn_file_actions_adddup2(&actions,STDOUT_FILENO,STDERR_FILENO))stop(4);
|
||||
const char *coldModes[]={"cold-miss","produce","consume","uuid-miss"},*firstModes[]={"consume","uuid-miss","cold-miss"};
|
||||
const unsigned cases=first?3:4;
|
||||
for(unsigned i=0;i<cases;++i) {
|
||||
const char *mode=first?firstModes[i]:coldModes[i];const auto &directory=first && i==2?empty:cache;
|
||||
char *arguments[]={const_cast<char *>("compiler-worker"),const_cast<char *>(driverPath),const_cast<char *>(directory.c_str()),const_cast<char *>(mode),nullptr};
|
||||
pid_t child=0;int error=posix_spawn(&child,"/private/var/tmp/compiler-worker",&actions,nullptr,arguments,environ);
|
||||
result[12+3*i]=child;result[13+3*i]=error;if(error || child<=1)stop(10+i);
|
||||
int status=0;while(waitpid(child,&status,0)<0)if(errno!=EINTR)stop(20+i);
|
||||
result[14+3*i]=status;if(!WIFEXITED(status) || WEXITSTATUS(status)!=0)stop(30+i);result[5]=i+1;
|
||||
}
|
||||
posix_spawn_file_actions_destroy(&actions);
|
||||
// A diagnostic duplicate of the sole actual driver record, not a Metal
|
||||
// binary archive. The full directory is independently hashed by summarize.
|
||||
if(!first) {
|
||||
std::error_code ec;std::filesystem::path record;unsigned files=0;
|
||||
std::filesystem::recursive_directory_iterator iterator(cache,ec),end;if(ec)stop(98);
|
||||
for(;iterator!=end;iterator.increment(ec)) {
|
||||
if(ec)stop(99);const auto status=iterator->symlink_status(ec);
|
||||
if(ec || std::filesystem::is_symlink(status))stop(100);
|
||||
if(std::filesystem::is_directory(status))continue;
|
||||
if(!std::filesystem::is_regular_file(status))stop(101);
|
||||
record=iterator->path();++files;
|
||||
}
|
||||
if(ec || files!=1 || !std::filesystem::copy_file(record,copy,std::filesystem::copy_options::none,ec) || ec)stop(102);
|
||||
}
|
||||
summarize();
|
||||
if(first) {
|
||||
if(memcmp(initial.data(),snapshot,initial.size()))stop(103);
|
||||
memcpy(snapshot+256,initial.data(),initial.size());result[6]=384;result[45]=2;result[46]=1;
|
||||
}
|
||||
stop(0);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
/* Offline only. Preserve the existing graphics/UUID counters and forward real
|
||||
* calls, additionally forbid/observe compute and graphics during device open. */
|
||||
#define vkGetInstanceProcAddr baselineGetInstanceProcAddr
|
||||
#define vkGetDeviceProcAddr baselineGetDeviceProcAddr
|
||||
#include "graphics_cache_loader.cpp"
|
||||
#undef vkGetInstanceProcAddr
|
||||
#undef vkGetDeviceProcAddr
|
||||
|
||||
static std::atomic<uint64_t> computeCounts[5]; // calls, successes, required, valid, app hits
|
||||
static bool forceCompilationForbidden() {
|
||||
const char *value=std::getenv("NMB_TEST_FORCE_PRIME_STRICT");return value && !std::strcmp(value,"1");
|
||||
}
|
||||
static bool hasFlags2(const void *next) {
|
||||
for(auto p=static_cast<const VkBaseInStructure *>(next);p;p=p->pNext)
|
||||
if(p->sType==VK_STRUCTURE_TYPE_PIPELINE_CREATE_FLAGS_2_CREATE_INFO_KHR)return true;
|
||||
return false;
|
||||
}
|
||||
static VKAPI_ATTR VkResult VKAPI_CALL primeGraphics(VkDevice device,VkPipelineCache cache,uint32_t count,
|
||||
const VkGraphicsPipelineCreateInfo *infos,const VkAllocationCallbacks *allocator,VkPipeline *pipelines) {
|
||||
if(!count || count>64)return VK_ERROR_INITIALIZATION_FAILED;
|
||||
std::vector<VkGraphicsPipelineCreateInfo> copies(infos,infos+count);
|
||||
if(forceCompilationForbidden())for(auto &info:copies) {
|
||||
// Explicit refusal rather than silently missing flags promoted to pNext.
|
||||
if(hasFlags2(info.pNext))return VK_ERROR_FEATURE_NOT_PRESENT;
|
||||
info.flags|=VK_PIPELINE_CREATE_FAIL_ON_PIPELINE_COMPILE_REQUIRED_BIT;
|
||||
}
|
||||
return createGraphics(device,cache,count,copies.data(),allocator,pipelines);
|
||||
}
|
||||
static VKAPI_ATTR VkResult VKAPI_CALL primeCompute(VkDevice device,VkPipelineCache cache,uint32_t count,
|
||||
const VkComputePipelineCreateInfo *infos,const VkAllocationCallbacks *allocator,VkPipeline *pipelines) {
|
||||
if(!count || count>64)return VK_ERROR_INITIALIZATION_FAILED;
|
||||
std::vector<VkComputePipelineCreateInfo> copies(infos,infos+count);
|
||||
std::vector<VkPipelineCreationFeedback> feedback(count);
|
||||
std::vector<VkPipelineCreationFeedbackCreateInfo> additions(count);
|
||||
std::vector<const VkPipelineCreationFeedback *> observed(count);
|
||||
for(uint32_t i=0;i<count;++i) {
|
||||
if(forceCompilationForbidden()) {
|
||||
if(hasFlags2(copies[i].pNext))return VK_ERROR_FEATURE_NOT_PRESENT;
|
||||
copies[i].flags|=VK_PIPELINE_CREATE_FAIL_ON_PIPELINE_COMPILE_REQUIRED_BIT;
|
||||
}
|
||||
for(auto p=static_cast<const VkBaseInStructure *>(copies[i].pNext);p;p=p->pNext)
|
||||
if(p->sType==VK_STRUCTURE_TYPE_PIPELINE_CREATION_FEEDBACK_CREATE_INFO)
|
||||
observed[i]=reinterpret_cast<const VkPipelineCreationFeedbackCreateInfo *>(p)->pPipelineCreationFeedback;
|
||||
if(!observed[i]) {
|
||||
auto &a=additions[i];a.sType=VK_STRUCTURE_TYPE_PIPELINE_CREATION_FEEDBACK_CREATE_INFO;
|
||||
a.pNext=copies[i].pNext;a.pPipelineCreationFeedback=&feedback[i];copies[i].pNext=&a;observed[i]=&feedback[i];
|
||||
}
|
||||
}
|
||||
auto real=reinterpret_cast<PFN_vkCreateComputePipelines>(realDevice.load()(device,"vkCreateComputePipelines"));
|
||||
computeCounts[0]+=count;const auto result=real(device,cache,count,copies.data(),allocator,pipelines);
|
||||
if(result==VK_PIPELINE_COMPILE_REQUIRED)computeCounts[2]++;
|
||||
for(uint32_t i=0;i<count;++i)if(pipelines[i]) {
|
||||
computeCounts[1]++;
|
||||
if(observed[i] && (observed[i]->flags&VK_PIPELINE_CREATION_FEEDBACK_VALID_BIT)) {
|
||||
computeCounts[3]++;
|
||||
if(observed[i]->flags&VK_PIPELINE_CREATION_FEEDBACK_APPLICATION_PIPELINE_CACHE_HIT_BIT)computeCounts[4]++;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
extern "C" VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL vkGetDeviceProcAddr(VkDevice device,const char *name) {
|
||||
if(!std::strcmp(name,"vkCreateGraphicsPipelines"))return reinterpret_cast<PFN_vkVoidFunction>(primeGraphics);
|
||||
if(!std::strcmp(name,"vkCreateComputePipelines"))return reinterpret_cast<PFN_vkVoidFunction>(primeCompute);
|
||||
return baselineGetDeviceProcAddr(device,name);
|
||||
}
|
||||
extern "C" VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL vkGetInstanceProcAddr(VkInstance instance,const char *name) {
|
||||
const auto result=baselineGetInstanceProcAddr(instance,name);
|
||||
if(!std::strcmp(name,"vkGetDeviceProcAddr"))return reinterpret_cast<PFN_vkVoidFunction>(vkGetDeviceProcAddr);
|
||||
if(!std::strcmp(name,"vkCreateGraphicsPipelines"))return reinterpret_cast<PFN_vkVoidFunction>(primeGraphics);
|
||||
if(!std::strcmp(name,"vkCreateComputePipelines"))return reinterpret_cast<PFN_vkVoidFunction>(primeCompute);
|
||||
return result;
|
||||
}
|
||||
extern "C" void nmb_test_prime_compute_counts(uint64_t out[5]) {for(unsigned i=0;i<5;++i)out[i]=computeCounts[i].load();}
|
||||
@@ -0,0 +1,202 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Execute unchanged target blur functions on a real host GPU. The reference
|
||||
// is an authored seven-tap binomial convolution, not a deployment renderer.
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Metal/Metal.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include "../NativeBridgeMetalFrontend.h"
|
||||
#include "NativeMetalObjects.h"
|
||||
|
||||
constexpr unsigned width=32,height=24;
|
||||
struct Vertex {float position[4],uv[2],unusedColor[4];};
|
||||
static const Vertex vertices[]={
|
||||
{{-1,-1,0,1},{0,1},{}},{{1,-1,0,1},{1,1},{}},{{-1,1,0,1},{0,0},{}},
|
||||
{{-1,1,0,1},{0,0},{}},{{1,-1,0,1},{1,1},{}},{{1,1,0,1},{1,0},{}}};
|
||||
struct alignas(16) UniformF {float offset[8][2],weight[8],divide;unsigned char padding[12];};
|
||||
struct alignas(8) UniformH {float offset[8][2];_Float16 weight[8],divide;unsigned char padding[6];};
|
||||
static_assert(sizeof(UniformF)==112 && sizeof(UniformH)==88);
|
||||
|
||||
static MTLRenderPipelineDescriptor *blurDescriptor(id<MTLLibrary> library,bool half) {
|
||||
NSString *suffix=half?@"lph":@"lpf";auto d=[MTLRenderPipelineDescriptor new];
|
||||
d.vertexFunction=[library newFunctionWithName:[@"downsample_blur_vert_" stringByAppendingString:suffix]];
|
||||
d.fragmentFunction=[library newFunctionWithName:[@"narrow_blur_7_frag_" stringByAppendingString:suffix]];
|
||||
d.colorAttachments[0].pixelFormat=MTLPixelFormatRGBA16Float;
|
||||
auto vd=[MTLVertexDescriptor vertexDescriptor];
|
||||
const unsigned indices[]={0,1,3},offsets[]={0,16,24};
|
||||
const MTLVertexFormat formats[]={MTLVertexFormatFloat4,MTLVertexFormatFloat2,MTLVertexFormatFloat4};
|
||||
for(unsigned i=0;i<3;++i){vd.attributes[indices[i]].format=formats[i];vd.attributes[indices[i]].offset=offsets[i];vd.attributes[indices[i]].bufferIndex=0;}
|
||||
vd.layouts[0].stride=sizeof(Vertex);d.vertexDescriptor=vd;return d;
|
||||
}
|
||||
static MTLRenderPipelineDescriptor *namedDescriptor(id<MTLLibrary> library,NSString *vertex,NSString *fragment) {
|
||||
auto d=[MTLRenderPipelineDescriptor new];d.vertexFunction=[library newFunctionWithName:vertex];d.fragmentFunction=[library newFunctionWithName:fragment];
|
||||
d.colorAttachments[0].pixelFormat=MTLPixelFormatBGRA8Unorm;
|
||||
auto vd=[MTLVertexDescriptor vertexDescriptor];
|
||||
const MTLVertexFormat formats[]={MTLVertexFormatFloat4,MTLVertexFormatFloat2,MTLVertexFormatFloat2,MTLVertexFormatFloat4,MTLVertexFormatFloat};
|
||||
const unsigned offsets[]={0,16,24,32,48};
|
||||
for(unsigned i=0;i<5;++i){vd.attributes[i].format=formats[i];vd.attributes[i].bufferIndex=0;vd.attributes[i].offset=offsets[i];}
|
||||
vd.layouts[0].stride=64;d.vertexDescriptor=vd;
|
||||
if([vertex hasPrefix:@"path_blit_"]){d.colorAttachments[1].pixelFormat=MTLPixelFormatBGRA8Unorm;d.colorAttachments[2].pixelFormat=MTLPixelFormatR32Float;}
|
||||
return d;
|
||||
}
|
||||
static bool cacheState(const char *phase) {
|
||||
NmbShaderCacheStats s={};uint64_t work[10]={};
|
||||
if(!native_bridge_metal_shader_cache_stats(&s) || !native_bridge_metal_work_stats(work))return false;
|
||||
std::printf("cache %s artifact_hit=%llu miss=%llu write=%llu driver_load=%llu hit=%llu miss=%llu save=%llu same=%llu vk_makings=%llu\n",phase,
|
||||
(unsigned long long)s.hits,(unsigned long long)s.misses,(unsigned long long)s.writes,
|
||||
(unsigned long long)s.driver_loads,(unsigned long long)s.driver_hits,(unsigned long long)s.driver_misses,
|
||||
(unsigned long long)s.driver_saves,(unsigned long long)s.driver_unchanged,(unsigned long long)work[0]);
|
||||
return !s.rejected && !s.read_failures && !s.write_failures && !s.driver_truncated && !s.driver_mismatched && !work[4];
|
||||
}
|
||||
static bool inventory(id<MTLDevice> device,id<MTLLibrary> library) {
|
||||
unsigned vertex=0,fragment=0,kernels=0,ordinary=0,tile=0,pairs=0,bare=0,attributes=0,attachments=0;
|
||||
for(NSString *name in library.functionNames) {
|
||||
id<MTLFunction> f=[library newFunctionWithName:name];if(!f)return false;
|
||||
NSError *error=nil;
|
||||
if(f.functionType==MTLFunctionTypeKernel) {
|
||||
++kernels;
|
||||
if([device newComputePipelineStateWithFunction:f error:&error])++ordinary;
|
||||
else if([error.localizedDescription containsString:@"NeedsInput(TileAttachments)"])++tile;
|
||||
else {std::printf("FAIL unclassified compute %s: %s\n",name.UTF8String,error.localizedDescription.UTF8String);return false;}
|
||||
} else if(f.functionType==MTLFunctionTypeFragment)++fragment;
|
||||
else if(f.functionType==MTLFunctionTypeVertex) {
|
||||
++vertex;NSString *other=[name stringByReplacingOccurrencesOfString:@"_vert_" withString:@"_frag_"];
|
||||
if([other isEqualToString:name] || ![library.functionNames containsObject:other])continue;
|
||||
++pairs;auto d=[MTLRenderPipelineDescriptor new];d.vertexFunction=f;d.fragmentFunction=[library newFunctionWithName:other];
|
||||
d.colorAttachments[0].pixelFormat=MTLPixelFormatBGRA8Unorm;
|
||||
if([device newRenderPipelineStateWithDescriptor:d error:&error])++bare;
|
||||
else if([error.localizedDescription containsString:@"InvalidDescriptor(VertexAttribute"])++attributes;
|
||||
else if([error.localizedDescription containsString:@"InvalidDescriptor(ColorAttachment"])++attachments;
|
||||
else {std::printf("FAIL unclassified render %s: %s\n",name.UTF8String,error.localizedDescription.UTF8String);return false;}
|
||||
} else return false;
|
||||
}
|
||||
std::printf("inventory functions=167 vertex=%u fragment=%u kernels=%u ordinary=%u tile_requires=%u pairs=%u bare_ok=%u missing_vertex=%u missing_attachment=%u\n",
|
||||
vertex,fragment,kernels,ordinary,tile,pairs,bare,attributes,attachments);
|
||||
return vertex==41 && fragment==111 && kernels==15 && ordinary==4 && tile==11 && pairs==22 && bare==14 && attributes==6 && attachments==2;
|
||||
}
|
||||
static std::vector<_Float16> convolution(const std::vector<_Float16> &input,bool vertical) {
|
||||
// Independent integer stencil: coefficients sum to 64, clamp at the edge.
|
||||
constexpr unsigned weight[]={1,6,15,20,15,6,1};std::vector<_Float16> result(input.size());
|
||||
for(unsigned y=0;y<height;++y)for(unsigned x=0;x<width;++x)for(unsigned c=0;c<4;++c) {
|
||||
float sum=0;
|
||||
for(int tap=-3;tap<=3;++tap) {
|
||||
const int sx=std::clamp((int)x+(vertical?0:tap),0,(int)width-1);
|
||||
const int sy=std::clamp((int)y+(vertical?tap:0),0,(int)height-1);
|
||||
sum+=(float)input[(sy*width+sx)*4+c]*weight[tap+3];
|
||||
}
|
||||
result[(y*width+x)*4+c]=(_Float16)(sum/64);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
static bool encode(id<MTLCommandBuffer> command,id<MTLRenderPipelineState> state,id<MTLTexture> source,
|
||||
id<MTLTexture> destination,id<MTLSamplerState> sampler,bool half,bool vertical) {
|
||||
auto pass=[MTLRenderPassDescriptor renderPassDescriptor];pass.colorAttachments[0].texture=destination;
|
||||
pass.colorAttachments[0].loadAction=MTLLoadActionDontCare;pass.colorAttachments[0].storeAction=MTLStoreActionStore;
|
||||
id<MTLRenderCommandEncoder> encoder=[command renderCommandEncoderWithDescriptor:pass];if(!encoder)return false;
|
||||
[encoder setRenderPipelineState:state];[encoder setVertexBytes:vertices length:sizeof vertices atIndex:0];
|
||||
const float identity[]={1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1},texmat[]={1,1,0,0};
|
||||
[encoder setVertexBytes:identity length:sizeof identity atIndex:2];[encoder setVertexBytes:texmat length:sizeof texmat atIndex:3];
|
||||
[encoder setFragmentTexture:source atIndex:3];[encoder setFragmentSamplerState:sampler atIndex:0];
|
||||
// Two bilinear pairs exactly represent the seven authored stencil taps.
|
||||
UniformF f={};UniformH h={};
|
||||
f.offset[0][vertical?1:0]=0.6f/(vertical?height:width);
|
||||
f.offset[1][vertical?1:0]=(15.0f/7)/(vertical?height:width);
|
||||
f.weight[0]=25.0f/64;f.weight[1]=7.0f/64;
|
||||
if(half) {
|
||||
memcpy(h.offset,f.offset,sizeof h.offset);h.weight[0]=(_Float16)f.weight[0];h.weight[1]=(_Float16)f.weight[1];
|
||||
const _Float16 scale=1;[encoder setFragmentBytes:&h length:sizeof h atIndex:1];[encoder setFragmentBytes:&scale length:sizeof scale atIndex:6];
|
||||
} else {
|
||||
const float scale=1;[encoder setFragmentBytes:&f length:sizeof f atIndex:1];[encoder setFragmentBytes:&scale length:sizeof scale atIndex:6];
|
||||
}
|
||||
[encoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6];[encoder endEncoding];return true;
|
||||
}
|
||||
static bool runBlur(id<MTLDevice> device,id<MTLRenderPipelineState> state,bool half) {
|
||||
std::vector<_Float16> data(width*height*4);
|
||||
for(unsigned y=0;y<height;++y)for(unsigned x=0;x<width;++x) {
|
||||
auto p=data.data()+(y*width+x)*4;p[0]=(_Float16)((x%7)/7.0f);p[1]=(_Float16)((y%5)/5.0f);
|
||||
p[2]=(_Float16)(x==width/2 && y==height/2);p[3]=1;
|
||||
}
|
||||
const auto expected=convolution(convolution(data,false),true);
|
||||
auto td=[MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA16Float width:width height:height mipmapped:NO];
|
||||
td.storageMode=MTLStorageModeShared;td.usage=MTLTextureUsageShaderRead|MTLTextureUsageRenderTarget;
|
||||
id<MTLTexture> input=[device newTextureWithDescriptor:td],middle=[device newTextureWithDescriptor:td],output=[device newTextureWithDescriptor:td];
|
||||
auto sd=[MTLSamplerDescriptor new];sd.minFilter=MTLSamplerMinMagFilterLinear;sd.magFilter=MTLSamplerMinMagFilterLinear;
|
||||
sd.sAddressMode=MTLSamplerAddressModeClampToEdge;sd.tAddressMode=MTLSamplerAddressModeClampToEdge;
|
||||
id<MTLSamplerState> sampler=[device newSamplerStateWithDescriptor:sd];id<MTLCommandQueue> queue=[device newCommandQueue];
|
||||
if(!input || !middle || !output || !sampler || !queue)return false;
|
||||
[input replaceRegion:MTLRegionMake2D(0,0,width,height) mipmapLevel:0 withBytes:data.data() bytesPerRow:width*8];
|
||||
float worst=0;
|
||||
for(unsigned round=0;round<6;++round){@autoreleasepool {
|
||||
NmbShaderCacheStats before={},after={};uint64_t workBefore[10]={},workAfter[10]={};
|
||||
if(!native_bridge_metal_shader_cache_stats(&before) || !native_bridge_metal_work_stats(workBefore))return false;
|
||||
id<MTLCommandBuffer> command=[queue commandBuffer];
|
||||
if(!encode(command,state,input,middle,sampler,half,false) || !encode(command,state,middle,output,sampler,half,true))return false;
|
||||
[command commit];[command waitUntilCompleted];
|
||||
if(command.status!=MTLCommandBufferStatusCompleted){std::printf("FAIL blur command: %s\n",command.error.localizedDescription.UTF8String);return false;}
|
||||
std::vector<_Float16> got(data.size());[output getBytes:got.data() bytesPerRow:width*8 fromRegion:MTLRegionMake2D(0,0,width,height) mipmapLevel:0];
|
||||
for(size_t i=0;i<got.size();++i){const float error=std::fabs((float)got[i]-(float)expected[i]);
|
||||
if(!std::isfinite((float)got[i]) || error>0.003f){std::printf("FAIL blur %s round=%u element=%zu got=%g expected=%g\n",half?"half":"float",round,i,(double)got[i],(double)expected[i]);return false;}
|
||||
worst=std::max(worst,error);
|
||||
}
|
||||
if(!native_bridge_metal_shader_cache_stats(&after) || !native_bridge_metal_work_stats(workAfter))return false;
|
||||
std::printf("round %s %u vk_makings=%llu artifact_hit=%llu miss=%llu driver_load=%llu hit=%llu miss=%llu\n",half?"half":"float",round,
|
||||
(unsigned long long)(workAfter[0]-workBefore[0]),(unsigned long long)(after.hits-before.hits),(unsigned long long)(after.misses-before.misses),
|
||||
(unsigned long long)(after.driver_loads-before.driver_loads),(unsigned long long)(after.driver_hits-before.driver_hits),(unsigned long long)(after.driver_misses-before.driver_misses));
|
||||
if(round && (workAfter[0]!=workBefore[0] || after.misses!=before.misses || after.driver_misses!=before.driver_misses)) {
|
||||
std::puts("FAIL repeated identical blur performs new pipeline/translation work");return false;
|
||||
}
|
||||
}}
|
||||
std::printf("blur %s rounds=6 passes=12 texels=%u max_error=%g GPU=pass\n",half?"half":"float",width*height,worst);return true;
|
||||
}
|
||||
|
||||
int main(int argc,char **argv) {
|
||||
if(argc!=6 || (strcmp(argv[5],"produce") && strcmp(argv[5],"consume") && strcmp(argv[5],"inventory")))return 2;
|
||||
if(setenv("METAL2VULKAN_LOG_PIPELINES","1",1))return 2;
|
||||
const bool produce=!strcmp(argv[5],"produce");
|
||||
@autoreleasepool {
|
||||
id<MTLDevice> device=native_bridge_metal_open(argv[1],argv[2],0x6e6d62,nullptr);if(!device)return 2;
|
||||
NSError *error=nil;id<MTLLibrary> library=[device newLibraryWithFile:@(argv[3]) error:&error];if(!library)return 2;
|
||||
if(library.functionNames.count!=167)return 1;
|
||||
if(!strcmp(argv[5],"inventory"))return inventory(device,library)?0:1;
|
||||
auto ad=[MTLBinaryArchiveDescriptor new];NSURL *url=[NSURL fileURLWithPath:@(argv[4])];if(!produce)ad.url=url;
|
||||
id<MTLBinaryArchive> archive=[device newBinaryArchiveWithDescriptor:ad error:&error];if(!archive)return 1;
|
||||
if(!cacheState("opened"))return 1;
|
||||
if(produce) {
|
||||
auto empty=blurDescriptor(library,false);empty.binaryArchives=@[archive];error=nil;
|
||||
if([device newRenderPipelineStateWithDescriptor:empty options:MTLPipelineOptionFailOnBinaryArchiveMiss reflection:nil error:&error] || !error)return 1;
|
||||
}
|
||||
unsigned pairs=0;
|
||||
for(NSString *name in library.functionNames) {
|
||||
id<MTLFunction> function=[library newFunctionWithName:name];if(function.functionType!=MTLFunctionTypeVertex)continue;
|
||||
NSString *fragment=[name stringByReplacingOccurrencesOfString:@"_vert_" withString:@"_frag_"];
|
||||
if([fragment isEqualToString:name] || ![library.functionNames containsObject:fragment])continue;
|
||||
auto d=namedDescriptor(library,name,fragment);d.binaryArchives=@[archive];
|
||||
if(produce && ![archive addRenderPipelineFunctionsWithDescriptor:d error:&error]){
|
||||
std::printf("FAIL archive add %s: %s\n",name.UTF8String,error.localizedDescription.UTF8String);return 1;}
|
||||
error=nil;
|
||||
if(![device newRenderPipelineStateWithDescriptor:d options:MTLPipelineOptionFailOnBinaryArchiveMiss reflection:nil error:&error] || error){
|
||||
std::printf("FAIL strict pair %s: %s\n",name.UTF8String,error.localizedDescription.UTF8String);return 1;}
|
||||
++pairs;
|
||||
}
|
||||
if(pairs!=22)return 1;
|
||||
std::printf("valid descriptor pairs=%u strict archive=pass\n",pairs);
|
||||
for(bool half:{false,true}) {
|
||||
auto d=blurDescriptor(library,half);d.binaryArchives=@[archive];
|
||||
if(produce && ![archive addRenderPipelineFunctionsWithDescriptor:d error:&error])return 1;
|
||||
error=nil;id<MTLRenderPipelineState> state=[device newRenderPipelineStateWithDescriptor:d options:MTLPipelineOptionFailOnBinaryArchiveMiss reflection:nil error:&error];
|
||||
if(!state){std::printf("FAIL blur pipeline: %s\n",error.localizedDescription.UTF8String);return 1;}
|
||||
const auto format=d.colorAttachments[0].pixelFormat;d.colorAttachments[0].pixelFormat=MTLPixelFormatBGRA8Unorm;error=nil;
|
||||
if([device newRenderPipelineStateWithDescriptor:d options:MTLPipelineOptionFailOnBinaryArchiveMiss reflection:nil error:&error] || !error)return 1;
|
||||
d.colorAttachments[0].pixelFormat=format;
|
||||
if(!runBlur(device,state,half))return 1;
|
||||
}
|
||||
if(produce && ![archive serializeToURL:url error:&error])return 1;
|
||||
if(!cacheState("finished"))return 1;
|
||||
uint64_t work[10]={};if(!native_bridge_metal_work_stats(work) || !work[0])return 1; // logging must be active, never a zero-counter shortcut
|
||||
std::puts("PASS: unchanged target blur functions execute repeated separable Gaussian reference cases on the host GPU");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Actual compiler/cache instrument. No draw is encoded. The runtime may initialize
|
||||
// neutral resources over the mandatory software peer; it executes no shader.
|
||||
#import <Metal/Metal.h>
|
||||
#include <TargetConditionals.h>
|
||||
#include <CommonCrypto/CommonDigest.h>
|
||||
#include "NativeMetalObjects.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unistd.h>
|
||||
|
||||
#define REQUIRE(x) do {if(!(x)){std::fprintf(stderr,"FAIL UI cache line %d: %s; %s\n",__LINE__,#x,nmb_last_error());return 1;}} while(0)
|
||||
static NmbRenderPipelineDescriptor descriptor(NmbLibrary *library,const std::string &vertex,const std::string &fragment,bool blur) {
|
||||
NmbRenderPipelineDescriptor d={};d.vertex=nmb_function_new(library,vertex.c_str());d.fragment=nmb_function_new(library,fragment.c_str());
|
||||
d.colors[0].format=blur?MTLPixelFormatRGBA16Float:MTLPixelFormatBGRA8Unorm;
|
||||
if(vertex.rfind("path_blit_",0)==0){d.colors[1].format=MTLPixelFormatBGRA8Unorm;d.colors[2].format=MTLPixelFormatR32Float;}
|
||||
for(auto &c:d.colors)if(c.format){c.write_mask=MTLColorWriteMaskAll;c.source_rgb=MTLBlendFactorOne;c.source_alpha=MTLBlendFactorOne;}
|
||||
d.samples=1;d.rasterization=1;d.vertex_inputs=1;
|
||||
if(blur) {
|
||||
d.attributes[0]={MTLVertexFormatFloat4,0,0};d.attributes[1]={MTLVertexFormatFloat2,16,0};d.attributes[3]={MTLVertexFormatFloat4,24,0};
|
||||
d.layouts[0]={40,MTLVertexStepFunctionPerVertex,1};
|
||||
} else {
|
||||
const uint64_t formats[]={MTLVertexFormatFloat4,MTLVertexFormatFloat2,MTLVertexFormatFloat2,MTLVertexFormatFloat4,MTLVertexFormatFloat};
|
||||
const uint64_t offsets[]={0,16,24,32,48};for(unsigned i=0;i<5;++i)d.attributes[i]={formats[i],offsets[i],0};
|
||||
d.layouts[0]={64,MTLVertexStepFunctionPerVertex,1};
|
||||
}
|
||||
return d;
|
||||
}
|
||||
int main(int argc,char **argv) {
|
||||
if(argc!=4)return 2;
|
||||
const bool produce=!std::strcmp(argv[3],"produce"),consume=!std::strcmp(argv[3],"consume"),uuid=!std::strcmp(argv[3],"uuid-miss"),empty=!std::strcmp(argv[3],"empty-driver");
|
||||
REQUIRE(produce||consume||uuid||empty);
|
||||
#if TARGET_OS_IPHONE
|
||||
REQUIRE(getpagesize()==4096 && sysconf(_SC_PAGESIZE)==4096 && !getenv("HOSTSIM_PAGE_SIZE"));
|
||||
#endif
|
||||
REQUIRE(!setenv("MESA_SHADER_CACHE_DISABLE","true",1) && !setenv("METAL2VULKAN_LOG_PIPELINES","1",1) && !setenv("NMB_TEST_REAL_VULKAN_LOADER",argv[1],1));
|
||||
std::string path=argv[1];path=path.substr(0,path.find_last_of('/')+1)+"libOfflineGraphicsCacheControl.dylib";
|
||||
if(const char *override=getenv("NMB_TEST_GRAPHICS_CACHE_LOADER"))path=override;
|
||||
void *loader=dlopen(path.c_str(),RTLD_NOW|RTLD_LOCAL);REQUIRE(loader);
|
||||
auto control=reinterpret_cast<void(*)(bool,bool)>(dlsym(loader,"nmb_test_graphics_cache_control"));
|
||||
auto counters=reinterpret_cast<void(*)(uint64_t *)>(dlsym(loader,"nmb_test_graphics_cache_counts"));
|
||||
auto submitted=reinterpret_cast<unsigned(*)()>(dlsym(RTLD_DEFAULT,"host_gpu_submissions"));REQUIRE(control&&counters&&submitted);
|
||||
const unsigned submissions=submitted();control(false,false);
|
||||
const std::string prefix=argv[2],archivePath=prefix+".archive",cachePath=prefix+(empty?".empty":".cache");
|
||||
NmbDevice *device=nmb_device_open(path.c_str(),cachePath.c_str());REQUIRE(device);
|
||||
uint64_t deviceCounts[10]={};counters(deviceCounts);control(!produce,uuid);
|
||||
const char *libraryPath="/System/Library/Frameworks/QuartzCore.framework/default.metallib";
|
||||
if(const char *override=getenv("NMB_TEST_QUARTZCORE_LIBRARY"))libraryPath=override;
|
||||
std::ifstream input(libraryPath,std::ios::binary);REQUIRE(input);
|
||||
std::vector<char> bytes(9670832);input.read(bytes.data(),bytes.size());REQUIRE(input.good() && input.peek()==EOF);
|
||||
unsigned char digest[CC_SHA256_DIGEST_LENGTH];CC_SHA256(bytes.data(),(CC_LONG)bytes.size(),digest);
|
||||
char digestHex[65]={};for(unsigned i=0;i<32;++i)std::snprintf(digestHex+2*i,3,"%02x",digest[i]);
|
||||
REQUIRE(!std::strcmp(digestHex,"606f913adf520420f9b4bb666efb782a68c830f082f1e559e17b20d0d13adbc9"));
|
||||
NmbLibrary *library=nmb_library_new(device,bytes.data(),bytes.size(),false);REQUIRE(library&&nmb_library_function_count(library)==167);
|
||||
std::vector<std::string> names;for(unsigned i=0;i<167;++i){char name[256]={};REQUIRE(nmb_library_function_name(library,i,name,sizeof name)<sizeof name);names.emplace_back(name);}
|
||||
std::vector<NmbRenderPipelineDescriptor> recipes;
|
||||
for(const auto &name:names) {
|
||||
const auto marker=name.find("_vert_");if(marker==std::string::npos)continue;
|
||||
auto fragment=name;fragment.replace(marker,6,"_frag_");if(std::find(names.begin(),names.end(),fragment)==names.end())continue;
|
||||
recipes.push_back(descriptor(library,name,fragment,false));
|
||||
}
|
||||
REQUIRE(recipes.size()==22);
|
||||
for(const auto *suffix:{"lpf","lph"})recipes.push_back(descriptor(library,std::string("downsample_blur_vert_")+suffix,std::string("narrow_blur_7_frag_")+suffix,true));
|
||||
for(const auto &d:recipes)REQUIRE(d.vertex&&d.fragment&&nmb_function_type(d.vertex)==1&&nmb_function_type(d.fragment)==2);
|
||||
NmbBinaryArchive *archive=nmb_binary_archive_new(device,produce?nullptr:archivePath.c_str());REQUIRE(archive);
|
||||
NmbPipelineOptions options={MTLPipelineOptionFailOnBinaryArchiveMiss,&archive,1};
|
||||
uint64_t coldRequired=0;bool coldObject=false;
|
||||
if(produce) {
|
||||
// Exercise the real driver's compile-forbidden cold refusal, separately
|
||||
// from strict binary-archive misses. Clear only the test policy afterwards.
|
||||
control(true,false);auto cold=nmb_render_pipeline_new(device,&recipes.back());
|
||||
coldObject=cold!=nullptr;if(cold)nmb_render_pipeline_release(cold);
|
||||
uint64_t counts[10]={};counters(counts);coldRequired=counts[2];REQUIRE(coldRequired>0);control(false,false);
|
||||
}
|
||||
uint64_t before[10]={};counters(before);unsigned made=0;
|
||||
for(const auto &d:recipes) {
|
||||
if(produce)REQUIRE(nmb_binary_archive_add_render(archive,&d));
|
||||
NmbRenderPipeline *state=nmb_render_pipeline_new_with_options(device,&d,&options);
|
||||
if(uuid||empty){if(state)nmb_render_pipeline_release(state);break;}
|
||||
REQUIRE(state);nmb_render_pipeline_release(state);++made;
|
||||
}
|
||||
uint64_t counts[10]={};counters(counts);
|
||||
std::fprintf(stderr,"graphics results %s: calls=%llu success=%llu required=%llu valid=%llu hits=%llu cached=%llu/%llu/%llu\n",argv[3],
|
||||
(unsigned long long)(counts[0]-before[0]),(unsigned long long)(counts[1]-before[1]),(unsigned long long)(counts[2]-before[2]),
|
||||
(unsigned long long)(counts[3]-before[3]),(unsigned long long)(counts[4]-before[4]),(unsigned long long)(counts[7]-before[7]),
|
||||
(unsigned long long)(counts[8]-before[8]),(unsigned long long)(counts[9]-before[9]));
|
||||
if(uuid||empty) {REQUIRE(counts[2]>before[2]);if(uuid)REQUIRE(counts[5]>before[5]);}
|
||||
else REQUIRE(made==24 && counts[1]>before[1]);
|
||||
if(consume)REQUIRE(counts[2]==before[2] && counts[7]>before[7] && counts[8]-before[8]==counts[7]-before[7] && counts[9]-before[9]==counts[7]-before[7]);
|
||||
if(produce)REQUIRE(nmb_binary_archive_serialize(archive,archivePath.c_str()));
|
||||
NmbShaderCacheStats stats={};REQUIRE(nmb_device_shader_cache_stats(device,&stats));
|
||||
REQUIRE(!stats.rejected&&!stats.read_failures&&!stats.write_failures&&!stats.driver_truncated&&!stats.driver_mismatched);
|
||||
if(consume)REQUIRE(!stats.misses&&!stats.writes&&!stats.driver_misses&&stats.driver_loads);
|
||||
const unsigned initializationSubmissions=submitted()-submissions;
|
||||
std::printf("PASS: QuartzCore compiler %s pid=%d recipes=%u calls=%llu success=%llu required=%llu valid=%llu hits=%llu uuid_mutations=%llu devices=%llu artifacts=%llu/%llu/%llu driver=%llu/%llu/%llu/%llu cached_calls=%llu/%llu/%llu device_graphics=%llu cold_required=%llu cold_object=%u init_submissions=%u; no shader execution\n",argv[3],getpid(),made,
|
||||
(unsigned long long)(counts[0]-before[0]),(unsigned long long)(counts[1]-before[1]),(unsigned long long)(counts[2]-before[2]),
|
||||
(unsigned long long)(counts[3]-before[3]),(unsigned long long)(counts[4]-before[4]),(unsigned long long)(counts[5]-before[5]),(unsigned long long)counts[6],
|
||||
(unsigned long long)stats.hits,(unsigned long long)stats.misses,(unsigned long long)stats.writes,
|
||||
(unsigned long long)stats.driver_loads,(unsigned long long)stats.driver_hits,(unsigned long long)stats.driver_misses,(unsigned long long)stats.driver_saves,(unsigned long long)(counts[7]-before[7]),
|
||||
(unsigned long long)(counts[8]-before[8]),(unsigned long long)(counts[9]-before[9]),(unsigned long long)deviceCounts[0],(unsigned long long)coldRequired,(unsigned)coldObject,initializationSubmissions);
|
||||
nmb_binary_archive_release(archive);for(const auto &d:recipes){nmb_function_release(d.vertex);nmb_function_release(d.fragment);}
|
||||
nmb_library_release(library);nmb_device_release(device);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
/* Owned QEMU-only PID 1 harness. Run the actual iOS UI compiler in four cold
|
||||
* children, flush own mutable Data, then expose a bounded manifest checkpoint.
|
||||
* Original dyld/AMFI, signatures, read-only OS and exact RAM-volume selection
|
||||
* remain in force. The linked software peer executes no shader. */
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <IOKit/IOKitLib.h>
|
||||
#include <CommonCrypto/CommonDigest.h>
|
||||
#include <spawn.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
extern char **environ;
|
||||
static uint64_t result[48];
|
||||
alignas(4096) static unsigned char snapshot[4096];
|
||||
alignas(4096) static unsigned char workerOutput[4096];
|
||||
static const char *workerLog="/private/var/tmp/quartzcore-compiler-worker.log";
|
||||
static const char *driverPath="/System/Library/Extensions/OfflineTurnipCompiler/libvulkan_freedreno.dylib";
|
||||
static const char *prefix="/private/var/tmp/quartzcore-ui";
|
||||
|
||||
[[noreturn]] static void stop(unsigned failure) {
|
||||
result[1]=failure;
|
||||
if(FILE *file=fopen(workerLog,"rb")) {
|
||||
result[11]=fread(workerOutput,1,sizeof workerOutput-1,file);
|
||||
if(fgetc(file)!=EOF || ferror(file))result[11]=sizeof workerOutput;
|
||||
fclose(file);
|
||||
}
|
||||
__asm__ volatile("mov x13, %0\nmov x14, %1\nmov x15, %2\nbrk #0"::"r"(result),"r"(snapshot),"r"(workerOutput):"x13","x14","x15","memory");
|
||||
_exit(99);
|
||||
}
|
||||
static void mountCompilerView() {
|
||||
char uuid[37]={};FILE *file=fopen("/private/var/tmp/compiler-data-uuid","rb");
|
||||
if(!file)stop(60);
|
||||
const size_t count=fread(uuid,1,37,file);fclose(file);
|
||||
if(count!=36 || uuid[36])stop(61);
|
||||
CFStringRef identity=CFStringCreateWithCString(nullptr,uuid,kCFStringEncodingASCII);
|
||||
CFMutableDictionaryRef match=IOServiceMatching("IOMedia");if(!identity || !match)stop(62);
|
||||
const void *keys[]={CFSTR("UUID")},*values[]={identity};
|
||||
CFDictionaryRef properties=CFDictionaryCreate(nullptr,keys,values,1,&kCFTypeDictionaryKeyCallBacks,&kCFTypeDictionaryValueCallBacks);
|
||||
if(!properties)stop(63);
|
||||
CFDictionarySetValue(match,CFSTR("IOPropertyMatch"),properties);CFRelease(properties);
|
||||
io_iterator_t iterator=0;if(IOServiceGetMatchingServices(0,match,&iterator) || !iterator)stop(64);
|
||||
char device[64]={};unsigned found=0;
|
||||
while(io_service_t media=IOIteratorNext(iterator)) {
|
||||
++found;
|
||||
CFTypeRef observed=IORegistryEntryCreateCFProperty(media,CFSTR("UUID"),nullptr,0);
|
||||
CFTypeRef name=IORegistryEntryCreateCFProperty(media,CFSTR("BSD Name"),nullptr,0);
|
||||
if(!observed || !CFEqual(observed,identity) || !name || CFGetTypeID(name)!=CFStringGetTypeID())stop(65);
|
||||
char bsd[56]={};if(!CFStringGetCString((CFStringRef)name,bsd,sizeof bsd,kCFStringEncodingASCII) || strncmp(bsd,"disk",4))stop(66);
|
||||
for(const char *p=bsd;*p;++p)if(!((*p>='a'&&*p<='z')||(*p>='0'&&*p<='9')))stop(67);
|
||||
bool backed=false;io_registry_entry_t node=media;
|
||||
for(unsigned depth=0;node && depth<64;++depth) {
|
||||
if(IOObjectConformsTo(node,"NativeRamBlock")){backed=true;break;}
|
||||
io_registry_entry_t above=0;auto error=IORegistryEntryGetParentEntry(node,"IOService",&above);
|
||||
if(node!=media)IOObjectRelease(node);node=error?0:above;
|
||||
}
|
||||
if(node && node!=media)IOObjectRelease(node);if(!backed)stop(68);
|
||||
snprintf(device,sizeof device,"/dev/%s",bsd);CFRelease(observed);CFRelease(name);IOObjectRelease(media);
|
||||
}
|
||||
IOObjectRelease(iterator);CFRelease(identity);if(found!=1 || !device[0])stop(69);
|
||||
const char *point="/System/Library/Extensions",*directory="/private/var/tmp/native-compiler-addons";
|
||||
struct stat block={},source={},before={},after={},original={},code={};struct statfs fs={};
|
||||
if(stat(device,&block) || !S_ISBLK(block.st_mode) || stat(point,&before) || stat(directory,&source) || source.st_dev!=block.st_rdev)stop(70);
|
||||
if(mount("bindfs",point,MNT_RDONLY|MNT_UNION,(void *)directory))stop(71);
|
||||
if(statfs(point,&fs) || (fs.f_flags&(MNT_RDONLY|MNT_UNION))!=(MNT_RDONLY|MNT_UNION) ||
|
||||
strcmp(fs.f_mntfromname,directory) || strcmp(fs.f_fstypename,"bindfs") || stat(point,&after) ||
|
||||
after.st_dev==before.st_dev || stat(driverPath,&code) || code.st_dev!=after.st_dev ||
|
||||
stat("/System/Library/Extensions/AGXMetalA10.bundle/Info.plist",&original) || original.st_dev!=before.st_dev)stop(72);
|
||||
result[38]=fs.f_flags;result[39]=(uint32_t)before.st_dev;result[40]=(uint32_t)after.st_dev;
|
||||
result[41]=(uint32_t)original.st_dev;result[42]=(uint32_t)code.st_dev;result[43]=found;result[44]=1;
|
||||
}
|
||||
static std::array<unsigned char,32> digestFile(const std::filesystem::path &path,uint64_t *length) {
|
||||
struct stat st={};if(lstat(path.c_str(),&st) || !S_ISREG(st.st_mode) || st.st_size<0 || st.st_size>16*1024*1024)stop(80);
|
||||
FILE *file=fopen(path.c_str(),"rb");if(!file)stop(81);
|
||||
CC_SHA256_CTX hash;CC_SHA256_Init(&hash);unsigned char buffer[16384];uint64_t total=0;
|
||||
for(;;){const size_t n=fread(buffer,1,sizeof buffer,file);if(n){CC_SHA256_Update(&hash,buffer,(CC_LONG)n);total+=n;}if(n<sizeof buffer)break;}
|
||||
const bool good=!ferror(file) && feof(file) && total==(uint64_t)st.st_size;fclose(file);if(!good)stop(82);
|
||||
std::array<unsigned char,32> digest={};CC_SHA256_Final(digest.data(),&hash);*length=total;return digest;
|
||||
}
|
||||
static void summarize() {
|
||||
const std::filesystem::path directory=std::string(prefix)+".cache",archive=std::string(prefix)+".archive";
|
||||
std::error_code ec;std::vector<std::string> names;
|
||||
std::filesystem::recursive_directory_iterator iterator(directory,std::filesystem::directory_options::none,ec),end;
|
||||
if(ec)stop(83);
|
||||
for(;iterator!=end;iterator.increment(ec)) {
|
||||
if(ec)stop(84);
|
||||
const auto status=iterator->symlink_status(ec);if(ec || std::filesystem::is_symlink(status))stop(85);
|
||||
if(std::filesystem::is_directory(status))continue;
|
||||
if(!std::filesystem::is_regular_file(status) || names.size()>=2048)stop(86);
|
||||
auto name=iterator->path().lexically_relative(directory).generic_string();
|
||||
if(name.empty() || name.size()>192 || name.find("..")!=std::string::npos)stop(87);
|
||||
for(unsigned char c:name)if(c<33 || c>126)stop(88);
|
||||
names.push_back(name);
|
||||
}
|
||||
if(ec || names.empty())stop(89);std::sort(names.begin(),names.end());
|
||||
CC_SHA256_CTX manifest;CC_SHA256_Init(&manifest);uint64_t total=0;
|
||||
for(const auto &name:names) {
|
||||
uint64_t size=0;const auto digest=digestFile(directory/name,&size);total+=size;if(total>64*1024*1024)stop(90);
|
||||
const uint32_t length=(uint32_t)name.size();CC_SHA256_Update(&manifest,&length,sizeof length);
|
||||
CC_SHA256_Update(&manifest,name.data(),length);CC_SHA256_Update(&manifest,&size,sizeof size);CC_SHA256_Update(&manifest,digest.data(),digest.size());
|
||||
}
|
||||
uint64_t archiveBytes=0,sourceBytes=0;const auto archiveDigest=digestFile(archive,&archiveBytes);
|
||||
const auto sourceDigest=digestFile("/System/Library/Frameworks/QuartzCore.framework/default.metallib",&sourceBytes);
|
||||
auto *words=reinterpret_cast<uint64_t *>(snapshot);words[0]=0x515455495041434bull;words[1]=archiveBytes;
|
||||
memcpy(snapshot+16,archiveDigest.data(),32);words[6]=names.size();words[7]=total;
|
||||
CC_SHA256_Final(snapshot+64,&manifest);memcpy(snapshot+96,sourceDigest.data(),32);
|
||||
result[6]=128;result[7]=sourceBytes;result[8]=names.size();result[9]=total;result[10]=archiveBytes;
|
||||
int fd=open(archive.c_str(),O_RDONLY);if(fd<0 || fsync(fd))stop(91);close(fd);
|
||||
sync();result[45]=1;result[47]=1;
|
||||
}
|
||||
int main(int argc,char **argv) {
|
||||
(void)argv;result[0]=0x515443504b494f31ull;result[2]=(uint64_t)getpid();result[3]=getpagesize();result[4]=sysconf(_SC_PAGESIZE);
|
||||
if(argc!=1 || getpid()!=1 || result[3]!=4096 || result[4]!=4096)stop(1);
|
||||
mountCompilerView();
|
||||
if(setenv("MESA_SHADER_CACHE_DISABLE","true",1) || unsetenv("DYLD_INSERT_LIBRARIES") || unsetenv("HOSTSIM_PAGE_SIZE") ||
|
||||
unsetenv("NMB_TEST_GRAPHICS_CACHE_LOADER") || unsetenv("NMB_TEST_QUARTZCORE_LIBRARY"))stop(2);
|
||||
// A fresh fixture must never reuse the earlier run's products.
|
||||
struct stat old={};if(!lstat((std::string(prefix)+".archive").c_str(),&old) || errno!=ENOENT)stop(3);
|
||||
if(unlink(workerLog) && errno!=ENOENT)stop(5);
|
||||
posix_spawn_file_actions_t actions;
|
||||
if(posix_spawn_file_actions_init(&actions) || posix_spawn_file_actions_addopen(&actions,STDOUT_FILENO,workerLog,O_WRONLY|O_CREAT|O_APPEND,0600) ||
|
||||
posix_spawn_file_actions_adddup2(&actions,STDOUT_FILENO,STDERR_FILENO))stop(4);
|
||||
const char *modes[]={"produce","consume","uuid-miss","empty-driver"};
|
||||
for(unsigned i=0;i<4;++i) {
|
||||
char *arguments[]={const_cast<char *>("compiler-worker"),const_cast<char *>(driverPath),const_cast<char *>(prefix),const_cast<char *>(modes[i]),nullptr};
|
||||
pid_t child=0;int error=posix_spawn(&child,"/private/var/tmp/compiler-worker",&actions,nullptr,arguments,environ);
|
||||
result[12+3*i]=(uint64_t)child;result[13+3*i]=(uint64_t)error;if(error || child<=1)stop(10+i);
|
||||
int status=0;while(waitpid(child,&status,0)<0)if(errno!=EINTR)stop(20+i);
|
||||
result[14+3*i]=(uint64_t)status;if(!WIFEXITED(status) || WEXITSTATUS(status)!=0)stop(30+i);result[5]=i+1;
|
||||
}
|
||||
posix_spawn_file_actions_destroy(&actions);summarize();stop(0);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
/* QEMU-only first-consumer harness. Reuse the exact volume/manifest/checkpoint
|
||||
* helpers, but never call the cold producer suite. Initial Data already holds
|
||||
* the actual iOS-generated archive and caches at the consumer's normal paths. */
|
||||
#define main unused_cold_suite_main
|
||||
#include "quartzcore_cache_ios.cpp"
|
||||
#undef main
|
||||
|
||||
int main(int argc,char **argv) {
|
||||
(void)argv;result[0]=0x515443504b494f32ull;result[2]=(uint64_t)getpid();result[3]=getpagesize();result[4]=sysconf(_SC_PAGESIZE);
|
||||
if(argc!=1 || getpid()!=1 || result[3]!=4096 || result[4]!=4096)stop(1);
|
||||
mountCompilerView();
|
||||
if(setenv("MESA_SHADER_CACHE_DISABLE","true",1) || unsetenv("DYLD_INSERT_LIBRARIES") || unsetenv("HOSTSIM_PAGE_SIZE") ||
|
||||
unsetenv("NMB_TEST_GRAPHICS_CACHE_LOADER") || unsetenv("NMB_TEST_QUARTZCORE_LIBRARY"))stop(2);
|
||||
// Hash the actual initial-image bytes before any compiler worker is spawned.
|
||||
summarize();std::array<unsigned char,128> initial={};memcpy(initial.data(),snapshot,initial.size());
|
||||
if(unlink(workerLog) && errno!=ENOENT)stop(5);
|
||||
posix_spawn_file_actions_t actions;
|
||||
if(posix_spawn_file_actions_init(&actions) || posix_spawn_file_actions_addopen(&actions,STDOUT_FILENO,workerLog,O_WRONLY|O_CREAT|O_APPEND,0600) ||
|
||||
posix_spawn_file_actions_adddup2(&actions,STDOUT_FILENO,STDERR_FILENO))stop(4);
|
||||
const char *modes[]={"consume","uuid-miss","empty-driver"};
|
||||
for(unsigned i=0;i<3;++i) {
|
||||
char *arguments[]={const_cast<char *>("compiler-worker"),const_cast<char *>(driverPath),const_cast<char *>(prefix),const_cast<char *>(modes[i]),nullptr};
|
||||
pid_t child=0;int error=posix_spawn(&child,"/private/var/tmp/compiler-worker",&actions,nullptr,arguments,environ);
|
||||
result[12+3*i]=(uint64_t)child;result[13+3*i]=(uint64_t)error;if(error || child<=1)stop(10+i);
|
||||
int status=0;while(waitpid(child,&status,0)<0)if(errno!=EINTR)stop(20+i);
|
||||
result[14+3*i]=(uint64_t)status;if(!WIFEXITED(status) || WEXITSTATUS(status)!=0)stop(30+i);result[5]=i+1;
|
||||
}
|
||||
posix_spawn_file_actions_destroy(&actions);summarize();
|
||||
if(memcmp(initial.data(),snapshot,initial.size()))stop(95);
|
||||
memcpy(snapshot+256,initial.data(),initial.size());result[6]=384;result[45]=2;result[46]=1;stop(0);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Actual host GPU work waits on an unsignaled event while every caller-owned
|
||||
// no-copy buffer/view reference is dropped. Its deallocator must not run yet.
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Metal/Metal.h>
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#include "../NativeBridgeMetalFrontend.h"
|
||||
#include "NativeMetalObjects.h"
|
||||
|
||||
struct State {std::atomic<unsigned> released{0};};
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 3) return 2;
|
||||
@autoreleasepool {
|
||||
id<MTLDevice> device = native_bridge_metal_open(argv[1], argv[2], 0x6e6d62, nullptr);
|
||||
if (!device) return 2;
|
||||
unsigned failures = 0;
|
||||
size_t airBytes = 0;
|
||||
const void *air = nmb_fixture_air(&airBytes);
|
||||
id<MTLLibrary> library = native_bridge_metal_library_with_air(air, airBytes);
|
||||
id<MTLComputePipelineState> pipeline = [device newComputePipelineStateWithFunction:[library newFunctionWithName:@"scale_and_offset"] error:nil];
|
||||
if (!pipeline) return 2;
|
||||
for (unsigned variant = 0; variant < 7; ++variant) {
|
||||
auto state = std::make_shared<State>();
|
||||
const size_t page = (size_t)getpagesize();
|
||||
void *pages = mmap(nullptr, page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
|
||||
if (pages == MAP_FAILED) return 2;
|
||||
memset(pages, variant == 6 ? 0xcc : 0x5a, page);
|
||||
if (variant == 5) for (unsigned i = 0; i < 256; ++i) ((float *)pages)[i] = (float)i;
|
||||
id<MTLBuffer> output = [device newBufferWithLength:4096 options:MTLResourceStorageModeShared];
|
||||
id<MTLSharedEvent> event = [device newSharedEvent];
|
||||
id<MTLCommandBuffer> commands = [[device newCommandQueue] commandBuffer];
|
||||
__weak id weakBuffer;
|
||||
if (!output || !event || !commands) return 2;
|
||||
@autoreleasepool {
|
||||
// Deliberately keep the mapping owned by the test, so the old
|
||||
// early-deallocator failure cannot make the pending GPU read
|
||||
// unmapped memory. No pixel or GPU completion is synthesized.
|
||||
id<MTLBuffer> buffer = [device newBufferWithBytesNoCopy:pages length:page options:MTLResourceStorageModeShared
|
||||
deallocator:^(void *, NSUInteger){state->released.fetch_add(1);}];
|
||||
if (!buffer) return 2;
|
||||
weakBuffer = buffer;
|
||||
[commands encodeWaitForEvent:event value:1];
|
||||
if (variant == 5) {
|
||||
id<MTLComputeCommandEncoder> compute = [commands computeCommandEncoder];
|
||||
[compute setComputePipelineState:pipeline];
|
||||
[compute setBuffer:buffer offset:0 atIndex:0];
|
||||
[compute setBuffer:output offset:0 atIndex:1];
|
||||
const float scale = 2;
|
||||
[compute setBytes:&scale length:sizeof scale atIndex:2];
|
||||
[compute dispatchThreads:MTLSizeMake(256,1,1) threadsPerThreadgroup:MTLSizeMake(64,1,1)];
|
||||
[compute endEncoding];
|
||||
} else if (variant == 1 || variant == 6) {
|
||||
auto d = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:16 height:16 mipmapped:NO];
|
||||
d.storageMode = MTLStorageModeShared;
|
||||
d.usage = MTLTextureUsageShaderRead | MTLTextureUsagePixelFormatView | MTLTextureUsageRenderTarget;
|
||||
id<MTLTexture> root = [buffer newTextureWithDescriptor:d offset:0 bytesPerRow:256];
|
||||
id<MTLTexture> view = [[root newTextureViewWithPixelFormat:MTLPixelFormatRGBA8Unorm]
|
||||
newTextureViewWithPixelFormat:MTLPixelFormatRGBA8Unorm];
|
||||
if (!view) return 2;
|
||||
if (variant == 6) {
|
||||
auto pass = [MTLRenderPassDescriptor renderPassDescriptor];
|
||||
pass.colorAttachments[0].texture = view;
|
||||
pass.colorAttachments[0].loadAction = MTLLoadActionClear;
|
||||
pass.colorAttachments[0].storeAction = MTLStoreActionStore;
|
||||
pass.colorAttachments[0].clearColor = MTLClearColorMake(90.0/255,90.0/255,90.0/255,90.0/255);
|
||||
[[commands renderCommandEncoderWithDescriptor:pass] endEncoding];
|
||||
}
|
||||
if (variant == 1) {
|
||||
id<MTLBlitCommandEncoder> blit = [commands blitCommandEncoder];
|
||||
[blit copyFromTexture:view sourceSlice:0 sourceLevel:0 sourceOrigin:MTLOriginMake(0,0,0) sourceSize:MTLSizeMake(16,16,1)
|
||||
toBuffer:output destinationOffset:0 destinationBytesPerRow:64 destinationBytesPerImage:1024];
|
||||
[blit endEncoding];
|
||||
}
|
||||
} else {
|
||||
id<MTLBlitCommandEncoder> blit = [commands blitCommandEncoder];
|
||||
if (variant == 2) [blit fillBuffer:buffer range:NSMakeRange(0, 1024) value:0x37];
|
||||
[blit copyFromBuffer:buffer sourceOffset:0 toBuffer:output destinationOffset:0 size:1024];
|
||||
id<MTLBuffer> missing = nil;
|
||||
if (variant == 3) [blit fillBuffer:missing range:NSMakeRange(0, 16) value:0]; // local refusal, never submit
|
||||
[blit endEncoding];
|
||||
}
|
||||
}
|
||||
const unsigned before = state->released.load();
|
||||
if (before || !weakBuffer) ++failures;
|
||||
unsigned pending = before;
|
||||
if (variant != 4) {
|
||||
[commands commit];
|
||||
// Completion must remain pending until the actual event is
|
||||
// signaled, except a locally refused command which submitted
|
||||
// no GPU work. Check with bounded dispatch waits.
|
||||
auto finished = dispatch_semaphore_create(0);
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
|
||||
@autoreleasepool {[commands waitUntilCompleted];}
|
||||
dispatch_semaphore_signal(finished);
|
||||
});
|
||||
const bool early = dispatch_semaphore_wait(finished, dispatch_time(DISPATCH_TIME_NOW, 100 * NSEC_PER_MSEC)) == 0;
|
||||
pending = state->released.load();
|
||||
if (variant != 3 && (early || pending || !weakBuffer)) ++failures;
|
||||
event.signaledValue = 1;
|
||||
if (!early && dispatch_semaphore_wait(finished, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC))) return 1;
|
||||
if (commands.status != (variant == 3 ? MTLCommandBufferStatusError : MTLCommandBufferStatusCompleted)) ++failures;
|
||||
if (variant != 3) for (unsigned i = 0; i < (variant == 5 ? 256 : 1024); ++i) {
|
||||
const bool bad = variant == 5 ? ((const float *)output.contents)[i] != 3.0f*i
|
||||
: variant == 6 ? ((const unsigned char *)pages)[(i / 64) * 256 + i % 64] != 0x5a
|
||||
: ((const unsigned char *)output.contents)[i] != (variant == 2 ? 0x37 : 0x5a);
|
||||
if (bad) {++failures;break;}
|
||||
}
|
||||
if (state->released.load() != 1 || weakBuffer) ++failures; // even while commands remains retained
|
||||
}
|
||||
commands = nil;
|
||||
// The real completion callback can release its own reference just
|
||||
// after waitUntilCompleted returns. Bound that scheduling margin.
|
||||
for (unsigned i = 0; i < 300 && state->released.load() == 0; ++i) usleep(1000);
|
||||
if (state->released.load() != 1 || weakBuffer) ++failures;
|
||||
std::printf("case %u before=%u pending=%u released=%u\n", variant, before, pending, state->released.load());
|
||||
munmap(pages, page);
|
||||
}
|
||||
if (failures) {std::printf("FAIL: resource owner lifetime (%u checks)\n", failures);return 1;}
|
||||
std::puts("PASS: no-copy owners/views survive encoding and gated GPU work, release once after completion/refusal/discard; actual GPU bytes match");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Keep real work pending behind an unsignaled shared event. Waiting for an unrelated
|
||||
// resource must return; waiting for its storage, views or IOSurface alias must not.
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Metal/Metal.h>
|
||||
#import <IOSurface/IOSurfaceRef.h>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#include "../NativeBridgeMetalFrontend.h"
|
||||
|
||||
@protocol ResourceWait
|
||||
- (void)waitUntilComplete;
|
||||
@end
|
||||
|
||||
static dispatch_time_t seconds(unsigned n) {return dispatch_time(DISPATCH_TIME_NOW, n * NSEC_PER_SEC);}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 3) return 2;
|
||||
@autoreleasepool {
|
||||
id<MTLDevice> device = native_bridge_metal_open(argv[1], argv[2], 0x6e6d62, nullptr);
|
||||
if (!device) return 2;
|
||||
for (unsigned variant = 0; variant < 9; ++variant) {
|
||||
@autoreleasepool {
|
||||
id<MTLBuffer> output = [device newBufferWithLength:4096 options:MTLResourceStorageModeShared];
|
||||
id<MTLBuffer> input = [device newBufferWithLength:4096 options:MTLResourceStorageModeShared];
|
||||
id query = output;
|
||||
id<MTLTexture> target = nil;
|
||||
IOSurfaceRef surface = nullptr;
|
||||
void *pages = nullptr;
|
||||
auto td = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:16 height:16 mipmapped:NO];
|
||||
td.storageMode = MTLStorageModeShared;
|
||||
td.usage = MTLTextureUsageShaderRead | MTLTextureUsageShaderWrite | MTLTextureUsagePixelFormatView;
|
||||
if (variant == 0) query = input; // never used by the pending fill
|
||||
if (variant == 2) {
|
||||
id<MTLTexture> texture = [output newTextureWithDescriptor:td offset:0 bytesPerRow:256];
|
||||
query = [texture newTextureViewWithPixelFormat:MTLPixelFormatRGBA8Unorm];
|
||||
}
|
||||
if (variant == 3 || variant == 7) {
|
||||
if (variant == 7) td.storageMode = MTLStorageModePrivate;
|
||||
target = [device newTextureWithDescriptor:td];
|
||||
query = [[target newTextureViewWithPixelFormat:MTLPixelFormatRGBA8Unorm] newTextureViewWithPixelFormat:MTLPixelFormatRGBA8Unorm];
|
||||
}
|
||||
if (variant == 4) {
|
||||
NSDictionary *p = @{(id)kIOSurfaceWidth:@16, (id)kIOSurfaceHeight:@16,
|
||||
(id)kIOSurfaceBytesPerElement:@4, (id)kIOSurfacePixelFormat:@((uint32_t)0x52474241)};
|
||||
surface = IOSurfaceCreate((__bridge CFDictionaryRef)p);
|
||||
target = [device newTextureWithDescriptor:td iosurface:surface plane:0];
|
||||
query = [device newTextureWithDescriptor:td iosurface:surface plane:0];
|
||||
}
|
||||
if (variant == 5) {
|
||||
auto hd = [MTLHeapDescriptor new];hd.size = 65536;hd.storageMode = MTLStorageModeShared;
|
||||
id<MTLHeap> heap = [device newHeapWithDescriptor:hd];
|
||||
output = [heap newBufferWithLength:4096 options:MTLResourceStorageModeShared];
|
||||
query = [heap newBufferWithLength:4096 options:MTLResourceStorageModeShared];
|
||||
}
|
||||
if (variant == 6) {
|
||||
const size_t page = (size_t)getpagesize();
|
||||
pages = mmap(nullptr, page * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
|
||||
if (pages == MAP_FAILED) return 2;
|
||||
auto keep = std::shared_ptr<void>(pages, [page](void *p){munmap(p, page * 2);});
|
||||
auto release = ^(void *, NSUInteger){(void)keep;};
|
||||
output = [device newBufferWithBytesNoCopy:pages length:page * 2 options:MTLResourceStorageModeShared deallocator:release];
|
||||
query = [device newBufferWithBytesNoCopy:(char *)pages + page length:page options:MTLResourceStorageModeShared deallocator:release];
|
||||
}
|
||||
if (variant == 8) {
|
||||
output = [device newBufferWithLength:4096 options:MTLResourceStorageModePrivate];
|
||||
query = output;
|
||||
}
|
||||
if (!output || !input || !query || ((variant == 3 || variant == 4 || variant == 7) && !target)) {
|
||||
std::printf("FAIL resource allocation %u\n", variant);return 2;
|
||||
}
|
||||
memset(input.contents, 0x5a, 4096);
|
||||
id<MTLSharedEvent> event = [device newSharedEvent];
|
||||
id<MTLCommandBuffer> commands = [[device newCommandQueue] commandBuffer];
|
||||
[commands encodeWaitForEvent:event value:1];
|
||||
id<MTLBlitCommandEncoder> blit = [commands blitCommandEncoder];
|
||||
if (target) [blit copyFromBuffer:input sourceOffset:0 sourceBytesPerRow:64 sourceBytesPerImage:1024 sourceSize:MTLSizeMake(16,16,1)
|
||||
toTexture:target destinationSlice:0 destinationLevel:0 destinationOrigin:MTLOriginMake(0,0,0)];
|
||||
else [blit fillBuffer:output range:NSMakeRange(0, output.length) value:0x5a];
|
||||
[blit endEncoding];[commands commit];
|
||||
auto entered = dispatch_semaphore_create(0), returned = dispatch_semaphore_create(0);
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
|
||||
dispatch_semaphore_signal(entered);
|
||||
[(id<ResourceWait>)query waitUntilComplete];
|
||||
dispatch_semaphore_signal(returned);
|
||||
});
|
||||
if (dispatch_semaphore_wait(entered, seconds(3))) return 1;
|
||||
const bool early = dispatch_semaphore_wait(returned, dispatch_time(DISPATCH_TIME_NOW, 250 * NSEC_PER_MSEC)) == 0;
|
||||
event.signaledValue = 1;
|
||||
if (!early && dispatch_semaphore_wait(returned, seconds(3))) {std::puts("FAIL resource wait stuck");return 1;}
|
||||
[commands waitUntilCompleted];
|
||||
if (commands.status != MTLCommandBufferStatusCompleted || early != (variant == 0)) {
|
||||
std::printf("FAIL variant=%u early=%d status=%lu\n", variant, early, (unsigned long)commands.status);return 1;
|
||||
}
|
||||
if (target) {
|
||||
unsigned char pixels[1024] = {};
|
||||
if (variant == 7) {
|
||||
memset(input.contents, 0, 4096);
|
||||
id<MTLCommandBuffer> readback = [[device newCommandQueue] commandBuffer];
|
||||
id<MTLBlitCommandEncoder> copy = [readback blitCommandEncoder];
|
||||
[copy copyFromTexture:target sourceSlice:0 sourceLevel:0 sourceOrigin:MTLOriginMake(0,0,0) sourceSize:MTLSizeMake(16,16,1)
|
||||
toBuffer:input destinationOffset:0 destinationBytesPerRow:64 destinationBytesPerImage:1024];
|
||||
[copy endEncoding];[readback commit];[readback waitUntilCompleted];
|
||||
if (readback.status != MTLCommandBufferStatusCompleted) return 1;
|
||||
memcpy(pixels, input.contents, sizeof pixels);
|
||||
} else [target getBytes:pixels bytesPerRow:64 fromRegion:MTLRegionMake2D(0,0,16,16) mipmapLevel:0];
|
||||
for (unsigned char pixel : pixels) if (pixel != 0x5a) return 1;
|
||||
} else if (variant == 8) {
|
||||
memset(input.contents, 0, 4096);
|
||||
id<MTLCommandBuffer> readback = [[device newCommandQueue] commandBuffer];
|
||||
id<MTLBlitCommandEncoder> copy = [readback blitCommandEncoder];
|
||||
[copy copyFromBuffer:output sourceOffset:0 toBuffer:input destinationOffset:0 size:4096];
|
||||
[copy endEncoding];[readback commit];[readback waitUntilCompleted];
|
||||
if (readback.status != MTLCommandBufferStatusCompleted) return 1;
|
||||
for (NSUInteger i = 0; i < 4096; ++i) if (((const unsigned char *)input.contents)[i] != 0x5a) return 1;
|
||||
} else for (NSUInteger i = 0; i < output.length; ++i) if (((const unsigned char *)output.contents)[i] != 0x5a) return 1;
|
||||
// The shared deallocator owns no-copy pages until both imports release them.
|
||||
query = nil;output = nil;target = nil;
|
||||
if (surface) CFRelease(surface);
|
||||
}
|
||||
}
|
||||
std::puts("PASS: unrelated resource wait returns while work is gated; storage, views, heap and IOSurface/no-copy aliases wait for real completion");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// A blocked completed handler must not hold waitUntilScheduled. No hardware timing claim:
|
||||
// the semaphores create a deterministic distinction between scheduling and completion.
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Metal/Metal.h>
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <initializer_list>
|
||||
#include "../NativeBridgeMetalFrontend.h"
|
||||
|
||||
static dispatch_time_t deadline() {return dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);}
|
||||
__attribute__((noinline)) static id nothing() {return nil;}
|
||||
@protocol CompletionQueue
|
||||
- (void)setCompletionQueue:(dispatch_queue_t)queue;
|
||||
@end
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 3) return 2;
|
||||
@autoreleasepool {
|
||||
id<MTLDevice> device = native_bridge_metal_open(argv[1], argv[2], 0x6e6d62, nullptr);
|
||||
if (!device) return 2;
|
||||
id<MTLCommandQueue> queue = [device newCommandQueue];
|
||||
id<MTLBuffer> buffer = [device newBufferWithLength:256 options:MTLResourceStorageModeShared];
|
||||
if (!queue || !buffer) return 2;
|
||||
for (unsigned variant : {0u, 1u, 2u, 3u}) {
|
||||
const bool refused = variant == 1 || variant == 3;
|
||||
[(id<CompletionQueue>)queue setCompletionQueue:variant == 2 ? dispatch_queue_create("scheduled-test", DISPATCH_QUEUE_SERIAL) : nil];
|
||||
id<MTLCommandBuffer> cb = [queue commandBuffer];
|
||||
auto entered = dispatch_semaphore_create(0), release = dispatch_semaphore_create(0);
|
||||
auto returned = dispatch_semaphore_create(0);
|
||||
std::atomic<unsigned> scheduledCount{0}, completedCount{0};
|
||||
auto scheduled = &scheduledCount, completed = &completedCount;
|
||||
[cb addScheduledHandler:^(id<MTLCommandBuffer> command) {
|
||||
// This also catches callbacks deferred to Completed by the frontend.
|
||||
if (command.status == MTLCommandBufferStatusScheduled) ++*scheduled;
|
||||
}];
|
||||
[cb addCompletedHandler:^(id<MTLCommandBuffer>) {
|
||||
++*completed;
|
||||
dispatch_semaphore_signal(entered);
|
||||
dispatch_semaphore_wait(release, deadline());
|
||||
}];
|
||||
id<MTLBlitCommandEncoder> blit = [cb blitCommandEncoder];
|
||||
if (variant == 1) {
|
||||
// A frontend encoding failure has no scheduling event to announce.
|
||||
[blit fillBuffer:(id<MTLBuffer>)nothing() range:NSMakeRange(0, 16) value:0];
|
||||
} else {
|
||||
[blit fillBuffer:buffer range:NSMakeRange(0, 256) value:0x5a];
|
||||
}
|
||||
if (variant != 3) [blit endEncoding]; // variant 3: runtime refuses commit with an encoder open
|
||||
[cb commit];
|
||||
if (dispatch_semaphore_wait(entered, deadline())) {std::puts("FAIL completion never arrived");return 1;}
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
|
||||
[cb waitUntilScheduled];
|
||||
dispatch_semaphore_signal(returned);
|
||||
});
|
||||
const bool independent = dispatch_semaphore_wait(returned, dispatch_time(DISPATCH_TIME_NOW, 300 * NSEC_PER_MSEC)) == 0;
|
||||
dispatch_semaphore_signal(release);
|
||||
if (!independent && dispatch_semaphore_wait(returned, deadline())) {std::puts("FAIL scheduled wait stuck");return 1;}
|
||||
[cb waitUntilCompleted];
|
||||
if (variant == 3) [blit endEncoding];
|
||||
if (!independent || completedCount != 1 || scheduledCount != (refused ? 0u : 1u) ||
|
||||
cb.status != (refused ? MTLCommandBufferStatusError : MTLCommandBufferStatusCompleted)) {
|
||||
std::printf("FAIL variant=%u independent=%d scheduled=%u completed=%u status=%lu\n",
|
||||
variant, independent, scheduledCount.load(), completedCount.load(), (unsigned long)cb.status);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
std::puts("PASS: scheduling precedes completion on runtime/serial queues; scheduled waits ignore blocked completed handlers; frontend/runtime refusals announce no scheduling");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Count actual frontend cache-maintenance calls while forwarding every call
|
||||
// to libSystem. GPU writes and alias reads must still reach the real pages.
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Metal/Metal.h>
|
||||
#import <IOSurface/IOSurfaceRef.h>
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
#include <vector>
|
||||
#include "../NativeBridgeMetalFrontend.h"
|
||||
|
||||
static std::atomic<uint64_t> calls, bytes;
|
||||
extern "C" void sys_dcache_flush(void *address, size_t length) {
|
||||
using Flush = void (*)(void *, size_t);
|
||||
static Flush real = reinterpret_cast<Flush>(dlsym(RTLD_NEXT, "sys_dcache_flush"));
|
||||
if (!real || real == sys_dcache_flush) std::abort();
|
||||
calls++;bytes += length;
|
||||
real(address, length);
|
||||
}
|
||||
|
||||
static bool run(id<MTLDevice> device, unsigned aliases, unsigned allocations) {
|
||||
@autoreleasepool {
|
||||
constexpr unsigned width = 64, height = 16, row = width * 4, frameBytes = row * height;
|
||||
std::vector<IOSurfaceRef> surfaces;
|
||||
auto textures = [NSMutableArray array];
|
||||
auto descriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
|
||||
width:width height:height mipmapped:NO];
|
||||
descriptor.storageMode = MTLStorageModeShared;
|
||||
descriptor.usage = MTLTextureUsageShaderRead | MTLTextureUsageShaderWrite | MTLTextureUsagePixelFormatView;
|
||||
for (unsigned i = 0; i < allocations; ++i) {
|
||||
NSDictionary *properties = @{(id)kIOSurfaceWidth:@(width), (id)kIOSurfaceHeight:@(height),
|
||||
(id)kIOSurfaceBytesPerElement:@4, (id)kIOSurfaceBytesPerRow:@(row),
|
||||
(id)kIOSurfacePixelFormat:@((uint32_t)0x52474241)};
|
||||
IOSurfaceRef surface = IOSurfaceCreate((__bridge CFDictionaryRef)properties);
|
||||
if (!surface || IOSurfaceLock(surface, 0, nullptr)) return false;
|
||||
memset(IOSurfaceGetBaseAddress(surface), i ? 0xa7 : 0xcc, IOSurfaceGetAllocSize(surface));
|
||||
if (IOSurfaceUnlock(surface, 0, nullptr)) return false;
|
||||
surfaces.push_back(surface);
|
||||
}
|
||||
for (unsigned i = 0; i < aliases; ++i) {
|
||||
// Include independent imports as well as nested views of an import.
|
||||
id<MTLTexture> texture = [device newTextureWithDescriptor:descriptor iosurface:surfaces[i % allocations] plane:0];
|
||||
if (i & 1) texture = [[texture newTextureViewWithPixelFormat:MTLPixelFormatRGBA8Unorm]
|
||||
newTextureViewWithPixelFormat:MTLPixelFormatRGBA8Unorm];
|
||||
if (!texture) return false;
|
||||
[textures addObject:texture];
|
||||
}
|
||||
std::vector<unsigned char> upload(frameBytes, 0x5a);
|
||||
id<MTLBuffer> input = [device newBufferWithBytes:upload.data() length:frameBytes options:MTLResourceStorageModeShared];
|
||||
id<MTLBuffer> output = [device newBufferWithLength:aliases * frameBytes options:MTLResourceStorageModeShared];
|
||||
id<MTLCommandBuffer> command = [[device newCommandQueue] commandBuffer];
|
||||
id<MTLBlitCommandEncoder> blit = [command blitCommandEncoder];
|
||||
if (!input || !output || !blit) return false;
|
||||
[blit copyFromBuffer:input sourceOffset:0 sourceBytesPerRow:row sourceBytesPerImage:frameBytes
|
||||
sourceSize:MTLSizeMake(width,height,1) toTexture:textures[0] destinationSlice:0 destinationLevel:0
|
||||
destinationOrigin:MTLOriginMake(0,0,0)];
|
||||
for (unsigned i = 0; i < aliases; ++i)
|
||||
[blit copyFromTexture:textures[i] sourceSlice:0 sourceLevel:0 sourceOrigin:MTLOriginMake(0,0,0)
|
||||
sourceSize:MTLSizeMake(width,height,1) toBuffer:output destinationOffset:i * frameBytes
|
||||
destinationBytesPerRow:row destinationBytesPerImage:frameBytes];
|
||||
[blit endEncoding];
|
||||
const uint64_t beforeCalls = calls.load(), beforeBytes = bytes.load();
|
||||
[command commit];[command waitUntilCompleted];
|
||||
uint64_t expectedBytes = 0;
|
||||
for (IOSurfaceRef surface : surfaces) expectedBytes += 2 * IOSurfaceGetAllocSize(surface);
|
||||
bool ok = command.status == MTLCommandBufferStatusCompleted;
|
||||
for (unsigned i = 0; i < aliases; ++i) for (unsigned j = 0; j < frameBytes; ++j)
|
||||
if (((const unsigned char *)output.contents)[i * frameBytes + j] != (i % allocations ? 0xa7 : 0x5a)) ok = false;
|
||||
// The GPU also changed the first surface; check its actual shared bytes.
|
||||
for (unsigned y = 0; y < height; ++y) for (unsigned x = 0; x < row; ++x)
|
||||
if (((const unsigned char *)IOSurfaceGetBaseAddress(surfaces[0]))[y * IOSurfaceGetBytesPerRow(surfaces[0]) + x] != 0x5a) ok = false;
|
||||
const uint64_t actualCalls = calls.load() - beforeCalls, actualBytes = bytes.load() - beforeBytes;
|
||||
std::printf("aliases=%u allocations=%u flushes=%llu bytes=%llu expected=%u/%llu pixels=%s\n", aliases, allocations,
|
||||
(unsigned long long)actualCalls, (unsigned long long)actualBytes, 2 * allocations,
|
||||
(unsigned long long)expectedBytes, ok ? "pass" : "FAIL");
|
||||
ok &= actualCalls == 2 * allocations && actualBytes == expectedBytes;
|
||||
for (IOSurfaceRef surface : surfaces) CFRelease(surface);
|
||||
return ok;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 3) return 2;
|
||||
@autoreleasepool {
|
||||
id<MTLDevice> device = native_bridge_metal_open(argv[1], argv[2], 0x6e6d62, nullptr);
|
||||
if (!device) return 2;
|
||||
bool ok = run(device, 1, 1);
|
||||
ok &= run(device, 128, 1);
|
||||
ok &= run(device, 128, 2);
|
||||
char telemetry[6144] = {};
|
||||
native_bridge_metal_telemetry(telemetry, sizeof telemetry);
|
||||
const char *line = strstr(telemetry, "J surface cache ");
|
||||
unsigned long long count = 0, maintained = 0, duplicates = 0;
|
||||
if (!line || sscanf(line, "J surface cache n=%llu bytes=%llu dup=%llu", &count, &maintained, &duplicates) != 3 ||
|
||||
count != calls.load() || maintained != bytes.load() || duplicates != 506) ok = false;
|
||||
if (!ok) {std::puts("FAIL: duplicate IOSurface cache maintenance");return 1;}
|
||||
std::puts("PASS: each live surface range flushed once per phase, independent imports/views deduplicated, distinct surfaces and GPU bytes preserved");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Count real frontend no-copy imports/releases, forwarding every call to the
|
||||
// unmodified bridge. Use actual GPU pixels and bounded pending-work lifetimes.
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Metal/Metal.h>
|
||||
#import <IOSurface/IOSurfaceRef.h>
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
#include <unistd.h>
|
||||
#include "../NativeBridgeMetalFrontend.h"
|
||||
#include "NativeMetalObjects.h"
|
||||
|
||||
static std::atomic<unsigned> imports, releases;
|
||||
static std::mutex handlesLock;
|
||||
static std::set<NmbBuffer *> handles;
|
||||
static uint64_t (*vkImports)();
|
||||
extern "C" NmbBuffer *tracked_surface_import(NmbDevice *d, void *p, uint64_t n, uint64_t options) {
|
||||
NmbBuffer *buffer = nmb_buffer_new_no_copy(d,p,n,options);
|
||||
if (buffer) {std::lock_guard<std::mutex> lock(handlesLock);handles.insert(buffer);imports++;}
|
||||
return buffer;
|
||||
}
|
||||
extern "C" void tracked_buffer_release(NmbBuffer *buffer) {
|
||||
{std::lock_guard<std::mutex> lock(handlesLock);if(handles.erase(buffer))releases++;}
|
||||
nmb_buffer_release(buffer);
|
||||
}
|
||||
static bool drained(unsigned expected) {
|
||||
for(unsigned i=0;i<300 && releases.load()!=expected;++i)usleep(1000);
|
||||
return releases.load()==expected;
|
||||
}
|
||||
static IOSurfaceRef makeSurface(unsigned byte) {
|
||||
IOSurfaceRef surface=IOSurfaceCreate((__bridge CFDictionaryRef)@{
|
||||
(id)kIOSurfaceWidth:@64,(id)kIOSurfaceHeight:@16,(id)kIOSurfaceBytesPerElement:@4,
|
||||
(id)kIOSurfaceBytesPerRow:@256,(id)kIOSurfacePixelFormat:@((uint32_t)0x52474241)});
|
||||
if(!surface || IOSurfaceLock(surface,0,nullptr))return nullptr;
|
||||
memset(IOSurfaceGetBaseAddress(surface),byte,IOSurfaceGetAllocSize(surface));
|
||||
if(IOSurfaceUnlock(surface,0,nullptr)){CFRelease(surface);return nullptr;}
|
||||
return surface;
|
||||
}
|
||||
static MTLTextureDescriptor *descriptor() {
|
||||
auto d=[MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:64 height:16 mipmapped:NO];
|
||||
d.storageMode=MTLStorageModeShared;
|
||||
d.usage=MTLTextureUsageShaderRead|MTLTextureUsageShaderWrite|MTLTextureUsagePixelFormatView;
|
||||
return d;
|
||||
}
|
||||
|
||||
static bool aliases(id<MTLDevice> device,unsigned count,unsigned allocations,bool parallel) {
|
||||
const unsigned before=imports.load(),released=releases.load();bool ok=true;
|
||||
const uint64_t vkBefore=vkImports?vkImports():0;
|
||||
@autoreleasepool {
|
||||
std::vector<IOSurfaceRef> surfaces;
|
||||
for(unsigned i=0;i<allocations;++i){auto s=makeSurface(i?0xa7:0xcc);if(!s)return false;surfaces.push_back(s);}
|
||||
auto textures=[NSMutableArray array];auto d=descriptor();
|
||||
if(parallel) {
|
||||
std::atomic<unsigned> failures{0};auto *failed=&failures;
|
||||
const IOSurfaceRef surface=surfaces[0];
|
||||
dispatch_apply(count,dispatch_get_global_queue(QOS_CLASS_DEFAULT,0),^(size_t i){
|
||||
(void)i;
|
||||
@autoreleasepool {
|
||||
id<MTLTexture> texture=[device newTextureWithDescriptor:d iosurface:surface plane:0];
|
||||
if(!texture){(*failed)++;return;}
|
||||
@synchronized(textures){[textures addObject:texture];}
|
||||
}
|
||||
});
|
||||
ok=failures.load()==0;
|
||||
} else for(unsigned i=0;i<count;++i) {
|
||||
id<MTLTexture> texture=[device newTextureWithDescriptor:d iosurface:surfaces[i%allocations] plane:0];
|
||||
if(i&1)texture=[[texture newTextureViewWithPixelFormat:MTLPixelFormatRGBA8Unorm] newTextureViewWithPixelFormat:MTLPixelFormatRGBA8Unorm];
|
||||
if(!texture)return false;[textures addObject:texture];
|
||||
}
|
||||
if(textures.count!=count)return false;
|
||||
std::vector<unsigned char> data(4096,0x5a);
|
||||
id<MTLBuffer> input=[device newBufferWithBytes:data.data() length:data.size() options:MTLResourceStorageModeShared];
|
||||
id<MTLBuffer> output=[device newBufferWithLength:count*4096 options:MTLResourceStorageModeShared];
|
||||
id<MTLCommandBuffer> command=[[device newCommandQueue] commandBuffer];id<MTLBlitCommandEncoder> copy=[command blitCommandEncoder];
|
||||
if(!input || !output || !copy)return false;
|
||||
[copy copyFromBuffer:input sourceOffset:0 sourceBytesPerRow:256 sourceBytesPerImage:4096 sourceSize:MTLSizeMake(64,16,1)
|
||||
toTexture:textures[0] destinationSlice:0 destinationLevel:0 destinationOrigin:MTLOriginMake(0,0,0)];
|
||||
for(unsigned i=0;i<count;++i)
|
||||
[copy copyFromTexture:textures[i] sourceSlice:0 sourceLevel:0 sourceOrigin:MTLOriginMake(0,0,0) sourceSize:MTLSizeMake(64,16,1)
|
||||
toBuffer:output destinationOffset:i*4096 destinationBytesPerRow:256 destinationBytesPerImage:4096];
|
||||
[copy endEncoding];[command commit];[command waitUntilCompleted];
|
||||
ok &= command.status==MTLCommandBufferStatusCompleted;
|
||||
bool mismatch=false;
|
||||
for(unsigned i=0;i<count;++i)for(unsigned j=0;j<4096;++j)
|
||||
if(((const unsigned char *)output.contents)[i*4096+j]!=(i%allocations?0xa7:0x5a)) {
|
||||
if(!mismatch)std::printf("first mismatch alias=%u byte=%u actual=%02x expected=%02x\n",i,j,
|
||||
((const unsigned char *)output.contents)[i*4096+j],i%allocations?0xa7:0x5a);
|
||||
mismatch=true;ok=false;
|
||||
}
|
||||
for(IOSurfaceRef surface:surfaces)CFRelease(surface);
|
||||
}
|
||||
const unsigned made=imports.load()-before;
|
||||
const bool retired=drained(released+made);
|
||||
// More than the metadata bound may evict live weak associations. The
|
||||
// runtime must still share each live import and retire every real owner.
|
||||
const bool reuse=allocations>256?(made>=allocations && made<=count):made==allocations;
|
||||
const uint64_t actualVk=vkImports?vkImports()-vkBefore:0;
|
||||
std::printf("aliases=%u surfaces=%u parallel=%d imports=%u releases=%u vk_imports=%llu gpu=%s retired=%s reuse=%s\n",
|
||||
count,allocations,parallel,made,releases.load()-released,(unsigned long long)actualVk,ok?"pass":"FAIL",retired?"pass":"FAIL",reuse?"pass":"FAIL");
|
||||
return ok && retired && reuse && (!vkImports || actualVk==allocations);
|
||||
}
|
||||
|
||||
static bool expires(id<MTLDevice> device) {
|
||||
const unsigned before=imports.load(),released=releases.load();IOSurfaceRef surface=makeSurface(0x35);if(!surface)return false;
|
||||
bool ok=true;
|
||||
for(unsigned i=0;i<2;++i) {
|
||||
@autoreleasepool {id<MTLTexture> texture=[device newTextureWithDescriptor:descriptor() iosurface:surface plane:0];if(!texture)ok=false;}
|
||||
ok &= drained(released+i+1) && imports.load()==before+i+1;
|
||||
}
|
||||
CFRelease(surface);
|
||||
std::printf("expired owners imports=%u releases=%u result=%s\n",imports.load()-before,releases.load()-released,ok?"pass":"FAIL");
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool pending(id<MTLDevice> device) {
|
||||
const unsigned before=imports.load(),released=releases.load();bool ok=true;
|
||||
@autoreleasepool {
|
||||
IOSurfaceRef surface=makeSurface(0x79);if(!surface)return false;
|
||||
id<MTLSharedEvent> event=[device newSharedEvent];id<MTLCommandBuffer> command=[[device newCommandQueue] commandBuffer];
|
||||
id<MTLBuffer> output=[device newBufferWithLength:4096 options:MTLResourceStorageModeShared];
|
||||
if(!event || !command || !output)return false;
|
||||
@autoreleasepool {
|
||||
id<MTLTexture> texture=[device newTextureWithDescriptor:descriptor() iosurface:surface plane:0];if(!texture)return false;
|
||||
[command encodeWaitForEvent:event value:1];id<MTLBlitCommandEncoder> copy=[command blitCommandEncoder];
|
||||
[copy copyFromTexture:texture sourceSlice:0 sourceLevel:0 sourceOrigin:MTLOriginMake(0,0,0) sourceSize:MTLSizeMake(64,16,1)
|
||||
toBuffer:output destinationOffset:0 destinationBytesPerRow:256 destinationBytesPerImage:4096];
|
||||
[copy endEncoding];[command commit];
|
||||
}
|
||||
auto finished=dispatch_semaphore_create(0);
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT,0),^{@autoreleasepool{[command waitUntilCompleted];}dispatch_semaphore_signal(finished);});
|
||||
if(!dispatch_semaphore_wait(finished,dispatch_time(DISPATCH_TIME_NOW,100*NSEC_PER_MSEC)))ok=false;
|
||||
@autoreleasepool {
|
||||
id<MTLTexture> alias=[device newTextureWithDescriptor:descriptor() iosurface:surface plane:0];
|
||||
if(!alias || imports.load()!=before+1 || releases.load()!=released)ok=false;
|
||||
// Drop the caller's last surface reference before the GPU reads it.
|
||||
CFRelease(surface);surface=nullptr;
|
||||
}
|
||||
event.signaledValue=1;
|
||||
if(dispatch_semaphore_wait(finished,dispatch_time(DISPATCH_TIME_NOW,3*NSEC_PER_SEC)))return false;
|
||||
if(command.status!=MTLCommandBufferStatusCompleted)ok=false;
|
||||
for(unsigned i=0;i<4096;++i)if(((const unsigned char *)output.contents)[i]!=0x79)ok=false;
|
||||
// Completed commands must release the import even if still retained.
|
||||
ok &= drained(released+1);
|
||||
}
|
||||
std::printf("pending owners imports=%u releases=%u result=%s\n",imports.load()-before,releases.load()-released,ok?"pass":"FAIL");
|
||||
return ok;
|
||||
}
|
||||
|
||||
int main(int argc,char **argv) {
|
||||
if(argc!=3 && !(argc==4 && !strcmp(argv[3],"parallel")))return 2;
|
||||
@autoreleasepool {
|
||||
id<MTLDevice> device=native_bridge_metal_open(argv[1],argv[2],0x6e6d62,nullptr);if(!device)return 2;
|
||||
void *loader=dlopen(argv[1],RTLD_NOW|RTLD_LOCAL);if(!loader)return 2;
|
||||
vkImports=reinterpret_cast<uint64_t (*)()>(dlsym(loader,"nmb_test_vk_host_import_count"));
|
||||
if(getenv("NMB_TEST_REAL_VULKAN_LOADER") && !vkImports)return 2;
|
||||
if(argc==4)return aliases(device,64,1,true)?0:1;
|
||||
bool ok=aliases(device,1,1,false);
|
||||
ok &= aliases(device,128,1,false);
|
||||
ok &= aliases(device,128,2,false);
|
||||
ok &= aliases(device,64,1,true);
|
||||
ok &= expires(device);
|
||||
ok &= pending(device);
|
||||
ok &= aliases(device,514,257,false);
|
||||
if(!ok || imports.load()!=releases.load()){std::puts("FAIL: live IOSurface import reuse");return 1;}
|
||||
std::puts("PASS: live IOSurface aliases reuse imports with real GPU bytes; distinct, parallel, expired and pending owners preserve lifetimes");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Interleave encoding on two queues, then reverse commit order. Global deltas assign
|
||||
// the first buffer's render work to the second buffer's blits; per-buffer counts must not.
|
||||
#include "FrontendChecks.h"
|
||||
#include "NativeMetalObjects.h"
|
||||
#include "../NativeBridgeMetalFrontend.h"
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 3) return 2;
|
||||
@autoreleasepool {
|
||||
id<MTLDevice> device = native_bridge_metal_open(argv[1], argv[2], 0x6e6d62, nullptr);
|
||||
if (!device) return 2;
|
||||
size_t length = 0;
|
||||
const void *air = nmb_fixture_air(&length);
|
||||
id<MTLLibrary> library = native_bridge_metal_library_with_air(air, length);
|
||||
frontendchecks::Report report = {};
|
||||
frontendchecks::gReport = &report;
|
||||
id<MTLRenderPipelineState> pipeline = frontendchecks::pipeline(device, library, @"colored_fragment", MTLPixelFormatRGBA8Unorm);
|
||||
auto td = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:16 height:16 mipmapped:NO];
|
||||
td.usage = MTLTextureUsageRenderTarget;
|
||||
id<MTLTexture> texture = [device newTextureWithDescriptor:td];
|
||||
id<MTLBuffer> buffer = [device newBufferWithLength:256 options:MTLResourceStorageModeShared];
|
||||
id<MTLCommandBuffer> render = [[device newCommandQueue] commandBuffer];
|
||||
id<MTLCommandBuffer> copy = [[device newCommandQueue] commandBuffer];
|
||||
if (!library || !pipeline || !texture || !buffer || !render || !copy) return 2;
|
||||
frontendchecks::drawQuad(render, pipeline, texture, nil, frontendchecks::kQuarter, 6);
|
||||
for (unsigned i = 0; i < 3; ++i) {
|
||||
id<MTLBlitCommandEncoder> blit = [copy blitCommandEncoder];
|
||||
[blit fillBuffer:buffer range:NSMakeRange(0, 256) value:0x5a];
|
||||
[blit endEncoding];
|
||||
}
|
||||
frontendchecks::drawQuad(render, pipeline, texture, nil, frontendchecks::kQuarter, 6);
|
||||
[copy commit];[render commit];
|
||||
if (!frontendchecks::completed(copy, "blit") || !frontendchecks::completed(render, "render")) return 1;
|
||||
char text[4608] = {};
|
||||
native_bridge_metal_telemetry(text, sizeof text);
|
||||
std::map<unsigned, std::pair<unsigned, unsigned>> counts;
|
||||
for (char *line = strtok(text, "\n"); line; line = strtok(nullptr, "\n")) {
|
||||
unsigned index, sec, ms, passes, draws, kpx, dispatches, blits;
|
||||
if (sscanf(line, "K%u t=%u.%us rp%u dr%u kpx%u cs%u bl%u", &index, &sec, &ms, &passes, &draws, &kpx, &dispatches, &blits) != 8) continue;
|
||||
std::printf("%s\n", line);
|
||||
counts[index] = {passes, blits};
|
||||
if (draws != (index == 2 ? 2u : 0u)) {std::puts("FAIL cross-buffer draw attribution");return 1;}
|
||||
}
|
||||
if (counts.size() != 2 || counts[1] != std::make_pair(0u, 3u) || counts[2] != std::make_pair(2u, 0u)) {
|
||||
std::puts("FAIL interleaved render/blit attribution");return 1;
|
||||
}
|
||||
uint8_t pixels[16 * 16 * 4] = {};
|
||||
[texture getBytes:pixels bytesPerRow:64 fromRegion:MTLRegionMake2D(0, 0, 16, 16) mipmapLevel:0];
|
||||
if (frontendchecks::wrongTexels(pixels, 64, 16)) return 1;
|
||||
for (unsigned i = 0; i < 256; ++i) if (((const uint8_t *)buffer.contents)[i] != 0x5a) return 1;
|
||||
std::puts("PASS: opposite encode/commit orders retain per-buffer render/blit counts and correct host GPU pixels");
|
||||
}
|
||||
}
|
||||
+223
-57
@@ -42,7 +42,7 @@ static uint64_t descriptor(const ArmSmmu4KRange *r,uint64_t iova)
|
||||
if(r->writeback)attrs|=1u<<2;
|
||||
return (r->physical+(iova-r->iova))|attrs;
|
||||
}
|
||||
static uint64_t expected(const ArmSmmu4KRange *ranges,size_t count,uint64_t iova)
|
||||
static const ArmSmmu4KRange *find(const ArmSmmu4KRange *ranges,size_t count,uint64_t iova)
|
||||
{
|
||||
size_t low=0,high=count;
|
||||
while(low<high) {
|
||||
@@ -50,12 +50,34 @@ static uint64_t expected(const ArmSmmu4KRange *ranges,size_t count,uint64_t iova
|
||||
const ArmSmmu4KRange *r=&ranges[mid];
|
||||
if(iova<r->iova)high=mid;
|
||||
else if(iova-r->iova>=r->bytes)low=mid+1;
|
||||
else return descriptor(r,iova);
|
||||
else return r;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
static int contains(const ArmSmmu4KRange *r,uint64_t iova)
|
||||
{return r && iova>=r->iova && iova-r->iova<r->bytes;}
|
||||
/* The walk visits addresses in ascending order, so one cursor per range list replaces a binary search for every leaf: lower_bound once per
|
||||
* leaf page, then forward. A range is behind the address when the address lies past its end (written as a difference: native addresses
|
||||
* may end at 2^64). */
|
||||
static size_t first_not_behind(const ArmSmmu4KRange *ranges,size_t count,uint64_t va)
|
||||
{
|
||||
size_t low=0,high=count;
|
||||
while(low<high) {
|
||||
size_t mid=low+(high-low)/2;
|
||||
if(va>=ranges[mid].iova && va-ranges[mid].iova>=ranges[mid].bytes)low=mid+1;
|
||||
else high=mid;
|
||||
}
|
||||
return low;
|
||||
}
|
||||
static const ArmSmmu4KRange *scan(const ArmSmmu4KRange *ranges,size_t count,size_t *cursor,uint64_t va)
|
||||
{
|
||||
while(*cursor<count && va>=ranges[*cursor].iova && va-ranges[*cursor].iova>=ranges[*cursor].bytes)++*cursor;
|
||||
if(*cursor<count && va>=ranges[*cursor].iova && va-ranges[*cursor].iova<ranges[*cursor].bytes)return &ranges[*cursor];
|
||||
return 0;
|
||||
}
|
||||
static uint64_t expected(const ArmSmmu4KRange *ranges,size_t count,uint64_t iova)
|
||||
{
|
||||
const ArmSmmu4KRange *r=find(ranges,count,iova);
|
||||
return r?descriptor(r,iova):0;
|
||||
}
|
||||
static ArmSmmu4KStatus validate_range(const ArmSmmu4KLiveArena *a,const ArmSmmu4KRange *r)
|
||||
{
|
||||
uint64_t physical_limit=UINT64_C(1)<<a->config.output_bits;
|
||||
@@ -75,8 +97,97 @@ static int conflicting_alias(const ArmSmmu4KRange *a,const ArmSmmu4KRange *b)
|
||||
((a->permissions^b->permissions)&ArmSmmu4KPrivileged)) &&
|
||||
a->physical<b->physical+b->bytes && b->physical<a->physical+a->bytes;
|
||||
}
|
||||
/* Only ranges of different cache/shareability/privilege classes can alias in conflict, and of two conflicting ranges at most one
|
||||
* is in the class most ranges of both sets have. So for every other class k, the members of class k in one set are compared with
|
||||
* the members of other classes in the other set, both ways. Each such comparison sorts the smaller side by physical address in
|
||||
* the arena's scratch and looks every range of the larger side up in it: a scattered buffer is one range per physical page, and
|
||||
* with coherent imports (write-back) beside the GPU's own non-cacheable buffers a pairwise loop over both was some 2.4 billion
|
||||
* comparisons per map (int-144: 368 ms per map on average, 8 s at worst). Without scratch, or a side larger than it, the
|
||||
* comparison is the pairwise loop. */
|
||||
static unsigned alias_class(const ArmSmmu4KRange *r)
|
||||
{return (r->writeback&1u)|((r->shareability&3u)<<1)|((r->permissions&ArmSmmu4KPrivileged)?8u:0u);}
|
||||
typedef struct {
|
||||
const ArmSmmu4KRange *ranges;size_t count;
|
||||
unsigned cls;int same; /* members: class == cls (same) or class != cls */
|
||||
} AliasSide;
|
||||
static int alias_member(const AliasSide *s,size_t i)
|
||||
{return (alias_class(&s->ranges[i])==s->cls)==(s->same!=0);}
|
||||
static void sift(const ArmSmmu4KRange *r,uint32_t *h,size_t root,size_t n)
|
||||
{
|
||||
for(;;) {
|
||||
size_t child=2*root+1;
|
||||
if(child>=n)return;
|
||||
if(child+1<n && r[h[child+1]].physical>r[h[child]].physical)++child;
|
||||
if(r[h[root]].physical>=r[h[child]].physical)return;
|
||||
uint32_t t=h[root];h[root]=h[child];h[child]=t;root=child;
|
||||
}
|
||||
}
|
||||
static int alias_pass(const ArmSmmu4KLiveArena *arena,const AliasSide *p,const AliasSide *q)
|
||||
{
|
||||
size_t np=0,nq=0;
|
||||
for(size_t i=0;i<p->count;++i)np+=(size_t)alias_member(p,i);
|
||||
for(size_t j=0;j<q->count;++j)nq+=(size_t)alias_member(q,j);
|
||||
if(!np || !nq)return 0;
|
||||
const AliasSide *sorted=np<=nq?p:q,*probe=np<=nq?q:p;
|
||||
const size_t n=np<=nq?np:nq;
|
||||
uint32_t *h=arena?arena->alias_scratch:0;
|
||||
if(!h || n>arena->alias_scratch_entries || sorted->count>UINT32_MAX) {
|
||||
for(size_t i=0;i<p->count;++i)if(alias_member(p,i))
|
||||
for(size_t j=0;j<q->count;++j)if(alias_member(q,j) && conflicting_alias(&p->ranges[i],&q->ranges[j]))return 1;
|
||||
return 0;
|
||||
}
|
||||
const ArmSmmu4KRange *r=sorted->ranges;
|
||||
size_t k=0;
|
||||
for(size_t i=0;i<sorted->count;++i)if(alias_member(sorted,i)) {
|
||||
h[k++]=(uint32_t)i;
|
||||
}
|
||||
for(size_t i=n/2;i-->0;)sift(r,h,i,n);
|
||||
for(size_t end=n;end>1;) {--end;uint32_t t=h[0];h[0]=h[end];h[end]=t;sift(r,h,0,end);}
|
||||
/* Drop intervals contained by an earlier interval. Both starts and ends of
|
||||
* the retained indices are now monotonic, even with nested/duplicate runs.
|
||||
* A distant long run cannot force gap queries to scan short runs backward.
|
||||
* Every compared pair belongs to different classes by AliasSide's filter. */
|
||||
size_t kept=0;
|
||||
for(size_t i=0;i<n;++i) {
|
||||
uint32_t index=h[i];
|
||||
if(!kept || r[index].physical+r[index].bytes>r[h[kept-1]].physical+r[h[kept-1]].bytes)h[kept++]=index;
|
||||
}
|
||||
for(size_t j=0;j<probe->count;++j) {
|
||||
if(!alias_member(probe,j))continue;
|
||||
const ArmSmmu4KRange *y=&probe->ranges[j];
|
||||
size_t lo=0,hi=kept; /* first interval ending after y's start */
|
||||
while(lo<hi) {size_t mid=lo+(hi-lo)/2;if(r[h[mid]].physical+r[h[mid]].bytes<=y->physical)lo=mid+1;else hi=mid;}
|
||||
if(lo<kept && r[h[lo]].physical<y->physical+y->bytes)return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
static int any_conflicting_alias(const ArmSmmu4KLiveArena *arena,const ArmSmmu4KRange *a,size_t na,
|
||||
const ArmSmmu4KRange *b,size_t nb)
|
||||
{
|
||||
if(!na || !nb)return 0;
|
||||
size_t members[16]={0};
|
||||
for(size_t i=0;i<na;++i)++members[alias_class(&a[i])];
|
||||
for(size_t j=0;j<nb;++j)++members[alias_class(&b[j])];
|
||||
unsigned reference=0;
|
||||
for(unsigned k=1;k<16;++k)if(members[k]>members[reference])reference=k;
|
||||
const int self=a==b && na==nb;
|
||||
for(unsigned k=0;k<16;++k) {
|
||||
if(k==reference || !members[k])continue;
|
||||
const AliasSide ak={a,na,k,1},bo={b,nb,k,0};
|
||||
if(alias_pass(arena,&ak,&bo))return 1;
|
||||
if(self)continue;
|
||||
const AliasSide bk={b,nb,k,1},ao={a,na,k,0};
|
||||
if(alias_pass(arena,&bk,&ao))return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
static ArmSmmu4KStatus validate_ranges_checked(const ArmSmmu4KLiveArena *a,
|
||||
const ArmSmmu4KRange *ranges,size_t count,size_t *leaves,int aliases);
|
||||
static ArmSmmu4KStatus validate_ranges(const ArmSmmu4KLiveArena *a,
|
||||
const ArmSmmu4KRange *ranges,size_t count,size_t *leaves)
|
||||
{return validate_ranges_checked(a,ranges,count,leaves,1);}
|
||||
static ArmSmmu4KStatus validate_ranges_checked(const ArmSmmu4KLiveArena *a,
|
||||
const ArmSmmu4KRange *ranges,size_t count,size_t *leaves,int aliases)
|
||||
{
|
||||
*leaves=0;
|
||||
if((count && !ranges) || count>a->config.leaf_budget ||
|
||||
@@ -85,25 +196,29 @@ static ArmSmmu4KStatus validate_ranges(const ArmSmmu4KLiveArena *a,
|
||||
ArmSmmu4KStatus status=validate_range(a,&ranges[i]);
|
||||
if(status!=ArmSmmu4KSuccess)return status;
|
||||
if(i && ranges[i].iova<=ranges[i-1].iova+(ranges[i-1].bytes-1))return ArmSmmu4KOverlap;
|
||||
for(size_t j=0;j<i;++j)if(conflicting_alias(&ranges[i],&ranges[j]))return ArmSmmu4KOverlap;
|
||||
size_t pages=(size_t)(ranges[i].bytes/PAGE);
|
||||
if(pages>a->config.leaf_budget-*leaves)return ArmSmmu4KBudget;
|
||||
*leaves+=pages;
|
||||
}
|
||||
return ArmSmmu4KSuccess;
|
||||
return aliases && any_conflicting_alias(a,ranges,count,ranges,count)?ArmSmmu4KOverlap:ArmSmmu4KSuccess;
|
||||
}
|
||||
/* Traverse the actual fixed topology, never generate/copy a reference table.
|
||||
* A fixed owned aperture reserves empty intermediate tables; every other slot
|
||||
* must be zero. Resource expectations are metadata, not translated page tables. */
|
||||
static ArmSmmu4KStatus walk(const ArmSmmu4KLiveArena *a,const ArmSmmu4KResult *shape,
|
||||
unsigned level,size_t page,uint64_t prefix,size_t *next,unsigned build,
|
||||
const ArmSmmu4KRange *ranges,size_t count,const ArmSmmu4KRange *change,
|
||||
const ArmSmmu4KRange *ranges,size_t count,const ArmSmmu4KRange *changes,size_t changed,
|
||||
unsigned map,ArmSmmu4KLiveResult *r)
|
||||
{
|
||||
uint64_t limit=UINT64_C(1)<<a->config.input_bits;
|
||||
uint64_t low=a->iova&(limit-1),high=low+a->bytes-1;
|
||||
unsigned shift=39-9*level;
|
||||
unsigned width=level==shape->start_level?a->config.input_bits-shift:9;
|
||||
size_t range_cursor=0,change_cursor=0;
|
||||
if(level==3) {
|
||||
uint64_t first=a->upper_region ? prefix|~(limit-1) : prefix;
|
||||
range_cursor=first_not_behind(ranges,count,first);change_cursor=first_not_behind(changes,changed,first);
|
||||
}
|
||||
for(unsigned slot=0;slot<512;++slot) {
|
||||
uint64_t begin=prefix+((uint64_t)slot<<shift),value=0;
|
||||
int reserved=slot<(1u<<width) && begin<=high && low<begin+(UINT64_C(1)<<shift);
|
||||
@@ -114,24 +229,45 @@ static ArmSmmu4KStatus walk(const ArmSmmu4KLiveArena *a,const ArmSmmu4KResult *s
|
||||
value=(a->config.table_physical+child*PAGE)|3u;
|
||||
} else if(level==3 && reserved && !build) {
|
||||
uint64_t va=a->upper_region ? begin|~(limit-1) : begin;
|
||||
value=contains(change,va)?(map?descriptor(change,va):0):expected(ranges,count,va);
|
||||
const ArmSmmu4KRange *c=scan(changes,changed,&change_cursor,va);
|
||||
if(c)value=map?descriptor(c,va):0;
|
||||
else {
|
||||
const ArmSmmu4KRange *r0=scan(ranges,count,&range_cursor,va);
|
||||
value=r0?descriptor(r0,va):0;
|
||||
}
|
||||
}
|
||||
size_t index=page*512+slot;
|
||||
if(build)a->mapping[index]=value;
|
||||
else {++r->descriptor_reads;if(a->mapping[index]!=value)return ArmSmmu4KStateMismatch;}
|
||||
if(level<3 && reserved) {
|
||||
ArmSmmu4KStatus status=walk(a,shape,level+1,child,begin,next,build,ranges,count,change,map,r);
|
||||
ArmSmmu4KStatus status=walk(a,shape,level+1,child,begin,next,build,ranges,count,changes,changed,map,r);
|
||||
if(status!=ArmSmmu4KSuccess)return status;
|
||||
}
|
||||
}
|
||||
return ArmSmmu4KSuccess;
|
||||
}
|
||||
static ArmSmmu4KStatus leaf_index(const ArmSmmu4KLiveArena *a,
|
||||
const ArmSmmu4KResult *shape,uint64_t va,size_t *index,ArmSmmu4KLiveResult *r);
|
||||
/* The changed leaves alone, through the table path each one's address takes: before the store (after=0) each holds what the
|
||||
* operation replaces (nothing for a map, its range's descriptor for an unmap), after it what the operation wrote. */
|
||||
static ArmSmmu4KStatus verify_changed(const ArmSmmu4KLiveArena *a,const ArmSmmu4KResult *shape,
|
||||
const ArmSmmu4KRange *changes,size_t changed,unsigned map,unsigned after,ArmSmmu4KLiveResult *r)
|
||||
{
|
||||
for(size_t i=0;i<changed;++i)for(uint64_t offset=0;offset<changes[i].bytes;offset+=PAGE) {
|
||||
const uint64_t va=changes[i].iova+offset;size_t index;
|
||||
ArmSmmu4KStatus status=leaf_index(a,shape,va,&index,r);if(status!=ArmSmmu4KSuccess)return status;
|
||||
const uint64_t want=(map==after)?descriptor(&changes[i],va):0;
|
||||
++r->descriptor_reads;
|
||||
if(a->mapping[index]!=want)return ArmSmmu4KStateMismatch;
|
||||
}
|
||||
return ArmSmmu4KSuccess;
|
||||
}
|
||||
static ArmSmmu4KStatus verify(const ArmSmmu4KLiveArena *a,const ArmSmmu4KResult *shape,
|
||||
const ArmSmmu4KRange *ranges,size_t count,const ArmSmmu4KRange *change,
|
||||
const ArmSmmu4KRange *ranges,size_t count,const ArmSmmu4KRange *changes,size_t changed,
|
||||
unsigned map,ArmSmmu4KLiveResult *r)
|
||||
{
|
||||
size_t next=1;
|
||||
ArmSmmu4KStatus status=walk(a,shape,shape->start_level,0,0,&next,0,ranges,count,change,map,r);
|
||||
ArmSmmu4KStatus status=walk(a,shape,shape->start_level,0,0,&next,0,ranges,count,changes,changed,map,r);
|
||||
return status==ArmSmmu4KSuccess && next!=shape->table_pages?ArmSmmu4KStateMismatch:status;
|
||||
}
|
||||
ArmSmmu4KStatus ArmSmmu4KLivePrepare(const ArmSmmu4KLiveArena *a,ArmSmmu4KLiveState *state)
|
||||
@@ -141,7 +277,7 @@ ArmSmmu4KStatus ArmSmmu4KLivePrepare(const ArmSmmu4KLiveArena *a,ArmSmmu4KLiveSt
|
||||
ArmSmmu4KStatus status=layout(a,&shape);
|
||||
if(status!=ArmSmmu4KSuccess)return status;
|
||||
size_t next=1;
|
||||
status=walk(a,&shape,shape.start_level,0,0,&next,1,0,0,0,0,0);
|
||||
status=walk(a,&shape,shape.start_level,0,0,&next,1,0,0,0,0,0,0);
|
||||
if(status!=ArmSmmu4KSuccess || next!=shape.table_pages) {state->poisoned=1;return ArmSmmu4KStateMismatch;}
|
||||
state->tables=shape;state->generation=0;state->prepared=1;
|
||||
return ArmSmmu4KSuccess;
|
||||
@@ -166,7 +302,7 @@ ArmSmmu4KStatus ArmSmmu4KLiveAudit(const ArmSmmu4KLiveArena *a,const ArmSmmu4KLi
|
||||
size_t leaves=0;
|
||||
status=validate_ranges(a,ranges,count,&leaves);if(status!=ArmSmmu4KSuccess)return status;
|
||||
if(leaves!=state->tables.leaf_pages)return ArmSmmu4KArgument;
|
||||
status=verify(a,&shape,ranges,count,0,0,r);
|
||||
status=verify(a,&shape,ranges,count,0,0,0,r);
|
||||
r->complete=status==ArmSmmu4KSuccess;return status;
|
||||
}
|
||||
static ArmSmmu4KStatus leaf_index(const ArmSmmu4KLiveArena *a,
|
||||
@@ -189,7 +325,7 @@ static int overlaps(uint64_t a,uint64_t abytes,uint64_t b,uint64_t bbytes)
|
||||
static int touches_pool(const ArmSmmu4KRange *r,const ArmSmmu4KLiveArena *a)
|
||||
{return overlaps(r->physical,r->bytes,a->config.table_physical,a->capacity_pages*PAGE);}
|
||||
static ArmSmmu4KStatus region_metadata(const ArmSmmu4KLiveRegion *region,
|
||||
const ArmSmmu4KConfig *config,const ArmSmmu4KResult *root,unsigned upper)
|
||||
const ArmSmmu4KConfig *config,const ArmSmmu4KResult *root,unsigned upper,int aliases)
|
||||
{
|
||||
if(!region)return (!config && !root)?ArmSmmu4KSuccess:ArmSmmu4KArgument;
|
||||
if(!region->arena || !region->state || region->arena->upper_region!=upper ||
|
||||
@@ -201,27 +337,26 @@ static ArmSmmu4KStatus region_metadata(const ArmSmmu4KLiveRegion *region,
|
||||
if(r->root_physical!=shape.root_physical || r->mair!=shape.mair || r->table_pages!=shape.table_pages ||
|
||||
r->start_level!=shape.start_level || r->upper_region!=shape.upper_region)return ArmSmmu4KArgument;
|
||||
size_t leaves=0;
|
||||
status=validate_ranges(region->arena,region->ranges,region->count,&leaves);if(status)return status;
|
||||
status=validate_ranges_checked(region->arena,region->ranges,region->count,&leaves,aliases);if(status)return status;
|
||||
return leaves==r->leaf_pages?ArmSmmu4KSuccess:ArmSmmu4KArgument;
|
||||
}
|
||||
/* aliases=0 (an update that is not its region's whole-verification one) skips the existing ranges' alias checks against each other
|
||||
* and across the regions: each range was checked against all of them when it was mapped, and the 64th update re-proves them. */
|
||||
static ArmSmmu4KStatus context_metadata(const ArmSmmu4KContextConfig *context,
|
||||
const ArmSmmu4KLiveRegion *lower,const ArmSmmu4KLiveRegion *upper)
|
||||
const ArmSmmu4KLiveRegion *lower,const ArmSmmu4KLiveRegion *upper,int aliases)
|
||||
{
|
||||
if(!context || (!lower && !upper))return ArmSmmu4KArgument;
|
||||
ArmSmmu4KStatus status=region_metadata(lower,context->lower,context->lower_tables,0);if(status)return status;
|
||||
status=region_metadata(upper,context->upper,context->upper_tables,1);if(status)return status;
|
||||
ArmSmmu4KStatus status=region_metadata(lower,context->lower,context->lower_tables,0,aliases);if(status)return status;
|
||||
status=region_metadata(upper,context->upper,context->upper_tables,1,aliases);if(status)return status;
|
||||
if(lower && upper) {
|
||||
const ArmSmmu4KLiveArena *a=lower->arena,*b=upper->arena;
|
||||
if(lower->state==upper->state || a->config.output_bits!=b->config.output_bits ||
|
||||
overlaps(a->config.table_physical,a->capacity_pages*PAGE,b->config.table_physical,b->capacity_pages*PAGE) ||
|
||||
overlaps((uintptr_t)a->mapping,a->capacity_pages*PAGE,(uintptr_t)b->mapping,b->capacity_pages*PAGE))
|
||||
return ArmSmmu4KOverlap;
|
||||
for(size_t i=0;i<lower->count;++i) {
|
||||
if(touches_pool(&lower->ranges[i],b))return ArmSmmu4KOverlap;
|
||||
for(size_t j=0;j<upper->count;++j)
|
||||
if(conflicting_alias(&lower->ranges[i],&upper->ranges[j]))return ArmSmmu4KOverlap;
|
||||
}
|
||||
for(size_t i=0;i<lower->count;++i)if(touches_pool(&lower->ranges[i],b))return ArmSmmu4KOverlap;
|
||||
for(size_t i=0;i<upper->count;++i)if(touches_pool(&upper->ranges[i],a))return ArmSmmu4KOverlap;
|
||||
if(aliases && any_conflicting_alias(lower->arena,lower->ranges,lower->count,upper->ranges,upper->count))return ArmSmmu4KOverlap;
|
||||
}
|
||||
return ArmSmmu4KSuccess;
|
||||
}
|
||||
@@ -230,23 +365,24 @@ ArmSmmu4KStatus ArmSmmu4KLiveAuditContext(const ArmSmmu4KContextConfig *context,
|
||||
{
|
||||
if(!r)return ArmSmmu4KArgument;
|
||||
*r=(ArmSmmu4KLiveResult){0};r->must_retain_resources=1;
|
||||
ArmSmmu4KStatus status=context_metadata(context,lower,upper);if(status)return status;
|
||||
ArmSmmu4KStatus status=context_metadata(context,lower,upper,1);if(status)return status;
|
||||
const ArmSmmu4KLiveRegion *regions[]={lower,upper};
|
||||
for(unsigned i=0;i<2;++i)if(regions[i]) {
|
||||
const ArmSmmu4KLiveRegion *v=regions[i];
|
||||
status=verify(v->arena,&v->state->tables,v->ranges,v->count,0,0,r);if(status)return status;
|
||||
status=verify(v->arena,&v->state->tables,v->ranges,v->count,0,0,0,r);if(status)return status;
|
||||
}
|
||||
r->complete=1;return ArmSmmu4KSuccess;
|
||||
}
|
||||
static ArmSmmu4KStatus update(const ArmSmmu4KLiveArena *a,ArmSmmu4KLiveState *state,
|
||||
const ArmSmmu4KIo *io,unsigned bank,const ArmSmmu4KContextConfig *context,
|
||||
const ArmSmmu4KLiveVisibility *visibility,const ArmSmmu4KRange *before,size_t count,
|
||||
const ArmSmmu4KLiveRegion *peer,const ArmSmmu4KRange *change,unsigned map,unsigned budget,ArmSmmu4KLiveResult *r)
|
||||
const ArmSmmu4KLiveRegion *peer,const ArmSmmu4KRange *changes,size_t change_count,unsigned map,unsigned budget,
|
||||
ArmSmmu4KLiveResult *r)
|
||||
{
|
||||
if(!r)return ArmSmmu4KArgument;
|
||||
*r=(ArmSmmu4KLiveResult){0};r->must_retain_resources=1;
|
||||
if(!state || !state->prepared || state->poisoned || !context ||
|
||||
!visibility || !visibility->publish_table_page || map>1 ||
|
||||
!visibility || !visibility->publish_table_page || map>1 || !change_count ||
|
||||
!budget || budget>1048576 || state->generation==UINT64_MAX)return ArmSmmu4KArgument;
|
||||
ArmSmmu4KResult shape;
|
||||
ArmSmmu4KStatus status=layout(a,&shape);
|
||||
@@ -258,31 +394,51 @@ static ArmSmmu4KStatus update(const ArmSmmu4KLiveArena *a,ArmSmmu4KLiveState *st
|
||||
state->tables.mair!=shape.mair || state->tables.table_pages!=shape.table_pages ||
|
||||
state->tables.start_level!=shape.start_level || state->tables.upper_region!=shape.upper_region)
|
||||
return ArmSmmu4KArgument;
|
||||
/* Every 64th update of a region (its first included) re-proves the whole region: the existing ranges against each other and every
|
||||
* table descriptor before and after. The others prove what they change: each changed leaf holds its old descriptor before the store
|
||||
* and its new one after it, the existing ranges are still sorted, disjoint and in budget, and the changes conflict with none of
|
||||
* them. The ranges were each checked as changes when they were added, and the tables were last proved whole at most 63 updates ago.
|
||||
* A whole-region walk read every descriptor of a 2 GiB aperture four times per update (int-143: 10 ms per buffer map). */
|
||||
const int whole=(state->generation&63)==0;
|
||||
r->whole_verification=whole?1u:0u;
|
||||
size_t leaves=0;
|
||||
status=validate_ranges(a,before,count,&leaves);if(status!=ArmSmmu4KSuccess)return status;
|
||||
status=validate_ranges_checked(a,before,count,&leaves,whole);if(status!=ArmSmmu4KSuccess)return status;
|
||||
if(leaves!=state->tables.leaf_pages)return ArmSmmu4KArgument;
|
||||
status=validate_range(a,change);if(status!=ArmSmmu4KSuccess)return status;
|
||||
/* The changes: sorted, disjoint, each valid; together within the leaf budget. */
|
||||
size_t changed=0;
|
||||
if(!changes)return ArmSmmu4KBadRange;
|
||||
if(change_count>a->config.leaf_budget)return ArmSmmu4KArgument;
|
||||
for(size_t i=0;i<change_count;++i) {
|
||||
status=validate_range(a,&changes[i]);if(status!=ArmSmmu4KSuccess)return status;
|
||||
if(i && changes[i].iova<=changes[i-1].iova+(changes[i-1].bytes-1))return ArmSmmu4KOverlap;
|
||||
size_t pages=(size_t)(changes[i].bytes/PAGE);
|
||||
if(pages>a->config.leaf_budget-changed)return ArmSmmu4KBudget;
|
||||
changed+=pages;
|
||||
}
|
||||
if(peer) {
|
||||
ArmSmmu4KLiveRegion target={a,state,before,count};
|
||||
status=context_metadata(context,a->upper_region?peer:&target,a->upper_region?&target:peer);
|
||||
status=context_metadata(context,a->upper_region?peer:&target,a->upper_region?&target:peer,whole);
|
||||
if(status)return status;
|
||||
if(touches_pool(change,peer->arena))return ArmSmmu4KOverlap;
|
||||
if(map)for(size_t i=0;i<peer->count;++i)
|
||||
if(conflicting_alias(change,&peer->ranges[i]))return ArmSmmu4KOverlap;
|
||||
for(size_t i=0;i<change_count;++i)if(touches_pool(&changes[i],peer->arena))return ArmSmmu4KOverlap;
|
||||
if(map && any_conflicting_alias(a,changes,change_count,peer->ranges,peer->count))return ArmSmmu4KOverlap;
|
||||
}
|
||||
size_t changed=(size_t)(change->bytes/PAGE);
|
||||
if(map && changed>a->config.leaf_budget-leaves)return ArmSmmu4KBudget;
|
||||
if(map)for(size_t i=0;i<count;++i)if(conflicting_alias(change,&before[i]))return ArmSmmu4KOverlap;
|
||||
for(uint64_t offset=0;offset<change->bytes;offset+=PAGE) {
|
||||
uint64_t va=change->iova+offset,old=expected(before,count,va);
|
||||
if(map?old!=0:old!=descriptor(change,va))return ArmSmmu4KStateMismatch;
|
||||
if(map && (any_conflicting_alias(a,changes,change_count,changes,change_count) ||
|
||||
any_conflicting_alias(a,changes,change_count,before,count)))return ArmSmmu4KOverlap;
|
||||
for(size_t i=0;i<change_count;++i)for(uint64_t offset=0;offset<changes[i].bytes;offset+=PAGE) {
|
||||
uint64_t va=changes[i].iova+offset,old=expected(before,count,va);
|
||||
if(map?old!=0:old!=descriptor(&changes[i],va))return ArmSmmu4KStateMismatch;
|
||||
}
|
||||
status=ArmSmmu4KObserveOwnedContext(io,bank,context,&r->synchronization);
|
||||
if(status!=ArmSmmu4KSuccess)goto poisoned;
|
||||
status=verify(a,&shape,before,count,0,0,r);if(status!=ArmSmmu4KSuccess)goto poisoned;
|
||||
if(peer) {
|
||||
status=verify(peer->arena,&peer->state->tables,peer->ranges,peer->count,0,0,r);
|
||||
if(status)goto poisoned;
|
||||
if(whole) {
|
||||
status=verify(a,&shape,before,count,0,0,0,r);if(status!=ArmSmmu4KSuccess)goto poisoned;
|
||||
if(peer) {
|
||||
status=verify(peer->arena,&peer->state->tables,peer->ranges,peer->count,0,0,0,r);
|
||||
if(status)goto poisoned;
|
||||
}
|
||||
} else {
|
||||
status=verify_changed(a,&shape,changes,change_count,map,0,r);if(status!=ArmSmmu4KSuccess)goto poisoned;
|
||||
}
|
||||
ArmSmmu4KProgramResult initial=r->synchronization;
|
||||
status=ArmSmmu4KObserveOwnedContext(io,bank,context,&r->synchronization);
|
||||
@@ -292,15 +448,16 @@ static ArmSmmu4KStatus update(const ArmSmmu4KLiveArena *a,ArmSmmu4KLiveState *st
|
||||
{status=ArmSmmu4KStateMismatch;goto poisoned;}
|
||||
/* All validation precedes the first store. Caller keeps every master
|
||||
* quiesced and retains both old/new object references through this call. */
|
||||
for(uint64_t offset=0;offset<change->bytes;offset+=PAGE) {
|
||||
uint64_t va=change->iova+offset;size_t index;
|
||||
for(size_t i=0;i<change_count;++i)for(uint64_t offset=0;offset<changes[i].bytes;offset+=PAGE) {
|
||||
uint64_t va=changes[i].iova+offset;size_t index;
|
||||
status=leaf_index(a,&shape,va,&index,r);if(status!=ArmSmmu4KSuccess)goto poisoned;
|
||||
a->mapping[index]=map?descriptor(change,va):0;++r->descriptor_writes;
|
||||
a->mapping[index]=map?descriptor(&changes[i],va):0;++r->descriptor_writes;
|
||||
}
|
||||
/* Sorted changes reach leaf pages in order, so each changed page is published once. */
|
||||
size_t last_page=SIZE_MAX;
|
||||
for(uint64_t offset=0;offset<change->bytes;offset+=PAGE) {
|
||||
for(size_t i=0;i<change_count;++i)for(uint64_t offset=0;offset<changes[i].bytes;offset+=PAGE) {
|
||||
size_t index;
|
||||
status=leaf_index(a,&shape,change->iova+offset,&index,r);if(status!=ArmSmmu4KSuccess)goto poisoned;
|
||||
status=leaf_index(a,&shape,changes[i].iova+offset,&index,r);if(status!=ArmSmmu4KSuccess)goto poisoned;
|
||||
size_t page=index/512;
|
||||
if(page!=last_page) {
|
||||
if(visibility->publish_table_page(visibility->cookie,a->mapping+page*512,4096)) {status=ArmSmmu4KIoError;goto poisoned;}
|
||||
@@ -309,10 +466,14 @@ static ArmSmmu4KStatus update(const ArmSmmu4KLiveArena *a,ArmSmmu4KLiveState *st
|
||||
}
|
||||
status=ArmSmmu4KSynchronizeOwnedContext(io,bank,context,budget,&r->synchronization);
|
||||
if(status!=ArmSmmu4KSuccess)goto poisoned;
|
||||
status=verify(a,&shape,before,count,change,map,r);if(status!=ArmSmmu4KSuccess)goto poisoned;
|
||||
if(peer) {
|
||||
status=verify(peer->arena,&peer->state->tables,peer->ranges,peer->count,0,0,r);
|
||||
if(status)goto poisoned;
|
||||
if(whole) {
|
||||
status=verify(a,&shape,before,count,changes,change_count,map,r);if(status!=ArmSmmu4KSuccess)goto poisoned;
|
||||
if(peer) {
|
||||
status=verify(peer->arena,&peer->state->tables,peer->ranges,peer->count,0,0,0,r);
|
||||
if(status)goto poisoned;
|
||||
}
|
||||
} else {
|
||||
status=verify_changed(a,&shape,changes,change_count,map,1,r);if(status!=ArmSmmu4KSuccess)goto poisoned;
|
||||
}
|
||||
ArmSmmu4KProgramResult after;
|
||||
status=ArmSmmu4KObserveOwnedContext(io,bank,context,&after);if(status!=ArmSmmu4KSuccess)goto poisoned;
|
||||
@@ -329,18 +490,23 @@ ArmSmmu4KStatus ArmSmmu4KLiveUpdate(const ArmSmmu4KLiveArena *a,ArmSmmu4KLiveSta
|
||||
const ArmSmmu4KIo *io,unsigned bank,const ArmSmmu4KContextConfig *context,
|
||||
const ArmSmmu4KLiveVisibility *visibility,const ArmSmmu4KRange *before,size_t count,
|
||||
const ArmSmmu4KRange *change,unsigned map,unsigned budget,ArmSmmu4KLiveResult *r)
|
||||
{return update(a,state,io,bank,context,visibility,before,count,0,change,map,budget,r);}
|
||||
ArmSmmu4KStatus ArmSmmu4KLiveUpdateContext(const ArmSmmu4KIo *io,unsigned bank,
|
||||
{return update(a,state,io,bank,context,visibility,before,count,0,change,1,map,budget,r);}
|
||||
ArmSmmu4KStatus ArmSmmu4KLiveUpdateContextBatch(const ArmSmmu4KIo *io,unsigned bank,
|
||||
const ArmSmmu4KContextConfig *context,const ArmSmmu4KLiveRegion *lower,
|
||||
const ArmSmmu4KLiveRegion *upper,unsigned change_upper,const ArmSmmu4KLiveVisibility *visibility,
|
||||
const ArmSmmu4KRange *change,unsigned map,unsigned budget,ArmSmmu4KLiveResult *r)
|
||||
const ArmSmmu4KRange *changes,size_t change_count,unsigned map,unsigned budget,ArmSmmu4KLiveResult *r)
|
||||
{
|
||||
if(!r)return ArmSmmu4KArgument;
|
||||
*r=(ArmSmmu4KLiveResult){0};r->must_retain_resources=1;
|
||||
if(change_upper>1)return ArmSmmu4KArgument;
|
||||
ArmSmmu4KStatus status=context_metadata(context,lower,upper);if(status)return status;
|
||||
const ArmSmmu4KLiveRegion *target=change_upper?upper:lower,*peer=change_upper?lower:upper;
|
||||
if(!target)return ArmSmmu4KArgument;
|
||||
if(!target || !target->state)return ArmSmmu4KArgument;
|
||||
ArmSmmu4KStatus status=context_metadata(context,lower,upper,(target->state->generation&63)==0);if(status)return status;
|
||||
return update(target->arena,target->state,io,bank,context,visibility,target->ranges,target->count,
|
||||
peer,change,map,budget,r);
|
||||
peer,changes,change_count,map,budget,r);
|
||||
}
|
||||
ArmSmmu4KStatus ArmSmmu4KLiveUpdateContext(const ArmSmmu4KIo *io,unsigned bank,
|
||||
const ArmSmmu4KContextConfig *context,const ArmSmmu4KLiveRegion *lower,
|
||||
const ArmSmmu4KLiveRegion *upper,unsigned change_upper,const ArmSmmu4KLiveVisibility *visibility,
|
||||
const ArmSmmu4KRange *change,unsigned map,unsigned budget,ArmSmmu4KLiveResult *r)
|
||||
{return ArmSmmu4KLiveUpdateContextBatch(io,bank,context,lower,upper,change_upper,visibility,change,1,map,budget,r);}
|
||||
@@ -11,6 +11,10 @@ typedef struct {
|
||||
unsigned upper_region;
|
||||
volatile uint64_t *mapping; /* Real writable CPU mapping of owned pool. */
|
||||
size_t capacity_pages;
|
||||
/* Optional caller-owned scratch for the physical-alias checks (one entry per range of the smaller compared side), used by one
|
||||
* call at a time; absent or too small, those checks compare pairwise. Never read as state. */
|
||||
uint32_t *alias_scratch;
|
||||
size_t alias_scratch_entries;
|
||||
} ArmSmmu4KLiveArena;
|
||||
typedef struct {
|
||||
ArmSmmu4KResult tables;
|
||||
@@ -33,6 +37,7 @@ typedef struct {
|
||||
typedef struct {
|
||||
size_t descriptor_reads,descriptor_writes,published_pages;
|
||||
unsigned complete,translation_removed,must_retain_resources;
|
||||
unsigned whole_verification; /* this update re-proved the whole region (every 64th of a region) */
|
||||
ArmSmmu4KProgramResult synchronization;
|
||||
} ArmSmmu4KLiveResult;
|
||||
/* Prepare an INACTIVE empty native4K arena. Preallocate only real intermediate
|
||||
@@ -89,6 +94,18 @@ ArmSmmu4KStatus ArmSmmu4KLiveUpdateContext(const ArmSmmu4KIo *,unsigned bank,
|
||||
const ArmSmmu4KLiveRegion *upper,unsigned change_upper,
|
||||
const ArmSmmu4KLiveVisibility *,const ArmSmmu4KRange *change,unsigned map,
|
||||
unsigned poll_budget,ArmSmmu4KLiveResult *);
|
||||
/* UpdateContext for several ranges of one region in one transaction: changes are
|
||||
* sorted by native IOVA and disjoint; map wants each entirely invalid, unmap
|
||||
* each exactly mapped. Every check of UpdateContext covers all of them before
|
||||
* the first store; their leaf pages are published once, the ASID synchronized
|
||||
* once and both graphs rechecked once. A buffer of scattered physical pages is
|
||||
* one change per page, so its mapping costs one transaction, not one per page.
|
||||
* The changes may lie inside the target region's ranges (an unmap). */
|
||||
ArmSmmu4KStatus ArmSmmu4KLiveUpdateContextBatch(const ArmSmmu4KIo *,unsigned bank,
|
||||
const ArmSmmu4KContextConfig *,const ArmSmmu4KLiveRegion *lower,
|
||||
const ArmSmmu4KLiveRegion *upper,unsigned change_upper,
|
||||
const ArmSmmu4KLiveVisibility *,const ArmSmmu4KRange *changes,size_t change_count,
|
||||
unsigned map,unsigned poll_budget,ArmSmmu4KLiveResult *);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
/* The sorted physical-alias check against the pairwise definition (conflicting_alias over every pair) on random sets: scattered
|
||||
* single pages and long runs, up to eight attribute classes, self and cross comparisons, with scratch, without it and with too
|
||||
* little of it; then one int-144-sized case (24k coherent import pages beside 82k non-cacheable pages) for its cost. */
|
||||
#ifndef ARM_SMMU4K_ALIAS_SOURCE
|
||||
#define ARM_SMMU4K_ALIAS_SOURCE "ArmSmmu4KLive.c"
|
||||
#endif
|
||||
#include ARM_SMMU4K_ALIAS_SOURCE
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
#ifdef ARM_SMMU4K_TEST_COVERAGE
|
||||
/* Compiler basic-block coverage counts, test-only. Production needs no hook or
|
||||
* counter. Volatile keeps the caller's reads intact before coverage insertion. */
|
||||
static volatile uint64_t alias_steps;
|
||||
__attribute__((no_sanitize("coverage"))) void __sanitizer_cov_trace_pc_guard(uint32_t *guard)
|
||||
{(void)guard;++alias_steps;}
|
||||
__attribute__((no_sanitize("coverage"))) void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,uint32_t *end)
|
||||
{if(start==end || *start)return;for(uint32_t *p=start;p<end;++p)*p=1;}
|
||||
#endif
|
||||
#define CHECK(x) do{if(!(x)){fprintf(stderr,"%s:%d: %s\n",__FILE__,__LINE__,#x);exit(1);}}while(0)
|
||||
static uint64_t seed=0x9e3779b97f4a7c15ull;
|
||||
static uint64_t rnd(void){seed^=seed<<13;seed^=seed>>7;seed^=seed<<17;return seed;}
|
||||
static int pairwise(const ArmSmmu4KRange *a,size_t na,const ArmSmmu4KRange *b,size_t nb)
|
||||
{
|
||||
for(size_t i=0;i<na;++i)for(size_t j=0;j<nb;++j) {
|
||||
const int different=a[i].writeback!=b[j].writeback || a[i].shareability!=b[j].shareability ||
|
||||
((a[i].permissions^b[j].permissions)&ArmSmmu4KPrivileged);
|
||||
if(different && a[i].physical<b[j].physical+b[j].bytes && b[j].physical<a[i].physical+a[i].bytes)return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
static void fill(ArmSmmu4KRange *r,size_t n,unsigned classes,uint64_t span)
|
||||
{
|
||||
for(size_t i=0;i<n;++i) {
|
||||
const unsigned c=(unsigned)(rnd()%classes);
|
||||
const uint64_t pages=(rnd()%8)?1:1+rnd()%64;
|
||||
r[i]=(ArmSmmu4KRange){0x100000+i*0x100000,0x80000000+(rnd()%span)*4096,pages*4096,
|
||||
(c&2)?3u|ArmSmmu4KPrivileged:3u,c&1u,(c&4u)?3u:2u};
|
||||
}
|
||||
}
|
||||
static uint32_t scratch[1u<<18];
|
||||
int main(void)
|
||||
{
|
||||
static ArmSmmu4KRange a[4096],b[4096];
|
||||
ArmSmmu4KLiveArena with={0},without={0},small={0};
|
||||
with.alias_scratch=scratch;with.alias_scratch_entries=sizeof scratch/sizeof *scratch;
|
||||
small.alias_scratch=scratch;small.alias_scratch_entries=3;
|
||||
unsigned cases=0,conflicts=0;
|
||||
for(unsigned round=0;round<3000;++round) {
|
||||
const size_t na=1+rnd()%(round<2000?40:600),nb=1+rnd()%(round<2000?40:600);
|
||||
const unsigned classes=1+(unsigned)(rnd()%8);
|
||||
const uint64_t span=1+rnd()%(round&1?200000:4000);
|
||||
fill(a,na,classes,span);fill(b,nb,classes,span);
|
||||
const int cross=pairwise(a,na,b,nb),self=pairwise(a,na,a,na);
|
||||
CHECK(any_conflicting_alias(&with,a,na,b,nb)==cross && any_conflicting_alias(&with,b,nb,a,na)==cross);
|
||||
CHECK(any_conflicting_alias(&without,a,na,b,nb)==cross && any_conflicting_alias(&small,a,na,b,nb)==cross);
|
||||
CHECK(any_conflicting_alias(&with,a,na,a,na)==self && any_conflicting_alias(&small,a,na,a,na)==self);
|
||||
cases+=5;conflicts+=(unsigned)(cross+self);
|
||||
}
|
||||
/* int-144's shape: 23855 coherent import pages and 82000 non-cacheable pages, disjoint, then one aliased page. */
|
||||
enum {Imports=23855,Owned=82000};
|
||||
ArmSmmu4KRange *all=calloc(Imports+Owned,sizeof *all);CHECK(all);
|
||||
for(size_t i=0;i<Imports+Owned;++i) {
|
||||
const int coherent=i<Imports;
|
||||
all[i]=(ArmSmmu4KRange){0x100000+i*4096,(coherent?UINT64_C(0x800000000):UINT64_C(0x900000000))+(rnd()%0x100000)*4096*2+
|
||||
(coherent?0:4096),4096,3,coherent?1u:0u,coherent?3u:2u};
|
||||
}
|
||||
const clock_t began=clock();
|
||||
CHECK(!any_conflicting_alias(&with,all,Imports+Owned,all,Imports+Owned));
|
||||
CHECK(!any_conflicting_alias(&with,all,Imports,all+Imports,Owned));
|
||||
all[Imports+Owned-1].physical=all[17].physical;
|
||||
CHECK(any_conflicting_alias(&with,all,Imports+Owned,all,Imports+Owned));
|
||||
CHECK(any_conflicting_alias(&with,all+Imports+Owned-1,1,all,Imports));
|
||||
const double ms=1000.0*(double)(clock()-began)/CLOCKS_PER_SEC;
|
||||
CHECK(ms<2000);
|
||||
free(all);
|
||||
printf("PASS: %u sorted alias checks equal the pairwise definition (%u conflicting) with, without and with too little scratch; "
|
||||
"a 23855+82000-page mixed-class set checked 4 times in %.0f ms\n",cases,conflicts,ms);
|
||||
/* One long interval lies beyond every probe. Its length must not make each
|
||||
* gap probe scan all preceding small intervals. Equal interval lengths in
|
||||
* the earlier large case do not expose that degeneration. */
|
||||
enum {Mixed=8192};
|
||||
ArmSmmu4KRange *left=calloc(Mixed+1,sizeof *left),*right=calloc(Mixed+2,sizeof *right);CHECK(left && right);
|
||||
for(size_t i=0;i<Mixed+2;++i) {
|
||||
if(i<Mixed)left[i]=(ArmSmmu4KRange){0,0x40000000+i*8192,4096,3,0,2};
|
||||
right[i]=(ArmSmmu4KRange){0,0x40001000+i*8192,4096,3,1,3};
|
||||
}
|
||||
left[Mixed]=(ArmSmmu4KRange){0,UINT64_C(0x200000000),UINT64_C(0x10000000),3,0,2};
|
||||
#ifdef ARM_SMMU4K_TEST_COVERAGE
|
||||
alias_steps=0;
|
||||
#endif
|
||||
const clock_t mixed_began=clock();
|
||||
CHECK(!any_conflicting_alias(&with,left,Mixed+1,right,Mixed+2));
|
||||
#ifdef ARM_SMMU4K_TEST_COVERAGE
|
||||
const uint64_t work=alias_steps;
|
||||
printf("mixed alias diagnostic: %u intervals, %llu coverage steps, %.1f host ms (not hardware timing)\n",2*Mixed+3,
|
||||
(unsigned long long)work,1000.0*(double)(clock()-mixed_began)/CLOCKS_PER_SEC);fflush(stdout);
|
||||
CHECK(work>0 && work<UINT64_C(512)*(2*Mixed+3));
|
||||
#else
|
||||
(void)mixed_began;
|
||||
#endif
|
||||
CHECK(!any_conflicting_alias(&with,right,Mixed+2,left,Mixed+1));
|
||||
right[Mixed+1].physical=left[Mixed].physical+4096;
|
||||
CHECK(any_conflicting_alias(&with,left,Mixed+1,right,Mixed+2));
|
||||
free(left);free(right);
|
||||
#ifdef ARM_SMMU4K_TEST_COVERAGE
|
||||
puts("PASS: mixed-size alias gap/long-range controls and bounded compiler-coverage work");
|
||||
#else
|
||||
puts("PASS: mixed-size alias gap/long-range controls; work counter not enabled");
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
@@ -22,6 +22,7 @@ typedef struct {
|
||||
} Fixture;
|
||||
static Fixture f;
|
||||
static ArmSmmu4KLiveArena arena;
|
||||
static uint32_t alias_scratch[64]; /* the main arena's alias checks sort; the peer's compare pairwise */
|
||||
static ArmSmmu4KLiveState state;
|
||||
static ArmSmmu4KContextConfig context;
|
||||
static ArmSmmu4KIo io;
|
||||
@@ -97,7 +98,7 @@ static void setup(unsigned bits,unsigned upper,unsigned output,unsigned spacing,
|
||||
memset(&state,0,sizeof(state));memset(&f,0,sizeof(f));memset(tables,0xa5,sizeof(tables));
|
||||
pair_mode=0;
|
||||
arena=(ArmSmmu4KLiveArena){{bits,output,0x10000000,16},
|
||||
(upper?UINT64_MAX-(UINT64_C(1)<<bits)+1:0)+low,4*4096,upper,tables,POOL_PAGES};
|
||||
(upper?UINT64_MAX-(UINT64_C(1)<<bits)+1:0)+low,4*4096,upper,tables,POOL_PAGES,alias_scratch,64};
|
||||
CHECK(ArmSmmu4KLivePrepare(&arena,&state)==ArmSmmu4KSuccess);
|
||||
CHECK(state.prepared && !state.poisoned && !state.tables.leaf_pages && state.tables.table_pages<=POOL_PAGES);
|
||||
ArmSmmu4KLiveResult audit;
|
||||
@@ -140,7 +141,8 @@ static void complete(unsigned removed,size_t pages)
|
||||
CHECK(result.complete && result.translation_removed==removed && !result.must_retain_resources);
|
||||
CHECK(!state.poisoned && state.tables.leaf_pages==pages && result.synchronization.tlb_synchronized);
|
||||
CHECK(f.tlbi_calls==1 && f.sync_calls==1 && f.synced && f.barriers>=4);
|
||||
CHECK(result.descriptor_reads>=state.tables.table_pages*1024);
|
||||
/* A whole-region update reads every descriptor; the others read each changed leaf's path before and after the store. */
|
||||
CHECK(result.whole_verification?result.descriptor_reads>=state.tables.table_pages*1024:result.descriptor_reads>=2);
|
||||
}
|
||||
static unsigned profiles(void)
|
||||
{
|
||||
@@ -263,7 +265,7 @@ static void setup_pair(unsigned bits,unsigned output,unsigned spacing)
|
||||
memset(&peer_state,0,sizeof(peer_state));
|
||||
unsigned peer_bits=80-bits;
|
||||
peer_arena=(ArmSmmu4KLiveArena){{peer_bits,output,0x11000000,16},
|
||||
UINT64_MAX-(UINT64_C(1)<<peer_bits)+1+0x1ff000,4*4096,1,peer_tables,POOL_PAGES};
|
||||
UINT64_MAX-(UINT64_C(1)<<peer_bits)+1+0x1ff000,4*4096,1,peer_tables,POOL_PAGES,0,0}; /* pairwise alias checks */
|
||||
CHECK(!ArmSmmu4KLivePrepare(&peer_arena,&peer_state));
|
||||
context.upper=&peer_arena.config;context.upper_tables=&peer_state.tables;
|
||||
/* Explicitly reset the HOST REGISTER fixture, never a live hardware bank. */
|
||||
@@ -352,6 +354,91 @@ static unsigned pair_failures(void)
|
||||
CHECK(!state.poisoned && !peer_state.poisoned);
|
||||
return controls;
|
||||
}
|
||||
static ArmSmmu4KStatus batch(const ArmSmmu4KRange *before,size_t count,const ArmSmmu4KRange *changes,size_t n,unsigned map)
|
||||
{
|
||||
ArmSmmu4KLiveRegion lo={&arena,&state,before,count};
|
||||
return ArmSmmu4KLiveUpdateContextBatch(&io,0,&context,&lo,0,0,&visibility,changes,n,map,4,&result);
|
||||
}
|
||||
/* Four scattered physical pages (descending addresses, never adjacent) over the arena's 2 MiB boundary. */
|
||||
static void scattered(ArmSmmu4KRange pages[4])
|
||||
{for(unsigned i=0;i<4;++i)pages[i]=(ArmSmmu4KRange){arena.iova+4096*i,0x30000000+0x20000*(3-i),4096,3,0,2};}
|
||||
static unsigned batches(void)
|
||||
{
|
||||
unsigned controls=0;
|
||||
setup(48,0,48,0,0x1ff000);ArmSmmu4KRange pages[4];scattered(pages);
|
||||
memcpy(snapshot,tables,sizeof(tables));
|
||||
ArmSmmu4KRange swapped[2]={pages[1],pages[0]};
|
||||
CHECK(batch(0,0,swapped,2,1)==ArmSmmu4KOverlap);++controls;
|
||||
ArmSmmu4KRange alias[2]={pages[0],pages[1]};alias[1].physical=alias[0].physical;alias[1].writeback=1;alias[1].shareability=3;
|
||||
CHECK(batch(0,0,alias,2,1)==ArmSmmu4KOverlap);++controls;
|
||||
alias[1].writeback=0;alias[1].shareability=2;alias[1].permissions|=ArmSmmu4KPrivileged;
|
||||
CHECK(batch(0,0,alias,2,1)==ArmSmmu4KOverlap);++controls;
|
||||
CHECK(batch(0,0,pages,0,1)==ArmSmmu4KArgument);++controls;
|
||||
CHECK(batch(0,0,0,1,1)==ArmSmmu4KBadRange);++controls;
|
||||
CHECK(!f.calls && !state.poisoned && !memcmp(snapshot,tables,sizeof(tables)));
|
||||
/* One transaction: four leaves written, two leaf pages published once each, one TLB synchronization. */
|
||||
CHECK(batch(0,0,pages,4,1)==ArmSmmu4KSuccess);complete(0,4);
|
||||
CHECK(result.descriptor_writes==4 && result.published_pages==2 && state.generation==1);
|
||||
for(unsigned i=0;i<4;++i)CHECK(pte(pages[i].iova)==(pages[i].physical|source_permission_pte(3,0)|3));
|
||||
++controls;
|
||||
/* Any already mapped page refuses the whole batch before MMIO or a store. */
|
||||
reset_counters();memcpy(snapshot,tables,sizeof(tables));
|
||||
CHECK(batch(pages,4,pages+2,2,1)==ArmSmmu4KStateMismatch && !f.calls && !memcmp(snapshot,tables,sizeof(tables)));++controls;
|
||||
/* Unmap a sub-batch given as part of the region's own ranges. */
|
||||
reset_counters();CHECK(batch(pages,4,pages+1,2,0)==ArmSmmu4KSuccess);complete(1,2);
|
||||
CHECK(pte(pages[0].iova) && !pte(pages[1].iova) && !pte(pages[2].iova) && pte(pages[3].iova));++controls;
|
||||
ArmSmmu4KRange rest[2]={pages[0],pages[3]},wrong[2]={pages[0],pages[3]};wrong[1].physical+=4096;
|
||||
reset_counters();CHECK(batch(rest,2,wrong,2,0)==ArmSmmu4KStateMismatch && !f.calls);++controls;
|
||||
reset_counters();CHECK(batch(rest,2,rest,2,0)==ArmSmmu4KSuccess);complete(1,0);++controls;
|
||||
/* Every register access of a batch failing, before or after its write, poisons and retains. */
|
||||
setup(48,0,48,0,0x1ff000);scattered(pages);
|
||||
CHECK(batch(0,0,pages,4,1)==ArmSmmu4KSuccess);size_t calls=f.calls;
|
||||
for(unsigned unmap=0;unmap<2;++unmap)for(unsigned after=0;after<2;++after)for(size_t failed=1;failed<=calls;++failed) {
|
||||
setup(48,0,48,0,0x1ff000);scattered(pages);
|
||||
if(unmap) {CHECK(batch(0,0,pages,4,1)==ArmSmmu4KSuccess);reset_counters();}
|
||||
f.fail_call=failed;f.after_write=after;
|
||||
CHECK(batch(unmap?pages:0,unmap?4:0,pages,4,!unmap)==ArmSmmu4KIoError);
|
||||
CHECK(state.poisoned && result.must_retain_resources && !result.complete);++controls;
|
||||
}
|
||||
return controls;
|
||||
}
|
||||
/* The leaf slot of va in the fixture's pool, by the oracle's own walk. */
|
||||
static uint64_t *leaf_of(uint64_t va)
|
||||
{
|
||||
size_t at=0;unsigned level=state.tables.start_level;
|
||||
for(;;) {
|
||||
unsigned shift=12+9*(3-level),width=level==state.tables.start_level?arena.config.input_bits-shift:9;
|
||||
uint64_t *entry=&tables[at+((va>>shift)&((1u<<width)-1))];
|
||||
if(level==3)return entry;
|
||||
CHECK((*entry&3)==3);
|
||||
at=(size_t)(((*entry&UINT64_C(0x0000fffffffff000))-arena.config.table_physical)/8);++level;
|
||||
}
|
||||
}
|
||||
/* Between whole-region updates (every 64th of a region) an update proves the leaves it changes: a changed leaf holding something
|
||||
* else refuses before any store; a corrupted descriptor elsewhere is found by the next whole-region update. */
|
||||
static unsigned incremental(void)
|
||||
{
|
||||
unsigned count=0;
|
||||
setup(48,1,48,0,0x1ff000);
|
||||
ArmSmmu4KRange r=resource(1,3,1),r2=r;r2.iova+=4096;r2.physical+=0x10000;
|
||||
CHECK(update(0,0,&r,1)==ArmSmmu4KSuccess && result.whole_verification);
|
||||
*leaf_of(r2.iova)=(r2.physical|0x703);reset_counters();
|
||||
CHECK(update(&r,1,&r2,1)==ArmSmmu4KStateMismatch && !result.whole_verification);
|
||||
CHECK(state.poisoned && !result.descriptor_writes && !f.writes && !result.complete);++count;
|
||||
setup(48,1,48,0,0x1ff000);
|
||||
CHECK(update(0,0,&r,1)==ArmSmmu4KSuccess);
|
||||
const ArmSmmu4KRange both[2]={r,r2};
|
||||
while(state.generation<63) {
|
||||
reset_counters();CHECK(update(&r,1,&r2,1)==ArmSmmu4KSuccess && !result.whole_verification);complete(0,2);
|
||||
CHECK(pte(r2.iova)==(r2.physical|source_permission_pte(3,1)|3));
|
||||
reset_counters();CHECK(update(both,2,&r2,0)==ArmSmmu4KSuccess && !result.whole_verification);complete(1,1);
|
||||
CHECK(!pte(r2.iova));
|
||||
}
|
||||
reset_counters();CHECK(update(&r,1,&r2,1)==ArmSmmu4KSuccess && !result.whole_verification && state.generation==64);
|
||||
*leaf_of(r.iova+8*4096)=UINT64_C(0x40000703);reset_counters();
|
||||
CHECK(update(both,2,&r2,0)==ArmSmmu4KStateMismatch && result.whole_verification && state.poisoned && !f.writes);++count;
|
||||
return count;
|
||||
}
|
||||
int main(void)
|
||||
{
|
||||
unsigned p=profiles(),fcount=failures(),m=mutations();refusals_and_partial();
|
||||
@@ -359,4 +446,6 @@ int main(void)
|
||||
puts("Host fixtures only: no physical table provenance, cache-coherence, hardware TLB, GPU execution or fence qualification");
|
||||
unsigned pairs=pair_profiles(),pair_faults=pair_failures();
|
||||
printf("PASS: %u dual-region native4K profiles and %u dual-region failure/alias/isolation controls, both graphs audited and retained on uncertainty\n",pairs,pair_faults);
|
||||
printf("PASS: %u batched scattered-page map/unmap transactions and refusals: one publication per leaf page, one TLB synchronization each\n",batches());
|
||||
printf("PASS: %u incremental-verification controls: a changed leaf is proved before and after its store, the whole region every 64th update\n",incremental());
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
diff --git a/meson.options b/meson.options
|
||||
index 680d08f..8b92a2b 100644
|
||||
--- a/meson.options
|
||||
+++ b/meson.options
|
||||
@@ -914,3 +914,6 @@ option('offline-air-nir-dir', type : 'string', value : '',
|
||||
description : 'Optional external AIR frontend source directory for offline tests')
|
||||
option('offline-air-nir-llvm-config', type : 'string', value : '',
|
||||
description : 'Explicit LLVM config executable for the offline AIR frontend')
|
||||
+
|
||||
+option('tu-builtin-source-id', type : 'string', value : '', description : 'Exact builtin compiler source and profile identity')
|
||||
+option('tu-builtin-pack-header', type : 'string', value : '', description : 'Explicit generated builtin pack header; empty retains compiler path')
|
||||
diff --git a/src/freedreno/vulkan/meson.build b/src/freedreno/vulkan/meson.build
|
||||
index 762ecc8..b544a9f 100644
|
||||
--- a/src/freedreno/vulkan/meson.build
|
||||
+++ b/src/freedreno/vulkan/meson.build
|
||||
@@ -139,7 +139,11 @@ endif
|
||||
|
||||
if freedreno_kmds.contains('darwin')
|
||||
tu_flags += '-DTU_HAS_DARWIN'
|
||||
- libtu_files += files('tu_knl_darwin.cc')
|
||||
+ libtu_files += files('tu_knl_darwin.cc', 'tu_builtin_cache.cc')
|
||||
+ tu_flags += '-DTU_BUILTIN_SOURCE_ID="' + get_option('tu-builtin-source-id') + '"'
|
||||
+ if get_option('tu-builtin-pack-header') != ''
|
||||
+ tu_flags += '-DTU_BUILTIN_PACK_HEADER="' + get_option('tu-builtin-pack-header') + '"'
|
||||
+ endif
|
||||
libtu_includes += include_directories(get_option('tu-darwin-abi-dir'), is_system : false)
|
||||
tu_deps += dependency('appleframeworks', modules : ['IOKit', 'CoreGraphics', 'Foundation', 'QuartzCore'])
|
||||
# The wsi layer's device entrypoint table references weak symbols that nothing defines; ld64 needs them allowed.
|
||||
diff --git a/src/freedreno/vulkan/tu_builtin_cache.cc b/src/freedreno/vulkan/tu_builtin_cache.cc
|
||||
new file mode 100644
|
||||
index 0000000..1251f35
|
||||
--- /dev/null
|
||||
+++ b/src/freedreno/vulkan/tu_builtin_cache.cc
|
||||
@@ -0,0 +1,259 @@
|
||||
+/* SPDX-License-Identifier: MIT */
|
||||
+/* Driver-owned, build-time embedded builtin variants. This is not a general
|
||||
+ * untrusted shader import API. Pack generation must use this exact compiler
|
||||
+ * source/profile and GPU, and the final driver must carry the normal owner
|
||||
+ * signature. No disk I/O, cache thread, fake completion or shader execution. */
|
||||
+#include "tu_builtin_cache.h"
|
||||
+#include "tu_device.h"
|
||||
+#include "tu_shader.h"
|
||||
+#include "tu_cs.h"
|
||||
+#include "ir3/ir3_compiler.h"
|
||||
+#include "ir3/ir3_shader.h"
|
||||
+#include "util/blob.h"
|
||||
+#include "util/mesa-blake3.h"
|
||||
+
|
||||
+#ifdef TU_BUILTIN_PACK_HEADER
|
||||
+#include TU_BUILTIN_PACK_HEADER
|
||||
+#endif
|
||||
+
|
||||
+static constexpr unsigned variant_count = GLOBAL_SH_COUNT + 2;
|
||||
+static constexpr size_t max_pack_size = 4 * 1024 * 1024;
|
||||
+static constexpr size_t max_variant_size = 256 * 1024;
|
||||
+
|
||||
+static void
|
||||
+identity(struct tu_device *dev, struct blob *out)
|
||||
+{
|
||||
+ blob_write_bytes(out, "TUBUILT1", 8);
|
||||
+ blob_write_uint32(out, 1);
|
||||
+ blob_write_uint32(out, variant_count);
|
||||
+ blob_write_bytes(out, TU_BUILTIN_SOURCE_ID, strlen(TU_BUILTIN_SOURCE_ID));
|
||||
+ blob_write_uint32(out, dev->physical_device->dev_id.gpu_id);
|
||||
+ blob_write_uint64(out, dev->physical_device->dev_id.chip_id);
|
||||
+ blob_write_uint64(out, ir3_shader_debug_hash_key());
|
||||
+ blob_write_uint32(out, sizeof(struct ir3_shader_variant));
|
||||
+ blob_write_uint32(out, VARIANT_CACHE_SIZE);
|
||||
+ blob_write_uint32(out, sizeof(struct ir3_const_state));
|
||||
+ blob_write_uint32(out, sizeof(struct ir3_shader_key));
|
||||
+ const struct ir3_compiler_options *o = &dev->compiler->options;
|
||||
+ uint32_t flags = o->push_ubo_with_preamble | (o->disable_cache << 1) |
|
||||
+ (o->storage_16bit << 2) | (o->storage_8bit << 3) |
|
||||
+ (o->lower_base_vertex << 4) | (o->shared_push_consts << 5) |
|
||||
+ (o->dual_color_blend_by_location << 6);
|
||||
+ blob_write_uint32(out, flags);
|
||||
+ blob_write_uint32(out, o->bindless_fb_read_descriptor);
|
||||
+ blob_write_uint32(out, o->bindless_fb_read_slot);
|
||||
+ blob_write_uint64(out, o->uche_trap_base);
|
||||
+}
|
||||
+
|
||||
+static bool
|
||||
+available(const struct blob_reader *in, size_t bytes)
|
||||
+{
|
||||
+ return !in->overrun && bytes <= (size_t)(in->end - in->current);
|
||||
+}
|
||||
+
|
||||
+/* Read only after bounding the record and every allocation. Unlike the
|
||||
+ * general IR3 disk-cache reader, failure is checked before dereferencing an
|
||||
+ * allocation, and no allocation size is taken from an unchecked short blob. */
|
||||
+static struct ir3_shader_variant *
|
||||
+read_variant(struct blob_reader *in, struct ir3_compiler *compiler, void *owner)
|
||||
+{
|
||||
+ if (!available(in, sizeof(struct ir3_shader_key) + 8))
|
||||
+ return NULL;
|
||||
+ struct ir3_shader_key key;
|
||||
+ blob_copy_bytes(in, &key, sizeof(key));
|
||||
+ const struct ir3_shader_key zero_key = {};
|
||||
+ if (memcmp(&key, &zero_key, sizeof(key)))
|
||||
+ return NULL;
|
||||
+ const uint32_t stage = blob_read_uint32(in);
|
||||
+ const uint32_t mergedregs = blob_read_uint32(in);
|
||||
+ if ((stage != MESA_SHADER_VERTEX && stage != MESA_SHADER_FRAGMENT) || mergedregs > 1)
|
||||
+ return NULL;
|
||||
+ struct ir3_shader_variant *v = rzalloc(owner, struct ir3_shader_variant);
|
||||
+ if (!v)
|
||||
+ return NULL;
|
||||
+ v->compiler = compiler;
|
||||
+ v->key = key;
|
||||
+ v->type = (mesa_shader_stage) stage;
|
||||
+ v->mergedregs = mergedregs;
|
||||
+ v->const_state = rzalloc(v, struct ir3_const_state);
|
||||
+ if (!v->const_state)
|
||||
+ return NULL;
|
||||
+ unsigned count = stage == MESA_SHADER_VERTEX && ir3_has_binning_vs(&key) ? 2 : 1;
|
||||
+ for (unsigned i = 0; i < count; ++i) {
|
||||
+ struct ir3_shader_variant *part = v;
|
||||
+ if (i) {
|
||||
+ part = rzalloc(v, struct ir3_shader_variant);
|
||||
+ if (!part)
|
||||
+ return NULL;
|
||||
+ v->binning = part;
|
||||
+ part->compiler = compiler;
|
||||
+ part->key = key;
|
||||
+ part->type = MESA_SHADER_VERTEX;
|
||||
+ part->mergedregs = mergedregs;
|
||||
+ part->binning_pass = true;
|
||||
+ part->nonbinning = v;
|
||||
+ part->const_state = v->const_state;
|
||||
+ }
|
||||
+ if (!available(in, VARIANT_CACHE_SIZE))
|
||||
+ return NULL;
|
||||
+ blob_copy_bytes(in, VARIANT_CACHE_PTR(part), VARIANT_CACHE_SIZE);
|
||||
+ const size_t size = part->info.size;
|
||||
+ if (!size || size > max_variant_size || size % 4 ||
|
||||
+ part->info.sizedwords * sizeof(uint32_t) > size ||
|
||||
+ part->info.constant_data_offset > size || !available(in, size))
|
||||
+ return NULL;
|
||||
+ part->bin = (uint32_t *) ralloc_size(part, size);
|
||||
+ if (!part->bin)
|
||||
+ return NULL;
|
||||
+ blob_copy_bytes(in, part->bin, size);
|
||||
+ if (!i) {
|
||||
+ if (!available(in, sizeof(*part->const_state)))
|
||||
+ return NULL;
|
||||
+ blob_copy_bytes(in, part->const_state, sizeof(*part->const_state));
|
||||
+ }
|
||||
+ if (!compiler->info->props.load_shader_consts_via_preamble) {
|
||||
+ if (!available(in, 4))
|
||||
+ return NULL;
|
||||
+ unsigned n = blob_read_uint32(in);
|
||||
+ if (n > max_variant_size / 4 || !available(in, (size_t)n * 4))
|
||||
+ return NULL;
|
||||
+ part->imm_state.size = part->imm_state.count = n;
|
||||
+ if (n) {
|
||||
+ part->imm_state.values = (uint32_t *) ralloc_size(part, (size_t)n * 4);
|
||||
+ if (!part->imm_state.values)
|
||||
+ return NULL;
|
||||
+ blob_copy_bytes(in, part->imm_state.values, (size_t)n * 4);
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ return v;
|
||||
+}
|
||||
+
|
||||
+static bool
|
||||
+decode(struct tu_device *dev, const void *data, size_t size, void *owner,
|
||||
+ struct ir3_shader_variant **variants)
|
||||
+{
|
||||
+ if (strlen(TU_BUILTIN_SOURCE_ID) != 64 || !data || size > max_pack_size || size < 32)
|
||||
+ return false;
|
||||
+ uint8_t digest[BLAKE3_OUT_LEN];
|
||||
+ _mesa_blake3_compute(data, size - sizeof(digest), digest);
|
||||
+ if (memcmp(digest, (const uint8_t *)data + size - sizeof(digest), sizeof(digest)))
|
||||
+ return false;
|
||||
+ struct blob header;
|
||||
+ blob_init(&header);
|
||||
+ identity(dev, &header);
|
||||
+ const bool matches = !header.out_of_memory && header.size <= size - sizeof(digest) &&
|
||||
+ !memcmp(header.data, data, header.size);
|
||||
+ const size_t prefix = header.size;
|
||||
+ blob_finish(&header);
|
||||
+ if (!matches)
|
||||
+ return false;
|
||||
+ struct blob_reader in;
|
||||
+ blob_reader_init(&in, (const uint8_t *)data + prefix, size - prefix - sizeof(digest));
|
||||
+ for (unsigned i = 0; i < variant_count; ++i) {
|
||||
+ if (!available(&in, 8) || blob_read_uint32(&in) != i)
|
||||
+ return false;
|
||||
+ const size_t length = blob_read_uint32(&in);
|
||||
+ if (!length || length > max_variant_size || !available(&in, length))
|
||||
+ return false;
|
||||
+ struct blob_reader entry;
|
||||
+ blob_reader_init(&entry, in.current, length);
|
||||
+ variants[i] = read_variant(&entry, dev->compiler, owner);
|
||||
+ if (!variants[i] || entry.overrun || entry.current != entry.end ||
|
||||
+ variants[i]->type != (i < 3 ? MESA_SHADER_VERTEX : MESA_SHADER_FRAGMENT))
|
||||
+ return false;
|
||||
+ in.current += length;
|
||||
+ }
|
||||
+ uint64_t words = 0;
|
||||
+ for (unsigned i = 0; i < GLOBAL_SH_COUNT; ++i)
|
||||
+ words += align(variants[i]->info.sizedwords, 32);
|
||||
+ return words <= ARRAY_SIZE(((struct tu6_global *)0)->shaders) &&
|
||||
+ !in.overrun && in.current == in.end;
|
||||
+}
|
||||
+
|
||||
+VkResult
|
||||
+tu_builtin_pack_load(struct tu_device *dev)
|
||||
+{
|
||||
+#ifdef TU_BUILTIN_PACK_HEADER
|
||||
+ void *owner = ralloc_context(NULL);
|
||||
+ if (!owner)
|
||||
+ return VK_ERROR_OUT_OF_HOST_MEMORY;
|
||||
+ if (!decode(dev, tu_builtin_pack_bytes, sizeof(tu_builtin_pack_bytes), owner, dev->builtin_variants)) {
|
||||
+ ralloc_free(owner);
|
||||
+ memset(dev->builtin_variants, 0, sizeof(dev->builtin_variants));
|
||||
+ /* A requested pack is mandatory: no silent runtime compiler fallback. */
|
||||
+ return VK_ERROR_INITIALIZATION_FAILED;
|
||||
+ }
|
||||
+ dev->builtin_cache_owner = owner;
|
||||
+#endif
|
||||
+ return VK_SUCCESS;
|
||||
+}
|
||||
+
|
||||
+/* Explicit offline tool interfaces. They never load a pack into a running
|
||||
+ * device, replace a shader, submit commands, or change Vulkan feature reports. */
|
||||
+extern "C" __attribute__((visibility("default"))) VkResult
|
||||
+tu_export_builtin_shader_pack(VkDevice handle, size_t *size, void *data)
|
||||
+{
|
||||
+ VK_FROM_HANDLE(tu_device, dev, handle);
|
||||
+ if (!dev || !size || strlen(TU_BUILTIN_SOURCE_ID) != 64)
|
||||
+ return VK_ERROR_INITIALIZATION_FAILED;
|
||||
+ struct blob out;
|
||||
+ blob_init(&out);
|
||||
+ identity(dev, &out);
|
||||
+ for (unsigned i = 0; i < variant_count; ++i) {
|
||||
+ const struct ir3_shader_variant *v = i < GLOBAL_SH_COUNT ? dev->global_shader_variants[i] :
|
||||
+ (i == GLOBAL_SH_COUNT ? dev->empty_fs->variant : dev->empty_fs_fdm->variant);
|
||||
+ if (!v) {
|
||||
+ blob_finish(&out);
|
||||
+ return VK_ERROR_INITIALIZATION_FAILED;
|
||||
+ }
|
||||
+ struct blob entry;
|
||||
+ blob_init(&entry);
|
||||
+ ir3_store_variant(&entry, v);
|
||||
+ blob_write_uint32(&out, i);
|
||||
+ blob_write_uint32(&out, entry.size);
|
||||
+ blob_write_bytes(&out, entry.data, entry.size);
|
||||
+ bool failed = entry.out_of_memory;
|
||||
+ blob_finish(&entry);
|
||||
+ if (failed) {
|
||||
+ blob_finish(&out);
|
||||
+ return VK_ERROR_OUT_OF_HOST_MEMORY;
|
||||
+ }
|
||||
+ }
|
||||
+ if (out.out_of_memory || out.size > max_pack_size - BLAKE3_OUT_LEN) {
|
||||
+ blob_finish(&out);
|
||||
+ return VK_ERROR_OUT_OF_HOST_MEMORY;
|
||||
+ }
|
||||
+ uint8_t digest[BLAKE3_OUT_LEN];
|
||||
+ _mesa_blake3_compute(out.data, out.size, digest);
|
||||
+ blob_write_bytes(&out, digest, sizeof(digest));
|
||||
+ if (out.out_of_memory || out.size > max_pack_size) {
|
||||
+ blob_finish(&out);
|
||||
+ return VK_ERROR_OUT_OF_HOST_MEMORY;
|
||||
+ }
|
||||
+ VkResult result = VK_SUCCESS;
|
||||
+ if (data) {
|
||||
+ if (*size < out.size)
|
||||
+ result = VK_INCOMPLETE;
|
||||
+ else
|
||||
+ memcpy(data, out.data, out.size);
|
||||
+ }
|
||||
+ *size = out.size;
|
||||
+ blob_finish(&out);
|
||||
+ return result;
|
||||
+}
|
||||
+
|
||||
+extern "C" __attribute__((visibility("default"))) VkResult
|
||||
+tu_validate_builtin_shader_pack(VkDevice handle, size_t size, const void *data)
|
||||
+{
|
||||
+ VK_FROM_HANDLE(tu_device, dev, handle);
|
||||
+ if (!dev)
|
||||
+ return VK_ERROR_INITIALIZATION_FAILED;
|
||||
+ void *owner = ralloc_context(NULL);
|
||||
+ if (!owner)
|
||||
+ return VK_ERROR_OUT_OF_HOST_MEMORY;
|
||||
+ struct ir3_shader_variant *variants[variant_count] = {};
|
||||
+ bool valid = decode(dev, data, size, owner, variants);
|
||||
+ ralloc_free(owner);
|
||||
+ return valid ? VK_SUCCESS : VK_ERROR_INITIALIZATION_FAILED;
|
||||
+}
|
||||
diff --git a/src/freedreno/vulkan/tu_builtin_cache.h b/src/freedreno/vulkan/tu_builtin_cache.h
|
||||
new file mode 100644
|
||||
index 0000000..74e97e6
|
||||
--- /dev/null
|
||||
+++ b/src/freedreno/vulkan/tu_builtin_cache.h
|
||||
@@ -0,0 +1,8 @@
|
||||
+/* SPDX-License-Identifier: MIT */
|
||||
+#ifndef TU_BUILTIN_CACHE_H
|
||||
+#define TU_BUILTIN_CACHE_H
|
||||
+#include "tu_common.h"
|
||||
+VkResult tu_builtin_pack_load(struct tu_device *dev);
|
||||
+extern "C" VkResult tu_export_builtin_shader_pack(VkDevice device, size_t *size, void *data);
|
||||
+extern "C" VkResult tu_validate_builtin_shader_pack(VkDevice device, size_t size, const void *data);
|
||||
+#endif
|
||||
diff --git a/src/freedreno/vulkan/tu_clear_blit.cc b/src/freedreno/vulkan/tu_clear_blit.cc
|
||||
index 47a2fc6..1d33085 100644
|
||||
--- a/src/freedreno/vulkan/tu_clear_blit.cc
|
||||
+++ b/src/freedreno/vulkan/tu_clear_blit.cc
|
||||
@@ -981,33 +981,39 @@ static void
|
||||
compile_shader(struct tu_device *dev, struct nir_shader *nir,
|
||||
unsigned consts, unsigned *offset, enum global_shader idx)
|
||||
{
|
||||
- nir->options = ir3_get_compiler_options(dev->compiler);
|
||||
+ /* A provisioned variant bypasses NIR/compiler work while retaining the
|
||||
+ * original global-BO upload, addresses and command emission below. */
|
||||
+ struct ir3_shader_variant *so = dev->builtin_variants[idx];
|
||||
+ struct ir3_shader *sh = NULL;
|
||||
+ if (so) {
|
||||
+ ralloc_free(nir);
|
||||
+ } else {
|
||||
+ nir->options = ir3_get_compiler_options(dev->compiler);
|
||||
|
||||
- nir_assign_io_var_locations(nir, nir_var_shader_in);
|
||||
- nir_assign_io_var_locations(nir, nir_var_shader_out);
|
||||
+ nir_assign_io_var_locations(nir, nir_var_shader_in);
|
||||
+ nir_assign_io_var_locations(nir, nir_var_shader_out);
|
||||
|
||||
- struct ir3_const_allocations const_allocs = {};
|
||||
- if (consts > 0)
|
||||
- ir3_const_alloc(&const_allocs, IR3_CONST_ALLOC_UBO_RANGES, align(consts, 8), 1);
|
||||
+ struct ir3_const_allocations const_allocs = {};
|
||||
+ if (consts > 0)
|
||||
+ ir3_const_alloc(&const_allocs, IR3_CONST_ALLOC_UBO_RANGES, align(consts, 8), 1);
|
||||
|
||||
- const struct ir3_shader_options options = {
|
||||
- .api_wavesize = IR3_SINGLE_OR_DOUBLE,
|
||||
- .real_wavesize = IR3_SINGLE_OR_DOUBLE,
|
||||
- .const_allocs = const_allocs,
|
||||
- .fragdata_dynamic_remap =
|
||||
- idx >= GLOBAL_SH_VS_CLEAR && idx <= GLOBAL_SH_FS_CLEAR_MAX,
|
||||
- };
|
||||
+ const struct ir3_shader_options options = {
|
||||
+ .api_wavesize = IR3_SINGLE_OR_DOUBLE,
|
||||
+ .real_wavesize = IR3_SINGLE_OR_DOUBLE,
|
||||
+ .const_allocs = const_allocs,
|
||||
+ .fragdata_dynamic_remap =
|
||||
+ idx >= GLOBAL_SH_VS_CLEAR && idx <= GLOBAL_SH_FS_CLEAR_MAX,
|
||||
+ };
|
||||
|
||||
- ir3_finalize_nir(dev->compiler, &options.nir_options, nir);
|
||||
+ ir3_finalize_nir(dev->compiler, &options.nir_options, nir);
|
||||
|
||||
- ir3_nir_lower_io(nir);
|
||||
+ ir3_nir_lower_io(nir);
|
||||
|
||||
- struct ir3_shader *sh =
|
||||
- ir3_shader_from_nir(dev->compiler, nir, &options);
|
||||
+ sh = ir3_shader_from_nir(dev->compiler, nir, &options);
|
||||
|
||||
- struct ir3_shader_key key = {};
|
||||
- struct ir3_shader_variant *so =
|
||||
- ir3_shader_get_variant(sh, &key, false, false, NULL, NULL);
|
||||
+ struct ir3_shader_key key = {};
|
||||
+ so = ir3_shader_get_variant(sh, &key, false, false, NULL, NULL);
|
||||
+ }
|
||||
|
||||
struct tu6_global *global = dev->global_bo_map;
|
||||
|
||||
@@ -1045,6 +1051,8 @@ tu_destroy_clear_blit_shaders(struct tu_device *dev)
|
||||
if (dev->global_shaders[i])
|
||||
ir3_shader_destroy(dev->global_shaders[i]);
|
||||
}
|
||||
+ ralloc_free(dev->builtin_cache_owner);
|
||||
+ dev->builtin_cache_owner = NULL;
|
||||
}
|
||||
|
||||
enum r3d_type {
|
||||
diff --git a/src/freedreno/vulkan/tu_device.cc b/src/freedreno/vulkan/tu_device.cc
|
||||
index 3b512f8..527a4d4 100644
|
||||
--- a/src/freedreno/vulkan/tu_device.cc
|
||||
+++ b/src/freedreno/vulkan/tu_device.cc
|
||||
@@ -34,6 +34,7 @@
|
||||
#include "fdl/freedreno_layout.h"
|
||||
#include "tu_acceleration_structure.h"
|
||||
#include "tu_clear_blit.h"
|
||||
+#include "tu_builtin_cache.h"
|
||||
#include "tu_cmd_buffer.h"
|
||||
#include "tu_cs.h"
|
||||
#include "tu_descriptor_set.h"
|
||||
@@ -3049,6 +3050,11 @@ tu_CreateDevice(VkPhysicalDevice physicalDevice,
|
||||
|
||||
global = (struct tu6_global *)device->global_bo->map;
|
||||
device->global_bo_map = global;
|
||||
+#ifdef TU_HAS_DARWIN
|
||||
+ result = tu_builtin_pack_load(device);
|
||||
+ if (result != VK_SUCCESS)
|
||||
+ goto fail_null_accel_struct;
|
||||
+#endif
|
||||
tu_init_clear_blit_shaders(device);
|
||||
|
||||
if (device->vk.enabled_features.accelerationStructure &&
|
||||
diff --git a/src/freedreno/vulkan/tu_device.h b/src/freedreno/vulkan/tu_device.h
|
||||
index ce1d494..356ab86 100644
|
||||
--- a/src/freedreno/vulkan/tu_device.h
|
||||
+++ b/src/freedreno/vulkan/tu_device.h
|
||||
@@ -136,6 +136,8 @@ struct tu_physical_device
|
||||
uint64_t va_size;
|
||||
|
||||
bool has_cached_coherent_memory;
|
||||
+ /* The kernel pins host pages as BOs (VK_EXT_external_memory_host). */
|
||||
+ bool has_host_import;
|
||||
bool has_cached_non_coherent_memory;
|
||||
/* Index for device local, host-coherent, host-cached memory.
|
||||
* Only set to positive index when overwriting device-local, uncached memory
|
||||
@@ -310,6 +312,8 @@ struct tu_device
|
||||
int fd;
|
||||
|
||||
struct ir3_compiler *compiler;
|
||||
+ void *builtin_cache_owner;
|
||||
+ struct ir3_shader_variant *builtin_variants[GLOBAL_SH_COUNT + 2];
|
||||
|
||||
/* Backup in-memory cache to be used if the app doesn't provide one */
|
||||
struct vk_pipeline_cache *mem_cache;
|
||||
diff --git a/src/freedreno/vulkan/tu_shader.cc b/src/freedreno/vulkan/tu_shader.cc
|
||||
index 8c228e9..d99d55c 100644
|
||||
--- a/src/freedreno/vulkan/tu_shader.cc
|
||||
+++ b/src/freedreno/vulkan/tu_shader.cc
|
||||
@@ -3858,10 +3858,21 @@ tu_empty_fs_create(struct tu_device *dev, struct tu_shader **shader,
|
||||
for (unsigned i = 0; i < MAX_SETS; i++)
|
||||
(*shader)->dynamic_descriptor_sizes[i] = -1;
|
||||
|
||||
- struct ir3_shader *ir3_shader =
|
||||
- ir3_shader_from_nir(dev->compiler, fs_b.shader, &options);
|
||||
- (*shader)->variant = ir3_shader_create_variant(ir3_shader, &key, false);
|
||||
- ir3_shader_destroy(ir3_shader);
|
||||
+ struct ir3_shader_variant *cached =
|
||||
+ dev->builtin_variants[GLOBAL_SH_COUNT + fragment_density_map];
|
||||
+ if (cached) {
|
||||
+ /* The shader object owns its variant independently of the global
|
||||
+ * builtin context: normal shader destruction remains unchanged. */
|
||||
+ ralloc_steal(NULL, cached);
|
||||
+ (*shader)->variant = cached;
|
||||
+ dev->builtin_variants[GLOBAL_SH_COUNT + fragment_density_map] = NULL;
|
||||
+ ralloc_free(fs_b.shader);
|
||||
+ } else {
|
||||
+ struct ir3_shader *ir3_shader =
|
||||
+ ir3_shader_from_nir(dev->compiler, fs_b.shader, &options);
|
||||
+ (*shader)->variant = ir3_shader_create_variant(ir3_shader, &key, false);
|
||||
+ ir3_shader_destroy(ir3_shader);
|
||||
+ }
|
||||
|
||||
return tu_upload_shader(dev, *shader);
|
||||
}
|
||||
@@ -170,6 +170,17 @@ index 82efb07..3b512f8 100644
|
||||
VkResult
|
||||
tu_allocate_lazy_memory(struct tu_device *dev,
|
||||
struct tu_device_memory *mem)
|
||||
diff --git a/src/freedreno/vulkan/tu_device.h b/src/freedreno/vulkan/tu_device.h
|
||||
--- a/src/freedreno/vulkan/tu_device.h
|
||||
+++ b/src/freedreno/vulkan/tu_device.h
|
||||
@@ -300,6 +300,7 @@
|
||||
{
|
||||
struct vk_device vk;
|
||||
struct tu_instance *instance;
|
||||
+ void *darwin_retire; /* Darwin KMD's bounded deferred BO retirement state. */
|
||||
|
||||
struct tu_queue *queues[TU_MAX_QUEUE_FAMILIES];
|
||||
int queue_count[TU_MAX_QUEUE_FAMILIES];
|
||||
diff --git a/src/freedreno/vulkan/tu_knl.cc b/src/freedreno/vulkan/tu_knl.cc
|
||||
index ab27798..9c545c6 100644
|
||||
--- a/src/freedreno/vulkan/tu_knl.cc
|
||||
@@ -257,10 +268,10 @@ index 4c2bf6a..a8ccd76 100644
|
||||
VkResult tu_knl_drm_msm_load(struct tu_instance *instance,
|
||||
diff --git a/src/freedreno/vulkan/tu_knl_darwin.cc b/src/freedreno/vulkan/tu_knl_darwin.cc
|
||||
new file mode 100644
|
||||
index 0000000..8601aaa
|
||||
index 0000000..b03c6ee
|
||||
--- /dev/null
|
||||
+++ b/src/freedreno/vulkan/tu_knl_darwin.cc
|
||||
@@ -0,0 +1,967 @@
|
||||
@@ -0,0 +1,1110 @@
|
||||
+/*
|
||||
+ * Native Darwin kernel backend for Turnip: talks to the NativeAdreno IOKit user client (NativeAdrenoABI.h, version 1).
|
||||
+ * Modelled on tu_knl_kgsl.cc (no libdrm) and tu_knl_drm_msm.cc (the client chooses GPU addresses).
|
||||
@@ -315,6 +326,23 @@ index 0000000..8601aaa
|
||||
+ NativeAdrenoAbiParams params;
|
||||
+} dw = { .lock = PTHREAD_MUTEX_INITIALIZER };
|
||||
+
|
||||
+/* Preserve the native handle, CPU view and IOVA until the captured ring fence
|
||||
+ * actually completed. An unrelated later submission never extends this lease.
|
||||
+ * The queue is bounded; pressure may wait, but ordinary destruction does not. */
|
||||
+enum { DARWIN_RETIRED_BOS = 256 };
|
||||
+struct darwin_retired_bo {
|
||||
+ uint64_t handle, iova, size;
|
||||
+ void *mapping;
|
||||
+ uint32_t token, fence;
|
||||
+};
|
||||
+struct darwin_retire_state {
|
||||
+ pthread_mutex_t lock;
|
||||
+ struct darwin_retired_bo entries[DARWIN_RETIRED_BOS];
|
||||
+ unsigned head, count;
|
||||
+ VkResult failure;
|
||||
+ uint64_t enqueued, reaped, pressure_waits;
|
||||
+};
|
||||
+
|
||||
+static kern_return_t
|
||||
+dw_call(uint32_t method, const void *in, size_t in_size, void *out,
|
||||
+ size_t out_size)
|
||||
@@ -418,7 +446,7 @@ index 0000000..8601aaa
|
||||
+
|
||||
+/* Waits for a fence in short kernel waits so the connection lock is never held for long. */
|
||||
+static VkResult
|
||||
+dw_wait_fence_locked_free(struct tu_device *dev, uint32_t fence, uint64_t abs_timeout_ns)
|
||||
+dw_wait_fence_raw(struct tu_device *dev, uint32_t fence, uint64_t abs_timeout_ns)
|
||||
+{
|
||||
+ for (;;) {
|
||||
+ uint64_t now = os_time_get_nano();
|
||||
@@ -438,6 +466,17 @@ index 0000000..8601aaa
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+static VkResult darwin_collect_retired(struct tu_device *dev);
|
||||
+
|
||||
+static VkResult
|
||||
+dw_wait_fence_locked_free(struct tu_device *dev, uint32_t fence, uint64_t abs_timeout_ns)
|
||||
+{
|
||||
+ VkResult result = dw_wait_fence_raw(dev, fence, abs_timeout_ns);
|
||||
+ /* Fence observation is also a retirement opportunity, so a quiet application
|
||||
+ * need not allocate another BO to release completed deferred storage. */
|
||||
+ return result == VK_SUCCESS ? darwin_collect_retired(dev) : result;
|
||||
+}
|
||||
+
|
||||
+static VkResult
|
||||
+darwin_queue_wait_fence(struct tu_queue *queue, uint32_t fence, uint64_t timeout_ns)
|
||||
+{
|
||||
@@ -494,6 +533,87 @@ index 0000000..8601aaa
|
||||
+ mtx_unlock(&dev->vma_mutex);
|
||||
+}
|
||||
+
|
||||
+/* Called under retire->lock. All fence polls are nonblocking. Failed/uncertain
|
||||
+ * releases retain the record and the IOVA; a busy BO is retried by a later sweep. */
|
||||
+static VkResult
|
||||
+darwin_reap_locked(struct tu_device *dev, struct darwin_retire_state *retire)
|
||||
+{
|
||||
+ if (retire->failure != VK_SUCCESS)
|
||||
+ return retire->failure;
|
||||
+ /* Cache only within this sweep. Persisting a bare 32-bit fence across sweeps
|
||||
+ * could mistake a later wrapped value for an old observed completion. */
|
||||
+ uint32_t completed_fence = 0;
|
||||
+ bool have_completed_fence = false;
|
||||
+ while (retire->count) {
|
||||
+ struct darwin_retired_bo *bo = &retire->entries[retire->head];
|
||||
+ if (bo->fence && (!have_completed_fence || bo->fence != completed_fence)) {
|
||||
+ VkResult result = dw_wait_fence_raw(dev, bo->fence, 0);
|
||||
+ if (result == VK_TIMEOUT)
|
||||
+ return VK_SUCCESS;
|
||||
+ if (result != VK_SUCCESS)
|
||||
+ return retire->failure = result;
|
||||
+ have_completed_fence = true;
|
||||
+ completed_fence = bo->fence;
|
||||
+ }
|
||||
+ if (bo->token) {
|
||||
+ if (bo->mapping) {
|
||||
+ kern_return_t kr = IOConnectUnmapMemory64(dw.conn, bo->token, mach_task_self(),
|
||||
+ (mach_vm_address_t)(uintptr_t)bo->mapping);
|
||||
+ if (kr != KERN_SUCCESS)
|
||||
+ return retire->failure = vk_device_set_lost(&dev->vk, "deferred CPU unmap failed: 0x%x", kr);
|
||||
+ bo->mapping = NULL;
|
||||
+ }
|
||||
+ NativeAdrenoAbiBoCpuUnmap undo = {.handle = bo->handle, .token = bo->token};
|
||||
+ kern_return_t kr = dw_call(NativeAdrenoMethodBoCpuUnmap, &undo, sizeof(undo), NULL, 0);
|
||||
+ if (kr != KERN_SUCCESS)
|
||||
+ return retire->failure = vk_device_set_lost(&dev->vk, "deferred CPU lease release failed: 0x%x", kr);
|
||||
+ bo->token = 0;
|
||||
+ }
|
||||
+ NativeAdrenoAbiBoFree release = {.handle = bo->handle};
|
||||
+ kern_return_t kr = dw_call(NativeAdrenoMethodBoFree, &release, sizeof(release), NULL, 0);
|
||||
+ if (kr == kIOReturnBusy)
|
||||
+ return VK_SUCCESS;
|
||||
+ if (kr != KERN_SUCCESS)
|
||||
+ return retire->failure = vk_device_set_lost(&dev->vk, "deferred BoFree failed: 0x%x", kr);
|
||||
+ darwin_free_iova(dev, bo->iova, bo->size);
|
||||
+ *bo = {};
|
||||
+ retire->head = (retire->head + 1) % DARWIN_RETIRED_BOS;
|
||||
+ --retire->count;
|
||||
+ ++retire->reaped;
|
||||
+ }
|
||||
+ return VK_SUCCESS;
|
||||
+}
|
||||
+
|
||||
+static VkResult
|
||||
+darwin_collect_retired(struct tu_device *dev)
|
||||
+{
|
||||
+ struct darwin_retire_state *retire = (struct darwin_retire_state *)dev->darwin_retire;
|
||||
+ pthread_mutex_lock(&retire->lock);
|
||||
+ VkResult result = darwin_reap_locked(dev, retire);
|
||||
+ pthread_mutex_unlock(&retire->lock);
|
||||
+ return result;
|
||||
+}
|
||||
+
|
||||
+static VkResult
|
||||
+darwin_map_failure(struct tu_device *dev, uint64_t handle, uint64_t iova,
|
||||
+ uint64_t size, kern_return_t map_error)
|
||||
+{
|
||||
+ NativeAdrenoAbiBoFree release = {.handle = handle};
|
||||
+ kern_return_t cleanup = dw_call(NativeAdrenoMethodBoFree, &release, sizeof(release), NULL, 0);
|
||||
+ if (cleanup == KERN_SUCCESS) {
|
||||
+ darwin_free_iova(dev, iova, size);
|
||||
+ return vk_errorf(dev, VK_ERROR_OUT_OF_DEVICE_MEMORY, "BoMap failed: 0x%x", map_error);
|
||||
+ }
|
||||
+ /* A failed map may have modified tables. Unknown cleanup never authorizes
|
||||
+ * reusing its IOVA, even though no tu_bo could be published to the caller. */
|
||||
+ struct darwin_retire_state *retire = (struct darwin_retire_state *)dev->darwin_retire;
|
||||
+ pthread_mutex_lock(&retire->lock);
|
||||
+ retire->failure = vk_device_set_lost(&dev->vk, "BoMap cleanup failed: 0x%x; retaining native allocation and IOVA", cleanup);
|
||||
+ VkResult result = retire->failure;
|
||||
+ pthread_mutex_unlock(&retire->lock);
|
||||
+ return result;
|
||||
+}
|
||||
+
|
||||
+/* ---- buffer objects ---- */
|
||||
+
|
||||
+static VkResult
|
||||
@@ -507,6 +627,10 @@ index 0000000..8601aaa
|
||||
+ if (lazy_vma)
|
||||
+ return vk_error(dev, VK_ERROR_FEATURE_NOT_PRESENT);
|
||||
+
|
||||
+ VkResult collected = darwin_collect_retired(dev);
|
||||
+ if (collected != VK_SUCCESS)
|
||||
+ return collected;
|
||||
+
|
||||
+ uint64_t iova;
|
||||
+ VkResult result = darwin_allocate_iova(dev, size, client_iova, flags, &iova);
|
||||
+ if (result != VK_SUCCESS)
|
||||
@@ -530,10 +654,7 @@ index 0000000..8601aaa
|
||||
+ NativeAdrenoAbiBoMapReply mapped = {};
|
||||
+ kr = dw_call(NativeAdrenoMethodBoMap, &map, sizeof(map), &mapped, sizeof(mapped));
|
||||
+ if (kr != KERN_SUCCESS) {
|
||||
+ NativeAdrenoAbiBoFree release = {.handle = reply.handle};
|
||||
+ dw_call(NativeAdrenoMethodBoFree, &release, sizeof(release), NULL, 0);
|
||||
+ darwin_free_iova(dev, iova, size);
|
||||
+ return vk_errorf(dev, VK_ERROR_OUT_OF_DEVICE_MEMORY, "BoMap failed: 0x%x", kr);
|
||||
+ return darwin_map_failure(dev, reply.handle, iova, size, kr);
|
||||
+ }
|
||||
+
|
||||
+ uint32_t gem_handle = (uint32_t)reply.handle;
|
||||
@@ -572,6 +693,9 @@ index 0000000..8601aaa
|
||||
+{
|
||||
+ if (!(dw.params.flags & NativeAdrenoParamsHostImport))
|
||||
+ return vk_error(dev, VK_ERROR_INVALID_EXTERNAL_HANDLE);
|
||||
+ VkResult collected = darwin_collect_retired(dev);
|
||||
+ if (collected != VK_SUCCESS)
|
||||
+ return collected;
|
||||
+ uint64_t iova;
|
||||
+ VkResult result = darwin_allocate_iova(dev, size, 0, TU_BO_ALLOC_NO_FLAGS, &iova);
|
||||
+ if (result != VK_SUCCESS)
|
||||
@@ -588,10 +712,7 @@ index 0000000..8601aaa
|
||||
+ NativeAdrenoAbiBoMapReply mapped = {};
|
||||
+ kr = dw_call(NativeAdrenoMethodBoMap, &map, sizeof(map), &mapped, sizeof(mapped));
|
||||
+ if (kr != KERN_SUCCESS) {
|
||||
+ NativeAdrenoAbiBoFree release = {.handle = reply.handle};
|
||||
+ dw_call(NativeAdrenoMethodBoFree, &release, sizeof(release), NULL, 0);
|
||||
+ darwin_free_iova(dev, iova, size);
|
||||
+ return vk_errorf(dev, VK_ERROR_OUT_OF_DEVICE_MEMORY, "BoMap of an import failed: 0x%x", kr);
|
||||
+ return darwin_map_failure(dev, reply.handle, iova, size, kr);
|
||||
+ }
|
||||
+
|
||||
+ uint32_t gem_handle = (uint32_t)reply.handle;
|
||||
@@ -690,14 +811,15 @@ index 0000000..8601aaa
|
||||
+
|
||||
+/* Waits until every submission made so far has retired, so a freed buffer cannot still be in flight. The kernel also
|
||||
+ * retires completed jobs itself; this only covers the window before the GPU passes the last fence. */
|
||||
+static void
|
||||
+static VkResult
|
||||
+darwin_wait_idle(struct tu_device *dev)
|
||||
+{
|
||||
+ pthread_mutex_lock(&dw.lock);
|
||||
+ uint32_t last = dw.last_fence;
|
||||
+ pthread_mutex_unlock(&dw.lock);
|
||||
+ if (last)
|
||||
+ dw_wait_fence_locked_free(dev, last, os_time_get_nano() + 5000000000ull);
|
||||
+ return dw_wait_fence_locked_free(dev, last, os_time_get_nano() + 5000000000ull);
|
||||
+ return VK_SUCCESS;
|
||||
+}
|
||||
+
|
||||
+static void
|
||||
@@ -715,30 +837,38 @@ index 0000000..8601aaa
|
||||
+ tu_debug_bos_del(dev, bo);
|
||||
+ tu_dump_bo_del(dev, bo);
|
||||
+
|
||||
+ /* An import has no token: its mapping is the owner's. */
|
||||
+ if (bo->map && bo->darwin_token) {
|
||||
+ if (bo->map && bo->darwin_token)
|
||||
+ TU_RMV(bo_unmap, dev, bo);
|
||||
+ IOConnectUnmapMemory64(dw.conn, bo->darwin_token, mach_task_self(),
|
||||
+ (mach_vm_address_t)(uintptr_t)bo->map);
|
||||
+ NativeAdrenoAbiBoCpuUnmap undo = {.handle = bo->darwin_handle, .token = bo->darwin_token};
|
||||
+ dw_call(NativeAdrenoMethodBoCpuUnmap, &undo, sizeof(undo), NULL, 0);
|
||||
+ }
|
||||
+
|
||||
+ TU_RMV(bo_destroy, dev, bo);
|
||||
+
|
||||
+ darwin_wait_idle(dev);
|
||||
+
|
||||
+ NativeAdrenoAbiBoFree release = {.handle = bo->darwin_handle};
|
||||
+ kern_return_t kr = dw_call(NativeAdrenoMethodBoFree, &release, sizeof(release), NULL, 0);
|
||||
+ if (kr == kIOReturnBusy) {
|
||||
+ /* A job still references it; give the CP a moment and retry once. */
|
||||
+ darwin_wait_idle(dev);
|
||||
+ kr = dw_call(NativeAdrenoMethodBoFree, &release, sizeof(release), NULL, 0);
|
||||
+ struct darwin_retire_state *retire = (struct darwin_retire_state *)dev->darwin_retire;
|
||||
+ pthread_mutex_lock(&retire->lock);
|
||||
+ darwin_reap_locked(dev, retire);
|
||||
+ if (retire->count == DARWIN_RETIRED_BOS && retire->failure == VK_SUCCESS) {
|
||||
+ ++retire->pressure_waits;
|
||||
+ pthread_mutex_unlock(&retire->lock);
|
||||
+ VkResult waited = darwin_wait_idle(dev);
|
||||
+ pthread_mutex_lock(&retire->lock);
|
||||
+ if (waited != VK_SUCCESS)
|
||||
+ retire->failure = vk_device_set_lost(&dev->vk, "deferred BO pressure wait failed");
|
||||
+ else
|
||||
+ darwin_reap_locked(dev, retire);
|
||||
+ }
|
||||
+ if (kr != KERN_SUCCESS)
|
||||
+ mesa_loge("BoFree failed: 0x%x (the address range is leaked)", kr);
|
||||
+ else
|
||||
+ darwin_free_iova(dev, bo->iova, bo->size);
|
||||
+ if (retire->count < DARWIN_RETIRED_BOS) {
|
||||
+ pthread_mutex_lock(&dw.lock);
|
||||
+ uint32_t fence = dw.last_fence;
|
||||
+ pthread_mutex_unlock(&dw.lock);
|
||||
+ unsigned slot = (retire->head + retire->count) % DARWIN_RETIRED_BOS;
|
||||
+ retire->entries[slot] = {bo->darwin_handle, bo->iova, bo->size, bo->map, bo->darwin_token, fence};
|
||||
+ ++retire->count;
|
||||
+ ++retire->enqueued;
|
||||
+ darwin_reap_locked(dev, retire);
|
||||
+ } else {
|
||||
+ /* No reusable range is reported on error. The kernel still owns the handle
|
||||
+ * and view; connection teardown must drain them, even if this record cannot fit. */
|
||||
+ retire->failure = vk_device_set_lost(&dev->vk, "deferred BO queue remains full; retaining native allocation");
|
||||
+ }
|
||||
+ pthread_mutex_unlock(&retire->lock);
|
||||
+
|
||||
+ /* The BO structs live in a sparse array: zero the entry to mark it free. */
|
||||
+ memset(bo, 0, sizeof(*bo));
|
||||
@@ -1057,6 +1187,18 @@ index 0000000..8601aaa
|
||||
+ if (result != VK_SUCCESS)
|
||||
+ return result;
|
||||
+ }
|
||||
+ struct darwin_retire_state *retire = (struct darwin_retire_state *)
|
||||
+ vk_zalloc(&dev->vk.alloc, sizeof(*retire), 8, VK_SYSTEM_ALLOCATION_SCOPE_DEVICE);
|
||||
+ if (!retire) {
|
||||
+ dw_close_connection();
|
||||
+ return vk_error(dev, VK_ERROR_OUT_OF_HOST_MEMORY);
|
||||
+ }
|
||||
+ if (pthread_mutex_init(&retire->lock, NULL)) {
|
||||
+ vk_free(&dev->vk.alloc, retire);
|
||||
+ dw_close_connection();
|
||||
+ return vk_error(dev, VK_ERROR_INITIALIZATION_FAILED);
|
||||
+ }
|
||||
+ dev->darwin_retire = retire;
|
||||
+ return VK_SUCCESS;
|
||||
+}
|
||||
+
|
||||
@@ -1065,6 +1207,18 @@ index 0000000..8601aaa
|
||||
+{
|
||||
+ /* Closing the connection makes the kernel drain the GPU and free every object this client created. */
|
||||
+ dw_close_connection();
|
||||
+ struct darwin_retire_state *retire = (struct darwin_retire_state *)dev->darwin_retire;
|
||||
+ if (retire) {
|
||||
+ /* Some CreateDevice failure paths already destroyed the VMA heap/mutex.
|
||||
+ * Never return ranges here: they are either gone or about to be destroyed. */
|
||||
+ if (debug_get_bool_option("TU_DARWIN_RETIRE_TRACE", false))
|
||||
+ mesa_logi("darwin BO retirement: enqueued=%llu reaped=%llu pending-at-close=%u pressure=%llu",
|
||||
+ (unsigned long long)retire->enqueued, (unsigned long long)retire->reaped,
|
||||
+ retire->count, (unsigned long long)retire->pressure_waits);
|
||||
+ pthread_mutex_destroy(&retire->lock);
|
||||
+ vk_free(&dev->vk.alloc, retire);
|
||||
+ dev->darwin_retire = NULL;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+static int
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
diff --git a/src/freedreno/vulkan/tu_pipeline.cc b/src/freedreno/vulkan/tu_pipeline.cc
|
||||
index dba43f9..8b010ed 100644
|
||||
--- a/src/freedreno/vulkan/tu_pipeline.cc
|
||||
+++ b/src/freedreno/vulkan/tu_pipeline.cc
|
||||
@@ -2854,13 +2858,16 @@ template <chip CHIP>
|
||||
static unsigned
|
||||
tu6_scissor_size(struct tu_device *dev, const struct vk_viewport_state *vp)
|
||||
{
|
||||
- return 1 + vp->scissor_count * 2;
|
||||
+ /* A pipeline without viewport state (rasterizer discard) has no scissors: a type-4 packet of zero registers is invalid. */
|
||||
+ return vp->scissor_count ? 1 + vp->scissor_count * 2 : 0;
|
||||
}
|
||||
|
||||
template <chip CHIP>
|
||||
void
|
||||
tu6_emit_scissor(struct tu_cs *cs, const struct vk_viewport_state *vp)
|
||||
{
|
||||
+ if (!vp->scissor_count)
|
||||
+ return;
|
||||
tu_cs_emit_pkt4(cs, GRAS_SC_SCREEN_SCISSOR_TL(CHIP, 0).reg, vp->scissor_count * 2);
|
||||
|
||||
for (uint32_t i = 0; i < vp->scissor_count; i++) {
|
||||
@@ -0,0 +1,25 @@
|
||||
diff --git a/src/freedreno/vulkan/tu_pipeline.cc b/src/freedreno/vulkan/tu_pipeline.cc
|
||||
--- a/src/freedreno/vulkan/tu_pipeline.cc
|
||||
+++ b/src/freedreno/vulkan/tu_pipeline.cc
|
||||
@@ -1958,7 +1958,10 @@
|
||||
|
||||
if (!executable_info) {
|
||||
cache_hit = true;
|
||||
- bool application_cache_hit = false;
|
||||
+ /* Every required shader (and retained NIR, when requested) must come
|
||||
+ * from the application's cache. Starting false makes this AND reduction
|
||||
+ * report a miss even when compile-forbidden creation succeeds. */
|
||||
+ bool application_cache_hit = true;
|
||||
|
||||
unsigned char shader_blake3[BLAKE3_KEY_LEN + 1];
|
||||
memcpy(shader_blake3, pipeline_blake3, sizeof(pipeline_blake3));
|
||||
@@ -1997,7 +2000,8 @@
|
||||
cache_hit &= !!nir_shaders;
|
||||
}
|
||||
|
||||
- if (application_cache_hit && builder->cache != builder->device->mem_cache) {
|
||||
+ if (cache_hit && application_cache_hit &&
|
||||
+ builder->cache != builder->device->mem_cache) {
|
||||
pipeline_feedback.flags |=
|
||||
VK_PIPELINE_CREATION_FEEDBACK_APPLICATION_PIPELINE_CACHE_HIT_BIT;
|
||||
}
|
||||
@@ -5,17 +5,20 @@
|
||||
cross-builds the ARM64/iOS 17 library. Neither runs a GPU: the backend talks to the NativeAdreno IOKit service, which
|
||||
exists only on the tablet. Output stays under build/ and is never committed."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from mesa_patch_identity import validate_recorded_patches
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--target', choices=('host', 'ios'), default='host')
|
||||
parser.add_argument('--configure-only', action='store_true')
|
||||
parser.add_argument('--builtin-pack', type=Path, help='Embed an exact-source/profile/GPU-bound own builtin pack; no runtime compiler fallback')
|
||||
args = parser.parse_args()
|
||||
SOURCE = ROOT / '.cache/mesa-work'
|
||||
OUT = ROOT / ('build/mesa-tu' if args.target == 'host' else 'build/mesa-tu-ios')
|
||||
@@ -29,9 +32,7 @@ def read(cmd):
|
||||
pins = json.loads((ROOT / 'scripts/data/adreno-compiler-inputs.json').read_text())
|
||||
if read(['git', '-C', SOURCE, 'rev-parse', 'HEAD']) != pins['mesa']['commit']:
|
||||
sys.exit('Pinned Mesa source differs')
|
||||
for patch in pins['patches']:
|
||||
if subprocess.run(['git', '-C', SOURCE, 'apply', '--reverse', '--check', ROOT / patch], capture_output=True).returncode:
|
||||
sys.exit('Recorded patch is not applied to the Mesa tree: ' + patch)
|
||||
validate_recorded_patches(ROOT, SOURCE, pins)
|
||||
if not shutil.which('glslangValidator'):
|
||||
sys.exit('glslangValidator is required by the Vulkan runtime build (brew install glslang)')
|
||||
|
||||
@@ -57,16 +58,49 @@ if args.target == 'ios':
|
||||
''.join('%s = %r\n' % (k, flags) for k in ('c_args', 'cpp_args', 'objc_args', 'c_link_args', 'cpp_link_args',
|
||||
'objc_link_args')))
|
||||
setup += ['--cross-file', cross]
|
||||
# Release with NDEBUG for the tablet: Mesa's default debugoptimized keeps assertions and NIR validation after
|
||||
# every pass, which the tablet's pipeline makings paid for (int-143: 58 pipeline states took 68 s). The host
|
||||
# build keeps them for the simulation checks.
|
||||
options += ['-Dlibunwind=disabled', '-Dlmsensors=disabled', '-Dvalgrind=disabled', '-Dexpat=disabled',
|
||||
'-Dzstd=disabled']
|
||||
'-Dzstd=disabled', '-Dbuildtype=release', '-Db_ndebug=true']
|
||||
env.update(CC=find('clang'), CXX=find('clang++'), CFLAGS='', CXXFLAGS='', CPPFLAGS='', LDFLAGS='')
|
||||
else:
|
||||
env.update(CC='/usr/bin/clang', CXX='/usr/bin/clang++')
|
||||
# Bind raw variant layout and code to every actual source edit, untracked
|
||||
# source addition, compiler, target and build option. The generated embedded
|
||||
# header stays outside SOURCE and is deliberately excluded from this key;
|
||||
# otherwise the pack would make its own producer identity circular.
|
||||
source_diff = subprocess.check_output(['git', '-C', SOURCE, 'diff', 'HEAD', '--binary'])
|
||||
untracked = subprocess.check_output(['git', '-C', SOURCE, 'ls-files', '--others', '--exclude-standard', '-z']).split(b'\0')
|
||||
source_extras = {name.decode(): hashlib.sha256((SOURCE / name.decode()).read_bytes()).hexdigest()
|
||||
for name in untracked if name and (SOURCE / name.decode()).is_file()}
|
||||
profile = dict(mesa_revision=pins['mesa']['commit'], target=args.target,
|
||||
source_diff_sha256=hashlib.sha256(source_diff).hexdigest(), extra_sources=source_extras,
|
||||
patch_guard_sha256=hashlib.sha256((ROOT / 'scripts/mesa_patch_identity.py').read_bytes()).hexdigest(),
|
||||
compiler=read([env['CXX'], '--version']), options=options,
|
||||
build_script_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest())
|
||||
source_id = hashlib.sha256(json.dumps(profile, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
pack_header = ''
|
||||
pack_sha = None
|
||||
if args.builtin_pack:
|
||||
pack = args.builtin_pack.resolve().read_bytes()
|
||||
if not 112 <= len(pack) <= 4 * 1024**2 or pack[:8] != b'TUBUILT1' or pack[16:80] != source_id.encode():
|
||||
sys.exit('Builtin pack is not from this exact compiler source/profile; regenerate rather than relabel it')
|
||||
header = OUT / 'tu_builtin_pack.generated.h'
|
||||
header.write_text('/* Generated own compiler output: never commit. */\nstatic const unsigned char tu_builtin_pack_bytes[] = {\n' +
|
||||
''.join(','.join('0x%02x' % b for b in pack[i:i+16]) + ',\n' for i in range(0,len(pack),16)) + '};\n')
|
||||
pack_header = str(header)
|
||||
pack_sha = hashlib.sha256(pack).hexdigest()
|
||||
(OUT / 'builtin-build-profile.json').write_text(json.dumps(dict(source_id=source_id, profile=profile,
|
||||
embedded_pack_sha256=pack_sha, builtin_pack_required=bool(args.builtin_pack)), indent=2) + '\n')
|
||||
options += ['-Dtu-builtin-source-id=' + source_id, '-Dtu-builtin-pack-header=' + pack_header]
|
||||
log = LOGS / ('mesa-tu-%s-configure.log' % args.target)
|
||||
if not (OUT / 'build.ninja').exists():
|
||||
with log.open('w') as stream:
|
||||
if subprocess.run([str(c) for c in setup + options], env=env, stdout=stream, stderr=subprocess.STDOUT).returncode:
|
||||
sys.exit('Configure failed; see ' + str(log) + '\n' + '\n'.join(log.read_text().splitlines()[-12:]))
|
||||
with log.open('w') as stream:
|
||||
# An existing build directory takes the current options (meson configure), so an option added here reaches it.
|
||||
command = setup + options if not (OUT / 'build.ninja').exists() else [ROOT / '.cache/mesa-venv/bin/meson', 'configure', OUT] + options
|
||||
if subprocess.run([str(c) for c in command], env=env, stdout=stream, stderr=subprocess.STDOUT).returncode:
|
||||
sys.exit('Configure failed; see ' + str(log) + '\n' + '\n'.join(log.read_text().splitlines()[-12:]))
|
||||
if args.configure_only:
|
||||
print('PASS: Turnip darwin-kmd configuration (%s)' % args.target)
|
||||
sys.exit(0)
|
||||
@@ -75,7 +109,7 @@ with build_log.open('w') as stream:
|
||||
result = subprocess.run(['ninja', '-C', str(OUT), '-j6'], env=env, stdout=stream, stderr=subprocess.STDOUT)
|
||||
if result.returncode:
|
||||
sys.exit('Build failed; see ' + str(build_log) + '\n' + '\n'.join(
|
||||
l for l in build_log.read_text().splitlines() if 'error' in l.lower())[:3000])
|
||||
l for l in build_log.read_text().splitlines() if ': error:' in l or 'fatal error:' in l or l.startswith('FAILED:'))[:3000])
|
||||
library = OUT / 'src/freedreno/vulkan/libvulkan_freedreno.dylib'
|
||||
symbols = subprocess.run(['nm', '-gU', str(library)], capture_output=True, text=True).stdout
|
||||
if ' T _vk_icdGetInstanceProcAddr' not in symbols:
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Qualify real device-priming cache hits while all compilation is forbidden.
|
||||
|
||||
Actual Turnip/IR3 plus the mandatory software transport; no shader execution,
|
||||
physical GPU, or deadline claim. Preserve exact source/driver/cache identities.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
KIT = ROOT / 'drivers/NativeMetal'
|
||||
OUT = ROOT / 'build/priming-cache'
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--output', type=Path, required=True)
|
||||
parser.add_argument('--baseline', type=Path, help='Optional exact preserved pre-fix executable for a before/after refusal control')
|
||||
parser.add_argument('--cross-build-ios', action='store_true', help='Link/sign the same instrument for iOS; never run or stage hardware')
|
||||
args = parser.parse_args()
|
||||
work = args.output.resolve()
|
||||
|
||||
|
||||
def require(value, reason):
|
||||
if not value:
|
||||
raise SystemExit(reason)
|
||||
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
require(not work.exists(), 'Preserve the existing controls; choose a fresh output directory')
|
||||
require(shutil.disk_usage(ROOT).free > 5 * 1024**3 + 256 * 1024**2, 'Preserve 5 GiB and bounded allowance')
|
||||
OUT.mkdir(exist_ok=True)
|
||||
work.mkdir(parents=True)
|
||||
loader, program = OUT / 'libOfflinePrimingCacheControl.dylib', OUT / 'priming_cache_host'
|
||||
bridge = ROOT / 'build/native-metal-bridge/host/libnative_metal_bridge.a'
|
||||
driver = ROOT / 'build/mesa-tu/src/freedreno/vulkan/libvulkan_freedreno.dylib'
|
||||
peer = ROOT / 'build/turnip-darwin-host/libhostiokit.dylib'
|
||||
flags = ['xcrun', 'clang++', '-O1', '-std=c++17', '-Wall', '-Wextra', '-Werror']
|
||||
subprocess.run([*flags, '-dynamiclib', '-I', ROOT / '.cache/mesa-work/include', KIT / 'bridge/priming_cache_loader.cpp', '-o', loader], check=True, timeout=60)
|
||||
subprocess.run([*flags, KIT / 'bridge/priming_cache_host.cpp', bridge, '-Wl,-dead_strip', '-Wl,-S',
|
||||
'-framework', 'Foundation', '-framework', 'CoreFoundation', '-framework', 'IOKit', '-o', program], check=True, timeout=90)
|
||||
ios_link = None
|
||||
if args.cross_build_ios:
|
||||
import plistlib
|
||||
import struct
|
||||
target = OUT / 'ios'
|
||||
target.mkdir(exist_ok=True)
|
||||
sdk = subprocess.check_output(['xcrun', '--sdk', 'iphoneos', '--show-sdk-path'], text=True, timeout=10).strip()
|
||||
ios_bridge = ROOT / 'build/native-metal-bridge/ios/libnative_metal_bridge.a'
|
||||
peer_dir = ROOT / 'build/turnip-ios-compiler-peer'
|
||||
ios_peer = peer_dir / 'libOfflineTurnipCompilerPeer.dylib'
|
||||
peer_build = json.loads((peer_dir / 'build-report.json').read_text())
|
||||
require(sha(ios_peer) == peer_build['outputs'][ios_peer.name]['sha256'], 'Exact own signed software peer')
|
||||
for name, digest in peer_build['sources'].items():
|
||||
require(sha(ROOT / name) == digest, 'Exact software peer source: ' + name)
|
||||
policy = peer_dir / 'compiler-entitlements.plist'
|
||||
require(plistlib.loads(policy.read_bytes()) == {'com.apple.private.security.no-container': True,
|
||||
'com.apple.private.bindfs-allow': True}, 'Exact own offline permissions; no no-sandbox')
|
||||
ios_flags = [*flags, '-target', 'arm64-apple-ios17.0', '-isysroot', sdk, '-fstack-protector-strong']
|
||||
ios_loader, worker = target / loader.name, target / 'compiler-worker'
|
||||
subprocess.run([*ios_flags, '-dynamiclib', '-install_name', '@rpath/' + loader.name, '-I', ROOT / '.cache/mesa-work/include',
|
||||
KIT / 'bridge/priming_cache_loader.cpp', '-o', ios_loader], check=True, timeout=60)
|
||||
subprocess.run([*ios_flags, KIT / 'bridge/priming_cache_host.cpp', ios_bridge, '-Wl,-dead_strip', '-Wl,-S',
|
||||
'-Wl,-needed_library,' + str(ios_peer), '-Wl,-rpath,/System/Library/Extensions/OfflineTurnipCompiler',
|
||||
'-framework', 'Foundation', '-framework', 'CoreFoundation', '-framework', 'IOKit', '-o', worker], check=True, timeout=120)
|
||||
outputs = {}
|
||||
for path in [ios_loader, worker]:
|
||||
command = ['codesign', '--force', '--sign', '-', '--pagesize', '4096', '--identifier', 'org.blu-sharky.offline-priming-cache.' + path.stem]
|
||||
if path == worker:
|
||||
command += ['--entitlements', str(policy)]
|
||||
subprocess.run([*command, str(path)], check=True, timeout=30)
|
||||
subprocess.run(['codesign', '--verify', '--strict', str(path)], check=True, capture_output=True, timeout=30)
|
||||
detail = subprocess.run(['codesign', '--display', '--verbose=4', str(path)], capture_output=True, text=True, check=True, timeout=30).stderr
|
||||
cdhash = re.search(r'^CDHash=([a-f0-9]{40})$', detail, re.M)
|
||||
raw = path.read_bytes()
|
||||
require(struct.unpack_from('<3I', raw) == (0xfeedfacf, 0x100000c, 0) and cdhash and re.search(r'^Page size=4096$', detail, re.M),
|
||||
'Actual own ordinary ARM64 4K CodeDirectory')
|
||||
cursor, builds, ids = 32, [], []
|
||||
for _ in range(struct.unpack_from('<I', raw, 16)[0]):
|
||||
command, size = struct.unpack_from('<2I', raw, cursor)
|
||||
require(size >= 8 and cursor + size <= len(raw), 'Bounded actual Mach-O')
|
||||
if command == 0x32:
|
||||
builds.append(struct.unpack_from('<2I', raw, cursor + 8))
|
||||
if command == 0x1b:
|
||||
ids.append(raw[cursor + 8:cursor + 24].hex())
|
||||
cursor += size
|
||||
require(builds == [(2, 0x110000)] and len(ids) == 1, 'Actual iOS17 build/UUID')
|
||||
outputs[path.name] = dict(sha256=sha(path), bytes=len(raw), cdhash=cdhash[1], macho_uuid=ids[0], platform=2, cpu_subtype=0, signature_page_bytes=4096)
|
||||
ios_link = dict(evidence='cross-link/signature verification only, not iOS execution', outputs=outputs,
|
||||
bridge_sha256=sha(ios_bridge), peer_sha256=sha(ios_peer), peer_report_sha256=sha(peer_dir / 'build-report.json'))
|
||||
env = {k: v for k, v in os.environ.items() if not k.startswith(('IR3_', 'FD_MESA_', 'NIR_', 'TU_', 'HOSTSIM_', 'MESA_', 'NMB_TEST_'))}
|
||||
env.update(DYLD_INSERT_LIBRARIES=str(peer), NMB_TEST_PRIMING_CACHE_LOADER=str(loader))
|
||||
cache = work / 'control.cache'
|
||||
pattern = re.compile(r'^PASS: device priming (\S+) pid=(\d+) compute=(\d+)/(\d+)/(\d+)/(\d+)/(\d+) graphics=(\d+)/(\d+)/(\d+)/(\d+)/(\d+) uuid_mutations=(\d+) submissions=(\d+) live=0; software peer, no shader execution$', re.M)
|
||||
fields = ['pid', 'compute_calls', 'compute_success', 'compute_required', 'compute_valid', 'compute_hits',
|
||||
'graphics_calls', 'graphics_success', 'graphics_required', 'graphics_valid', 'graphics_hits', 'uuid_mutations', 'submissions']
|
||||
cases = {}
|
||||
|
||||
|
||||
def case(label, mode, directory):
|
||||
run = subprocess.run([str(program), str(driver), str(directory), mode], env=env, capture_output=True, text=True, timeout=60)
|
||||
log = work / (label + '.log')
|
||||
log.write_text(run.stdout + run.stderr)
|
||||
row = pattern.search(run.stdout)
|
||||
require(run.returncode == 0 and row and row[1] == mode, 'Actual priming control failed: ' + label)
|
||||
data = dict(zip(fields, map(int, row.groups()[1:])))
|
||||
require(data['submissions'] == 1 and data['compute_calls'] == 1, 'Full priming still submits its barriers; observed non-generated compute shape')
|
||||
if mode in ('cold-miss', 'uuid-miss'):
|
||||
require(data['compute_success'] == 0 and data['compute_required'] == 1 and data['graphics_calls'] == 0,
|
||||
'Actual cold/invalid priming fails instead of silently compiling')
|
||||
if mode == 'uuid-miss':
|
||||
require(data['uuid_mutations'] > 0, 'Actual driver cache UUID refusal control')
|
||||
else:
|
||||
require(data['compute_success'] == data['compute_valid'] == data['graphics_calls'] == data['graphics_success'] == data['graphics_valid'] == 1,
|
||||
'Both actual helper creations and their feedback are valid')
|
||||
if mode == 'consume':
|
||||
require(data['compute_hits'] == data['graphics_hits'] == 1 and data['compute_required'] == data['graphics_required'] == 0,
|
||||
'Both helpers truly hit with compilation forbidden')
|
||||
data['log_sha256'] = sha(log)
|
||||
cases[label] = data
|
||||
print(row[0], flush=True)
|
||||
|
||||
|
||||
case('cold-miss', 'cold-miss', cache)
|
||||
case('produce', 'produce', cache)
|
||||
records = [p for p in cache.rglob('*') if p.is_file()]
|
||||
require(len(records) == 1 and records[0].stat().st_size < 16 * 1024**2, 'Exactly one bounded native priming record')
|
||||
record = records[0]
|
||||
original = record.read_bytes()
|
||||
require(original.startswith(b'M2VPPLC\0') and hashlib.sha256(original[:-32]).digest() == original[-32:], 'Actual driver record and checksum')
|
||||
case('consume', 'consume', cache)
|
||||
case('uuid-miss', 'uuid-miss', cache)
|
||||
require(record.read_bytes() == original, 'Warm and UUID controls preserve producer record bytes')
|
||||
for label in ['truncated-miss', 'checksum-miss']:
|
||||
directory = work / (label + '.cache')
|
||||
shutil.copytree(cache, directory)
|
||||
bad = directory / record.relative_to(cache)
|
||||
if label == 'truncated-miss':
|
||||
bad.write_bytes(original[:20])
|
||||
else:
|
||||
data = bytearray(original);data[-33] ^= 1;bad.write_bytes(data)
|
||||
case(label, 'cold-miss', directory)
|
||||
require(len({v['pid'] for v in cases.values()}) == len(cases), 'Independent cold processes for all controls')
|
||||
baseline_evidence = None
|
||||
if args.baseline:
|
||||
baseline = args.baseline.resolve()
|
||||
baseline_report = ROOT / 'build/ui-shader-coverage/turnip-graphics/committed-qualified/checkpoint.json'
|
||||
old = json.loads(baseline_report.read_text())
|
||||
require(baseline.is_file() and sha(baseline) == old['process']['sha256'], 'Exact previously qualified executable with uncached priming')
|
||||
old_env = dict(env, NMB_TEST_GRAPHICS_CACHE_LOADER=str(loader), NMB_TEST_FORCE_PRIME_STRICT='1')
|
||||
run = subprocess.run([str(baseline), str(driver), str(work / 'control'), 'consume'], env=old_env, capture_output=True, text=True, timeout=60)
|
||||
log = work / 'baseline.log'
|
||||
log.write_text(run.stdout + run.stderr)
|
||||
require(run.returncode != 0 and 'device: Vulkan(VkResultCode(1000297000))' in log.read_text(),
|
||||
'Old executable must still fail during device open, using this exact populated cache directory')
|
||||
require(record.read_bytes() == original, 'The old refusal also preserves all producer record bytes')
|
||||
baseline_evidence = dict(process_sha256=sha(baseline), log_sha256=sha(log), exit_code=run.returncode)
|
||||
patches = KIT / 'bridge/patches'
|
||||
report = dict(evidence='real host Turnip compiler, mandatory software peer; no shader execution', checker_sha256=sha(Path(__file__)),
|
||||
sources_sha256={str(p.relative_to(ROOT)): sha(p) for p in [KIT / 'bridge/priming_cache_host.cpp', KIT / 'bridge/priming_cache_loader.cpp', KIT / 'bridge/graphics_cache_loader.cpp']},
|
||||
runtime_base_revision=subprocess.check_output(['git', '-C', str(ROOT / '.cache/gpu-reference/metal2vulkan'), 'rev-parse', 'HEAD'], text=True).strip(),
|
||||
runtime_patches_sha256={p.name: sha(p) for p in sorted(patches.glob('*.patch'))}, host_bridge_sha256=sha(bridge),
|
||||
driver_sha256=sha(driver), peer_sha256=sha(peer), ios_cross_link=ios_link, process_sha256=sha(program), loader_sha256=sha(loader),
|
||||
cache_record_path=str(record.relative_to(cache)), cache_record_sha256=sha(record), cache_record_bytes=len(original),
|
||||
driver_cache_uuid=original[20:36].hex(), cases=cases, baseline=baseline_evidence,
|
||||
exit_guard_priming_preserved=True, generated_command_priming_qualified=False, ios_execution=False, shader_execution=False,
|
||||
physical_device_tested=False, active_native_adreno_4k=False, complete_ui_variant_coverage=False, hardware_ready=False, stable_120_fps_qualified=False)
|
||||
(work / 'checkpoint.json').write_text(json.dumps(report, indent=2) + '\n')
|
||||
(OUT / 'checkpoint.json').write_text(json.dumps(report, indent=2) + '\n')
|
||||
print('PASS: both priming helpers hit with compilation forbidden; invalid data refuses and leaves no live software objects')
|
||||
if baseline_evidence:
|
||||
print('PASS: preserved pre-fix executable still fails in device open with the same populated cache')
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Qualify Turnip cache import without executing a shader or contacting hardware.
|
||||
|
||||
Requires the built host Turnip and the software peer from check-turnip-darwin-host.py.
|
||||
Each cache case is a fresh process with Mesa's disk shader cache disabled. The
|
||||
consumer must succeed under FAIL_ON_PIPELINE_COMPILE_REQUIRED and report an
|
||||
application cache hit. This does not qualify cross-build cache portability.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUT = ROOT / 'build/turnip-darwin-host'
|
||||
LIBRARY = ROOT / 'build/mesa-tu/src/freedreno/vulkan/libvulkan_freedreno.dylib'
|
||||
IOS = ROOT / 'build/mesa-tu-ios/src/freedreno/vulkan/libvulkan_freedreno.dylib'
|
||||
SHIM = OUT / 'libhostiokit.dylib'
|
||||
SHADERS = ROOT / 'build/turnip-smoke/NativeTurnipShaders.h'
|
||||
for required in [LIBRARY, SHIM, SHADERS]:
|
||||
if not required.is_file():
|
||||
raise SystemExit('Build the host Turnip/peer/own shader fixtures first: ' + str(required))
|
||||
|
||||
|
||||
def binary_identity(path):
|
||||
data = path.read_bytes()
|
||||
if struct.unpack_from('<I', data)[0] != 0xfeedfacf:
|
||||
raise SystemExit('Expected a thin little-endian Mach-O64: ' + str(path))
|
||||
count, size = struct.unpack_from('<II', data, 16)
|
||||
offset, end = 32, 32 + size
|
||||
if end > len(data):
|
||||
raise SystemExit('Truncated Mach-O load commands')
|
||||
cpu, subtype = struct.unpack_from('<II', data, 4)
|
||||
if cpu != 0x100000c or subtype != 0:
|
||||
raise SystemExit('Expected ordinary ARM64 subtype 0: ' + str(path))
|
||||
result = dict(sha256=hashlib.sha256(data).hexdigest(), cpu_type=cpu, cpu_subtype=subtype)
|
||||
for _ in range(count):
|
||||
command, length = struct.unpack_from('<II', data, offset)
|
||||
if length < 8 or offset + length > end:
|
||||
raise SystemExit('Invalid Mach-O load command')
|
||||
if command == 0x1b and length == 24: # LC_UUID
|
||||
result['macho_uuid'] = data[offset + 8:offset + 24].hex()
|
||||
if command == 0x32 and length >= 24: # LC_BUILD_VERSION
|
||||
result['platform'] = struct.unpack_from('<I', data, offset + 8)[0]
|
||||
offset += length
|
||||
if offset != end or 'macho_uuid' not in result:
|
||||
raise SystemExit('Missing/invalid Mach-O UUID')
|
||||
return result
|
||||
|
||||
|
||||
executable = OUT / 'pipeline_cache'
|
||||
subprocess.run(['xcrun', 'clang++', '-std=c++23', '-O1', '-Wall', '-Wextra', '-Werror',
|
||||
'-Wno-unused-parameter', '-Wno-unused-const-variable', '-I', '/opt/homebrew/include',
|
||||
'-I', SHADERS.parent, ROOT / 'drivers/NativeAdreno/tests/hostsim/pipeline_cache.cpp',
|
||||
'-o', executable], check=True)
|
||||
env = {k: v for k, v in os.environ.items() if not k.startswith(('IR3_', 'FD_MESA_', 'NIR_', 'TU_', 'HOSTSIM_', 'MESA_'))}
|
||||
env.update(DYLD_INSERT_LIBRARIES=str(SHIM), MESA_SHADER_CACHE_DISABLE='true', MESA_LOG='')
|
||||
report = dict(evidence='host Turnip compilation/cache import over software kernel transport; no shader execution',
|
||||
host=binary_identity(LIBRARY),
|
||||
own_shader_header_sha256=hashlib.sha256(SHADERS.read_bytes()).hexdigest(),
|
||||
cross_platform_portability_qualified=False, cases={})
|
||||
if IOS.is_file():
|
||||
report['ios_static'] = binary_identity(IOS)
|
||||
report['host_ios_build_uuid_differ'] = report['host']['macho_uuid'] != report['ios_static']['macho_uuid']
|
||||
with tempfile.TemporaryDirectory(prefix='pipeline-cache-', dir=OUT) as temporary:
|
||||
for mode in ['produce', 'consume', 'uuid-miss', 'shader-miss']:
|
||||
run = subprocess.run([str(executable), str(LIBRARY), str(Path(temporary) / 'compute.bin'), mode],
|
||||
env=env, text=True, capture_output=True, timeout=120)
|
||||
log = OUT / ('pipeline-cache-' + mode + '.log')
|
||||
log.write_text(run.stdout + run.stderr)
|
||||
match = re.search(r'^PASS: Turnip pipeline cache (\S+); uuid=([0-9a-f]{32}) bytes=(\d+) strict=(\d+) feedback=0x([0-9a-f]+) strict_us=([\d.]+) compile_us=([\d.]+); no shader execution$', run.stdout, re.M)
|
||||
if run.returncode or not match or match[1] != mode:
|
||||
raise SystemExit('Turnip pipeline cache %s failed; see %s' % (mode, log))
|
||||
report['cases'][mode] = dict(pipeline_cache_uuid=match[2], bytes=int(match[3]), strict_result=int(match[4]),
|
||||
feedback_flags=int(match[5], 16), strict_us=float(match[6]), compile_us=float(match[7]))
|
||||
print(match[0], flush=True)
|
||||
(OUT / 'pipeline-cache-report.json').write_text(json.dumps(report, indent=2) + '\n')
|
||||
print('PASS: separate-process Turnip cache hit and UUID/shader miss controls; host only, cross-platform portability unqualified')
|
||||
@@ -49,7 +49,10 @@
|
||||
"patches/mesa/ir3-darwin-build-id.patch",
|
||||
"patches/mesa/ir3-portable-asm-roundtrip.patch",
|
||||
"patches/mesa/offline-adreno-compiler-probe.patch",
|
||||
"patches/mesa/turnip-darwin-kmd.patch"
|
||||
"patches/mesa/turnip-darwin-kmd.patch",
|
||||
"patches/mesa/turnip-graphics-cache-feedback.patch",
|
||||
"patches/mesa/turnip-empty-scissor.patch",
|
||||
"patches/mesa/turnip-builtin-shader-pack.patch"
|
||||
],
|
||||
"integer_fma_reference": {
|
||||
"repository": "https://github.com/ucb-bar/berkeley-softfloat-3",
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""Verify overlapping recorded Mesa patches without mutating the source tree."""
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
|
||||
def validate_recorded_patches(root, source, pins):
|
||||
paths = set()
|
||||
for patch in pins['patches']:
|
||||
text = (root / patch).read_text()
|
||||
paths.update(re.findall(r'^diff --git a/(\S+) b/\1$', text, re.M))
|
||||
changed = set(subprocess.check_output(['git', '-C', source, 'diff', 'HEAD', '--name-only'], text=True).splitlines())
|
||||
changed.update(subprocess.check_output(['git', '-C', source, 'ls-files', '--others', '--exclude-standard'], text=True).splitlines())
|
||||
if changed - paths:
|
||||
raise SystemExit('Unrecorded Mesa source changes: ' + ', '.join(sorted(changed - paths)))
|
||||
with tempfile.TemporaryDirectory(prefix='mesa-patch-check-', dir=root / '.cache') as name:
|
||||
stripped = Path(name)
|
||||
subprocess.run(['git', 'init', '-q', str(stripped)], check=True, capture_output=True, timeout=20)
|
||||
for path in paths:
|
||||
actual = source / path
|
||||
if actual.is_file():
|
||||
if actual.is_symlink():
|
||||
raise SystemExit('Unexpected source symlink: ' + path)
|
||||
destination = stripped / path
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
destination.write_bytes(actual.read_bytes())
|
||||
for patch in reversed(pins['patches']):
|
||||
result = subprocess.run(['git', 'apply', '--reverse', str(root / patch)], cwd=stripped,
|
||||
capture_output=True, text=True, timeout=20)
|
||||
if result.returncode:
|
||||
raise SystemExit('Recorded Mesa patch sequence differs: ' + patch + '\n' + result.stderr)
|
||||
for path in paths:
|
||||
original = subprocess.run(['git', '-C', source, 'show', 'HEAD:' + path], capture_output=True, timeout=20)
|
||||
actual = stripped / path
|
||||
if (original.returncode == 0 and (not actual.is_file() or actual.read_bytes() != original.stdout)) or (original.returncode != 0 and actual.exists()):
|
||||
raise SystemExit('Recorded Mesa patches do not reconstruct pinned source: ' + path)
|
||||
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Count real IR3 backend entries hidden below public pipeline cache feedback.
|
||||
|
||||
Unchanged actual host Turnip plus the mandatory software transport. LLDB stops
|
||||
at entries and immediately resumes; no shader executes and timing is unqualified.
|
||||
The canonical driver, original inputs and prior qualified packs stay unchanged.
|
||||
"""
|
||||
import argparse
|
||||
from collections import Counter
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def require(condition, reason):
|
||||
if not condition:
|
||||
raise SystemExit(reason)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--output', type=Path, required=True)
|
||||
parser.add_argument('--qualification', type=Path, required=True,
|
||||
help='Exact passing check-priming-cache-host output directory')
|
||||
args = parser.parse_args()
|
||||
output, qualification = args.output.resolve(), args.qualification.resolve()
|
||||
require(not output.exists(), 'Preserve existing evidence; choose a fresh directory')
|
||||
require(shutil.disk_usage(ROOT).free > 5 * 1024**3 + 32 * 1024**2, 'Preserve 5 GiB and bounded logs')
|
||||
previous_path = qualification / 'checkpoint.json'
|
||||
previous = json.loads(previous_path.read_text())
|
||||
paths = dict(program='build/priming-cache/priming_cache_host',
|
||||
driver='build/mesa-tu/src/freedreno/vulkan/libvulkan_freedreno.dylib',
|
||||
peer='build/turnip-darwin-host/libhostiokit.dylib',
|
||||
loader='build/priming-cache/libOfflinePrimingCacheControl.dylib')
|
||||
# This is an additional observation of an exact previously qualified pair.
|
||||
# Never silently use a newly rebuilt compiler, wrapper or executable.
|
||||
for name, digest in previous['sources_sha256'].items():
|
||||
require(sha(ROOT / name) == digest, 'Exact qualified source: ' + name)
|
||||
require(sha(ROOT / paths['driver']) == previous['driver_sha256'], 'Exact previously qualified host compiler')
|
||||
for name, digest in previous['runtime_patches_sha256'].items():
|
||||
require(sha(ROOT / 'drivers/NativeMetal/bridge/patches' / name) == digest, 'Exact qualified runtime patch: ' + name)
|
||||
for name, field in [('program', 'process_sha256'), ('peer', 'peer_sha256'), ('loader', 'loader_sha256')]:
|
||||
require(sha(ROOT / paths[name]) == previous[field], 'Exact qualified binary: ' + name)
|
||||
require(sha(ROOT / 'build/native-metal-bridge/host/libnative_metal_bridge.a') == previous['host_bridge_sha256'], 'Exact linked runtime input')
|
||||
output.mkdir(parents=True)
|
||||
identities = {name: sha(ROOT / path) for name, path in paths.items()}
|
||||
cases = {}
|
||||
cache = output / 'cache'
|
||||
produced = None
|
||||
for mode in ('cold-miss', 'produce', 'consume'):
|
||||
work = output / mode
|
||||
work.mkdir()
|
||||
config = dict(root=str(ROOT), work=str(work), cache=str(cache), mode=mode, **paths)
|
||||
config_path = work / 'config.json'
|
||||
config_path.write_text(json.dumps(config, indent=2) + '\n')
|
||||
commands = work / 'commands.lldb'
|
||||
commands.write_text('command script import ' + shlex.quote(str(ROOT / 'scripts/turnip_compiler_lldb.py')) +
|
||||
'\nscript turnip_compiler_lldb.run(lldb.debugger)\n')
|
||||
env = dict(os.environ, NMB_OFFLINE_COMPILER_CONFIG=str(config_path))
|
||||
run = subprocess.run(['xcrun', 'lldb', '--batch', '--source', str(commands)],
|
||||
capture_output=True, text=True, env=env, timeout=60)
|
||||
(work / 'debugger.log').write_text(run.stdout + run.stderr)
|
||||
require(run.returncode == 0, 'Bounded own debugger failed: ' + mode)
|
||||
trace_path = work / 'trace.json'
|
||||
trace = json.loads(trace_path.read_text())
|
||||
require(trace['launch_success'] and trace['exited'] and trace['exit_status'] == 0 and
|
||||
trace['breakpoint_locations'] == 1 and not trace['trace_overflow'], 'Actual complete compiler trace: ' + mode)
|
||||
text = (work / 'worker.stdout').read_text()
|
||||
pattern = (r'^PASS: device priming ' + mode + r' pid=\d+ compute=(\d+)/(\d+)/(\d+)/(\d+)/(\d+) '
|
||||
r'graphics=(\d+)/(\d+)/(\d+)/(\d+)/(\d+) uuid_mutations=0 submissions=1 live=0; software peer, no shader execution$')
|
||||
row = re.search(pattern, text, re.M)
|
||||
require(row, 'Complete actual worker feedback: ' + mode)
|
||||
c, g = tuple(map(int, row.groups()[:5])), tuple(map(int, row.groups()[5:]))
|
||||
require((c, g) == (((1, 0, 1, 0, 0), (0, 0, 0, 0, 0)) if mode == 'cold-miss' else
|
||||
((1, 1, 0, 1, int(mode == 'consume')), (1, 1, 0, 1, int(mode == 'consume')))),
|
||||
'Actual empty refusal/producer/strict-consumer feedback: ' + mode)
|
||||
counts = Counter(entry['scope'] for entry in trace['rows'])
|
||||
require(counts['clear-blit'] == 18 and counts['empty'] == 2, 'Exact observed internal startup compiler calls')
|
||||
require(all(entry['stack'][0] == 'ir3_compile_shader_nir' and
|
||||
any('tu_CreateDevice(' in s for s in entry['stack']) for entry in trace['rows'] if entry['scope'] != 'other'),
|
||||
'Actual IR3 entries occur inside original device initialization')
|
||||
require(counts['other'] == 0 if mode != 'produce' else counts['other'] > 0,
|
||||
'Strict public-pipeline refusal/hits distinguish actual helper backend compilation')
|
||||
records = {str(p.relative_to(cache)): dict(bytes=p.stat().st_size, sha256=sha(p))
|
||||
for p in cache.rglob('*') if p.is_file()}
|
||||
if mode == 'cold-miss':
|
||||
require(not records, 'Cold refusal never writes a successful helper cache')
|
||||
elif mode == 'produce':
|
||||
require(len(records) == 1, 'Exactly one bounded own priming record')
|
||||
produced = records
|
||||
else:
|
||||
require(records == produced, 'Strict consumer leaves every producer-cache byte unchanged')
|
||||
cases[mode] = dict(compiler_entries=len(trace['rows']), scopes=dict(counts), compute=c, graphics=g,
|
||||
trace_sha256=sha(trace_path), worker_stdout_sha256=sha(work / 'worker.stdout'))
|
||||
print(mode, dict(counts), flush=True)
|
||||
require(identities == {name: sha(ROOT / path) for name, path in paths.items()}, 'Canonical compiler/fixture binaries unchanged')
|
||||
report = dict(evidence='actual host Turnip IR3 backend entries under LLDB; software peer executes no shader',
|
||||
qualification_sha256=sha(previous_path), identities=identities, cases=cases, cache_manifest=produced,
|
||||
checker_sha256=sha(Path(__file__)), callbacks_sha256=sha(ROOT / 'scripts/turnip_compiler_lldb.py'),
|
||||
mesa_base_revision=subprocess.check_output(['git', '-C', ROOT / '.cache/mesa-work', 'rev-parse', 'HEAD'], text=True).strip(),
|
||||
patches_sha256={p: sha(ROOT / p) for p in json.loads((ROOT / 'scripts/data/adreno-compiler-inputs.json').read_text())['patches']},
|
||||
timing_qualified=False, shader_execution=False, physical_device_tested=False,
|
||||
active_native_adreno_4k=False, hardware_ready=False, stable_120_fps_qualified=False)
|
||||
(output / 'checkpoint.json').write_text(json.dumps(report, indent=2) + '\n')
|
||||
print('PASS: real internal compilation persists despite strict public-pipeline hits; no timing/hardware qualification')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Record only the builtin-cache change over the other pinned Mesa patches."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MESA = ROOT / '.cache/mesa-work'
|
||||
OUT = ROOT / 'patches/mesa/turnip-builtin-shader-pack.patch'
|
||||
FILES = ['meson.options', 'src/freedreno/vulkan/meson.build', 'src/freedreno/vulkan/tu_device.cc',
|
||||
'src/freedreno/vulkan/tu_device.h', 'src/freedreno/vulkan/tu_shader.cc',
|
||||
'src/freedreno/vulkan/tu_clear_blit.cc']
|
||||
NEW = ['src/freedreno/vulkan/tu_builtin_cache.cc', 'src/freedreno/vulkan/tu_builtin_cache.h']
|
||||
|
||||
|
||||
def git(directory, *args):
|
||||
return subprocess.check_output(['git', '-c', 'user.email=a@b', '-c', 'user.name=n', *args], cwd=directory)
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory(dir=ROOT / '.cache', prefix='builtin-patch-') as name:
|
||||
tmp = Path(name)
|
||||
git(tmp, 'init', '-q')
|
||||
for path in FILES:
|
||||
(tmp / path).parent.mkdir(parents=True, exist_ok=True)
|
||||
(tmp / path).write_bytes(git(MESA, 'show', 'HEAD:' + path))
|
||||
for patch in json.loads((ROOT / 'scripts/data/adreno-compiler-inputs.json').read_text())['patches']:
|
||||
if patch != str(OUT.relative_to(ROOT)):
|
||||
git(tmp, 'apply', *['--include=' + path for path in FILES], str(ROOT / patch))
|
||||
git(tmp, 'add', '-A')
|
||||
git(tmp, 'commit', '-q', '-m', 'recorded-base')
|
||||
for path in FILES + NEW:
|
||||
(tmp / path).parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(MESA / path, tmp / path)
|
||||
git(tmp, 'add', '-N', *NEW)
|
||||
OUT.write_bytes(git(tmp, 'diff', '--binary'))
|
||||
print('Recorded builtin-cache source patch')
|
||||
@@ -0,0 +1,55 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""Bounded debugger callbacks for the own host-only compiler probe.
|
||||
|
||||
No patched instruction, changed argument, forced return, hardware connection,
|
||||
or shader execution. Breakpoints perturb timing; only invocation counts qualify.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import lldb
|
||||
|
||||
ROWS = []
|
||||
LIMIT = 256
|
||||
|
||||
|
||||
def entered(frame, location, internal_dict):
|
||||
if len(ROWS) == LIMIT:
|
||||
return True
|
||||
thread = frame.GetThread()
|
||||
stack = [thread.GetFrameAtIndex(i).GetFunctionName() or '?'
|
||||
for i in range(min(thread.GetNumFrames(), 32))]
|
||||
scope = ('clear-blit' if any('tu_init_clear_blit_shaders' in s for s in stack)
|
||||
else 'empty' if any('tu_init_empty_shaders' in s for s in stack)
|
||||
else 'other')
|
||||
ROWS.append(dict(scope=scope, stack=stack))
|
||||
return False
|
||||
|
||||
|
||||
def run(debugger):
|
||||
config = json.loads(Path(os.environ['NMB_OFFLINE_COMPILER_CONFIG']).read_text())
|
||||
root, work = Path(config['root']), Path(config['work'])
|
||||
debugger.SetAsync(False)
|
||||
target = debugger.CreateTarget(str(root / config['program']))
|
||||
breakpoint = target.BreakpointCreateByName('ir3_compile_shader_nir', 'libvulkan_freedreno.dylib')
|
||||
breakpoint.SetScriptCallbackFunction(__name__ + '.entered')
|
||||
launch = lldb.SBLaunchInfo([str(root / config['driver']), config['cache'], config['mode']])
|
||||
environment = {k: v for k, v in os.environ.items() if not k.startswith(
|
||||
('IR3_', 'FD_MESA_', 'NIR_', 'TU_', 'HOSTSIM_', 'MESA_', 'NMB_TEST_'))}
|
||||
environment.update(DYLD_INSERT_LIBRARIES=str(root / config['peer']),
|
||||
NMB_TEST_PRIMING_CACHE_LOADER=str(root / config['loader']))
|
||||
launch.SetEnvironmentEntries([k + '=' + v for k, v in environment.items()], True)
|
||||
launch.AddOpenFileAction(1, str(work / 'worker.stdout'), False, True)
|
||||
launch.AddOpenFileAction(2, str(work / 'worker.stderr'), False, True)
|
||||
error = lldb.SBError()
|
||||
process = target.Launch(launch, error)
|
||||
exited = process.GetState() == lldb.eStateExited
|
||||
result = dict(launch_success=error.Success(), exited=exited,
|
||||
exit_status=process.GetExitStatus() if exited else None,
|
||||
breakpoint_locations=breakpoint.GetNumLocations(),
|
||||
rows=ROWS, trace_limit=LIMIT, trace_overflow=len(ROWS) == LIMIT,
|
||||
timing_qualified=False, physical_device_tested=False, shader_execution=False)
|
||||
if not exited:
|
||||
process.Kill()
|
||||
(work / 'trace.json').write_text(json.dumps(result, indent=2) + '\n')
|
||||
print('Recorded %d actual backend entries; timing is unqualified' % len(ROWS))
|
||||
Reference in new issue
Block a user