DEV Community

cy cy
cy cy

Posted on

Your USDZ will be rejected by an iPhone — three packaging rules Apple doesn't negotiate

Last week I wrote about a GLB that shipped as a 134-meter headphone. This week is the sequel: we converted that same model to USDZ — the format Apple's AR Quick Look actually loads — and the auditor caught a different set of bombs. Same model, different container, new ways to embarrass yourself in front of a client.

Here are the three USDZ packaging rules that trip up pipelines, each with a ~15-line stdlib check. All verified against Apple's published USDZ packaging spec.

1. USDZ is a zip, but it's not your zip

The temptation: "it's a zip, let me deflate it." Apple's spec requires every entry stored uncompressed (ZIP_STORED). Compressed entries are the classic build-machine-works-phone-fails bug:

import zipfile

with zipfile.ZipFile(path) as zf:
    for info in zf.infolist():
        if info.compress_type != zipfile.ZIP_STORED:
            fail("container/compression",
                 f"'{info.filename}' is compressed; USDZ requires all entries ZIP_STORED")
Enter fullscreen mode Exit fullscreen mode

A desktop preview tool will happily open a deflated USDZ. The iPhone will not. This is exactly the class of bug that passes your CI and explodes in the client's demo.

2. The 64-byte alignment rule nobody reads

The spec requires each file's data to start on a 64-byte boundary. Zip writers pad with an extra field between the local header and the data — if your zip library doesn't do this (most don't, because plain zips don't care), offsets drift:

# data start = local header offset + 30 + name + extra
data_off = info.header_offset + 30 + len(info.filename.encode()) + len(info.extra)
if data_off % 64:
    warn("container/alignment", f"'{info.filename}' data at byte {data_off}, not 64-byte aligned")
Enter fullscreen mode Exit fullscreen mode

Build your USDZ with the alignment (pad extra with zeros so the data lands on the boundary) and check it on every build. Alignment problems are silent until a loader with stricter memory mapping gets involved.

3. Scale drift is a mm away

glTF says 1 unit = 1 meter. USD says metersPerUnit = 1. But a Blender scene set to millimeters exports a model 1000× too big, and both formats will happily accept it. Our own catalog model, converted honestly, spans:

USDZcheck — real_headphone.usdz
  container : 2 entries, stored=True, aligned=True, 0.03 MB
  structure : defaultPrim=Root, 1 meshes, 1 materials, 1 bindings, 1 textures
  geometry  : 716 tris / 573 verts, span 71.333 units
  ! [WARN] geometry/scale: model spans ~71.33 units - AR Quick Look expects real-world meters
  verdict   : PASS_WITH_WARNINGS (0 fails, 1 warnings)
Enter fullscreen mode Exit fullscreen mode

71 meters of headphone. Same defect GLBcheck caught on the GLB side — which is the point: audit both containers, because conversion doesn't fix scale, it just re-wraps it. The bbox check is five lines: read point3f[] points, take the max spread across axes, and warn outside 0.01–10 units for product-scale assets.

Bonus: two structure checks that catch hand-made packages

  • defaultPrim declared and actually present (structure/no-defaultprim) — AR Quick Look picks the first root otherwise, which may not be your model.
  • faceVertexIndices length must equal sum(faceVertexCounts) — hand-edited or buggy exports get this wrong and crash triangulation (geometry/index-mismatch).

The gate, not the viewer

Desktop viewers are forgiving because they want to render something. Your delivery gate is allowed to be rude:

python usdzcheck.py model.usdz --json --strict && ship_to_client.sh
Enter fullscreen mode Exit fullscreen mode

The full auditor (~200 lines, stdlib only, exit codes 0/1/2 for CI) plus its 14-check self-test is USDZcheck ($29), and the GLB-side twin is GLBcheck. Together they're the pre-flight for "GLB to the web, USDZ to the iPhone" — the two containers your clients will hand you in 2026.

The deeper point stands from last week: the bugs that cost you the client are the ones the desktop can't see. Audit for the runtime your customer actually runs.

Top comments (0)