Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

K1 Modern Driver Compatibility

K1 Modern Driver Compatibility is a patch for Star Wars: Knights of the Old Republic that restores the game’s full lighting on current graphics drivers.

The Lower Shadowlands on Kashyyyk, distant fog and grass are rendered incorrectly and missing. The same location, with grass covering the ground, and the distant fog rendering correctly.
The Lower Shadowlands. Without the patch, no grass is drawn, and the distant fog doesn't blend properly.

KOTOR supports two ways of shading each pixel, one from ATI and one from NVIDIA. AMD’s and Intel’s Windows drivers offer neither, so on those drivers the game draws with much of its lighting off: lightmapped surfaces ignore the lights in the room, shiny surfaces lose most of their reflection, there is no bump lighting, no soft shadows, and no screen effects, and the grass stretches into the sky. The patch rebuilds the game’s lighting on GL_ARB_fragment_program, which AMD’s, Intel’s, and NVIDIA’s drivers and Mesa all carry, and puts every card on that path.

Where to start

Installing K1 Modern Driver Compatibility

To install version 2.0.0 of the patch, you need the KotOR Patch Manager, version 0.7.2 or later.

The patch doesn’t change swkotor.exe on disk. The Patch Manager applies the patch to the game as it starts, and you can turn the patch off in the Patch Manager.

Install the patch

  1. Download the zip file from the releases page, and unzip it.
  2. Move the .kpatch file out of the Copy into patches folder folder and into the folder that your Patch Manager reads patches from. The Patch Manager’s release includes a patches folder for this. Move the file itself, not the folder that contains it.
  3. Open the Patch Manager. Set Game to your swkotor.exe, and set Patches to the folder from step 2. The patch appears in the Patch Manager’s list of patches.
  4. Select the patch’s checkbox, and then select Apply.
  5. Start the game with the Patch Manager’s Launch button. On Linux, if you play through Proton, you can start the game the way you usually do instead.

You don’t need to copy anything into your game folder. The Patch Manager adds the files it needs there itself.

On Windows, the Patch Manager by default applies the patch only to a game it starts. If you start the game from Steam or a desktop shortcut instead of the Launch button, the game runs without the patch, and the game looks as if the patch does nothing. To have the patch load however you start the game, turn on the Patch Manager’s library proxy, as Load the patch however you start the game describes.

Load the patch however you start the game

On Windows, you can have the patch load whenever the game starts, from Steam, a desktop shortcut, or Launch, by using the Patch Manager’s library proxy:

  1. If any patches are installed, clear their checkboxes, and then select Apply. The Patch Manager lets you change this option only while no patches are installed.
  2. In the Options menu, select Use library proxy.
  3. Select the patches again, and then select Apply.

With the library proxy, the Patch Manager puts a stand-in binkw32.dll in your game folder and renames the game’s own copy to binkw32Hooked.dll. The game loads binkw32.dll every time it starts, and the stand-in loads the patch. On Linux, the Patch Manager always uses this method.

What appears in your game folder

The first time you run the game with the patch, the patch creates two files:

  • logs/K1DriverCompat.log records one session, and the patch replaces it each time the game starts. If something looks wrong, send this file.
  • K1DriverCompat.ini lists every setting the patch has, with an explanation of each, all commented out. You don’t need to change either file to play. For more information, see Settings.

If the Mod Options patch is installed, the patch also writes two menus for its settings into the Mod Options folder, K1DriverCompat.toml and K1DriverCompat Troubleshooting.toml. They appear in the game’s options screen.

The first run also turns on soft shadows and the screen effects in the game’s own graphics options. The Steam release’s settings file has soft shadows off, and the game’s options menu saved both off on cards that the unpatched game judged unable to run them. The patch does this only on the run that creates K1DriverCompat.ini. If you turn either option off afterwards, it stays off unless you delete that file.

Troubleshooting

The patch needs a newer Patch Manager

Cause: version 2.0.0 and later of the patch passes values from the game’s registers, and fixed values, to its hooks. The Patch Manager supports both from version 0.7.2, and earlier versions can’t install those hooks.

Solution: check the Patch Manager’s version, and update it to 0.7.2 or later.

Uninstall the patch

In the Patch Manager, clear the patch’s checkbox, and then select Apply. The Patch Manager removes the files it added.

Some things stay behind. logs/K1DriverCompat.log and K1DriverCompat.ini remain in your game folder, and so do the two menus in Mod Options if the patch wrote them. You can delete all of them. Soft shadows and the screen effects stay on in the game’s graphics options until you turn them off there.

Compatibility

The patch works with version 1.03 of the game as GOG and Steam ship it. It also works with the 1.03 executable that CD copies run with the common no-CD crack.

The patch includes the separate cube map fix and soft shadow fix. Don’t install either of them alongside this patch.

Installing without the Patch Manager

If the KotOR Patch Manager accepts your game, use the standard install instead. It leaves swkotor.exe unchanged on disk, and you can turn the patch off in the Patch Manager.

This install is for one case: another tool has already modified your swkotor.exe, so the Patch Manager doesn’t recognise it and refuses to patch it. Examples include an executable patched to use more memory, patched for a widescreen resolution, or unpacked from a store’s wrapper. This install checks only the places in the game that the patch changes, so other modifications don’t stop it.

The install adds two files to your game folder and renames nothing. If your game folder already has a dinput8.dll, copying replaces it. If that file is another mod’s copy of Ultimate ASI Loader, you can keep it and copy only the .asi file.

Install the patch

  1. Download the standalone zip file from the releases page, and unzip it.
  2. Copy both files from the Copy into game folder folder into your game folder, which is the folder that contains swkotor.exe. The files are dinput8.dll and k1-modern-driver-compatibility.asi.
  3. On Linux, set the environment variable that the next section describes. On Windows, skip this step.
  4. Start the game the way you usually do, for example from Steam, a desktop shortcut, or Proton.

You don’t need to open a manager or configure anything.

On Linux, through Wine or Proton

On Linux, this step is required. Wine includes its own dinput8.dll and uses it in place of the one in your game folder, so unless you tell Wine otherwise, the game starts without the patch.

Set this environment variable when you start the game:

WINEDLLOVERRIDES="dinput8=n,b"

For a Steam game, add it to the game’s launch options, followed by %command%:

WINEDLLOVERRIDES="dinput8=n,b" %command%

In Lutris or Heroic, use the DLL overrides screen instead: add dinput8 and set it to native, builtin. This setting tells Wine to load the file in the game folder first, and to use its own only if that file is missing.

Check that the patch is working

The first time you run the game with the patch, the patch creates logs/K1DriverCompat.log in your game folder. Look for these two lines, in this order. Each line in the log also starts with the time since the game started and a level, such as [INFO].

hooks installed=21/21 waited=0.000
capability ati_fragment_shader=accepted
  • The hooks installed line means that the patch changed the game. Both numbers must be the same.
  • The capability line means that the game reached the patch in time.

If the log has the first line but not the second, and you haven’t set decline = on, the patch loaded too late to take effect. Report it, and include the log.

If the log says hooks nothing matched, the patch couldn’t change any place in the game. The log gives the reason for each place. If a place holds bytes the patch didn’t expect, your executable isn’t one that this patch can change.

Troubleshooting

The game starts, but no log appears

Cause: the game didn’t load the patch. This usually has one of two causes:

  • On Linux, the WINEDLLOVERRIDES environment variable isn’t set.
  • On Windows, dinput8.dll isn’t in the same folder as swkotor.exe, for example because it’s still in the folder from the zip file.

Solution: on Linux, set the environment variable as the preceding section describes. On Windows, move dinput8.dll and k1-modern-driver-compatibility.asi into the folder that contains swkotor.exe.

The patch doesn’t take effect with the standard install also active

Cause: this install and the standard install change the same places in the game. Whichever runs second finds those places already changed, and doesn’t apply.

Solution: use only one of the two installs.

What appears in your game folder

The first time you run the game with the patch, the patch creates the same two files as the standard install:

  • logs/K1DriverCompat.log records one session, and the patch replaces it each time the game starts. If something looks wrong, send this file.
  • K1DriverCompat.ini lists every setting the patch has, with an explanation of each, all commented out. You don’t need to change either file to play. For more information, see Settings.

The first run also turns on soft shadows and the screen effects in the game’s own graphics options. The Steam release’s settings file has soft shadows off, and the game’s options menu saved both off on cards that the unpatched game judged unable to run them. The patch does this only on the run that creates K1DriverCompat.ini. If you turn either option off afterwards, it stays off unless you delete that file.

Uninstall the patch

Delete dinput8.dll and k1-modern-driver-compatibility.asi from your game folder.

Two things stay behind. logs/K1DriverCompat.log and K1DriverCompat.ini remain in your game folder, and you can delete them. Soft shadows and the screen effects stay on in the game’s graphics options until you turn them off there.

Compatibility

This install works with version 1.03 of the game as GOG and Steam ship it, and with the 1.03 executable that CD copies run with the common no-CD crack. It also works with copies that another tool has already modified, as long as that tool didn’t change one of the places this patch changes. The log names any such place.

The patch includes the separate cube map fix and soft shadow fix. Don’t install either of them alongside this patch.

dinput8.dll is Ultimate ASI Loader, which isn’t part of this patch. Its licence terms, and those of everything built into it, are in THIRD-PARTY-NOTICES.

What the patch fixes

This page describes each problem the patch fixes: what you see without the patch, what causes it, and what the patch changes. Why the game looked wrong explains the cause these problems share.

Each comparison shows the same view twice. Drag the divider, or use the arrow keys, to compare the two, and select Enlarge to see them at full size. Both sides were taken on Mesa, with the same widescreen patches installed. For the side without the patch, Mesa was set to hide ATI’s fragment shaders, so the game found neither of its shading methods and took the path it takes on a driver that offers neither.

Grass stays on the ground

The Lower Shadowlands on Kashyyyk, with bare ground and broken fog. The same spot, with grass covering the ground and fog rendering properly.
The Lower Shadowlands. Without the patch, grass and fog are rendered incorrectly or are missing.

Without the patch: blades of grass stretch from the ground into the sky, in long streaks across the view, or are missing entirely depending on what is in memory.

Cause: the game has three ways to draw grass. Two of them tell the graphics card to read the positions of the blades from the wrong place in memory. On a driver that offers neither of the game’s shading methods, the game uses one of those two.

With the patch: the game uses the third way for textured grass, which reads the right place, so the grass stands on the ground. For the exact cause, see Why the grass breaks.

The Matale Grounds on Dantooine, with incorrect grass rendering and some grass exploding in the sky. The same location, with the grass appearing correctly on the ground.
The Matale Grounds on Dantooine. Without the patch, grass stretches from the ground into the sky or is missing entirely.

Lights reach lightmapped walls and floors

The Upper City cantina on Taris. The same view of the cantina, with the ceiling lamp glowing and walls showing more details
The Upper City cantina on Taris. The difference is small here: with the patch, the ceiling lamps glow, and some wall textures are properly reflecting detail.

Without the patch: walls and floors show only their lightmap, which is the lighting stored in the level when the level was built. The lights in the room don’t affect them.

Cause: on the path the game uses when it finds neither shading method, the game turns lighting off while it draws a lightmapped surface.

With the patch: the lights in the room add to the lightmap.

Reflections are restored

A Sith soldier on Taris, lighting is flat on the armour. The same Sith soldier, environment lighting effects the armour.
A Sith soldier on Taris. With the patch, the armour is lit properly.

Without the patch: shiny surfaces, such as armour, droids, water, and the Ebon Hawk’s cockpit glass, lose most of their reflection. The Ebon Hawk comparisons in Glow and screen effects appear also show the cockpit glass.

Cause: the game draws its reflections in one pass with one of its two shading methods. On a driver that offers neither, it falls back to an older method that draws each shiny surface twice without per-pixel shading, once for the reflection and once for its texture, and blends the two.

With the patch: the game draws its reflections in one pass on every card.

HK-47 on Tatooine, missing his eyes and metallic details. The same view, with HK-47's plating details visible and eyes present.
HK-47 on Tatooine. With the patch, his metal plating shows the details it is supposed to have, and his eyes are glowing as intended.
The river below the grove on Dantooine, with flat fallback rendering on the water. The same river, with the water semi-transparent and shaded as intended.
The grove river on Dantooine. With the patch, the water is correctly semi-transparent and is shaded properly, and the grass on the near bank is drawn.

Cube maps on Intel

On Intel’s Windows driver, the game also loses its best reflections a second way. It draws them with cube maps, and it looks for cube maps under the name GL_EXT_texture_cube_map. Intel’s Windows driver lists cube maps only as GL_ARB_texture_cube_map, which is the same feature under a newer name. The game concludes that the card has no cube maps and uses sphere maps, an older and simpler kind of reflection.

The patch makes the game look for the newer name, which Intel’s driver lists, and which Mesa also lists. Mesa lists the older name too, so the comparisons on this page, which were taken on Mesa, don’t show this fix.

Technical details

RenderEnvironmentMapped dispatches modes 0x13 and 0x14 only when AurOnePassEnvMapAvailable returns true, which requires AurBumpMapAvailable. Otherwise it draws the surface twice through the fixed function stage: once with the environment map and generated texture coordinates, and once with the material’s own texture, blending the two passes. environmentMapDest chooses which pass comes first and how they blend.

Creatures get bump lighting

Ajuur the Hutt in the Upper City cantina on Taris. The same view, with bump lighting on Ajuur's skin.
Ajuur in the Upper City cantina. With the patch, bump lighting brings out the folds of his skin, most visibly along his tail.
Master Zhar in the Dantooine Enclave, his lekku are shaded black. The same scene, with proper shading on the lekku.
Master Zhar. The shading on his lekku is properly visible and no longer looks black.

Without the patch: bump mapped creatures, such as the Hutts, are drawn without per-pixel lighting.

Cause: bump lighting lights each point of a surface according to the direction that point faces, which a bump map stores. It needs per-pixel shading, so on a driver that offers neither of the game’s methods, the game draws no bump lighting at all. Even with a method, the game draws the Hutts and the Rancor with bump lighting only when it uses NVIDIA’s. On every other card, including ATI’s, it skips that branch.

With the patch: the game takes that branch on every card. While one of these creatures fades out, the patch sends it down the game’s plain branch instead, because the bump branches can’t fade.

Lighting for mods works

Without the patch: on every card, NVIDIA’s included, three of the game’s lighting routes don’t work: bump lighting on lightmapped surfaces, normal maps, and reflective skins with a flat reflection map. The shipped game barely uses them, but mods can.

Cause: bugs in the game. The first and third use vertex programs that no driver accepts in the form the game builds them, so those surfaces draw nothing. The switch for normal maps is compiled to always answer no, and the game loads TGA normal maps with two colour channels swapped.

With the patch: all three work. For details, see How the patch works.

Shadows are soft

Characters standing on sand on Tatooine, casting hard-edged shadows. The same view, with the shadows' edges softened.
Tatooine. With the patch, the shadows cast by characters are softened.

Without the patch: shadows have hard edges.

Cause: the game softens shadows by drawing them into an image off screen and blurring that image. It does this only when it uses one of its two shading methods. A separate check in the game also keeps soft shadows off unless the game’s own settings file allows them.

With the patch: soft shadows work on every card whose driver offers framebuffer objects, and the separate check no longer blocks them.

Glow and screen effects appear

The crystal cave on Dantooine, crystals are missing their textures and glow. The same cave, with the crystals rendering properly.
The crystal cave on Dantooine. With the patch, the crystals render properly and glow again.

Without the patch: bright lights have no glow, and the game shows none of its screen effects: motion blur, film grain, the distortion around Force powers.

Cause: the game draws these effects into images off screen, the same way it draws soft shadows, and it does this only when it uses one of its two shading methods.

With the patch: the effects draw on every card whose driver offers framebuffer objects.

The Ebon Hawk on Dantooine in daylight. The same view, with the sunlit hull glowing and the cockpit glass reflecting.
The Ebon Hawk in daylight. With the patch, the sunlit part of the hull glows like all metallic objects, and the cockpit glass reflects again.
The Ebon Hawk on Kashyyyk at night. The same view, with a faint glow on the moonlit hull and the cockpit glass reflecting.
The Ebon Hawk at night. With the patch, a faint glow appears where the moonlight hits the hull, and the cockpit glass reflects again.
The waterfall in the grove on Dantooine. The same waterfall, with its spray glowing.
The grove waterfall on Dantooine. With the patch, the spray glows.

Glow and soft shadows hold still

Without the patch: on every card that draws them, the glow around lights and the edges of soft shadows flicker as the camera moves. The flicker is worse at higher resolutions, and small lights can lose their glow entirely.

Cause: to draw the glow and the soft shadows, the game first shrinks the whole screen into a small square. It does this by reading a scattered sample of the screen’s pixels, and the larger the screen, the more pixels it skips. As the camera moves, a small light or the edge of a shadow moves in and out of the sampled pixels.

With the patch: the patch shrinks the screen with a filter that takes every pixel into account, so the glow and the shadows stay steady at every resolution.

The brightness slider works

Without the patch: on some drivers, moving the brightness slider has no effect.

Cause: the game applies brightness by asking the display to use a brightness curve. A driver is allowed to refuse that request, and NVIDIA’s Windows driver refuses it.

With the patch: the patch applies the game’s brightness curve to the picture itself, including in cutscenes.

Choose between the two looks

The game’s two shading methods, ATI’s fragment shaders and NVIDIA’s register combiners, don’t draw the game identically. The patch includes both versions, called looks, and you choose between them with the look setting:

look = nvidia    the default
look = ati       what an ATI Radeon of the time showed

How the looks differ

The looks differ in two ways. In the game’s own content both differences are small, and the comparison at the end of this page measures one of them.

Light that adds up past full brightness. Light can add up past full brightness where a lamp shines on a lightmapped surface, or where a reflection sits on top of a lit texture. ATI’s fragment shaders keep the excess and multiply it into the texture, so strong light washes the surface out towards white. NVIDIA’s register combiners limit every step to full brightness, so the same light looks softer. Where light stays under full brightness, which is most of the game’s own lighting, the two looks match.

Highlights on bumpy shiny surfaces. ATI’s fragment shaders square a value without first limiting it to zero or above, which puts a highlight on parts of a surface that face away from the light. The effect shows on bump lit creatures, such as the Hutts in the Taris cantinas, most on the parts that face away from the light. NVIDIA’s register combiners limit the value first, so the GeForce look has no highlight there. They also drop the highlight where the half vector, halfway between the directions to the light and to the viewer, points into the surface.

Neither difference is a bug in the patch. Each look follows its own method, and each carries corrections where the game’s own version was broken.

The Radeon look follows the game’s ATI shaders, except where an ATI shader computes something different from the game’s register combiner setup for the same surface. The combiner setup is what players saw, so the Radeon look follows it there:

  • Reflections are added on top of the lit texture, not blended with it.
  • Bump lighting keeps two results that the ATI shader computes and then throws away: the squaring of the highlight, and the light colour. The Radeon look corrects both of its bump lighting modes, 0x06 and 0x07, this way.

The GeForce look follows the game’s register combiner setups, with two repairs where the game never finished them:

  • The setup for bump lighting with a normal map is unfinished: it computes the lighting in grey, with no texture and no light colour.
  • The setup for bump lighting passes one of its inputs in a form that NVIDIA’s driver refuses, so the result depended on whatever an earlier draw left behind. The patch uses the input the game meant.

Rendering the game never ran

The patch also turns on rendering that the shipped game never ran on any card, for mods to use: normal mapped surfaces, bump lighting on lightmapped surfaces, and reflective skins with a flat reflection map. Lighting for mods works explains why they never ran. Nobody saw them drawn, so each look draws them by the same rules, and with the same corrections, as the rest of that look:

  • Normal mapped surfaces use mode 0x07, which each look draws the way it draws 0x06.
  • The game has no ATI shader for bump lighting on lightmapped surfaces, modes 0x08 and 0x09, so both looks use the game’s register combiner setups for them.
  • The repair for reflective skins with a flat reflection map is to a vertex program, which both looks share.

The default look

The default is the GeForce look, look = nvidia, which follows what NVIDIA’s register combiners compute, with the two repairs. To use the Radeon look, set look = ati. Settings explains where to put the setting.

Ajuur the Hutt three times: with look = nvidia, with look = ati, and the difference between the two. The difference covers his body and is brightest on his tail.
Ajuur in the Upper City cantina, with each look, and the difference between them. On Ajuur the looks differ most on his tail.

Settings

You don’t need to change any settings to play. The settings help you narrow down the cause when something looks wrong.

Change a setting in the settings file

The patch reads its settings from K1DriverCompat.ini, in the same folder as swkotor.exe. The patch creates this file the first time it runs, with every setting listed, explained, and commented out.

  1. Open K1DriverCompat.ini in a text editor.
  2. Find the setting you want, and delete the # at the start of its line.
  3. Change the value, and save the file.
  4. Start the game.

The file has one setting per line:

look=ati
log=debug

The patch ignores blank lines and lines that start with #. You can write setting names and the values on and off in any capitalisation, but the value of look must be lowercase. If a line has a mistake, the patch still reads the rest of the file.

To confirm that the patch read the file, look for settings source=K1DriverCompat.ini near the top of the log. The patch writes this line only when the file sets at least one setting.

Change a setting in the game

If you have the Mod Options patch installed, the game’s options screen has two menus for these settings. K1 Modern Driver Compatibility has the settings for everyday use, and K1 Modern Driver Compatibility: Troubleshooting has the settings for finding a fault. The menus change K1DriverCompat.ini for you. A change to how much the patch writes to its log takes effect straight away, and any other change takes effect the next time you start the game.

The K1 Modern Driver Compatibility menu in the game's options screen. On the left: nvidia, Screen effects, Cube map reflections, Bumpy shiny surfaces and info. On the right, the description of the selected option, Log detail.
The standard options menu. All settings outside of the log detail require a game restart.

The patch writes the two menus into the Mod Options folder each time the game starts, when Mod Options is installed.

A menu finds a setting in the file only below the [K1DriverCompat] line, and only when the setting has no spaces around its =. If you edit the file by hand, write settings as the examples in this page and in the file do.

A file written by a version before 2.0.0 has no [K1DriverCompat] line. The first time you open a menu, Mod Options adds that line at the end of the file, with the menu’s settings at their defaults below it. The patch reads the file from top to bottom, and a later line replaces an earlier one, so those defaults replace any settings you made higher up. To keep your settings, make them again in the menu.

Change a setting for one run

To try a setting once without editing the file, set an environment variable with the same name and the prefix K1DC_, for example K1DC_LOOK=ati. If a setting is in both places, the environment variable takes precedence.

Check that a setting took effect

If you mistype a value, the patch keeps the default and writes a warning to the log. The exception is disable_modes: the patch skips any entry that isn’t a mode number, without a warning.

To see which settings the patch used, find the settings line near the top of the log. It lists every setting that isn’t at its default, including the modes in disable_modes.

Settings for everyday use

SettingEffect
look=atiUses the Radeon look instead of the GeForce look. For more information, see Choose between the two looks.
log=debugWrites much more detail to the log. Set this if you’re reporting a problem.

Settings for finding a fault

Most of these settings turn off part of the patch. If the problem goes away with one of them, the part that setting turns off is the cause. warm and frames turn nothing off: they measure.

SettingEffect
decline=onAnswers the game’s startup question with no, so the game takes the path it takes on a driver that offers neither of its shading methods. This isn’t the same as the unpatched game. For more information, see What decline=on leaves in place.
framebuffer=offTurns off the screen effects and soft shadows, and keeps the lighting.
cubemap=offTells the game that cube maps are unavailable, so shiny surfaces use the simpler sphere map reflections.
bump=offTurns off bump mapping everywhere: the bump lighting on creatures and surfaces, and the reflections that a bump map bends. Bump mapped surfaces draw flat.
disable_modes=18,1DStops shading particular kinds of surface. The next table lists the modes.
warm=onMeasures how long your driver takes to prepare the shading programs. Use this to find the cause of a stutter.
frames=onReports the frame rate and how much work the patch did, every 600 frames. When this setting is off, the patch doesn’t count frames.

disable_modes takes a comma-separated list of mode numbers, as the log names them. The modes line, which the patch writes when the game’s renderer shuts down, counts the draws of each mode. With log=debug, the log also names each mode after mode= the first time it’s drawn.

ModeSurfaces
1ALightmapped walls and floors, the most common kind
02Flat coloured surfaces
03Surfaces that fade towards a colour with distance
13Reflective surfaces
14Reflective surfaces that also fade into fog
16Reflective surfaces that also have a lightmap, which use three textures
18Reflective surfaces whose normal map bends the reflection
1DThe setup of the six textures that mode 18 reads
06Bump lighting, on creatures and on bump mapped surfaces
07Bump lighting on surfaces whose bump map is a normal map
08Lightmapped surfaces lit by a bump map light
09Mode 08 with a highlight
00The full reset between draws
0D, 1C, 20, 25The cleanup after the bump, normalisation cube, reflective, and cube texture modes

When you turn a mode off, surfaces of that kind draw incorrectly. If the fault you’re chasing disappears, it belongs to that kind of surface.

What decline=on leaves in place

decline=on isn’t the unpatched game. It answers only the game’s startup question. The following parts of the patch stay active:

  • The edits to the game’s code that change how the game looks for cube maps and soft shadows, and which branches it takes for bump mapped meshes and creatures, and the edits that remove game calls the driver refuses.
  • The parts that answer the game’s normal map question, point the vertex programs of bump mapped surfaces and skins at the patch’s own, and correct the colour channels of TGA normal maps.

To compare with the unpatched game, uninstall the patch.

Why the game looked wrong

The problems on What the patch fixes share one cause: the game chooses how to draw when it starts, and on many current drivers it chooses its most limited path. This page explains that choice, which drivers lead to which path, and the grass bug in that path.

The game chooses a shading method at startup

KOTOR supports two ways of shading each pixel, one from each of the two big graphics vendors of its time: ATI’s fragment shaders and NVIDIA’s register combiners. Most of the game’s lighting, its reflections, the fog on its shiny surfaces, and its screen effects are written for both methods. Some lighting is written only for NVIDIA’s, as How the patch works describes.

When the game starts, it asks the driver whether it offers ATI’s fragment shaders. If it does, the game uses them. If it doesn’t, the game sets up NVIDIA’s path, and then uses each NVIDIA feature only where the driver offers the extensions that feature needs. Most of what the game draws follows from that choice.

Which drivers offer which method

The method the game uses depends on your driver:

DriverATI’s fragment shadersNVIDIA’s register combinersPath the unpatched game uses
NVIDIA, on Windows or LinuxNoYesNVIDIA’s register combiners
Mesa, on LinuxYesNoATI’s fragment shaders
AMD or Intel, on WindowsNoNoThe path without pixel shaders

If you play on Windows with an AMD or Intel card, the game has been using the path without pixel shaders. That path is where the problems the patch fixes come from.

Where this table comes from

  • The NVIDIA row is an RTX 2070 on driver 610.57. Its compatibility profile lists GL_NV_register_combiners and doesn’t list GL_ATI_fragment_shader.
  • The Mesa row is Mesa 26.2.4, on its radeonsi driver and on llvmpipe, its software rasteriser. Both list GL_ATI_fragment_shader and don’t list the register combiners.
  • The AMD and Intel row comes from the logs of testers on that hardware.

The path without pixel shaders

The game also supports cards older than either method, which can’t shade per pixel at all. When the game finds neither method, its check for per-pixel shading fails for every surface, and it draws with the path it has for those cards. That path is missing the following:

  • Light on lightmapped surfaces. A lightmap is the lighting stored in a level when the level was built. On this path the game draws the lightmap but turns lighting off while it draws the surface, so the lights in the room never reach it.
  • Bump lighting, which needs per-pixel shading. Some bump mapped surfaces come out flat, and some come out black, such as the lekku of Twi’lek characters.
  • One-pass reflections. On this path the game draws shiny surfaces in two passes without per-pixel shading. Armour and metal look flat and lose their detail, water loses its transparency, and parts drawn mostly by their reflection, such as HK-47’s eyes, can disappear.
  • Soft shadows and screen effects. The game draws both only when it uses one of its two methods, together with other extensions it checks for.
  • Fog that draws correctly. In some places, such as the Lower Shadowlands on Kashyyyk, the distance doesn’t fade into the fog.
  • Grass that draws correctly. The next section explains why.

Why the grass breaks

The game has three ways to draw grass, and two of them have a bug.

To draw anything, the game tells the graphics card where in memory to find the corners of each shape. The two buggy ways give the card the location of the grass’s own bookkeeping instead of the positions of the blades. The card reads whatever values are there and uses them as positions. Those values aren’t positions, so the blades land wherever the values place them, depending on what is in memory at the time. Usually the blades stretch from the ground far into the sky. Sometimes they land out of view, and the grass is missing entirely.

The third way gives the card the correct location. The game uses it only when its check for bump mapping passes, which needs one of its two shading methods and several other extensions. NVIDIA’s drivers and Mesa each offer one of the methods, so grass draws correctly on them. AMD’s and Intel’s Windows drivers offer neither, so on those the game uses a buggy way, and “turn off grass” became the usual advice.

The Matale Grounds on Dantooine, with grass stretched into two columns that reach the sky. The same location, with the grass appearing correctly on the ground.
The Matale Grounds on Dantooine. Without the patch, the game draws the grass with one of its buggy ways, and the blades stretch into two columns that reach the sky.

Technical details

CAurTriangleBin::RenderGrassPolys has three draw paths. Each path calls GLRender::DrawLightmappedGrass, which passes its first argument to glVertexPointer. Two of the paths pass the address of the bin’s own field at offset 0x40, so OpenGL reads the bin itself as vertex coordinates:

  • The first path, which the game takes when bit 0x08 of the byte at offset 0x02 is clear, or when renderTextureGrass is clear. In the second case it also leaves the grass untextured.
  • The multitexture fallback, which the game takes when aurATIFragmentShadersBumpMapAvailable() returns false and AurMultiTextureAvailable() returns true. When both return false, the game draws no grass.

The shaded path, at 0x004A6FFC, passes the blade buffer whose address is stored at offset 0x38.

aurATIFragmentShadersBumpMapAvailable depends on AurBumpMapAvailable. That function returns true when the driver offers GL_NV_register_combiners together with GL_ARB_vertex_program, GL_EXT_texture_cube_map, GL_ARB_multitexture, and GL_EXT_bgra, or when the game is on the ATI path and uses ARB vertex programs.

Why the patch uses one path for every driver

On NVIDIA’s drivers and on Mesa, the unpatched game already uses a shaded path. On Mesa that is the ATI path, which leaves out the lighting that only NVIDIA cards drew. Each of those methods also depends on a single vendor or project continuing to support it. Register combiners are NVIDIA’s own extension, and Mesa doesn’t implement them. ATI’s fragment shaders aren’t offered by AMD’s own Windows driver. Leaving each driver on its own method means maintaining three paths, two of which depend on a single vendor or project.

Instead, the patch puts every card on one path, built on GL_ARB_fragment_program, which AMD’s, Intel’s, and NVIDIA’s drivers and Mesa all carry. How the patch works explains that path.

How the patch works

This page explains what the patch changes in the game, and how its replacements are checked against the game’s own. Each section ends with technical details for readers who work on the game or on the patch.

The patch answers yes, then supplies the method

When the game starts, it asks the driver whether it offers ATI’s fragment shaders. The patch answers that question itself, and answers yes unless you set decline = on. The game then takes its ATI path on every driver.

The patch then supplies everything that path needs. The patch skips the step where the game builds its ATI fragment shaders. The game keeps calling the functions it always calls, and the patch handles those calls: when the game selects one of its ATI fragment shaders for a draw, the patch binds its own replacement instead. A small number of edits to the game’s code change where the game looks for features and which branches it takes, and remove game calls that the driver refuses. The rest of the game runs unchanged.

If the driver offers GL_NV_register_combiners2, the patch also hides it from the game, so that the game doesn’t run its NVIDIA path alongside the ATI one.

Technical details

The game tests for GL_ATI_fragment_shader once, in GLRender::InitExtensions, and its other choices follow from that answer, so that test is the one the patch replaces.

The patch clears GL_NV_register_combiners2 from the game’s capability word because the game’s NVIDIA gates for soft shadows and screen effects test for it. On a driver that offers it, the game runs its NVIDIA versions of those effects beside the ATI path, and clearing it keeps those gates closed.

To supply the ATI path, the patch takes over each place the game reaches for it:

  • EnablePixelShaders, the dispatch pointer the game calls to set up the shading for each draw, so that the patch chooses which of its programs to bind.
  • The ATI entries in the game’s extension table, through which the game binds its shaders and sets their constants. The patch turns those into binds of its programs and writes to program.env.
  • glEnable and glDisable, because the game turns shading on and off by enabling GL_FRAGMENT_SHADER_ATI, which means nothing to a driver without the extension.
  • glFogi, because each of the patch’s programs has its fog equation built in, so the patch watches which equation the game selects to bind the matching program.
  • glFinish, which the game calls before every texture upload on the ATI path, as a workaround for ATI hardware of 2003. On Intel’s Windows driver each call blocks for over four seconds, and the game rebuilds every texture when the renderer restarts. Nothing depends on the call, so the patch drops it.
  • The pixel buffer entries, wglMakeCurrent, and glDrawBuffer, which the game uses to switch between its off-screen images and the window. The patch’s off-screen images are framebuffer objects, so it translates those calls.
  • SwapBuffers, SetDeviceGammaRamp, and Bink’s BinkCopyToBuffer, for the brightness slider, as The patch applies the brightness slider to the picture explains.

The patch replaces the game’s shading programs

The game’s per-pixel lighting is a set of small programs: one for each kind of surface and one for each screen effect. The patch replaces each with a program in GL_ARB_fragment_program, which AMD’s, Intel’s, and NVIDIA’s drivers and Mesa all carry. Each surface program comes in four versions, one for each way the game draws fog, because on this path a program applies fog itself. Three surface programs blend the game’s fog in their own code instead, so their four versions are the same.

The patch adds two programs of its own: one applies the brightness slider, and one shrinks images for the glow and soft shadows.

Technical details

All fourteen world shaders and nine effect shaders are translated to GL_ARB_fragment_program.

An ATI fragment shader runs before the fixed function fog stage, so the game’s shaders got fog without doing anything. An ARB fragment program replaces that stage, and its own fog option doesn’t work for this game: the game measures fog distance as the absolute depth in front of the camera, and Mesa doesn’t take that absolute value for programs that use the option, so fog vanishes. So each world shader applies fog itself, and is built against each of the four fog equations: off, linear, exp, and exp2. Applying it in the program also gives the same result on every driver.

The tests compare each replacement with the game’s program

Each replacement is translated from the game’s own program, as the game builds it. The tests draw the game’s program and the replacement side by side, and require the two images to match: exactly with fog off, and within one step of rounding with fog on. Where the patch corrects the game’s program, as Choose between the two looks describes, the tests require the images to differ, and to match exactly once the correction is undone. The tests read the game’s programs from your copy of the game each time they run, so none of the game’s programs are stored in this repository.

The game’s programs come in two kinds, ATI’s and NVIDIA’s, and so do the patch’s, as the two looks. The tests compare the Radeon look with the game’s ATI programs on Mesa, which still runs them. They compare most of the GeForce look with the game’s register combiner setups on an NVIDIA driver, which still runs those.

Technical details

The game has no shader text: it builds each ATI shader at startup from a sequence of construction calls. arbfp-differential reads those calls out of the executable and replays them on Mesa, which still implements GL_ATI_fragment_shader, so each comparison is against the game’s own shader on a real driver. The tests pass on Mesa’s radeonsi driver. Twelve world shaders come from those calls. Modes 0x08 and 0x09 have no ATI shader, so the patch translates them from their register combiner setups.

With fog off, both programs run through the same pipeline, so the tests require identical pixels. With fog on, the game’s shader takes its fog from the driver’s fog stage, and the patch’s program computes it. The two round to a byte by different routes, so the tests allow one step. The tests that are meant to fail differ by tens to hundreds of steps. The three programs that blend their own fog are compared with fog off only, because the game draws them with the fixed function fog turned off.

The NVIDIA comparison reads each mode’s register combiner setup out of the executable and replays it on an NVIDIA driver. It can’t cover modes 0x03 and 0x14, because the game’s NVIDIA dispatcher has no case for them: those two blend their own fog, which NVIDIA cards did in the last combiner stage instead. It can’t cover 0x18 either, because NVIDIA’s version of that mode is built on texture shaders, which the comparison doesn’t replay. It holds modes 0x06 and 0x07 to their setups as repaired, because the game never finished them.

The patch enables lighting that only NVIDIA cards drew

Some of the game’s lighting ran only on NVIDIA cards, so even an ATI card of the time didn’t show it. The patch enables it on every card.

The game draws bump mapped creatures and objects with bump lighting only when it uses NVIDIA’s method. These include the Hutts, the Rancor, and some of the turret minigame’s models. On every other card, ATI’s included, it draws them with their texture and no bump lighting. The patch sends them down the bump lighting branch on every card.

The bump branches can’t fade, so while a bump mapped creature or head fades out, the patch sends it down the game’s plain branch, which fades with the rest of the body.

Technical details

Each NVIDIA-only branch is chosen by a call that asks which path the game is on. The patch answers those calls, and leaves every other use of the answer as the game wrote it.

In PartTriMesh::GetRenderPath, the answer to getATIFragmentShadersDisabled chooses between path 5, RenderBumpMapped, which dispatches mode 0x06, and path 6, which draws the mesh with its texture and no bump lighting. An edit makes the function return path 5.

In RenderSkinnedVP, the patch answers AurBumpyShinyAvailable and three calls to getATIFragmentShadersDisabled from whether the skin is fading. The bump branches draw with blend modes such as (GL_ONE, GL_ZERO), which ignore the skin’s alpha, so a fading skin stays solid on those branches. The fade arrives through matLastAlpha, which PartTriMesh::Mat sets just before each part is drawn.

k1_sys::arm lists every call that asks which path the game is on, and how the patch answers each one.

The patch repairs lighting that no card drew

Some of the game’s lighting never worked on any card, NVIDIA’s included, because of bugs in the game. The shipped game uses almost none of it, but mods can:

  • Bump lighting on lightmapped surfaces. Two lighting modes exist only as NVIDIA register combiner setups, with no ATI version. Their vertex programs are written in a format that only NVIDIA’s drivers read, and the game adds code to them that no driver accepts, NVIDIA’s included, so those surfaces draw nothing. The patch translates both modes, and builds their vertex programs from the developers’ own conversion of a related program. In the shipped game, only a test model that no level loads uses these modes.
  • Normal mapped surfaces. The game has a route for normal maps on meshes without tangents, but the function that enables it always returns false. The patch answers that function instead, and repairs the vertex program that the skinned version of that route uses. It also corrects TGA normal maps, whose colour channels the game’s loader swaps. No mesh in the shipped game has a normal map.
  • Reflective skins with a flat reflection map. Their vertex program exists only in NVIDIA’s format, so those skins vanished. The patch builds the program from the developers’ conversion of the cube map version. In the shipped game, only a test model uses this route.

Technical details

The lightmapped bump renderers build their vertex programs from !!VP1.0 text, NVIDIA’s format, append the ARB light code that the game adds to every lit program, and load the result through glProgramStringARB. NVIDIA’s driver accepts the !!VP1.0 text alone, but rejects it with the light code appended, and Mesa rejects the text outright. So no card drew these surfaces.

The developers converted a related program to ARB themselves, at 0x0078E8C8. Its registers and its hookup for the appended light code are what the rest of the game expects, so the patch builds each replacement from it and changes only what differs. This repository holds none of the game’s text, so the patch makes those changes at runtime, to the game’s own copy in memory. It swaps each replacement in where the renderer stores the program it has just built, before the renderer first enables it.

The same mechanism repairs three of RenderSkinnedVP’s vertex programs:

  • The sphere map program exists only as !!VP1.0 text. It also reads its sphere coordinates from the wrong register and never normalises the reflection. The patch builds it from the developers’ conversion of the cube map version at 0x007938C8, with the cube lookup replaced by the fixed function stage’s sphere map formula.
  • The normal map program subtracts each bone’s translation from the light and half vectors, which are directions that a translation doesn’t move. It weights one term by the wrong bone, and writes the normal map’s coordinate where mode 0x07 doesn’t read it. The patch corrects each of these.
  • The bump light program doesn’t write the primary colour or the fog coordinate. Mesa reads an unwritten primary colour as white, which mode 0x06 adds to the light, so the skin’s texture was drawn whole and then added again.

k1_sys::vertex::builds lists every vertex program the game builds and enables, and a test compiles each one that a draw can use, so a program that no driver accepts can’t go unnoticed.

The patch answers Material::HasNormalMap at all three of its calls, because they have to agree. The call in MdlNodeTriMesh::InternalPostProcess decides at load whether to skip building tangents for a mesh. The calls in PartTriMesh::GetRenderPath and RenderSkinnedVP send a mesh without tangents to path 15, RenderNormalMapped, and to the skinned normal map branch, both of which draw with mode 0x07. The answer is the game’s own test for a normal map: an isbumpmap 2 texture at 32 bits per texel.

The game’s TGA loader swaps the first and third byte of every texel to make RGB, and the normal map upload reads the data as BGRA, so the X and Z of every normal traded places. The patch swaps them back in the copy that the upload reads, which the mipmaps are built from as well.

Modes the patch leaves unshaded

NVIDIA’s dispatcher shades more modes than ATI’s. The patch shades the ones that a route can reach once a shading method exists, and leaves the rest doing nothing, as ATI’s did. A mode that does nothing has to stay that way, because binding something there takes the shading away from the draw that follows it. arbfp_dispatch::mode::inert_on_purpose gives the reason for each:

  • 0x01, 0x0F, and 0x12 are RenderBumpMapped’s passes for hardware without pixel shaders. That function runs only at a level of hardware that always has them.
  • 0x15 is RenderNV1xEMLM, path 9, which the game chooses only below that level.
  • 0x0A and 0x0B are specular exponents that the game never sets.
  • 0x22 is skinned motion blur, which nothing in the game turns on.

Mode 0x17, skinning on the CPU, is the fallback for a skin drawn without a scene, which hasn’t been seen to happen. It has no stated reason, so the patch logs a warning if it ever arrives. The game never sends 0x04, 0x05, 0x0E, 0x10, 0x11, 0x19, 0x1B, or 0x1F, although NVIDIA’s dispatcher has a case for each. k1_sys::dispatches lists every call that sends a mode.

The game’s ATI dispatcher does nothing for mode 0x1C, the teardown after 0x1D. ATI’s 0x1D sets up cube maps on units 0 to 5, and the bump teardown, 0x0D, clears only units 1 to 3, so cube maps stayed enabled on units 4 and 5 into later draws. NVIDIA’s fixed function stage stops at four units, so only drivers with more, such as Mesa, ever had the problem. The patch’s 0x1C clears units 4 and 5.

The screen effects draw into framebuffer objects

The game drew its glow, soft shadows, and screen effects into pixel buffers, an older kind of off-screen image. Modern drivers handle pixel buffers inconsistently, which is why those effects behaved differently from one machine to the next. The patch draws them into framebuffer objects, the modern equivalent.

The glow and the soft shadows each start by shrinking the screen into a small square. The game did this by reading a scattered sample of the screen’s pixels, which made the effects flicker as the camera moved. The patch shrinks the screen with a filter that takes every pixel into account, and stores the small images at a higher precision, so rounding doesn’t bring the flicker back.

Technical details

The off-screen targets use GL_RGBA16 where the driver can render to it, and eight bits per channel, which is what the pixel buffers had, where it can’t.

RenderOverlayConvolution and FinishSoftShadowsATI draw the screen into a 512 by 512 square with one bilinear read per texel, which past a ratio of two to one skips pixels. MipmapFilter then halves the glow’s square to 64 by 64 with one read at exactly two to one, which averages fixed pairs and steps the same way. Just before the game draws each square, the patch draws it itself through a chain of DOWNSAMPLE steps that weight each source pixel by a triangle one output pixel wide on either side, so every screen pixel counts. It then turns colour writes off, so that the game’s own quad leaves the result alone. The halving steps bind the downsample in place of the decal shader.

The glow’s bright pass is a threshold, so it runs on each screen pixel before any averaging. That way a small light stays above the threshold at high resolutions, and keeps its glow.

The patch applies the brightness slider to the picture

The game applied brightness by asking the display to use a brightness curve, which a driver can ignore. The patch keeps the curve the game asked for and applies it to the finished picture, and to movies as they are decoded.

Technical details

CAurInternal::ResetGammaRamp turns the slider position into a pow(i / 255, gamma) table and passes it to SetDeviceGammaRamp. The patch takes over that import: it keeps the table, and passes a flat table to the display so that the display applies nothing. The patch then applies the table to each frame as a dependent texture read in a pass at SwapBuffers. It applies the table to movies in BinkCopyToBuffer, because PlayMovieLoop copies each movie frame straight to the window, and UpdateScreen skips the swap while a movie plays.

The patch runs the same path on every card

The patch doesn’t check which card you have. It replaces the game’s shading even where the game’s own path works, on NVIDIA and on Mesa, so every player runs the same code, and that code is the code everyone tests.

Replacement shaders

This page is for modders who want to change how the game looks by writing their own shading programs. You don’t need it to play.

You can put your own shader files in the game folder, and the patch uses them in place of the programs it generates. You don’t need to rebuild anything or use any tools. Each file is plain text in ARB fragment program assembly, the same language the patch’s own programs use.

Where replacement files go

Put replacement files in override/shaders/ in the game folder. If the folder doesn’t exist, create it.

The folder is inside override so that a mod can install replacement shaders with TSLPatcher, like any other override file. The files are in a subfolder because they aren’t game resources. The game never reads them: it searches override for the file types it knows, it doesn’t search subfolders, and .arb isn’t one of its file types. Only the patch reads them.

How to name a replacement file

Each file replaces one program and is named after it:

  • World shaders, which draw surfaces, take a fog variant: <name>_<fog>.arb, where <fog> is off, linear, exp, or exp2.
  • Effect shaders, which draw screen effects, are named <name>.arb. Fog never reaches screen effects, so they have no fog variants.
  • brightness.arb applies the brightness slider to the finished frame. It reads the frame on texture unit 0 and the slider’s curve on unit 1, with one texel for each byte value.
  • downsample.arb shrinks the images for the glow and soft shadows. It reads the larger image on texture unit 0, and the values that Downsample::locals describes from program.local[0] to program.local[3].

The patch ignores capitalisation in file names, so a file named on Linux loads on Windows, and the reverse.

The program names are:

flat                 diffuseBlend          lightmap          envMap2D
envMapCube           envThreeTex2D         envThreeTexCube   envMapBlend2D
envMapBlendCube      normalTransform       bumpShinyA        bumpShinyB
lightmapBumpDiffuse  lightmapBumpSpecular1

overbrighten         forceDistortion       filmNoise         accumulationBlur
saturation           perPixelFlare         decal             decal4
softShadow

brightness           downsample

These are the names the log uses. To find the program a surface uses, set log = debug. The log then records each world program and fog variant the first time the patch binds it, and each effect program the first time the game uses it. For example, a line that contains probe mode=1A shader=lightmap fog=Linear means that the file to write is lightmap_linear.arb, and a line that contains probe effect=filmNoise means filmNoise.arb.

What a replacement file contains

Each file is a complete program, from !!ARBfp1.0 to END. The patch adds nothing to it. In particular, each world shader variant applies fog itself, which is why there are four variants. If you replace a fogged variant, your program must apply the fog, or that surface stops fogging.

Three world shaders are an exception: diffuseBlend, envMapBlend2D, and envMapBlendCube already fade towards the fog colour in their own code, as the game’s programs do. All four variants of each are the same program, without the fog step in the following example. The game turns the fixed function fog off while it draws these three, so only the _off variant of each ever runs. If you replace one of those three, write <name>_off.arb and leave fog alone, or the surface fades to the fog colour twice.

This is the generated lightmap_linear.arb for the Radeon look. The last five instructions apply the fog:

!!ARBfp1.0
PARAM fogParam = state.fog.params;
PARAM fogColour = state.fog.color;
TEMP fogCoord, fogFactor;
TEMP oCol;
TEMP base, lmap, sum;
TEX base, fragment.texcoord[0], texture[0], 2D;
TEX lmap, fragment.texcoord[1], texture[1], 2D;
ADD sum, fragment.color.primary, lmap;
MUL oCol.rgb, base, sum;
MUL oCol.a, fragment.color.primary, base;
ABS fogCoord.x, fragment.fogcoord.x;
SUB fogFactor.x, fogParam.z, fogCoord.x;
MUL_SAT fogFactor.x, fogFactor.x, fogParam.w;
LRP result.color.rgb, fogFactor.x, oCol, fogColour;
MOV result.color.a, oCol;
END

In the GeForce look, the default, ADD is ADD_SAT. That is the only difference between the two looks in this program.

The text of every world and effect program for the Radeon look is in the repository, in crates/arbfp-shaders/tests/golden/. A replacement is easiest to start from the program it replaces.

Programs that differ between the two looks

Nine programs differ between the two looks:

  • lightmap, envThreeTex2D, envThreeTexCube, envMap2D, envMapCube, envMapBlend2D, and envMapBlendCube limit a step that can pass full brightness. This is the brightness difference that Choose between the two looks describes.
  • bumpShinyA and bumpShinyB limit two dot products to zero or above, as the register combiners do, before the highlight is squared. Without that limit, a negative value becomes positive when squared, and lights the side of a surface that faces away from the light. The Hutts in the Taris cantinas show this difference, most on the parts that face away from the light. The GeForce versions also drop the highlight where the half vector points into the surface.

Every other program is the same in both looks. This includes lightmapBumpDiffuse and lightmapBumpSpecular1: the game has no ATI version of those modes, so both looks use the translation of the register combiner setup.

A replacement file replaces its program in both looks, because file names don’t include the look. To keep one look’s behaviour for one of the nine programs, write the replacement from that look’s version.

What your program receives

A replacement changes only the program text. The patch binds the same textures and sets the same constants as for its own program.

Your program receives two kinds of input:

  • Textures, on numbered texture units. Read them with TEX dst, fragment.texcoord[n], texture[n], 2D, or CUBE or 1D for those targets.
  • Values the game computes for each draw, as program.env[n]. Your program can read these but not write them.

World programs:

ProgramTexture unitsprogram.env[n]
flatNoneNone
diffuseBlend0 diffuse[1] colour to blend towards
lightmap0 diffuse, 1 lightmapNone
envMap2D0 diffuse, 1 environment[0].a output alpha
envMapCube0 diffuse, 1 environment (cube)[0].a output alpha
envMapBlend2D0 diffuse, 1 environment[0].a output alpha, [1] blend colour
envMapBlendCube0 diffuse, 1 environment (cube)[0].a output alpha, [1] blend colour
envThreeTex2D0 diffuse, 1 lightmap, 2 environment[0].a output alpha
envThreeTexCube0 diffuse, 1 lightmap, 2 environment (cube)[0].a output alpha
normalTransform0 normal map, 1 to 3 basis (cube), 4 refinement (cube), 5 environment (cube)[0].a output alpha
bumpShinyA0 normal map, 1 to 2 basis (cube), 3 modulating[0], [1] light colours
bumpShinyB0 modulating, 1 normal map, 2 to 3 basis (cube)[0], [1] light colours
lightmapBumpDiffuse0 normal map, 1 light vector (cube), 2 lightmap, 3 diffuse[1] light colour
lightmapBumpSpecular10 normal map, 1 half vector (cube), 2 lightmap, 3 diffuse[0] light colour

Effect programs, each of which shades a rectangle that covers the screen:

ProgramTexture unitsprogram.env[n]Output alpha
overbrighten0 frame, 1 to 3 tone curve tables (1D)[0] to [2] colour matrix rowsPrimary colour alpha
perPixelFlare0 frame, 1 to 3 tone curve tables (1D)[0] to [2] colour matrix rows1
forceDistortion0 normal map, 2 frameNoneNormal map alpha
filmNoise0 frame, 1 noiseNone1
accumulationBlur0 frame, 1 accumulation buffer[0], [1] weights[0].a
saturation0 frame[0] to [2] luminance rows, [3] to [5] weights for each channelFrame alpha
decal0 frameNone1
decal40 to 3, four samples of the frame[0] to [3] sample weightsPrimary colour alpha
softShadow0 to 3, four samples of the frame[0] to [3] sample weightsThe weighted sum’s alpha

Inputs to check

  • Name the texture target that is bound. Each TEX instruction names the target it samples, and the wrong target isn’t a compile error. The tone curve tables on units 1 to 3 of overbrighten and perPixelFlare are 1D, not 2D. If you name 2D, the program samples an incomplete target, and several drivers return white. The same applies to every CUBE in the preceding tables, which is why the environment mapped programs come in 2D and Cube pairs. The brightness curve on unit 1 of brightness is 1D as well.
  • In the reflective programs, the alpha of the diffuse texture is the reflectivity mask. An alpha of 1 keeps the lit texel, and 0 shows the reflection.
  • forceDistortion reads the frame on unit 2, not unit 0. Unit 0 is the normal map, and fragment.texcoord[1] and fragment.texcoord[2] are the basis vectors it takes dot products with.
  • Write the alpha the original writes. The game blends many draws by the alpha a program writes, so a replacement that writes a different alpha changes how its surface or effect blends, and can stop a surface fading. The effect table gives the alpha each effect writes, and the world table marks the programs that take theirs from program.env[0].a. For the rest, read the alpha from the patch’s own version of the program.
  • The interpolated colours are inputs too. fragment.color.primary is the lit vertex colour, and fragment.color.secondary.r is the amount the blend modes fade by. In the two lightmapped bump programs, the secondary colour is the light’s attenuation.

Check that your replacement loaded

When the patch loads a replacement, it writes a line like this to the log:

[    0.412] [INFO] program override envMapCube_linear.arb

If a file’s name doesn’t match any program, the patch writes a warning instead. A misnamed replacement looks the same as no replacement in the game, so this warning is how you find one:

[    0.412] [WARN] program override envMapCube.arb matches no program and was ignored

If the driver refuses to compile a replacement, the log reports the position where compiling stopped and the driver’s message. The patch doesn’t fall back to its own program. What happens instead depends on the program:

  • A refused world program leaves the surfaces that use it drawn without shading.
  • A refused effect program leaves that effect without a program.
  • A refused brightness program leaves the frame without the brightness pass.
  • A refused downsample program leaves the glow and soft shadows to the game’s own shrink.

Known limitations

  • overbrighten is never used. The game uses it only when a flag at 0x007A68BC is set, and nothing in the game sets that flag. A replacement for overbrighten has no effect.
  • bumpShinyB draws only normal mapped surfaces, whose bump map is an isbumpmap 2 texture at 32 bits per texel, on meshes without tangents. The game reaches it through Material::HasNormalMap, which every build of the game compiles to return false. The patch answers that function, so the program runs for such a mesh. No mesh in the shipped game has a normal map, so a replacement for bumpShinyB shows only on mod content.
  • lightmapBumpDiffuse and lightmapBumpSpecular1 draw only lightmapped bump mapped surfaces. In the shipped game, only a test model that no level loads has one, so a replacement for either shows only on mod content.
  • Only the off and linear fog variants have been seen in play. The patch still builds and tests the exp and exp2 variants.

Programs from other games

These programs aren’t the game’s own shaders. They’re translations of the game’s ATI_fragment_shader constructions and register combiner setups, so they expect this game’s texture units and constants, as the preceding tables list them. A program written for another game, including KOTOR 2, needs remapping to those inputs before it renders correctly here.

Building

This page explains how to build the patch and run its tests. To play, download the zip file from the releases page, which contains the .kpatch, and follow Installing.

Build the patch

The patch is a 32-bit Windows DLL. You can build it on any machine, natively or by cross compiling, if you have the Rust target and a linker for it. If your machine doesn’t already have a linker for that target, install MinGW-w64.

  1. Add the Rust target:

    rustup target add i686-pc-windows-gnu
    
  2. Build the DLL and write the .kpatch:

    cargo xtask package
    

To build the zip files that a release publishes, run cargo xtask release for the standard install and cargo xtask release --standalone for the standalone install. The minimum Rust version is in rust-version in the workspace’s Cargo.toml.

Run the tests

The tests check the patch’s shading programs against the game’s own programs, which they read from a copy of the game’s executable each time they run. This repository stores nothing from the game, so the tests that need the game skip unless you point them at your copy:

K1DC_SWKOTOR_EXE=/path/to/swkotor.exe cargo test

Which tests run on which driver

The tests that compare with the game’s ATI programs need a driver that still runs GL_ATI_fragment_shader, which means Mesa or an older AMD/ATI driver. They render the game’s program and the patch’s in the same process and compare the images pixel for pixel. 7 These tests pass on Mesa’s radeonsi driver.

You can also run them without a GPU on llvmpipe, Mesa’s software rasteriser, by setting LIBGL_ALWAYS_SOFTWARE=1. On llvmpipe, one comparison fails: accumulationBlur rounds one step differently from the game’s program on about 6% of the pixels.

The tests that compare with the game’s register combiner setups and NVIDIA vertex programs need NVIDIA’s driver, the only driver that still runs them.

Make skipped tests fail

A test that can’t run on your machine skips, and a skipped test counts as a pass. No single machine runs every test, because the ATI comparisons need Mesa and the NVIDIA comparisons need NVIDIA’s driver.

To make sure the tests your machine can run do run, list what your machine has in K1DC_REQUIRE, separated by commas. A test that skips for lack of a listed item then fails instead. For example, on a Mesa machine:

K1DC_REQUIRE=game,context,framebuffers,texture-units,GL_ATI_fragment_shader,GL_ARB_vertex_program \
K1DC_SWKOTOR_EXE=/path/to/swkotor.exe cargo test

On NVIDIA’s driver, remove texture-units and GL_ATI_fragment_shader, and add GL_NV_register_combiners and GL_NV_vertex_program.

Compare images

Most comparisons write an image to target/differential/, whether they pass or fail. Each image shows the game’s program, the patch’s program, and the difference between them, amplified so that a difference of one step is visible.

Test both looks

The tests on Mesa check the Radeon look, look = ati. The tests on NVIDIA’s driver check the GeForce look, look = nvidia, which is the default. If you change one of the programs that differ between the looks, run the tests on both drivers.

Build this documentation

The chapters are Markdown files in docs/, which you can read directly. mdBook builds them into a website in target/book:

mdbook build docs

To rebuild the website each time you save a change, and view it at http://localhost:3000:

mdbook serve docs