Shader configs and effects
Two things make the picture look different in 1.8:
- An effect is HLSL with a declared list of options. The launcher ships a library of effects, and an addon can add its own.
- A config is a
.swfxtext file that lists effect instances in order and sets their options. Players pick one on the Shaders page.
An addon bundles configs, effects or both.
A config in full
# Comments start with #.
name = "Realistic" # required
author = "Swofty"
description = "Flatter contrast, calmer reds and HDR-like highlights."
swfx = 1 # the format version; optional, only 1 exists
# Each [table] is an effect instance. They run in file order.
[auto_hdr]
shadow_tuning = 1.94 # float: a number; an integer is fine
highlight_saturation = 1.5
auto_hdr_method = "highlights" # enum: one of the effect's values
hdr_tonemap = true # bool
[ambient_light]
al_int = 0.96
al_lens = true
al_lens_int = 10
[color_mod]
chroma = 0.917
brightness = [-0.039, -0.033, -0.033] # color3: r, g, b
# Switch an instance off without deleting it.
[perspective]
enabled = false
fov_angle = 120
# A second instance of the same effect takes a label.
[glare]
bloom = 35
[glare.wide]
bloom = 10
threshold = 60Anything a table leaves out takes the effect's default. The effect library lists every effect with its options and defaults.
What the checker enforces
The launcher checks a config on every change in the editor, in swofty shaders lint, and when the profile builds. A config with an error is not applied; the last valid version stays live. Each message below is the checker's own wording.
Top-level keys:
name = "Mine" # required: A config needs a `name = "..."` (quick fix: Add a name); an empty name is `name` must not be empty
author = "Me" # a string
description = "..." # a string
swfx = 1 # must be 1: `swfx` must be 1, the only config format version
colour = "x" # warning: Unknown top-level key `colour`; it is ignoredTables:
[grane] # error: Unknown effect `grane`; if it comes from an addon, that addon is not installed
# quick fix: Change to `grain`
[vignette]
amoutn = 0.4 # error: `vignette` has no param `amoutn` (quick fix: Change to `amount`)
amount = "high" # error: `amount` expects a number, not a string
radius = 9 # warning: `radius` is above its maximum of 1.5; it is clamped to 1.5
# quick fix: Clamp to 1.5
enabled = 1 # error: `enabled` expects true or false, not a whole number
[vignette] # error: `[vignette]` appears twice; give the second a label, like `[vignette.second]`Value types follow the option's type:
al_int = 0.96 # float: a number
al_adapt_base_black_lvl = 2 # int: a whole number
al_lens = true # bool
al_dirt_tex = "smudges" # enum: a string; error: "smudge" is not a value of `al_dirt_tex`; use one of "smudges", "specks"
brightness = [0, 0, 0] # float2, color3 and color4: an array of 2, 3 or 4 numbers
# error: `brightness` expects 3 numbers, not 2- A value outside the option's
minandmaxis a warning, and the value is clamped. - A label cannot repeat one of the effect's option names:
The labelbloomis also a param ofglare``. - A config with no enabled instance gets the info
This config changes nothing. - An effect from an addon that is not installed is the unknown-effect error. When two addons declare the same effect id, or an addon declares a library id, the first in load order wins and the other effect is ignored with a warning in the build output. Load order is the profile's addon order, then the remaining addons by id.
Bundle a config in an addon
{
"schema_version": 1,
"id": "swofty.realistic-shader",
"name": "Realistic Shader",
"version": "1.0.0",
"author": "Swofty",
"category": "shaders",
"compat": { "game": "f1_25", "launcher": ">=1.8.0" },
"shaders": [
{ "config": "shaders/realistic.swfx" },
{ "config": "shaders/realistic-hdr.swfx", "force": false }
]
}swofty.realistic-shader/
swofty.json
shaders/realistic.swfx
shaders/realistic-hdr.swfxshadersis a list of entries;configis a relative.swfxpath inside the addon. Its file name without.swfxis the config's id:addon:swofty.realistic-shader/realistic. Two entries with the same file name are refused.effectsis a list of relative<id>.effect.jsonpaths inside the addon.- The configs show on the Shaders page under From addons with the addon's name, read-only. A player can copy one to personal.
- The configs stay available while the addon is switched off. Deleting the addon removes them, and every profile that had one of them active goes back to no config.
- Set
compat.launcherto>=1.8.0: an older launcher refuses the addon at install withneeds Swofty Launcher >=1.8.0. - An addon never turns its own config on. The player picks it.
Forcing a config
"shaders": [
{ "config": "shaders/broadcast.swfx", "force": true }
]While the addon is switched on, force: true makes that config the active one, locks the Shaders page and shows the banner "You have an addon which forces this shader configuration", with a View addon button. Use it for an addon whose other content only looks right with that config.
An addon may force one config; the manifest is refused with only one config may be forced otherwise. When several enabled addons force a config, the last one in load order wins, as later addons override earlier ones everywhere else in the launcher. A shader-only addon has no switch and counts as on whenever it is installed.
Shader-only addons
An addon whose content is only shaders and effects, plus metadata, an icon and a category, is a shader-only addon. Any options, operations, launcher, hud, hub_tab, career_setup, multiplayer, shader or postfx entry makes it a mixed addon. It has no on/off switch on the Addons page, and shows a Shader config chip with Open in Shaders. swofty addons list shows it as shaders. An addon that has other content keeps its switch, and says under it "Its shader config stays in Shaders while it's off".
An effect in full
An effect is a JSON file named <id>.effect.json beside its HLSL. This one tints the picture and adds an optional darkening towards a corner.
{
"id": "warm_tint",
"name": "Warm Tint",
"description": "Pulls the picture towards a colour, optionally darker towards the bottom.",
"params": [
{ "id": "amount", "type": "float", "default": 0.3, "min": 0, "max": 1, "step": 0.01,
"label": "Amount", "doc": "How far the picture moves towards the tint colour." },
{ "id": "tint", "type": "color3", "default": [1.0, 0.85, 0.7], "min": 0, "max": 2,
"label": "Tint", "doc": "The colour in linear light." },
{ "id": "mode", "type": "enum", "values": ["multiply", "add"], "default": "multiply",
"label": "Mode", "doc": "Multiply darkens, add lifts." },
{ "id": "fade_bottom", "type": "bool", "default": false,
"label": "Fade bottom", "doc": "Darkens the lower third." }
],
"textures": [],
"passes": [
{ "name": "apply", "shader": "warm_tint.hlsl", "entry": "apply" }
]
}#include "swofty/params.hlsli"
float4 apply(float2 uv, uint2 pixel)
{
float4 c = scene_at(int2(pixel));
float3 rgb = to_linear(c.rgb);
float3 tinted = param_mode() == MODE_ADD ? rgb + param_tint() * 0.1 : rgb * param_tint();
rgb = lerp(rgb, tinted, param_amount());
if (param_fade_bottom())
rgb *= 1 - 0.4 * smoothstep(0.66, 1.0, uv.y);
return float4(from_linear(rgb), c.a);
}Register it in the manifest. A config in the same addon, or any other addon, can use it:
"effects": ["effects/warm_tint.effect.json"],
"shaders": [{ "config": "shaders/warm.swfx" }]name = "Warm"
[warm_tint]
amount = 0.4
tint = [1.0, 0.8, 0.6]
mode = "add"An effect id is [a-z][a-z0-9_]* and unique. The library owns the unprefixed names, so name yours with something of your own.
Params
{ "id": "amount", "type": "float", "default": 0.3, "min": 0, "max": 1, "step": 0.01, "label": "Amount", "doc": "..." }
{ "id": "levels", "type": "int", "default": 4, "min": 1, "max": 8, "label": "Levels", "doc": "..." }
{ "id": "smooth", "type": "bool", "default": true, "label": "Smooth", "doc": "..." }
{ "id": "mode", "type": "enum", "values": ["off", "low", "high"], "default": "low", "label": "Mode", "doc": "..." }
{ "id": "offset", "type": "float2", "default": [0, 0], "min": -1, "max": 1, "label": "Offset", "doc": "..." }
{ "id": "tint", "type": "color3", "default": [1, 1, 1], "min": 0, "max": 2, "label": "Tint", "doc": "..." }
{ "id": "border", "type": "color4", "default": [0, 0, 0, 1], "min": 0, "max": 1, "label": "Border", "doc": "..." }idis[a-z][a-z0-9_]*and unique in the effect.labelanddocare what the editor shows on hover.minandmaxapply to every number of a vector.stepis the editor's nudge size. Adefaultoutsideminandmaxis refused.- An int's
default,minandmaxare whole numbers. - An enum needs at least one value, and its default is one of them.
Slots
An effect's params pack into 32 float slots, in declaration order. float, int, bool and enum take one slot, float2 two, color3 three and color4 four. An effect that needs more than 32 is refused: effect `x` needs 33 param slots; the limit is 32.
amount float slot 0
levels int slot 1
mode enum slot 2
offset float2 slots 3-4
tint color3 slots 5-7
border color4 slots 8-11 12 of 32 usedA pass reads slot n with option(n). option(0) to option(15) come from the original options, and option(16) to option(31) from the four extra float4s that follow them in the constant buffer.
The generated accessors
Before it compiles an effect, the build writes swofty/params.hlsli on the include path. For the params above it holds
#pragma once
#include "swofty/postfx.hlsli"
float param_amount() { return option(0); }
int param_levels() { return int(round(option(1))); }
uint param_mode() { return uint(round(option(2))); }
#define MODE_OFF 0
#define MODE_LOW 1
#define MODE_HIGH 2
float2 param_offset() { return float2(option(3), option(4)); }
float3 param_tint() { return float3(option(5), option(6), option(7)); }
float4 param_border() { return float4(option(8), option(9), option(10), option(11)); }- Effect HLSL does
#include "swofty/params.hlsli". It pulls inswofty/postfx.hlsli. - A bool accessor is
bool param_<id>() { return option(n) > 0.5; }. - An enum constant is
<ID>_<VALUE>upper-cased with every other character as_: the value"two-way"ofmodeisMODE_TWO_WAY. Two constants with the same name are refused.
Textures and passes
An effect's textures and passes are the post-process passes contract with names local to the effect. Two instances of the same effect, such as [glare] and [glare.wide], each get their own textures.
"textures": [
{ "name": "half", "scale": 0.5, "format": "rgba16f" },
{ "name": "adapt", "width": 1, "height": 1, "format": "rgba16f", "persistent": true },
{ "name": "dirt", "file": "ambient_light_dirt.png" }
],
"passes": [
{ "name": "bright", "shader": "ambient_light.hlsl", "entry": "bright", "output": "half" },
{ "name": "down", "shader": "ambient_light.hlsl", "entry": "down", "inputs": ["half"], "output": "half2" },
{ "name": "apply", "shader": "ambient_light.hlsl", "entry": "apply", "inputs": ["half", "adapt", "dirt"] }
]- A pass has
name,shader,entry,raw,threads,inputs,output,whenandbudget_ms, as in post-process passes. inputsnames the effect's textures, and alsooriginal(the picture before any pass this frame) andprevious(the last frame's picture after every pass). Read them withinput(i, uv)by their place in the list.- No
outputwrites the picture. A pass that writes a texture is sized by it. - A
persistenttexture keeps its contents from frame to frame. The ambient light effect stores its eye adaptation in one. - A
filetexture is an image from the effect's folder, read-only. - A texture with
mipscarries a mip chain, described below. - The pass order is the order in the file. Passes of the next instance start where this one ends.
- A pass that reads
previoussees the picture after all the instances, so it lags one frame behind.
Mipmapped textures
mips is the number of levels of a texture, including the full-size one: 1 (the default) up to 12, and no more than floor(log2(max(width, height))) + 1.
"textures": [
{ "name": "lens", "width": 1920, "height": 1080, "format": "rgba16f", "mips": 5 },
{ "name": "stats", "width": 64, "height": 32, "format": "rg16f", "mips": 7 }
]mipsworks with everyformat, withscale,widthandheight, and withpersistent. Afiletexture takes no mips. Amipsabove what the size allows is lowered to it, with a warning.- A pass writes level 0 only. After every pass that writes the texture, the core rebuilds levels 1 and up, each the 2x2 average of the level above (an odd size rounds down, at least 1x1).
- A pass that reads the texture sees the chain as of the last write.
input(i, uv)reads level 0. These read any level:
float4 input_lod(uint i, float2 uv, float lod); // linear within and between levels, clamped to the edge
float4 input_lod_point(uint i, float2 uv, float lod); // the nearest texel of the nearest level
uint2 input_size_lod(uint i, uint lod); // the size of a levelRules
A config can change its own options while something happens in the game, such as a car on a kerb, a hit or a yellow flag. Each [[when]] block names a signal and the values to use while it is on. The core fades from the config's own values to the rule's values and back.
name = "1980s - VHS Off-Air Recording"
author = "Jonno"
[rewind_video]
tvl = 240
noise = 0.04
detail = 0.3
smear = 0.25
saturation = 1.0
# The tape loses tracking while two wheels are on a kerb.
[[when]]
name = "kerb tracking"
on = "car.surface"
is = "kerb"
wheels = 2
min_ms = 40
hold_ms = 120
fade_in_ms = 60
fade_out_ms = 350
[when.set.rewind_video]
noise = 0.12
detail = 0.15
smear = 0.6
# The signal breaks up on a hard hit.
[[when]]
name = "impact dropout"
on = "car.impact"
above = 3.0
for_ms = 500
fade_in_ms = 0
fade_out_ms = 500
priority = 20
[when.set.rewind_video]
noise = 0.35
saturation = 0.4
# Smear scales with speed: nothing at 250 km/h or below, full at 320 km/h or above.
[[when]]
name = "flat out"
on = "car.speed"
from = 250
to = 320
[when.set.rewind_video]
smear = 0.5The Rewind effect is rewind_video; see the effect library. A rule only moves options the config's effects already have.
A rule in full
[[when]]
name = "..." # optional; shown in the editor, the Preview and the log
on = "<signal>" # required unless `all` is given
is = "kerb" # a value of a name signal, or a list: is = ["grass", "gravel"]
wheels = 2 # 1-4, car.surface only; default 1
above = 3.0 # a number signal, or the magnitude of an event
below = 50.0
from = 250 # a ramp; needs `to`, from < to, number signals only
to = 320
all = [ # every entry must hold
{ on = "car.surface", is = "kerb" },
{ on = "car.speed", above = 150 },
]
min_ms = 0 # the condition must hold this long before the rule turns on
hold_ms = 0 # the rule stays on this long after the condition clears
for_ms = 400 # event rules only
fade_in_ms = 150
fade_out_ms = 300
priority = 0 # integer -100 to 100
[when.set.rewind_video] # also [when.set.<effect>.<label>]
noise = 0.12 # any option of that instance
enabled = false # turns the instance off while the rule is on[when.set.<effect>] and [when.set.<effect>.<label>] belong to the most recent [[when]]. when is reserved: no effect can be named when, and [when] with single brackets is an error with the quick fix [[when]]. An array may span several lines, with comments at line ends.
Conditions
A rule has one kind of condition. Each block below is a separate rule's keys:
on = "view.replay" # none: a yes/no signal is on while it is true
on = "race.start" # none: an event turns the rule on for `for_ms`
on = "car.surface"
is = "kerb" # is: a name signal equals this value
is = ["grass", "gravel"] # is: equals any of these
wheels = 2 # at least 2 of the 4 wheels on those surfaces
on = "car.speed"
above = 150 # above: on above this value
on = "car.speed"
below = 50 # below: on below this value
on = "car.speed"
above = 100
below = 200 # both: on between the two values
on = "car.speed"
from = 250
to = 320 # from + to: strength 0 at 250, 1 at 320, no fade
all = [{ on = "car.surface", is = "kerb" }, { on = "car.speed", above = 150 }]aboveandbelowon an event test the event's magnitude:car.impactis in g,car.damageis 0 to 1.fromandtotake number signals, only at the top level, and not together withall.allentries useonwith one ofis,above,below, andwheels. An event signal is only ever a rule's top-levelon.
Timing
All values are milliseconds, 0 to 60000.
min_ms = 0 # default 0: the condition must hold this long before the rule turns on
hold_ms = 0 # default 0: the rule stays on this long after the condition clears
for_ms = 400 # default 400: how long one event keeps the rule on; a new event restarts it
fade_in_ms = 150 # default 150: 0 to full strength
fade_out_ms = 300 # default 300: full strength to 0A fade of 0 jumps. A ramp rule takes its strength from the signal: its fades default to 0, and a fade set on it smooths a signal that jumps. min_ms, hold_ms and for_ms don't apply to a ramp.
Priority and overrides
[[when]]
on = "car.impact"
above = 3.0
priority = 20 # beats a priority 0 rule on the same option
[when.set.rewind_video]
noise = 0.35
saturation = 0.4
[[when]]
on = "session.flag"
is = "yellow"
[when.set.bloom.wide] # the instance bloom.wide
enabled = false # only false is allowedenabled = false turns the instance off while the rule is on. A rule cannot turn an instance on, and cannot name an instance that is off in the config. Options take the same types and clamping as the instance tables.
How values blend
numbers, vectors, colours move from the config's value to the rule's value per component, on a smoothstep curve
integers blend, then round
yes/no, choice, enabled switch when the rule is halfway in
several rules, one option applied in priority order, each blending over the result so far- The highest priority ends on top. Equal priorities apply in file order, so the later rule wins.
- Rules that set different options do not affect each other.
- A smooth on and off for a yes/no option needs an effect with a strength option.
- A ramp rule sets its strength straight from the signal.
Signals
Every signal is for the player's car. The car.* signals read the player's own car, also in replays and while spectating. The car.* signals and session.wetness are read from the Steam copy of F1 25; on the EA app copy they have no value yet.
in game now
view.replay yes/no
view.paused yes/no
view.photo yes/no
view.menu yes/no in the menus and on loading screens: no session is running
view.cutscene yes/no
view.spectating yes/no
session.start event the session loads
race.start event lights out
session.lap event the player completes a lap
session.end event the session ends
car.speed number km/h, 0-400
car.rpm number 0-1 of the limiter
car.gear number -1 reverse, 0 neutral, 1-8
car.throttle number 0-1
car.brake number 0-1
car.steer number -1 to 1
car.g_lateral number g, -6 to 6
car.g_long number g, -6 to 6, braking negative
car.surface name tarmac kerb rumble grass gravel sand, per wheel
car.wetness number 0-1 water under the car's wheels: the wettest of the four
session.wetness number 0 dry to 1 soaked: how wet the track is
not read in game yet (works in the Preview)
view.camera name cockpit tcam tcam_offset chase_near chase_far other
car.impact event collision g
car.damage event part damage increase, 0-1
car.in_pit_lane yes/no
car.off_track yes/no all four wheels off the track
session.flag name none green blue yellow red
session.safety_car name none sc vsc formation
session.weather name clear light_cloud overcast light_rain heavy_rain storm
session.flashback event
session.chequered event the player takes the flagcar.surface groups the game's surfaces: kerb is its rumble strips, rumble its ridged strips, gravel takes gravel, rock and mud, tarmac takes tarmac, concrete, cobbles, metal and standing water, and grass and sand are themselves. A wheel in the air has no surface.
A signal the game does not read, or one with no live session, has no value: its conditions are false and its events never occur.
Only on track
A config that should leave the menus and loading screens alone turns each of its instances off while view.menu is on:
[[when]]
name = "menus"
on = "view.menu"
fade_in_ms = 0
fade_out_ms = 0
[when.set.rewind_video]
enabled = false
[when.set.rewind_cadence]
enabled = falseLimits
- At most 16 rules per config and 32 overrides per rule.
- A rule changes options and turns instances off. It cannot add effects, change passes or swap to another config.
- An instance a rule turns off keeps its persistent textures, and
previouskeeps flowing. - A rule cannot override another rule.
- A change to the config resets every rule to off.
Rule checks
The checker marks these as you type, with quick fixes where a name is close:
unknown signal `car.surfce`; did you mean `car.surface`?
`is` needs a value of car.surface: tarmac, kerb, rumble, grass, gravel, sand
`above` needs a number signal; car.surface is a name
`for_ms` is for events; car.speed is a number
`from` must be below `to`
[when.set.bloom] names no instance of this config
[when.set.x] names an instance that's off in this config
rewind_video has no option `noiz`; did you mean `noise`?
`saturation` is 3.0, outside 0.0-2.0 for rewind_video; it is clamped
car.impact isn't read in game yet; this rule only fires in the Preview (warning)Rule log lines
[shaders] rule "kerb tracking" on (car.surface = kerb, 2 wheels)
[shaders] rule "kerb tracking" off after 1.4 s
[shaders] rule "flat out" on (car.speed = 287 km/h)
[shaders] rule "impact dropout": car.impact isn't read in game yet; it never fires
[shaders] rule "kerb tracking": 37 time(s) on in the last 60 sEvery on and off is logged for the first 60 seconds after a config loads. After that, one line per minute gives the count for each rule that turned on. The "isn't read in game yet" line is logged once per rule per config load.
The swofty shaders command
swofty shaders list # every config: ref, name, source; marks the active (or forced) one
swofty shaders effects # every effect available
swofty shaders effects ambient_light # one effect with its params, slots and defaults
swofty shaders show personal:night-race # a config's text; a config's name works in place of a ref
swofty shaders lint my-look.swfx # check a file or a ref; exits non-zero on an error
swofty shaders use addon:swofty.realistic-shader/realistic
swofty shaders use off
swofty shaders new "Night race" # from the starter template
swofty shaders copy addon:swofty.realistic-shader/realistic
swofty shaders rename personal:night-race "Night race 2"
swofty shaders delete personal:night-race
swofty shaders import my-look.swfx
swofty shaders export personal:night-race night-race.swfx
swofty shaders key F9 # the in-game on/off key: A-Z, 0-9, F1-F24 or "None"
swofty shaders key # print the keyA ref is personal:<slug> for a file in the launcher's shaders/ folder, or addon:<addon id>/<file name without .swfx>.
swofty shaders lint prints each diagnostic as file:line:column: severity: message, followed by a fix: line when there is a quick fix, and exits 1 when the config has an error. swofty shaders use also says when an addon forces a config over the one you chose.
What the game logs
The core logs each time the active config changes. serial counts the changes:
[shaders] config "Realistic" (serial 42): 5 effect(s), 11 pass(es)
[shaders] config "Realistic" (serial 43): 4 effect(s), 9 pass(es)A parameter-only change updates the values and logs the new serial. The per-pass GPU time lines name the effect and its instance, like ambient_light or glare.wide. A pass whose average GPU time stays over its budget_ms is turned off, as in post-process passes.
A plain postfx pass in an addon's manifest still runs, after the config's effects.