postprocess
The postprocess namespace — 28 functions.
globals/postprocess/add
postprocess.add(name: string, shader: string | AssetRef, opts: PostprocessOpts?) -> boolean
Register a fullscreen post-process effect. This call is what
puts a pass into the frame — a post-process .shader asset defines an
effect, and renders only once registered here. The chain applies the
registration on the caller's own stack and the returned boolean is its
answer, so a setProperty or setTexture naming the effect in the same
call finds it. shader is a .shader asset reference whose shader.wgsl
provides fn fragment(in: PostInput) -> vec4<f32> and whose
properties.yaml declares the effect's properties; the engine generates the
group(0) framework + schema-driven group(1) from that schema. Editing that
shader afterwards recompiles this effect in place, keeping its enabled
state, priority, layer and tuned property values. WGSL text is also
accepted, and then opts.properties is the whole schema. Effects run in
priority order (lower first, default 100).
A registered effect runs over the live viewport's frame AND over every
offscreen one — a capture from a world-space station, one orbiting an
entity, one of a named camera, a render-to-texture camera. In each of those
the effect's engine.view_proj / engine.prev_view_proj /
engine.inv_view_proj are the camera THAT render was drawn from and
engine.resolution is that target's own size, so a pass reconstructing
world space from zero_scene_depth(uv) reconstructs against the station
and lens the capture asked for. An offscreen capture is therefore an oracle
for an authored grade: it photographs a chosen station without taking the
on-screen camera from whoever else is driving the scene, and a capture's
postProcessing = false is the one control that takes the chain off the
frame it returns. An offscreen render keeps no view history of its own, so
engine.prev_view_proj there holds that same matrix rather than the frame
before it, and a pass taking camera motion from the two reads none.
Parameters
namestring— Unique effect name.shaderstring | AssetRef— A resolvedshaderasset reference, or author WGSL (fn fragment(in: PostInput)only).optsPostprocessOpts(optional) —{ priority = 100, enabled = true, layer = "all"|"scene", properties = {{name, type, default?, min?, max?, textureDefault?}} }— with a shader asset,propertieslayers over the asset's own schema.layerpicks which composited image the effect grades:"scene"runs it before the UI is drawn, so it grades the rendered picture and leaves every widget on screen as authored, and"all"(the default) runs it after the UI has landed, so the interface is graded along with the picture — an effect that belongs to the world's look wants"scene", since a screen another author drew is otherwise graded by it too.textureDefaultis what atype = "texture"property samples while nothing is bound to it:"white"(1,1,1,1 — the default),"black"(0,0,0,1),"normal"(0.5,0.5,1,1) or"transparent"(0,0,0,0). An effect that lays its texture over the scene wants"transparent", so the frame is untouched untilsetTexturebinds a texture that exists.
Returns boolean — True when the chain registered the effect; false when it refused it. A shader that does not compile draws nothing at any property value, so it is not registered and postprocess.list() never names it — the compiler's message is in the engine log. A call made from inside queue() or batch(), where the engine has not run the registration by the time the call returns, answers true for the queued request.
postprocess.add("vignette", asset.resolve("@builtin::shaders.post.vignette", "shader"))
postprocess.add("vignette", VIGNETTE_WGSL, { properties = {{ name = "intensity", type = "float", default = 0.5 }} })
globals/postprocess/describe
postprocess.describe(name: string) -> PostprocessDescription?
One effect by name, read in full: the chain state
postprocess.status() lists for it, and on top of that properties — the
schema the effect declared, each entry { name, type, default?, min?, max?, textureDefault? } in the shape add takes — and values, what each of
those properties currently holds. A property's value is the one the last
setProperty wrote, or the schema's own default where nothing has written
one, and it comes back as a number for a scalar and as the array for a
wider value, which is what setProperty takes, so a property read here is
written straight back.
This is the read-back for a property write. setProperty answers whether
the uniform took the value; this answers what the effect holds now, which
is the reading a pass that writes its properties every frame needs and the
one that tells a mistyped property name from an effect that is not
grading. The schema and the values are the engine's own record of the
effect — the schema it was registered with and every write the chain
accepted into its uniform, the same record /runtime/fx/<name>/meta.json
is serialized from. A write the chain refused is not in it, and neither is
one made against a property the schema does not declare.
Before an effect is registered its schema lives on the .shader asset it
will render: asset.resolve("@builtin::shaders.post.bloom", "shader"):getProperties() names what that shader declares.
Parameters
namestring— Effect name.
Returns PostprocessDescription? — The effect's state, schema and live values, or nil when nothing is registered under the name. An effect the renderer registers itself declares no properties of its own, and its properties and values are empty.
local e = postprocess.describe("vignette")
print(if e then e.values.intensity else "not registered")
for _, p in ipairs(postprocess.describe("bloom").properties) do
print(p.name, p.type, p.min, p.max)
end
globals/postprocess/list
postprocess.list() -> { string }
List all registered post-process effect names in renderer priority order (lower priority runs first).
Returns { string } — Array of effect names.
for _, n in ipairs(postprocess.list()) do print(n) end
globals/postprocess/remove
postprocess.remove(name: string) -> boolean
Queue removal of a post-process effect. Takes effect on the next frame. Removing a name that isn't registered is a silent no-op.
Parameters
namestring— Effect name to remove.
Returns boolean — True — the mutation was queued.
postprocess.remove("vignette")
globals/postprocess/setEnabled
postprocess.setEnabled(name: string, enabled: boolean) -> boolean
Queue an enable/disable toggle on a registered post-process effect. Targeting an unknown name is a silent no-op.
Parameters
namestring— Effect name.enabledboolean— True to enable, false to disable.
Returns boolean — True — the mutation was queued.
postprocess.setEnabled("bloom", false)
globals/postprocess/setProperty
postprocess.setProperty(name: string, prop: string, value: (number | { number })) -> boolean
Set a named material property on a registered post-process
effect. The property must be declared in the effect's properties
schema; read in WGSL as material.<prop>. value is a number or a
number array (vec/color).
Parameters
namestring— Effect name.propstring— Declared property name.value(number | { number })— Number or array of numbers.
Returns boolean — True when the effect's uniform took the value; false when it did not — an effect that is not registered, or one that declares no property by that name, is named in a WARN in the engine log. A call made from inside queue() or batch(), where the engine has not run the write by the time the call returns, answers true for the queued request.
postprocess.setProperty("vignette", "intensity", 0.6)
globals/postprocess/setSampler
postprocess.setSampler(name: string, opts: { [string]: any }) -> boolean
Configure the per-effect user sampler shared by the effect's declared texture properties. opts.filter = "linear" (default) or "nearest". opts.wrap (alias .address) = "clamp" (default), "repeat", or "mirror" — applied to all axes.
Parameters
namestring— Effect name.opts{ [string]: any }—{ filter = "linear"|"nearest", wrap = "clamp"|"repeat"|"mirror" }.
Returns boolean — True — the mutation was queued.
postprocess.setSampler("blur", { filter = "linear", wrap = "clamp" })
globals/postprocess/setTexture
postprocess.setTexture(name: string, prop: string, path: string) -> boolean
Bind a texture to one of an effect's declared texture properties.
Declare it in properties ({ name = "noise", type = "texture" }) and
sample in WGSL as textureSample(noise, noise_sampler, in.uv). path
is any TextureCache-resolvable spec (@builtin::textures.foo,
color:1,0,0, default:white, a render-target name, ...). A path whose
texture has not reached the GPU yet — one this same script created — is
held and bound as soon as it does; postprocess.status() reports it under
pendingTextures until then.
Parameters
namestring— Effect name.propstring— Declared texture-property name.pathstring— Texture path / spec.
Returns boolean — True when the slot took the binding, including one held until its texture reaches the GPU; false when it did not — an effect that is not registered, or one that declares no texture property by that name, is named in a WARN in the engine log. A call made from inside queue() or batch(), where the engine has not run the binding by the time the call returns, answers true for the queued request.
postprocess.setTexture("__vsky_composite", "cloud", "cloud_target")
globals/postprocess/status
postprocess.status() -> { PostprocessStatus }
Every registered effect in chain order with the state that decides
whether it reaches the frame — enabled flag, priority, layer, the
shader's compile error when it has one, the .shader asset it renders
when it was registered from one, the texture each declared slot is bound
to (textures) and the bindings still waiting for their texture
(pendingTextures). This is what the renderer draws with, so a survey of
the chain answers "is this one affecting the picture right now?" without
capturing a frame and reading pixels.
An effect this script has just registered is listed with pending = true until the renderer publishes it, since a registration is queued
for the next frame.
Returns { PostprocessStatus } — Array of per-effect state, in the order the chain runs them.
for _, e in ipairs(postprocess.status()) do
print(e.name, e.enabled, e.layer, e.error)
end
modules/postprocess/README
require("@builtin/modules/api/engine/postprocess") -- postprocess (also available as global 'postprocess')
Fullscreen post-process effects — register / remove / toggle / named-property updates / list the chain / read one effect's declared property schema and the value each property holds. Public Luau surface over the __postprocess Internal FFI namespace.
Usage: local postprocess = require("@builtin/modules/api/engine/postprocess") Also available as global: postprocess
modules/postprocess/add
add(name: string, shader: string | AssetRef, opts: PostprocessOpts?): boolean
Register a fullscreen post-process effect. This call is what
puts a pass into the frame — a post-process .shader asset defines an
effect, and renders only once registered here. The chain applies the
registration on the caller's own stack and the returned boolean is its
answer, so a setProperty or setTexture naming the effect in the same
call finds it. shader is a .shader asset reference whose shader.wgsl
provides fn fragment(in: PostInput) -> vec4<f32> and whose
properties.yaml declares the effect's properties; the engine generates the
group(0) framework + schema-driven group(1) from that schema. Editing that
shader afterwards recompiles this effect in place, keeping its enabled
state, priority, layer and tuned property values. WGSL text is also
accepted, and then opts.properties is the whole schema. Effects run in
priority order (lower first, default 100).
A registered effect runs over the live viewport's frame AND over every
offscreen one — a capture from a world-space station, one orbiting an
entity, one of a named camera, a render-to-texture camera. In each of those
the effect's engine.view_proj / engine.prev_view_proj /
engine.inv_view_proj are the camera THAT render was drawn from and
engine.resolution is that target's own size, so a pass reconstructing
world space from zero_scene_depth(uv) reconstructs against the station
and lens the capture asked for. An offscreen capture is therefore an oracle
for an authored grade: it photographs a chosen station without taking the
on-screen camera from whoever else is driving the scene, and a capture's
postProcessing = false is the one control that takes the chain off the
frame it returns. An offscreen render keeps no view history of its own, so
engine.prev_view_proj there holds that same matrix rather than the frame
before it, and a pass taking camera motion from the two reads none.
Parameters
namestring— Unique effect name.shaderstring | AssetRef— A resolvedshaderasset reference, or author WGSL (fn fragment(in: PostInput)only).optsPostprocessOpts?(optional) —{ priority = 100, enabled = true, layer = "all"|"scene", properties = {{name, type, default?, min?, max?, textureDefault?}} }— with a shader asset,propertieslayers over the asset's own schema.layerpicks which composited image the effect grades:"scene"runs it before the UI is drawn, so it grades the rendered picture and leaves every widget on screen as authored, and"all"(the default) runs it after the UI has landed, so the interface is graded along with the picture — an effect that belongs to the world's look wants"scene", since a screen another author drew is otherwise graded by it too.textureDefaultis what atype = "texture"property samples while nothing is bound to it:"white"(1,1,1,1 — the default),"black"(0,0,0,1),"normal"(0.5,0.5,1,1) or"transparent"(0,0,0,0). An effect that lays its texture over the scene wants"transparent", so the frame is untouched untilsetTexturebinds a texture that exists.
postprocess.add("vignette", asset.resolve("@builtin::shaders.post.vignette", "shader"))
postprocess.add("vignette", VIGNETTE_WGSL, { properties = {{ name = "intensity", type = "float", default = 0.5 }} })
modules/postprocess/describe
describe(name: string): PostprocessDescription?
One effect by name, read in full: the chain state
postprocess.status() lists for it, and on top of that properties — the
schema the effect declared, each entry { name, type, default?, min?, max?, textureDefault? } in the shape add takes — and values, what each of
those properties currently holds. A property's value is the one the last
setProperty wrote, or the schema's own default where nothing has written
one, and it comes back as a number for a scalar and as the array for a
wider value, which is what setProperty takes, so a property read here is
written straight back.
This is the read-back for a property write. setProperty answers whether
the uniform took the value; this answers what the effect holds now, which
is the reading a pass that writes its properties every frame needs and the
one that tells a mistyped property name from an effect that is not
grading. The schema and the values are the engine's own record of the
effect — the schema it was registered with and every write the chain
accepted into its uniform, the same record /runtime/fx/<name>/meta.json
is serialized from. A write the chain refused is not in it, and neither is
one made against a property the schema does not declare.
Before an effect is registered its schema lives on the .shader asset it
will render: asset.resolve("@builtin::shaders.post.bloom", "shader"):getProperties() names what that shader declares.
Parameters
namestring— Effect name.
local e = postprocess.describe("vignette")
print(if e then e.values.intensity else "not registered")
for _, p in ipairs(postprocess.describe("bloom").properties) do
print(p.name, p.type, p.min, p.max)
end
modules/postprocess/list
list(): { string }
List all registered post-process effect names in renderer priority order (lower priority runs first).
for _, n in ipairs(postprocess.list()) do print(n) end
modules/postprocess/remove
remove(name: string): boolean
Queue removal of a post-process effect. Takes effect on the next frame. Removing a name that isn't registered is a silent no-op.
Parameters
namestring— Effect name to remove.
postprocess.remove("vignette")
modules/postprocess/setEnabled
setEnabled(name: string, enabled: boolean): boolean
Queue an enable/disable toggle on a registered post-process effect. Targeting an unknown name is a silent no-op.
Parameters
namestring— Effect name.enabledboolean— True to enable, false to disable.
postprocess.setEnabled("bloom", false)
modules/postprocess/setProperty
setProperty(name: string, prop: string, value: (number | { number })): boolean
Set a named material property on a registered post-process
effect. The property must be declared in the effect's properties
schema; read in WGSL as material.<prop>. value is a number or a
number array (vec/color).
Parameters
namestring— Effect name.propstring— Declared property name.value(number | { number })— Number or array of numbers.
postprocess.setProperty("vignette", "intensity", 0.6)
modules/postprocess/setSampler
setSampler(name: string, opts: { [string]: any }): boolean
Configure the per-effect user sampler shared by the effect's declared texture properties. opts.filter = "linear" (default) or "nearest". opts.wrap (alias .address) = "clamp" (default), "repeat", or "mirror" — applied to all axes.
Parameters
namestring— Effect name.opts{ [string]: any }—{ filter = "linear"|"nearest", wrap = "clamp"|"repeat"|"mirror" }.
postprocess.setSampler("blur", { filter = "linear", wrap = "clamp" })
modules/postprocess/setTexture
setTexture(name: string, prop: string, path: string): boolean
Bind a texture to one of an effect's declared texture properties.
Declare it in properties ({ name = "noise", type = "texture" }) and
sample in WGSL as textureSample(noise, noise_sampler, in.uv). path
is any TextureCache-resolvable spec (@builtin::textures.foo,
color:1,0,0, default:white, a render-target name, ...). A path whose
texture has not reached the GPU yet — one this same script created — is
held and bound as soon as it does; postprocess.status() reports it under
pendingTextures until then.
Parameters
namestring— Effect name.propstring— Declared texture-property name.pathstring— Texture path / spec.
postprocess.setTexture("__vsky_composite", "cloud", "cloud_target")
modules/postprocess/status
status(): { PostprocessStatus }
Every registered effect in chain order with the state that decides
whether it reaches the frame — enabled flag, priority, layer, the
shader's compile error when it has one, the .shader asset it renders
when it was registered from one, the texture each declared slot is bound
to (textures) and the bindings still waiting for their texture
(pendingTextures). This is what the renderer draws with, so a survey of
the chain answers "is this one affecting the picture right now?" without
capturing a frame and reading pixels.
An effect this script has just registered is listed with pending = true until the renderer publishes it, since a registration is queued
for the next frame.
for _, e in ipairs(postprocess.status()) do
print(e.name, e.enabled, e.layer, e.error)
end
typed/builtin//modules/api/engine/postprocess/postprocess/add
postprocess.add(name: string, shader: string | AssetRef, opts: PostprocessOpts?) -> boolean
Register a fullscreen post-process effect. This call is what
puts a pass into the frame — a post-process .shader asset defines an
effect, and renders only once registered here. The chain applies the
registration on the caller's own stack and the returned boolean is its
answer, so a setProperty or setTexture naming the effect in the same
call finds it. shader is a .shader asset reference whose shader.wgsl
provides fn fragment(in: PostInput) -> vec4<f32> and whose
properties.yaml declares the effect's properties; the engine generates the
group(0) framework + schema-driven group(1) from that schema. Editing that
shader afterwards recompiles this effect in place, keeping its enabled
state, priority, layer and tuned property values. WGSL text is also
accepted, and then opts.properties is the whole schema. Effects run in
priority order (lower first, default 100).
A registered effect runs over the live viewport's frame AND over every
offscreen one — a capture from a world-space station, one orbiting an
entity, one of a named camera, a render-to-texture camera. In each of those
the effect's engine.view_proj / engine.prev_view_proj /
engine.inv_view_proj are the camera THAT render was drawn from and
engine.resolution is that target's own size, so a pass reconstructing
world space from zero_scene_depth(uv) reconstructs against the station
and lens the capture asked for. An offscreen capture is therefore an oracle
for an authored grade: it photographs a chosen station without taking the
on-screen camera from whoever else is driving the scene, and a capture's
postProcessing = false is the one control that takes the chain off the
frame it returns. An offscreen render keeps no view history of its own, so
engine.prev_view_proj there holds that same matrix rather than the frame
before it, and a pass taking camera motion from the two reads none.
Parameters
namestring— Unique effect name.shaderstring | AssetRef— A resolvedshaderasset reference, or author WGSL (fn fragment(in: PostInput)only).optsPostprocessOpts(optional) —{ priority = 100, enabled = true, layer = "all"|"scene", properties = {{name, type, default?, min?, max?, textureDefault?}} }— with a shader asset,propertieslayers over the asset's own schema.layerpicks which composited image the effect grades:"scene"runs it before the UI is drawn, so it grades the rendered picture and leaves every widget on screen as authored, and"all"(the default) runs it after the UI has landed, so the interface is graded along with the picture — an effect that belongs to the world's look wants"scene", since a screen another author drew is otherwise graded by it too.textureDefaultis what atype = "texture"property samples while nothing is bound to it:"white"(1,1,1,1 — the default),"black"(0,0,0,1),"normal"(0.5,0.5,1,1) or"transparent"(0,0,0,0). An effect that lays its texture over the scene wants"transparent", so the frame is untouched untilsetTexturebinds a texture that exists.
Returns boolean — True when the chain registered the effect; false when it refused it. A shader that does not compile draws nothing at any property value, so it is not registered and postprocess.list() never names it — the compiler's message is in the engine log. A call made from inside queue() or batch(), where the engine has not run the registration by the time the call returns, answers true for the queued request.
postprocess.add("vignette", asset.resolve("@builtin::shaders.post.vignette", "shader"))
postprocess.add("vignette", VIGNETTE_WGSL, { properties = {{ name = "intensity", type = "float", default = 0.5 }} })
typed/builtin//modules/api/engine/postprocess/postprocess/describe
postprocess.describe(name: string) -> PostprocessDescription?
One effect by name, read in full: the chain state
postprocess.status() lists for it, and on top of that properties — the
schema the effect declared, each entry { name, type, default?, min?, max?, textureDefault? } in the shape add takes — and values, what each of
those properties currently holds. A property's value is the one the last
setProperty wrote, or the schema's own default where nothing has written
one, and it comes back as a number for a scalar and as the array for a
wider value, which is what setProperty takes, so a property read here is
written straight back.
This is the read-back for a property write. setProperty answers whether
the uniform took the value; this answers what the effect holds now, which
is the reading a pass that writes its properties every frame needs and the
one that tells a mistyped property name from an effect that is not
grading. The schema and the values are the engine's own record of the
effect — the schema it was registered with and every write the chain
accepted into its uniform, the same record /runtime/fx/<name>/meta.json
is serialized from. A write the chain refused is not in it, and neither is
one made against a property the schema does not declare.
Before an effect is registered its schema lives on the .shader asset it
will render: asset.resolve("@builtin::shaders.post.bloom", "shader"):getProperties() names what that shader declares.
Parameters
namestring— Effect name.
Returns PostprocessDescription? — The effect's state, schema and live values, or nil when nothing is registered under the name. An effect the renderer registers itself declares no properties of its own, and its properties and values are empty.
local e = postprocess.describe("vignette")
print(if e then e.values.intensity else "not registered")
for _, p in ipairs(postprocess.describe("bloom").properties) do
print(p.name, p.type, p.min, p.max)
end
typed/builtin//modules/api/engine/postprocess/postprocess/list
postprocess.list() -> { string }
List all registered post-process effect names in renderer priority order (lower priority runs first).
typed/builtin//modules/api/engine/postprocess/postprocess/remove
postprocess.remove(name: string) -> boolean
Queue removal of a post-process effect. Takes effect on the next frame. Removing a name that isn't registered is a silent no-op.
Parameters
namestring— Effect name to remove.
Returns boolean — True — the mutation was queued.
postprocess.remove("vignette")
typed/builtin//modules/api/engine/postprocess/postprocess/setEnabled
postprocess.setEnabled(name: string, enabled: boolean) -> boolean
Queue an enable/disable toggle on a registered post-process effect. Targeting an unknown name is a silent no-op.
Parameters
namestring— Effect name.enabledboolean— True to enable, false to disable.
Returns boolean — True — the mutation was queued.
postprocess.setEnabled("bloom", false)
typed/builtin//modules/api/engine/postprocess/postprocess/setProperty
postprocess.setProperty(name: string, prop: string, value: (number | { number })) -> boolean
Set a named material property on a registered post-process
effect. The property must be declared in the effect's properties
schema; read in WGSL as material.<prop>. value is a number or a
number array (vec/color).
Parameters
namestring— Effect name.propstring— Declared property name.value(number | { number })— Number or array of numbers.
Returns boolean — True when the effect's uniform took the value; false when it did not — an effect that is not registered, or one that declares no property by that name, is named in a WARN in the engine log. A call made from inside queue() or batch(), where the engine has not run the write by the time the call returns, answers true for the queued request.
postprocess.setProperty("vignette", "intensity", 0.6)
typed/builtin//modules/api/engine/postprocess/postprocess/setSampler
postprocess.setSampler(name: string, opts: { [string]: any }) -> boolean
Configure the per-effect user sampler shared by the effect's declared texture properties. opts.filter = "linear" (default) or "nearest". opts.wrap (alias .address) = "clamp" (default), "repeat", or "mirror" — applied to all axes.
Parameters
namestring— Effect name.opts{ [string]: any }—{ filter = "linear"|"nearest", wrap = "clamp"|"repeat"|"mirror" }.
Returns boolean — True — the mutation was queued.
postprocess.setSampler("blur", { filter = "linear", wrap = "clamp" })
typed/builtin//modules/api/engine/postprocess/postprocess/setTexture
postprocess.setTexture(name: string, prop: string, path: string) -> boolean
Bind a texture to one of an effect's declared texture properties.
Declare it in properties ({ name = "noise", type = "texture" }) and
sample in WGSL as textureSample(noise, noise_sampler, in.uv). path
is any TextureCache-resolvable spec (@builtin::textures.foo,
color:1,0,0, default:white, a render-target name, ...). A path whose
texture has not reached the GPU yet — one this same script created — is
held and bound as soon as it does; postprocess.status() reports it under
pendingTextures until then.
Parameters
namestring— Effect name.propstring— Declared texture-property name.pathstring— Texture path / spec.
Returns boolean — True when the slot took the binding, including one held until its texture reaches the GPU; false when it did not — an effect that is not registered, or one that declares no texture property by that name, is named in a WARN in the engine log. A call made from inside queue() or batch(), where the engine has not run the binding by the time the call returns, answers true for the queued request.
postprocess.setTexture("__vsky_composite", "cloud", "cloud_target")
typed/builtin//modules/api/engine/postprocess/postprocess/status
postprocess.status() -> { PostprocessStatus }
Every registered effect in chain order with the state that decides
whether it reaches the frame — enabled flag, priority, layer, the
shader's compile error when it has one, the .shader asset it renders
when it was registered from one, the texture each declared slot is bound
to (textures) and the bindings still waiting for their texture
(pendingTextures). This is what the renderer draws with, so a survey of
the chain answers "is this one affecting the picture right now?" without
capturing a frame and reading pixels.
An effect this script has just registered is listed with pending = true until the renderer publishes it, since a registration is queued
for the next frame.
Returns { PostprocessStatus } — Array of per-effect state, in the order the chain runs them.
for _, e in ipairs(postprocess.status()) do
print(e.name, e.enabled, e.layer, e.error)
end