Articles

Grixel for game devs

Before she opens a 48 MB Unreal asset, an artist runs one command: grixel edit. The server hands her an exclusive lock on the file. The art director then tries to lock the same asset and is refused, with the holder's name in the error. That refusal is the feature: the collision surfaced at the start of the day, before either had painted anything worth losing. When the artist submits, the lock releases itself. The transcripts are below.

Game studios are the people Perforce was built for. One depot holding code and art together. Atomic changelists. Exclusive locks so two artists never both lose a day inside the same un-mergeable binary. Workspace views so the audio team doesn't sync cinematics. No other version control system ever took studio problems that seriously, which is why, decades on, studios still run it.

For anyone who never ran a depot: imagine a shared company drive with perfect memory: every project lives at a path, every save is kept forever, certain folders have a lock on the door, and each person pulls down only the folders they need. Git, by default, photocopies the entire filing cabinet onto every laptop and reconciles the copies later; Perforce, and Grixel after it, keeps one cabinet at the center and hands you exactly the drawers you asked for.

Watching it age has been uncomfortable anyway. Review lives in a separate product. p4 jobs stopped evolving around the time bug trackers moved to the web. Federation is bolted on. And there is no governed answer for the newest thing reading and writing files at your studio: the AI assistant.

I've spent most of my career in games, much of it inside a Perforce depot. Grixel is my attempt to rebuild that shape from first principles: keep what studios rely on, replace what aged badly. The genuinely new part is an AI-context layer, which I'll get to at the end.

#Muscle memory, mapped

The Perforce vocabulary transfers almost directly. The depot is one global, server-authoritative namespace; canonical paths look like /companies/acme/games/seasonal/art/keyart_main.psd, and a branch is just another path in that namespace. Workspace views become grixel checkout with -path and -exclude patterns, and grixel sync can widen or narrow the view later. Changelists are still changelists: atomic, submitted as a unit or not at all. Protections become a path-prefix ACL file, with one large difference: denial means invisibility, not "access denied". The +l filetype becomes a lockable.yaml of extensions and paths that require a held exclusive lock to submit. And p4 jobs becomes grixel job, rebuilt the way I wish jobs had grown up.

The server daemon, grixeld, is authoritative for all of it: metadata, ACLs, chunk storage, locks, review state, jobs. The CLI is a thin client, so there is no policy a client can route around; clients never held the enforcement logic in the first place.

#The choices and their costs

Server-authoritative is a choice with a cost: the server matters. If it is unreachable you cannot submit, though a read-only offline mode still answers grixel changes and local status. In exchange, every invariant in this piece is enforced at one chokepoint no client can bypass. Invisibility ACLs in particular are impossible in a Git-shaped world where a full copy of the data sits on a contractor's laptop.

On storage: metadata lives in a purpose-built Go substrate rather than an embedded database. Commits still serialize through a single writer, deliberately; that discipline is part of what makes changelists atomic. The substrate's job is to keep that critical section short: chunk upload happens before it, outside any lock, and only the metadata commit is serialized. What it replaces is lock-everything metadata contention, not write ordering, and the throughput ceiling is unmeasured. File content lives in a content-addressed chunk store on the local filesystem, split by FastCDC and hashed with BLAKE3, with an optional caching read proxy (shaped like p4p) for LAN fan-out. The server is one binary, no sidecars; a single operator can run it.

A housekeeping note: every command and output below is real, captured against binaries built from main, in local mode against a scratch data root. Commands carry -as alice or -as bob because local mode asserts who is acting; in a team deployment you grixel login once and the flag disappears. Long scratch paths are shortened to ~/demo; nothing else in any output is edited.

The cast is a small studio: alice, artist; bob, platform engineer; maya, art director.

· · ·

One depot, un-mergeable binaries, and the lock that saves the afternoon

The seasonal project keeps YAML spawn tables, config, dialogue, and multi-megabyte art in one ref, submitted with the same command. No Git LFS pointer files, no second storage system with its own quota and failure modes. Binary assets are the native case.

Two mechanisms make that livable. Chunk-level dedup: files are split into content-defined chunks, each hashed, and only chunks the server has never seen are stored or transferred. And the +l inheritance: a PSD or an FBX cannot be merged, so the only honest concurrency model is one person at a time, enforced by the system. An admin declares once which types are un-mergeable:

$ cat data/.grixel/lockable.yaml
version: 1
extensions: [".psd", ".fbx", ".uasset", ".umap", ".wav"]

Alice submits the first pass of a 48 MB .uasset; the type is lockable, so the submit claims the exclusive lock itself, just long enough to land the change. (The base ... (advanced) line is bookkeeping: her checkout advanced to the commit it just made.)

$ grixel submit -as alice -m "first pass boss arena"
grixel: holding exclusive lock on /companies/acme/games/seasonal/art/boss_arena.uasset (lockable type)
Committed: 4afe1f546fc4a2bfd51b305cb3797c2c6abe620ae7af11b8fe92a28c7d0b8cfc
  ref     : companies/acme/games/seasonal
  changes : 1
  stored  : 48.0 MiB new, 0 B deduped
  parent  : 0fc058b00345fb50caab34601935c49281e4c6b28b7a535fd853f4eb76a09898
  base    : 4afe1f546fc4a2bfd51b305cb3797c2c6abe620ae7af11b8fe92a28c7d0b8cfc (advanced)

Now the habit that matters. Alice is about to spend the morning in that file, so she claims the lock before opening it, exactly like p4 edit on a +l file:

$ grixel edit -as alice art/boss_arena.uasset
editing /companies/acme/games/seasonal/art/boss_arena.uasset — exclusive lock held (required for this type)

Maya, about to do the same, finds out immediately:

$ grixel lock -as maya /companies/acme/games/seasonal/art/boss_arena.uasset
grixel: lock /companies/acme/games/seasonal/art/boss_arena.uasset: locked by alice — run 'grixel locks /companies/acme/games/seasonal/art/boss_arena.uasset' for details

That is the refusal from the top of the piece, before any work existed. When alice submits, the lock releases itself and maya's next attempt succeeds:

$ grixel submit -as alice -m "boss arena: relight the pit"
grixel: holding exclusive lock on /companies/acme/games/seasonal/art/boss_arena.uasset (lockable type)
Committed: 2ef2f6e4c977591eb483a6cc4d4615a017528853ff9ee4994c2371155ac32698
  ref     : companies/acme/games/seasonal
  changes : 1
  stored  : 19.8 MiB new, 28.2 MiB deduped
  parent  : 4afe1f546fc4a2bfd51b305cb3797c2c6abe620ae7af11b8fe92a28c7d0b8cfc
  base    : 2ef2f6e4c977591eb483a6cc4d4615a017528853ff9ee4994c2371155ac32698 (advanced)

$ grixel lock -as maya /companies/acme/games/seasonal/art/boss_arena.uasset   # after the release
locked /companies/acme/games/seasonal/art/boss_arena.uasset (holder maya, id 6c43eb2a53ec5aeb)

Relighting the arena stored 19.8 MiB of new chunks against 28.2 MiB reused, with nothing configured per-file; big files chunk under a "large-asset" profile the chunker picks by size and extension.

Two honest details. Locks are enforced server-side at the submit chokepoint, so a client that never heard of lockable.yaml still cannot land a change to a locked path; the submit is refused with the holder named. And auto-claim protects a forgetful artist from clobbering anyone else's work; it does not hand back an afternoon spent editing a file somebody else had claimed. The habit that saves the afternoon is grixel edit first. Admins keep a break-glass release for the lock someone took on holiday.

#Workspace views survive as sparse checkout

A Perforce client spec maps the subset of the depot you want on disk; it is how a 2 TB project fits on a 1 TB laptop. In Grixel the view is flags on checkout. Bob wants design without art:

$ grixel checkout -as bob -exclude 'art/**' companies/acme/games/seasonal sparse-ws
Checked out companies/acme/games/seasonal
  dir   : ~/demo/sparse-ws
  base  : 3c4bf58d08b850204e2140fa0a8fb1721468b5917536da5c9278280bd3639fcb
  files : 2
  view  : all (excl. art/**) (partial checkout)

Edit files, then: grixel changes / grixel diff / grixel submit -m ...

$ du -sh sparse-ws
 20K    ~/demo/sparse-ws

The ref carries a 64 MB key-art PSD under art/; bob's disk carries twenty kilobytes. To be clear: there is no virtual filesystem paging content in behind your back. The view is explicit, which also makes it predictable.

#Text still merges like text

Locks are for the un-mergeable; spawn tables, configs, and code take the ordinary 3-way path. When bob's working copy falls behind alice's submit on the same YAML file:

$ grixel sync -as bob
companies/acme/games/seasonal — synced 5294102c -> 698f1e7b
  CONFLICTS (1):
    design/spawn_table.yaml

resolve conflicts — 'grixel mergetool' (binary) or edit the markers + 'grixel resolve' (text) — then submit.

$ grixel resolve -as bob --mine design/spawn_table.yaml
resolved 1 path(s) (mine); staged for submit

grixel mergetool handles the same situation for binaries by launching whatever tool the team registered for that extension, P4Merge included.

#Branching, and what replaces p4 integrate

Studios run mainline, dev, release. Each is a ref, a ref is a path, and creating one is a single server-side operation:

$ grixel branch -as bob companies/acme/games/seasonal companies/acme/games/seasonal-dev
Branched companies/acme/games/seasonal -> companies/acme/games/seasonal-dev @ b4fb8f15e9e0b83993fea60ee579510c309ee8f76c9434c67e29023ad821d1d9

Bob checks out seasonal-dev and submits an experiment like any other ref:

$ grixel submit -as bob -m "dev: try softer spawn curve"
Committed: 20182c4de9f4613ca8a541e380083c9a4a2d367adcf2ea840b48ae204b9364da
  ref     : companies/acme/games/seasonal-dev
  changes : 1
  stored  : 35 B new, 0 B deduped
  parent  : b4fb8f15e9e0b83993fea60ee579510c309ee8f76c9434c67e29023ad821d1d9
  base    : 20182c4de9f4613ca8a541e380083c9a4a2d367adcf2ea840b48ae204b9364da (advanced)

grixel merge is the p4 integrate role plus the resolve: the server computes the common ancestor, the changes apply into a checkout of the destination ref, conflicts use the same machinery as above, and the result lands as an ordinary atomic changelist:

$ grixel merge-base -as bob companies/acme/games/seasonal companies/acme/games/seasonal-dev
merge-base of companies/acme/games/seasonal and companies/acme/games/seasonal-dev:
  b4fb8f15e9e0b83993fea60ee579510c309ee8f76c9434c67e29023ad821d1d9

$ grixel merge -as bob companies/acme/games/seasonal-dev
Merging companies/acme/games/seasonal-dev into companies/acme/games/seasonal
  base : b4fb8f15
  src  : 20182c4d
  merged (1):
    design/spawn_table.yaml

review, then: grixel submit -m "merge ..."  (creates a merge commit)
$ grixel submit -as bob -m "merge dev spawn experiment"
Committed: 7e0d8dbf3160df362258fc07e21126cc6210a0f7148643c31da171370e82c365
  ref     : companies/acme/games/seasonal
  changes : 1
  stored  : 0 B new, 35 B deduped
  parent  : b4fb8f15e9e0b83993fea60ee579510c309ee8f76c9434c67e29023ad821d1d9
  base    : 7e0d8dbf3160df362258fc07e21126cc6210a0f7148643c31da171370e82c365 (advanced)

Note the merge commit's storage line: 0 B new. The store already held every byte, so carrying and integrating branches is cheap.

· · ·

#Sign-off for art direction, inside the depot

Here is where Grixel goes past Perforce. Perforce's first-party answer, Swarm, is a real review product; the seam is that it rides beside the depot as a separate system with its own store of approvals. Git hosting has required reviews too, and on a protected branch they do block direct pushes. The seam there is that protection is hosting-layer configuration: per-branch, off by default, with admin bypass toggles, and the approval record lives outside the repository's object store, so none of it travels to a mirror, a second remote, or a self-hosted bare repo. Grixel builds review into the server that owns the content: a gate.json committed at a path declares who must sign off before content can be promoted to another ref.

$ cat gate.json
{
  "version": 1,
  "rules": [
    {
      "appliesTo": "art/**",
      "target": "each-file",
      "enforce": "gate",
      "stages": [
        { "role": "art-director", "by": "group:art-directors", "order": 1 }
      ]
    }
  ]
}

Maya promotes the working ref to the live one before signing off:

$ grixel review promote -as maya /companies/acme/games/seasonal /companies/acme/games/seasonal-live
BLOCKED /companies/acme/games/seasonal → /companies/acme/games/seasonal-live — 1 gate target(s) not approved:
  none      /companies/acme/games/seasonal/art/keyart_main.psd

$ grixel approve -as maya -m "key art matches palette; ship it" /companies/acme/games/seasonal/art/keyart_main.psd
approve recorded on /companies/acme/games/seasonal/art/keyart_main.psd
/companies/acme/games/seasonal/art/keyart_main.psd  [content a724bb3c]
state: approved
  [✓] art-director   group:art-directors    signed by maya

$ grixel review promote -as maya /companies/acme/games/seasonal /companies/acme/games/seasonal-live
OK promoted /companies/acme/games/seasonal → /companies/acme/games/seasonal-live @ cf131439 (1 gate target(s) verified)

The gate sits at promote rather than raw submit, so artists iterate freely on the working ref and the live ref only moves once coverage is satisfied. A sign-off also names the exact content revision it approved. When alice submits another pass of the key art, the approval goes stale on its own:

$ grixel review status -as alice /companies/acme/games/seasonal/art/keyart_main.psd
/companies/acme/games/seasonal/art/keyart_main.psd  [content 590ee1ea]
state: none
  [ ] art-director   group:art-directors    open — needs 1 of group:art-directors

Maya approved content a724bb3c; the file is now 590ee1ea; the gate reopened on its own. If your studio has ever shipped an asset the art director approved three versions ago, you know why sign-offs are content-addressed.

#Jobs, or p4 jobs if it had kept evolving

I have a soft spot for p4 jobs: work tracked in the same system as the files, linked to the changelists that resolve it. Grixel rebuilds the idea: an admin-editable jobspec defines named types, each with its own status machine and custom fields, and no baked-in methodology vocabulary.

$ grixel job create -as maya -type bug -anchor /companies/acme/games/seasonal -title "Hero texture seams on consoles" -field severity=major -field platform=ps5 -assignee alice
note: "alice" is not a known principal — assignment is advisory, so it still applies
job000001  open  (bug)
  anchor   : /companies/acme/games/seasonal
  title    : Hero texture seams on consoles
  assignee : alice
  platform : ps5
  severity : major
  created  : 2026-07-05T23:32:40Z by maya

(That note line: assignee names are not validated against a user registry; assignment is an advisory label, so a bot or contractor name works. The changelist author is separate and comes from the verified login.) A submit cites the job the way a Perforce fix record would:

$ grixel submit -as alice --job job000001 -m "fix seams on hero texture"
grixel: holding exclusive lock on /companies/acme/games/seasonal/art/keyart_main.psd (lockable type)
Committed: 4bb92aee3ed1c442e92da6ecc482c0561231ca0631fdb91a4dfce77f90caf507
  ref     : companies/acme/games/seasonal
  changes : 1
  stored  : 5.1 MiB new, 58.9 MiB deduped
  parent  : ad439db18fa79626577df2f8f35dd4225e7a131a325034bb135e1bc1731582bf
  base    : 4bb92aee3ed1c442e92da6ecc482c0561231ca0631fdb91a4dfce77f90caf507 (advanced)
  jobs    : 1 cited

Jobs nest into a freeform hierarchy with a progress rollup, and everyone gets a worklist:

$ grixel jobs -as alice --mine
assigned to me (1):
  job000001  bug        fixed        Hero texture seams on consoles
available (unassigned) (1):
  job000002  task       open         Spring festival content drop

$ grixel job show -as maya job000002
job000002  open  (task)
  anchor   : /companies/acme/games/seasonal
  title    : Spring festival content drop
  created  : 2026-07-05T23:32:40Z by maya
  children : 1  (fixed: 1)
    job000001  fixed      Hero texture seams on consoles

Jobs are event-sourced, so several actors edit at once without clobbering each other, and the history is a pollable feed for dashboards or automation:

$ grixel job events -as maya
2026-07-05T23:32:40Z  job000001  created maya     bug "Hero texture seams on consoles" [open]
2026-07-05T23:32:40Z  job000002  created maya     task "Spring festival content drop" [open]
2026-07-05T23:32:40Z  job000001  link    maya     link parent job000002
2026-07-05T23:32:41Z  job000001  link    alice    link commit 4bb92aee3ed1c442e92da6ecc482c0561231ca0631fdb91a4dfce77f90caf507
2026-07-05T23:32:41Z  job000001  status  alice    → triaged
2026-07-05T23:32:41Z  job000001  status  alice    → fixed
next cursor: 1783294361490152000

#The unannounced project

Every studio eventually has a project whose existence is the secret. Grixel's ACL model makes a harder promise than "access denied": if you lack read access, the path is simply gone. It never appears in a listing or a search result, and it is never echoed in an error. Bob can see both games; alice sees one:

$ grixel ls -as bob /companies/acme/games
/companies/acme/games/
  seasonal/  -> ref @ 4ee43c8d
    design/
    readme.md
  secret/  -> ref @ 6db73c65
    pitch.md

$ grixel ls -as alice /companies/acme/games
/companies/acme/games/
  seasonal/  -> ref @ 4ee43c8d
    design/
    readme.md

$ grixel ls -as alice /companies/acme/games/secret
grixel: not found: /companies/acme/games/secret

$ grixel ls -as alice /companies/acme/games/doesnotexist
grixel: not found: /companies/acme/games/doesnotexist

Alice's two errors are byte-identical, so she cannot even probe for existence. The invariant is structural: every metadata-returning handler routes through a single view-resolution chokepoint, and a model-based leak test asserts the less-privileged view never differs by a byte.

Because jobs are anchored to paths, they inherit the same rule. Bob files a job under the secret project; for alice it does not exist:

$ grixel job create -as bob -type task -anchor /companies/acme/games/secret -title "Nightjar vertical slice"
job000003  open  (task)
  anchor   : /companies/acme/games/secret
  title    : Nightjar vertical slice
  created  : 2026-07-05T23:32:41Z by bob

$ grixel job show -as alice job000003
grixel: repo: job not found

No codename leaks through ticket titles; the tracker and the depot are the same governed namespace.

#The standards wiki, versioned and pinned

Studios keep brand guidelines and coding standards in a wiki that drifts away from reality. Grixel makes them depot content that projects depend on. The seasonal .grixel/scope.json maps two subtrees from elsewhere in the namespace into the workspace:

$ cat .grixel/scope.json
{
  "name": "SeasonalEvent2026",
  "owner": "team:seasonal",
  "description": "Spring festival content drop",
  "mappings": [
    {
      "source": "/companies/acme/standards/brand",
      "mountAt": ".standards/brand",
      "pin": { "mode": "head" }
    },
    {
      "source": "/companies/acme/standards/coding/go",
      "mountAt": ".standards/go",
      "pin": { "mode": "head" }
    }
  ],
  "ai_hints": "Tone: playful spring festival. Palette and voice live in .standards/brand."
}

grixel scope update resolves each mapping and commits a scope.lock pinning the exact source revisions, so every checkout reproduces the same standards. When bob later edits the brand voice upstream, the project does not silently absorb the change; status reports the drift and the team re-pins deliberately:

$ grixel status -as alice   # after the upstream edit
ref      : companies/acme/games/seasonal
base     : 9d42cd83 (up to date)
changes  : none (clean)
mappings : 2 total, 1 current, 1 drift-on-head
  ↻ .standards/brand (drift-on-head)
  → run 'grixel scope update' to refresh the mappings

#What the AI sees

This part is the capability Perforce never had a shape for. grixel-mcp is a read-only MCP server that Claude Code launches per workspace. It authenticates as you and passes through the same ACL chokepoint, so an assistant's context is exactly what its user is allowed to see, pinned to the revisions in scope.lock. grixel mcp preview prints the assistant's entire session view before you trust it; two excerpts from alice's tell the story. First, the session's reach, which is exactly hers:

reach — file_tree(/), ACL-filtered (paths you can't see are simply absent):
  //
    companies/
      acme/
        games/
        standards/

The secret project cannot appear in any listing this session produces; the preview runs through the same chokepoint as grixel ls. Second, the governed context bundle carries the brand standards at the pinned revision:

  <grixel:doc path="/companies/acme/standards/brand" revision="2b55b19b9de198035e7faf679dbd1060655eaad00ea522beb58532066b389369" pin="head" locked="true">
  # Acme palette
  Primary: #2B6CB0. Accent: #ED8936. Never use pure black.
  
  # Acme brand voice
  Playful, concrete, never snarky. Numbers over adjectives.
  Address the player as 'you'; we never say 'users'.
  
  </grixel:doc>

So an assistant drafting festival dialogue addresses the player as "you" because the pinned voice doc says so, not because someone pasted a wiki page into a prompt. The full preview, and the rest of the AI surface, is covered in Agentic freedom with guardrails.

There are no write tools on this surface. When an agent submits, it goes through the CLI write path under the same locks and gates as a human, with provenance recording the tool and model.

· · ·

#What Perforce still does that Grixel doesn't

Plainly, because this audience can smell evasion:

There is no P4V. There is no GUI at all yet; artists work in the CLI, with grixel difftool and grixel mergetool shelling out to P4Merge, Beyond Compare, kdiff3, or whatever a team registers per extension. For plenty of art teams that alone rules out a switch today. There are no Unreal or Unity plugins either, so nothing like the checkout-from-the-editor flow the P4 engine plugins give you. Both are obvious next steps, and neither exists yet.

There is no virtual filesystem. Sparse checkout is explicit: you declare a view and that is what materializes. Nothing pages in transparently the way a virtual drive would.

There is no shelving. Perforce can park an in-progress changelist on the server for a handoff or a machine swap; Grixel cannot, yet.

On scale, the biggest file in this piece is 64 MB. The chunker already selects the larger-chunk profile for big files and the mechanism does not change at 40 GB, but behavior at that size is unmeasured, so treat it as untested rather than promised.

On multi-site, the caching read proxy is the per-site read answer, the same shape as a p4p per studio, though its multi-node benchmark is unpublished and no multi-region mesh runs in production. There is no edge-server write answer yet: a remote studio's submits cross the WAN to the one authoritative server.

And if you have twenty years of history in a Perforce depot, I cannot migrate it for you today. Git import exists, both as a one-shot snapshot into a new project and as read-only vendoring pinned at a specific revision. Perforce migration tooling does not. And Grixel is pre-release.

#Where this leaves a studio

The pitch, compressed: your depot, your review tool, your bug tracker, and your standards wiki are today four systems with four permission models that drift apart, with AI assistants sitting across all of them and governed by none. Grixel collapses those into one namespace behind one chokepoint, so "allowed" means the same thing to an artist at the CLI, a CI bot, and an agent over MCP. That is what I mean by a system of record rather than a file store.

The Perforce shape is the anchor and the AI-context layer is the wedge. If you run a depot today, the muscle-memory mapping near the top is the part to argue with, and the concessions list is the part to price against your studio. Sign-off: the missing link goes deeper on review, and Building around agents on agent identity.


Grixel is pre-release. Read the docs, or request early access.