Jump to content

1 Screenshot

About This File

KOTOR 1 engine fixes — grass rendering, two crashes, and game speed

Hey all. I've been poking at swkotor.exe for a while and ended up finding a handful of engine bugs worth sharing. The patches are now merged into the KotOR Patch Manager, so they're available to anyone.

Quick note on me: I'm not a reverse engineering person by trade, I'm a student. Most of this came from trial and error — build a patched exe, play it, watch it break, go back and try again. All the function names I was working from come from Lane's KOTOR 1 reverse engineering project, and without those this would have been impossible.


The grass one

If you've ever turned Grass off in the graphics options because it looked like this — long streaks and beams flying across the sky, especially on Dantooine — that was a bug, not your hardware.

RenderGrassPolys draws grass through one of three code paths depending on what your graphics driver supports. Two of those three hand OpenGL the wrong pointer: instead of the buffer holding the grass blade positions, they pass the address of a field that's never filled in. So the game reads whatever happens to be sitting in memory there and treats it as the coordinates of the grass. Hence the streaks.

The third path — the one used when your driver reports GL_ATI_fragment_shader — does it correctly. That's why this was never widely reported: plenty of setups take the good path and never see a problem. If your machine takes one of the others, grass has been broken for you this whole time. In at least one case it was because KOTOR 1 was running on integrated graphics rather than the dedicated GPU.

Lane confirmed this independently by forcing his own game down the broken path and reproducing it exactly.

There were two more issues in the same function: a memory leak of OpenGL state that broke lighting on things drawn after grass, and a double free of the grass buffer that was corrupting the heap. Both fixed.

The crashes

Two 0xC0000005 crashes, both from the same underlying problem. The renderer keeps three lists sized for exactly 5000 textures and looks things up in them using the ID number the graphics driver assigns. Those numbers keep climbing as you change areas — the driver doesn't reuse them — so after enough loading the game indexes past the end of the list and writes into memory that isn't its own.

More likely on heavily modded installs. Both spots are now bounds-checked.

Worth being clear: this stops the crash, it doesn't raise the 5000 limit. Textures past that point are skipped instead of drawn. A proper fix is possible but needs more work.

Game speed

KOTOR ties simulation speed to framerate, so on a modern machine it runs fast. The engine already contains a frame limiter, but the value that switches it on defaults to zero, so it never runs. Setting it makes the game pace itself using its own code — no driver cap or 60Hz monitor mode needed.

This is also why the post-combat movement freeze gets rarer with a cap. The direct fix for that is J's Post-Combat Movement Fix, separately available in KPM.


How to get it

Through the KotOR Patch Manager (recommended) — three patches, now merged upstream:

  • Texture Bucket Memory Safety — the two crash fixes
  • Grass Buffer Double Free
  • Grass Rendering Fix

KPM injects at runtime, so your executable isn't modified and you can turn individual fixes on and off.

Or a standalone script if you'd rather patch the exe directly — it also includes the frame cap and the 4GB flag, writes a backup first, and has a --revert that restores the original bytes exactly. Link in my signature.

Either way: this needs the KOTOR Editable Executable from here on DeadlyStream if you're on Steam, since the shipped exe is encrypted. GOG's is usually fine already. The script checks and refuses anything it doesn't recognise.

Turn Grass back on once it's applied. That's the point.


Compatibility

No game content is touched, only the executable — so Community Patch, mod builds, texture packs and so on are all unaffected. Widescreen (UniWS) and the HR menu patch are fine too; apply those first and this last.

Tested on the standard PC v1.03 / editable-exe layout with a ~180 mod build (the Spoiler-Free build from the neocities guide) on AMD hardware. Should apply to GOG as well — that build is the same apart from a signature — but I haven't tested it there myself.


Credits

Lane's KOTOR 1 (GoG) reverse engineering work supplied every function name I used. Finding a bug in RenderGrassPolys is only possible once something tells you that's what the function is called. He also reviewed the patches and caught two real mistakes in my first attempt — one of which was silently breaking shadows.

Synchro is working on a much broader fix in the same area, porting the old ATI-specific shader paths to modern ARB equivalents, which fixes cubemaps, bump maps and more besides. Worth watching — it addresses the root of why so much of this game renders oddly on current hardware.

Thanks also to J, Vriff and JC in the OpenKotOR Discord for the testing and the sanity checks.

Happy to answer questions. If you've had grass turned off for years, give it a try.

kotor_patch.py

Edited by VexFlint
Update after a long time testing

  • Like 1

User Feedback

Recommended Comments

th3w1zard1

Posted

Here is the text converted to BBCode, styled cleanly for forum posts:

[h1]KOTOR 1 has a built-in frame limiter — BioWare shipped it switched off[/h1]

[b]TL;DR:[/b] [i]Star Wars: Knights of the Old Republic[/i] (2003, Odyssey engine) contains a complete busy-wait frame limiter in its main loop. It is controlled by a single [font=monospace]float[/font] in the executable that defaults to [font=monospace]0.0[/font], which leaves the limiter dormant, so the game free-runs at uncapped framerate. On high-refresh displays this uncapped rate is what causes the long-standing [b]movement / "stuck after combat turn" freeze[/b] and related timing bugs, which the community has only ever worked around with external FPS caps or by dropping the monitor to 60 Hz.

Writing [font=monospace]60.0[/font] (or any target) to that one float [b]arms the engine's own limiter[/b] — a native, self-contained fix. No injected code, no wrappers, no driver settings. It's a 4-byte data patch.

Verified: on a 144 Hz display with all external caps removed, the patched exe holds a hard 60 FPS and the freeze does not occur.

[hr]

[h2]The bug[/h2]

KOTOR's main loop ([font=monospace]WinMain[/font], [font=monospace]FUN_004041f0[/font] in a Ghidra auto-analysis of the standard PC exe) is a classic [font=monospace]PeekMessage[/font] real-time loop:

[code]

while (quit_flag == 0) {

read timer (game clock)

if no pending message:

render scene (delta-time based)

SwapBuffers (present)

... optional limiter here ...

}

[/code]

Gameplay logic — movement, and the combat round/turn state machine — advances by frame [b]delta-time[/b]. With no frame cap, delta-times on a fast display become tiny and irregular, and the 2003-era state machine mis-steps: the controlled character's action queue stalls until something rebuilds it (switching party member, or a save/load clears it). That is the freeze players have reported for 20 years.

[h2]The dormant limiter[/h2]

Immediately after [font=monospace]SwapBuffers[/font], the loop contains this (Ghidra decompilation, lightly annotated):

[code=c]

if (DAT_007a3c58 != 0) // a separate, also-off coarse limiter

Sleep(DAT_0078d1e8);

if (0.0f < cap) { // cap = _DAT_007a3c64 (the frame-cap float)

target = 1000.0f / cap; // target frame time

// measure elapsed since frame start, then:

if (elapsed < target) {

do {

// busy-spin: re-read the game timer

} while (elapsed < target); // hold the frame until target reached

}

}

[/code]

With real constant values read from the binary:

[table]

[tr]

[th]Symbol[/th]

[th]RVA[/th]

[th]Value[/th]

[th]Meaning[/th]

[/tr]

[tr]

[td][font=monospace]_DAT_007a3c64[/font][/td]

[td][font=monospace]0x7a3c64[/font][/td]

[td][b]0.0[/b][/td]

[td]frame cap (FPS). [b]This is it.[/b][/td]

[/tr]

[tr]

[td]numerator const[/td]

[td][font=monospace]0x73d6fc[/font][/td]

[td]1000.0[/td]

[td][font=monospace]target_ms = 1000.0 / cap[/font][/td]

[/tr]

[tr]

[td]zero const[/td]

[td][font=monospace]0x73d700[/font][/td]

[td]0.0[/td]

[td]used in the [font=monospace]0.0 < cap[/font] gate[/td]

[/tr]

[tr]

[td]timer scale const[/td]

[td][font=monospace]0x73d708[/font][/td]

[td]0.001[/td]

[td]raw timer -> milliseconds[/td]

[/tr]

[/table]

The gate is literally [b]"if cap > 0, limit the frame."[/b] With [font=monospace]cap = 60[/font], the loop busy-waits until each frame has taken [font=monospace]1000/60 ≈ 16.67 ms[/font], i.e. it caps at 60 FPS. This is the [i]same[/i] formula the engine uses elsewhere — the movie-playback path caps to 30 with it — so the mechanism is proven, not speculative.

[h2]Why it's off[/h2]

[font=monospace]_DAT_007a3c64[/font] starts at [font=monospace]0.0[/font]. The [b]only[/b] code that writes it is a render-setup function ([font=monospace]FUN_004c5880[/font]), and that write is guarded:

[code=c]

if (DAT_00832904 != 0) {

...

_DAT_007a3c64 = (float)DAT_00832904; // arm the cap

}

[/code]

[font=monospace]DAT_00832904[/font] lives in [b]BSS[/b] — zero-initialized, and it has [b]no writer anywhere in the binary[/b] (it's a config variable exposed by address, never set by default). So the guard is never true, the cap value is never assigned, and [font=monospace]_DAT_007a3c64[/font] stays [font=monospace]0.0[/font] for the entire session. The limiter is complete, correct, and permanently asleep.

Because nothing ever writes the cap during normal play, [b]patching its initial [font=monospace].data[/font] value is safe and permanent[/b] — no code path overwrites it.

[h2]The patch[/h2]

Standard PC v1.03 / GOG / "editable" executable:

[code]

file offset 0x3A3C64 : 00 00 00 00 (float 0.0, limiter off)

-> 00 00 70 42 (float 60.0, 60 FPS cap)

[/code]

Other caps if you prefer: [font=monospace]30.0[/font] = [font=monospace]00 00 F0 41[/font], [font=monospace]72.0[/font] = [font=monospace]00 00 90 42[/font], [font=monospace]120.0[/font] = [font=monospace]00 00 F0 42[/font].

A Python patcher ([font=monospace]kotor_fpscap_patch.py[/font]) is included; it fingerprints the two [font=monospace].rdata[/font] constants before writing, backs up the exe, and refuses to touch an executable whose layout it doesn't recognize:

[code]

python kotor_fpscap_patch.py "path\to\swkotor.exe" 60

[/code]

[h2]Verification[/h2]

[],144 Hz display, all external FPS caps and driver "wait for vertical refresh" overrides removed. [],Patched exe launched directly.

[*],Framerate held a hard [b]60[/b]; the movement/turn freeze did not reproduce on transitions that previously triggered it.

[h2]Caveats[/h2]

[],[b]Busy-wait, by design.[/b] The limiter spins a CPU core while pacing each frame (this is BioWare's own implementation — the movie path spins the same way). On a modern machine, one pegged core for a single-threaded 2003 game is harmless, but it is not a [font=monospace]Sleep[/font]-based idle cap. A future refinement could redirect the gate to the engine's own [font=monospace]Sleep(DAT_0078d1e8)[/font] limiter instead. [],[b]Offset is per-exe-layout.[/b] [font=monospace]0x3A3C64[/font] is verified for the standard PC exe. Other builds (Steam-with-different-packing, 4-CD, Mac) may differ; find [font=monospace]_DAT_007a3c64[/font] by locating the [font=monospace]1000.0 / cap[/font] main-loop math.

[],[b]Steam "Verify integrity" reverts it[/b] (restores the stock exe). Re-apply after any verify. [],Stacks cleanly with the 4GB LARGE_ADDRESS_AWARE patch, UniWS widescreen, and the community mod builds — it only touches one unrelated float.

[h2]Method[/h2]

Static reverse engineering only (Ghidra). Chain: locate timing-API imports ([font=monospace]QueryPerformanceCounter[/font], [font=monospace]GetTickCount[/font], [font=monospace]Sleep[/font]) → their callers → the [font=monospace]PeekMessage[/font] message pump → its caller ([font=monospace]WinMain[/font]) → the frame-limiter block in the loop → the cap float and its dormant guard. Full decompiled C of the engine was exported and searched locally to trace the call chain.

[hr]

[i]Reverse-engineered from a legally-owned copy for interoperability and bug-fixing. Not affiliated with BioWare/LucasArts/Aspyr.[/i]

 

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

By using this site, you agree to our Guidelines.