.hexshader Format Reference
Open .hexshader bundle schema 4, native shader ABI, capabilities, resources and integration rules
Last updated: 2026-10-17
.hexshader packages a shader's source, parameter definitions and values, named Looks, provenance, and optional image inputs. The 360 Hextile Shader Workshop is Alpha. This page describes the source-inspected current export profile; a schema number alone does not prove that any installed runtime or another renderer supports every feature. For the format overview, the Workshop guide, or a first shader and animation tutorial, follow those separate paths.
Permission and scope
Build .hexshader support into your own apps, including commercial products. No Hextile purchase, format fee or attribution is required. This specification and its original tutorial examples are dedicated to the public domain under CC0 1.0. Shader packages and their resources may carry their own licenses. Publishing this format does not open-source the 360 Hextile desktop application or license imported shader content. A package's provenance is descriptive metadata, not proof of redistribution rights.
Container and versions
The file is a ZIP with media type application/vnd.360hextile.shader+zip. bundle.json is the envelope: kind is 360hextile.shader-bundle, schema is the bundle version, asset is the snapshot, members is an inventory, and extensions is optional. Current exports use bundle schema 4 and document schema 4. The current reader can admit compatible legacy bundles with schemas 1–3. Treat these as distinct from the source-header schema 1 and runtime ABI hx-shader/1. A future bundle schema, document schema, required capability or expression grammar needs explicit support; refuse it rather than guessing.
| ZIP member | Role |
|---|---|
bundle.json, source.glsl, manifest.json, values.json, provenance.json |
Required envelope, native root GLSL, parsed source manifest, current typed values, content provenance |
modules/<logical_path> |
Optional declared UTF-8 GLSL helpers; the logical path already ends in .glsl |
looks/<id>.json |
Optional compatible named Looks |
resources/<64-lowercase-hex>.png |
Optional content-addressed image or sequence frames |
original/source.glsl, thumb.png |
Optional retained original and preview image |
Every ZIP member except bundle.json has exactly one members record with path, role, mime, bytes, and sha256 (sha256: followed by 64 lowercase hex digits). The record's byte count and digest cover the raw member bytes. The member set and inventory must match exactly: no duplicate, missing, extra, colliding or escaping paths. Validate ZIP structure and limits before reading payloads or writing to a destination. The codec checks every declared resource frame, not only the selected frame.
Schema-4 asset requires suggested_id, title, tags, abi, space, exported_revision, source_hash, manifest_hash, document_hash, capture_defaults, output, document_extensions, document_schema, source_files, source_module_paths, look_files, provenance_file, original_files, and asset_metadata. source_files records every root/helper logical path with raw bytes and sha256, sorted by UTF-8 path bytes; source_module_paths is the matching sorted helper list. The root is source.glsl; a helper at lib/noise.glsl is stored at modules/lib/noise.glsl. Looks and provenance have their own exact raw-byte descriptors. original_files has zero or one content-addressed original descriptor. The snapshot's document_extensions omits the helper source text; the reader hydrates declared modules/ into source_modules before rebuilding the document.
The schema-4 asset field shapes are:
| Fields | Required value |
|---|---|
suggested_id, title |
Nonempty strings; title equals asset_metadata.title |
tags |
String list equal to asset_metadata.tags; asset_metadata has exactly title and tags |
abi, space, document_schema, exported_revision |
hx-shader/1, a declared shader space, integer 4, and a positive integer revision |
capture_defaults |
Exactly frame (signed 32-bit integer), time and time_delta (finite binary32-range numbers), frame_rate ({"num": positive integer, "den": positive integer}), pointer (four finite binary32-range numbers), seed (unsigned 32-bit integer), and ray_origin (three finite binary32-range numbers). The rational rate must be positive and within binary32 range. |
output |
Exactly width (integer 1–16384), height (integer 1–8192), and background_srgb (three finite numbers in [0,1]); width equals twice height. |
document_extensions |
Object of admitted extensions and sorted unique required_capabilities. Schema 4 requires source-modules/1; helper source bytes are represented by ZIP members, so source_modules is absent here. |
source_files, source_module_paths |
Sorted list of {logical_path, bytes, sha256} descriptors for all source files; matching sorted helper-path list. Include the root even if there are no helpers. |
look_files |
Sorted list of {logical_path, id, bytes, sha256} descriptors; each logical path is looks/<id>.json and matches a ZIP member. |
provenance_file, original_files |
{logical_path: "provenance.json", bytes, sha256} and a list of zero or one {logical_path, bytes, sha256} original descriptor. An original's logical path is originals/<raw-SHA-256>.glsl, while its ZIP path is original/source.glsl. |
source_hash, manifest_hash, document_hash |
sha256: plus 64 lowercase hex digits, recomputed as below. |
Descriptor bytes are nonnegative exact integers and descriptor sha256 hashes the raw UTF-8 or PNG member bytes. provenance.json is a JSON object; the minimal admitted record is {"license":"CC0-1.0"}. If it supplies kind, use 360hextile.shader-provenance; if it supplies schema, use integer 1. An optional extensions.distribution schema-1 declaration has exactly schema, rights (declared or unresolved), source_hash, nullable original_sha256, and nonempty licenses entries of {id, license_text, notices}. A declared grant requires content-specific rights and coverage checks; admission alone does not grant them.
source_hash in schema 4 hashes the normalized complete source set, with domain 360hextile.shader-source-set.v1\0, a canonical JSON object containing entry: "source.glsl" and the normalized root/helper file map. Normalization strips a leading BOM and converts CRLF/CR to LF for this identity only; member digests still use exact bytes. manifest_hash hashes canonical JSON of the parsed parameters array. document_hash hashes 360hextile.shader-document.v4\0 followed by canonical JSON with exactly schema, abi, space, values, capture_defaults, output, extensions, source_files, asset_metadata, look_files, provenance_file, original_files, source_hash, and manifest_hash. Its extensions value is the stored document_extensions (no source_modules source text). The document's kind, revision, raw source and document_hash itself are excluded. Canonical JSON uses sorted keys, compact UTF-8, no nonfinite numbers, and turns an integral float within ±2⁵³ into an integer (including negative zero). Rebuild and compare all three identities; do not equate a raw ZIP hash or manifest.json member hash with them.
Minimal schema-4 bundle
This standalone Python 3 recipe writes an exact five-member .hexshader, with no modules, Looks or resources. It is an original CC0 1.0 example and imports no 360 Hextile application code. The root source and four JSON members are generated as UTF-8; bundle.json inventories the other four. Run it in a writable directory with python3 example.py.
import hashlib
import json
import zipfile
def canonical(value):
def numbers(item):
if type(item) is float and item.is_integer() and abs(item) <= 2**53:
return int(item)
if isinstance(item, list):
return [numbers(child) for child in item]
if isinstance(item, dict):
return {key: numbers(child) for key, child in item.items()}
return item
return json.dumps(numbers(value), sort_keys=True, ensure_ascii=False,
allow_nan=False, separators=(',', ':')).encode('utf-8')
def digest(raw):
return 'sha256:' + hashlib.sha256(raw).hexdigest()
manifest = {'schema': 1, 'abi': 'hx-shader/1',
'space': 'direction_360', 'parameters': []}
source = (b'/*@hextile-shader\n' + canonical(manifest) +
b'\n*/\nvec4 hxMain(in HxFrag hx) { return vec4(1.0); }\n')
values = {}
provenance = {'license': 'CC0-1.0'}
members = {'source.glsl': source, 'manifest.json': canonical(manifest),
'values.json': canonical(values), 'provenance.json': canonical(provenance)}
def descriptor(path, raw):
return {'logical_path': path, 'bytes': len(raw), 'sha256': digest(raw)}
source_files = [descriptor('source.glsl', source)]
provenance_file = descriptor('provenance.json', members['provenance.json'])
capture = {'frame': 0, 'time': 0, 'time_delta': 0,
'frame_rate': {'num': 30, 'den': 1}, 'pointer': [0, 0, 0, 0],
'seed': 0, 'ray_origin': [0, 0, 0]}
output = {'width': 1024, 'height': 512, 'background_srgb': [0, 0, 0]}
extensions = {'required_capabilities': ['source-modules/1']}
source_hash = digest(b'360hextile.shader-source-set.v1\0' + canonical({
'entry': 'source.glsl', 'files': {'source.glsl': source.decode('utf-8')}
}))
manifest_hash = digest(canonical(manifest['parameters']))
projection = {'schema': 4, 'abi': manifest['abi'], 'space': manifest['space'],
'values': values, 'capture_defaults': capture, 'output': output,
'extensions': extensions, 'source_files': source_files,
'asset_metadata': {'title': 'First Shader', 'tags': []},
'look_files': [], 'provenance_file': provenance_file,
'original_files': [], 'source_hash': source_hash,
'manifest_hash': manifest_hash}
document_hash = digest(b'360hextile.shader-document.v4\0' + canonical(projection))
asset = {'suggested_id': 'first_shader', 'title': 'First Shader', 'tags': [],
'abi': manifest['abi'], 'space': manifest['space'], 'exported_revision': 1,
'document_schema': 4, 'document_extensions': extensions,
'source_files': source_files, 'source_module_paths': [],
'look_files': [], 'provenance_file': provenance_file,
'original_files': [], 'asset_metadata': projection['asset_metadata'],
'capture_defaults': capture, 'output': output,
'source_hash': source_hash, 'manifest_hash': manifest_hash,
'document_hash': document_hash}
roles = {'source.glsl': ('source', 'text/x-glsl'),
'manifest.json': ('manifest', 'application/json'),
'values.json': ('values', 'application/json'),
'provenance.json': ('provenance', 'application/json')}
inventory = [{'path': path, 'role': roles[path][0], 'mime': roles[path][1],
'bytes': len(raw), 'sha256': digest(raw)}
for path, raw in sorted(members.items())]
bundle = {'kind': '360hextile.shader-bundle', 'schema': 4, 'asset': asset,
'extensions': {}, 'members': inventory}
with zipfile.ZipFile('first_shader.hexshader', 'w', zipfile.ZIP_DEFLATED) as archive:
for path, raw in sorted(members.items()):
archive.writestr(path, raw)
archive.writestr('bundle.json', canonical(bundle))
print('first_shader.hexshader: schema 4, five members')
Source and host ABI
source.glsl begins with a /*@hextile-shader JSON header declaring schema: 1, abi: "hx-shader/1", space, and parameters. The native entry is vec4 hxMain(in HxFrag hx); the host supplies the GLSL profile, HxFrag, uniforms and main(). Do not add your own main(). A quoted relative #include may resolve a declared helper within the source set. Paths use forward slashes and must remain confined, portable and collision-free; includes have bounded depth, share one namespace, and only the root defines the entry. Validate the complete source set before expansion.
For the introductory direction_360 profile, hx.direction is a unit direction with +Y up and +Z forward. hx.uv runs south to north; hx.mapUv runs north to south. hx.fragCoord is the full-image pixel center from the bottom left, and hx.resolution is the full output size. The context also exposes rayOrigin, rayDirection, time, timeDelta, frame, frameRate, pointer, and seed. Default ray origin is zero; ray direction follows the sphere direction. Native output is linear sRGB with straight alpha; the current PNG writer composites the background and writes opaque encoded sRGB. Other declared spaces are equirect_map, ray_360, and shadertoy_image; Shadertoy's mainImage is for that space, not native direction_360 source.
Parameters are typed: float, int, bool, angle, vec2, vec3, vec4, color, color_ramp, and enum. Each has an id, type, and default; at most 32 parameters are admitted. Validate IDs, values, limits and storage cost. The host generates hxParam_<id> uniforms and uploads current values.json or compatible Look values using the declared type. A Look is a saved compatible state; it is not executable source. Check its manifest/resource identities and all values before applying it.
Time, images and required capabilities
The host supplies sampled content time (hx.time), frame number, delta and exact rational FPS. A sequence samples Start + n/FPS with End exclusive; preview looping is a separate behavior. Expressions and modulation can drive typed targets, but dependency cycles and invalid samples must refuse sequence preflight. A preview fallback does not prove a sequence will render. For the user-facing sequence steps, see the Workshop timeline and first timed shader.
Image inputs are immutable inputN slots with hxSampleInputN helpers. The supported PNG profile is srgb-rgba8-png/1, top-left oriented. Sampling converts RGB to linear values and preserves straight alpha. An image sequence has ordered frames and rational FPS; a still uses frame 0 with no FPS. Supported time policies include follow/hold and hold/loop. Validate dimensions, hashes and every ordered frame before freezing any sample.
The recognized required-capability identifiers are master-clock/1, modulation/1, modulation/2, modulation/3, mouse/1, resources/1, conventions/1, source-modules/1, shader-timeline/1, parameter-expressions/1, and parameter-expressions/2. Required declarations must agree with extension schemas. conventions/1 carries resolved input/control definitions and optional camera helpers; live input history and seeking have different semantics from frozen sequence output. A host must implement every required capability and its grammar or refuse execution. Names in this list are negotiation identifiers, not a promise that every installed build or other engine supports them.
Integration
- Read the ZIP within bounded memory and file limits; verify exact allowed names, canonical paths, member roles, MIME, byte lengths and SHA-256 digests before staging any files.
- Admit the bundle and document schema, source-header schema and
hx-shader/1ABI separately. Parse the root header and requiremanifest.jsonto agree with it. Hydrate declared helpers and rebuild the schema-4 snapshot, including source, manifest and document identities. - Validate typed values, Looks, originals, provenance structure and all resource frames. Check every required capability, expression grammar and source include against the host implementation. Refuse unsupported or inconsistent content with an actionable reason.
- Compile for the declared space using the host ABI, then supply full-frame coordinates, typed uniforms, resources and sampled time. Keep interactive state separate from deterministic sequence sampling. Admit the complete package before publishing it to a library or running it.
Start with the complete native shader tutorial, then use the Workshop guide to see how Hextile authors, accepts and exports its own packages. A minimal shader in one host is not a cross-GPU parity certificate.