I Hacked Diablo II To Use Modern Graphics
Summary
TLDRIn this video, the creator tackles the challenge of enhancing the graphics of the 25-year-old game, Diablo I, for modern hardware. They explore the game's rendering backends, focusing on Glide, and develop a Glide wrapper to translate its API calls to the more contemporary Vulkan API. The process involves debugging, understanding Glide's texture management, and overcoming API differences. The video offers a deep dive into graphics API translation, showcasing the complexities of bridging old and new technologies.
Takeaways
- 🎮 The video discusses hacking the 25-year-old game Diablo I to render with modern graphics technology.
- 📽️ The original Diablo I is still popular, and many prefer it over the remastered edition, despite its outdated graphics on modern screens.
- 💻 The game supports three rendering backends: Direct Draw, Direct 3D, and Glide, with Glide being the most optimized but unsupported on modern hardware.
- 🛠️ A Glide wrapper has been created to trick the game into using OpenGL, serving as a bridge between the old and new APIs.
- 🔧 The process involves creating a custom library that mimics the Glide API, with functions that log calls to a file for analysis.
- 👨💻 The video creator attempts to use the Vulkan API, a modern graphics system, to replace Glide, despite its complexity.
- 📚 The video highlights the importance of reading documentation, as misunderstanding function return values and data formats caused initial failures.
- 🖼️ The process of extracting and rendering textures from the game is detailed, including handling palettes and converting pixel data.
- 🔄 The video creator encounters synchronization issues between the game's texture loading and the Vulkan API's rendering process.
- 🕹️ The final result is a proof of concept that shows the game's sprites rendered with textures in a modern graphics context, though not without challenges.
Q & A
What is the main challenge the author faces when trying to render Diablo I with modern graphics technology?
-The main challenge is that Diablo I, a 25-year-old game, was not designed with modern graphics hardware in mind. The game's rendering backends, Direct Draw, Direct 3D, and Glide, are outdated, and Glide, which the game was optimized for, is not supported by modern graphics cards without additional workarounds.
What does the author mean by 'hacks' in the context of enhancing Diablo I's graphics?
-The 'hacks' refer to modifications and workarounds that the author implements to enable the game to render with modern graphics technology despite its age and the limitations of its original design.
Why does the author choose to work with the Glide API instead of Direct Draw or Direct 3D?
-The author chooses to work with the Glide API because the game was optimized for it. However, Glide is not supported by modern graphics cards, so the author aims to create a wrapper that makes the game think it's using Glide while actually utilizing a more modern API like OpenGL or Vulkan.
What is the purpose of creating a Glide wrapper?
-The purpose of creating a Glide wrapper is to provide a translation layer between the old Glide API and a modern graphics API like Vulkan. This allows the game to continue using its Glide calls while actually rendering with modern graphics capabilities.
What is Vulkan, and why does the author choose to use it for this project?
-Vulkan is a modern graphics API designed to give graphics programmers full control over their hardware. It is chosen for this project because it offers a more direct and efficient way to interact with the graphics card, which is necessary for translating the older Glide API calls.
How does the author force Diablo I to use the Glide backend?
-The author forces Diablo I to use the Glide backend by passing a specific flag ('D3dfx') to the game, which directs the game to use the Glide API for rendering.
What issues arise when the author tries to load a 64-bit library with a 32-bit game?
-When the author tries to load a 64-bit library with a 32-bit game, the game crashes on the first function call due to a bad read pointer, indicating a compatibility issue between the game's architecture and the library.
Why does the game crash when the author's custom Glide library is loaded?
-The game crashes because the author's library is 64-bit and the game is 32-bit, leading to compatibility issues. Additionally, the game expects the functions to clean up the stack, which is not happening due to the calling convention used in the author's library.
How does the author address the issue of function names being decorated by the compiler?
-The author addresses the issue of function names being decorated by providing a .def file, which instructs the compiler to not change the function names, allowing the game to correctly call the functions from the custom library.
What is the significance of the function 'grSTWinOpen' in the context of this project?
-The function 'grSTWinOpen' is significant because it is part of the Glide API that initializes the Glide rendering context. The author's incorrect return value from a function with this name initially caused the game to crash, highlighting the importance of accurate API emulation.
Outlines
🎮 Enhancing Diablo I with Modern Graphics APIs
The script begins with an exploration of enhancing the graphics of Diablo I, a 25-year-old game, for modern hardware. Despite Blizzard's remastered edition, the original game remains popular. The author addresses the poor visual quality on modern displays due to the lack of graphical settings and the game's reliance on outdated rendering backends like Glide, which is unsupported on modern graphics cards. To resolve this, a Glide wrapper is mentioned, which tricks the game into using OpenGL calls, serving as a bridge between the old and new APIs. The author embarks on creating a similar wrapper for the Vulkan API, which offers full control over graphics hardware but is significantly more complex than Glide. The process involves creating a custom library that mimics Glide's interface, handling calling conventions, and debugging issues related to function arguments and memory management.
🔧 Debugging and Integrating Vulkan with Diablo I
In the second paragraph, the focus shifts to debugging and integrating Vulkan with Diablo I. The author details the process of intercepting Glide commands and redirecting them to Vulkan. This involves understanding and replicating Glide's API functions, managing graphics memory, and handling window size and resolution issues. The author also discusses the challenges of translating Glide's state machine approach to Vulkan's more hands-on hardware control. The narrative includes overcoming validation errors, dealing with window resizing, and addressing issues related to texture mapping and palette formats. The author captures the palette data and decodes it to store images, eventually rendering them using Vulkan, despite initial color and synchronization challenges.
🖼️ Rendering Textures and Sprites with Vulkan
The final paragraph delves into the rendering process, where the author tackles the complexities of translating Diablo I's 2D sprite rendering using Glide to Vulkan. It covers the intricacies of vertex data interpretation, the conversion of draw calls to Vulkan, and the challenges of coordinate systems and winding orders. The author experiments with different approaches to texture management, eventually settling on updating a descriptor set for texture binding. The narrative highlights the API design differences between Glide and Vulkan, the synchronization issues between texture access and game state, and the trade-offs made in the implementation. The author concludes with a reflection on the learning process and the decision to share the experience in a follow-up video on reverse-engineering Starcraft.
Mindmap
Keywords
💡Diablo I
💡Modern Graphics Technology
💡Hacks
💡Glide API
💡Vulkan API
💡Rendering Backends
💡Graphics Card Memory Allocations
💡Stack Cleanup
💡Graphics Memory
💡Coordinate Systems
💡Texture Mapping
Highlights
The quest to enhance the graphics of the 25-year-old game Diablo I using modern technology.
Diablo I's enduring popularity despite the release of a remastered edition.
The original game's poor visual appearance on modern hardware due to lack of settings.
Discovery of a Blizzard Forum post discussing Diablo 2's three rendering backends: Direct Draw, Direct 3D, and Glide.
Innovation of a Glide wrapper to trick the game into using a modern API like OpenGL.
Decision to create a Vulcan API wrapper instead for full control over graphics hardware.
Vulkan's complexity with hundreds of lines of code required to render a simple triangle.
Process of forcing the game to use the Glide backend by passing the 'D 3dfx' flag.
Creation of a custom library 'Glide 3x.dll' to emulate the Glide API for the game.
Debugging the game to understand how it interacts with the Glide API.
Fixing a critical bug by changing the calling convention to 'stdcall'.
Overcoming issues with function name decoration by using a .def file in MSVC.
Tracing and fixing a critical section deletion error in the game's code.
Understanding the Glide API's texture management through 'grTexMinAddress' and 'grTexMaxAddress'.
Initial failure and subsequent success in rendering a model using Vulkan.
Addressing a Vulkan validation error by recreating the swap chain.
Extracting and decoding texture data from the game using 'grTexDownloadMipMap'.
Challenges in interpreting the game's vertex data for rendering.
Conversion of Glide draw calls to Vulkan calls and troubleshooting a blank screen.
Synchronization issues between Glide and Vulkan APIs in texture management.
Final visualization of the game's graphics using the custom Vulcan wrapper.
Reflections on the complexity of juggling two different graphics APIs and the learning process.
Transcripts
I'm here to answer the age-old question
can you hack a 25-year-old game to
render with modern Graphics
technology despite being released in the
quarter of a century ago Diablo I is
still immensely popular and despite
Blizzard's attempt to resell the
experience with their remastered Edition
the original is still a lot of people's
preferred
option however there's some interesting
hacks that you can do to get the most
out of the original on Modern Hardware
hacks that are worth taking a deep dive
into so I bought a copy of the original
from blizzard and and because it's still
for sale I've not reverse engineered the
CD key like I've had to for other games
before the problem is that it looks like
total ass on my modern big screen
monitor and there's no settings to be
able to change
it looking around and I've ended up at
this blizzard Forum post the gist is
that Diablo 2 supports three rendering
backends direct draw direct 3D and
glide the game was optimized for the
Glide API but this isn't supported by
modern graphics cards unless you've got
a card lying
around so what some enterprising person
done is write a Glide rapper basically
to the game it thinks it's cing the
Glide API but really it forwards those
calls onto openg g a more modern API
effectively it's providing a translation
layer between the old and the new
API now presumably this all works but I
don't see why Sven gets to have all the
fun this is an interesting problem to
solve so let's do it ourselves but for
Vulcan if you're unaware then then
Vulcan is a modern Graphics API designed
to give Graphics programmers full
control over their Hardware it's a
similar design philosophy to direct X12
and metal and it's the API used to
program the latest Doom
games the fun part is that Vulcan is
immensely complicated it takes hundreds
of lines of code just to render a
triangle and involves managing your own
graphics card memory allocations and
fences and semaphores so presumably it's
going to be quite a bit different to a
20-year-old Graphics API
first things first we need to see how
the game tries to load an interact with
glide you can Force the game to use the
Glide backend by passing the flag D 3dfx
to the game which obviously it's not
happy
about opening the game in gidra and
tracing that flag through we end up at
this function which tries to load the
Glide DL from dis and resolve all the
functions this will be our in if we
create our own Library called Glide 3x.
DL and Export the same functions then
the game will happily load and call
those for us I found some header files
from the Glide API so I've recreated the
exact interface for the library with the
correct arguments and types I've just
added a simple implementation that logs
the call to a file and I've built it as
a shared Windows Library however it's
not loading of course by default it's
64-bit which Diablo certainly isn't okay
so it loads my library but it crashes on
the first call somewhere in is bad read
pointer if we open up in a debugger we
can see that the args for the function
are still on the stack after the
function call which suggests that the
game was expecting the c e i the
function to clean it up and if we look
inside the function we can see that
there's just a r at the end so there is
no stack cleanup from the function this
can be fixed by changing the calling
convention to stood call which among
other things will cause the emitted code
to clean up the stack ARS at the end of
the function and now I can't find any of
my functions turns out that when you use
store with msvc it decorates the
exported function names essentially
changing their names but we can fix this
by providing a def file which tells the
compiler no please don't change my
function names for me okay so we're
starting to call more functions so still
getting an error but from the log it's
been called from GL Glide shutdown so
something is failing and presumably
doing some cleanup tracing it through we
can see it's called from a function that
deletes a critical section and frees the
Glide library and this is called as an
at Exit Handler so when the game exit it
automatically calls this function so
I've traced through the code and the
failing call is gr SST win open and the
problem is entirely my fault I naively
assumed that returning zero would mean
success and then any other integer would
indicate a failure however if i'
bothered to read the documents I would
see that this function returns an fx bll
so in fact one means success and zero
means failure and after fixing that it's
calling some graphics looking functions
but now it crashed in gr text combine
never mind it was a bug in my code don't
roll your own log
Library so the program now ends after
calling grx Min address and grx Max
address H so I found the docs for the
Glide API and things are starting to
make a bit more sense now the way the
Glide API works is you upload texture
data to a specific address of your
choosing in the texture map unit or tmu
but to find the address range you can
copy to you call gr text Min address and
gr text Max address as I just returned
Zero from both of these the game thinks
there's zero Graphics memory and
promptly exits now we're not emulating a
tmu we're just going to pass that data
eventually onto Vulcan so let's just let
the game think it has the maximum
possible amount of Graphics memory it
works kind of we get the game sound and
a continuous stream of Glide commands in
the log so now we need to start wiring
this up to Vulcan luckily I've been
playing around with Vulcan as part of a
separate side project so I've got a lot
of the boiler plate already written
we've got a black box we're basically
done just got to figure out how to do
Vulcan things within it now so the game
calls gr buffer swap at the end of the
frame so we can use that to issue some
Vulcan draw calls and we can render a
random model I mean it's supposed to be
rotating and it crashes after a few
seconds but it's a start it doesn't like
the size of my window
H obviously need to pass W popup to
adjust window wck now I'm getting a
weird Vulcan validation error V error
out of date cah so apparently I need to
recreate my swap chain but now I get a v
error native window Inus error
so most of the time a swapchain goes out
a date it is because the window was
resized but as far as I can tell I'm not
resizing the window however having a
look through my bodge log file I can see
there are several calls to the window
open function with different resolutions
so maybe it's creating a separate window
for the intro cinematic and then the
game itself just going to add some hack
into the code to ignore the first two
calls kind of works audio still playing
and it doesn't crash however this model
is supposed to be rotating I think the
frame rate isn't right if if I just
increase the rotation speed then it
works I'm pretty sure that's how most
game bugs are
fixed I think the next step is to try
and extract the textures and looking
through the logs and the docs gr text
download M map is probably the function
we want this allows you to specify the
texture data at potentially multiple
levels of detail so the arguments are a
bit strange I was hoping for a width
height and pixel format but instead
we've got small L log 2 and large L log
2 small L log 2 is the logarithmic base
two of the largest dimension of the
lowest resolution M
map small L log 2 is the logarithmic
base 2 of the largest dimension of the
lowest resolution M
map small log log 2 is the logarithmic
base 2 of the largest dimension of the
lowest resolution M map I am slowly
descending into madness with this okay
so after passing the doc several times I
think I finally understand what's going
on basically your image data array can
contain the same image at several
different resolutions small L log 2
tells you what the smallest dimension of
those images will be and large log log 2
tells you the largest but is that the
width or the height well for that you
need to look at the aspect ratio log two
which states that if the aspect ratio is
positive then s will be the larger
dimension of the M map and if it is
negative then T will be the larger di
menion so basically given these three
bits of information you can infer how
many images are in the data array and
what their resolutions
are or you can just use this lookup
table the do gives
you remember this log 2 nonsense was
added in Glide 3 so just bear that in
mind if you're porting code from Glide
2 luckily for us the game is Just
rendered as 2D Sprite so there's just
one log level per image of course just
knowing the Size Doesn't really help us
when we're staring down the barrel of a
void pointer we need to know what form
the pixel data is in I was hoping for
RGB triplets or something easy to decode
but all our data is in format number
five or
p8 this is an 8bit pallet where each
bite is actually an index into a table
which stores the RGB values so it's
compressed via a lookup table but where
is the pallet stored this looks like it
comes from the gr text download table
which has the gr text pallet format
seems a missed opportunity for a game
called Diablo to not use the gr text
pallet 6666 text
format so I capture the pallet table in
flight and use that data to decode and
store the images to disk they're kind of
right I can make out what they're
supposed to be but the colors are all
wrong if only I had read the
docs a color palette is an array of 256
a RGB colors not RGB colors even though
it says that the AL component is ignored
fixing that and a few other bugs and now
I can dump all the games textures as
they're loaded nice
now for the rendering it looks like the
game is using gr draw vertex array
contiguous one of my favorites the args
are pretty straightforward which is a
surprise given what we've seen so far
mode says how to interpret the vertices
count is how many vertices are pointed
to the vertex data and the stride which
is the distance in bites between each
block of vertex data all the Calles are
in Triangle fan mode and we have four
vertices so presumably we're rendering
2D Sprites as quads
so how does Glide know how to interpret
the data hanging off this void pointer
that's done by some proceeding calls to
gr vertex layout which tells the API
what each block of vertex data looks
like for us that's an XY chord A cpram
that seems to always be one a Vertex
color which seems to always be white and
then the texture coordinates I've
recreated this layout as a struct so we
can interact with the raw vertex data
interestingly it size is 24 bytes but
the stride is 28 bytes so maybe there's
some padding just had a a minor disaster
whilst I'm not actually rendering
anything to the screen it's still
accepting Mouse clicks and I
accidentally clicked something on the
blank screen which caused the game to
update and now every time I run it I
just get this error box ah a reinstall
has fixed it which is a relief I've
converted these draw calls to Vulcan
calls and I get a blank screen which is
not what I was
expecting using render I can see the
sprite's been sent to Vulcan but there's
a lot that can go wrong here difference
in Glide and Vulcan coordinate systems
different winding order a bug in my
Vulcan
code a lot of Graphics debugging is
trying to figure out why a screen is
blank when you would 100% like it to not
be anyway I had a few bugs which I fixed
and I realized that Glide was supplying
the coordinates in screen space and I
needed to translate those to normalized
device
coordinates obviously so now we can draw
a random colored quad wherever the game
will draw a
texture so now we just need to connect
up the textures due to the differences
in API my initial ual thought was just
to use bindless textures this is a
unbounded global array of textures that
the shaders can access and then as the
game was loading the textures I could
just update that Global array and
provide each Sprite with an index into
that ironically this would then mean
that the game would require a fairly
more than graphics card however like
everything in Vulcan it's pretty
complicated and the mess of Vulcan code
that I've lashed together to get me this
far doesn't really lend itself well to
this design so instead I'm just going to
update the descriptor set to have one
large fix size array that's that's bound
to the fragment Shader I'm wondering
what the maximum number of textures I
combin at once is the Oracle says it's
128 however my GPU says it'll handle 10
14,870 so I think we're going to be okay
so I've uploaded all the textures and it
doesn't look right getting
better and we're pretty much there all
of these graphical issues are definitely
solvable problems however the real issue
is that Glide and Vulcan are just chalk
and cheese when it comes to API design
Glide is a state machine where you just
update the state that you need for each
call and Vulcan is just a full control
over your Hardware Beast the current
solution relies on you running the game
for a bit dumping the textures and then
the Vulcan run uploading them all in one
go to the GPU so if the game accesses
Textures in a different order between
runs then it will just get out of sync
which is probably What's Happening Here
of course the ideal solution would be to
modify the Vulcan to upload these
textures on the fly but then you have to
handle the synchronization between
various cues and pipelines and not to
ruin the illusion of YouTube magic but
it's taking me 4 weeks just to get to
this and my brain is starting to melt
from juggling two completely different
Graphics apis but if you found this lowl
dive fun and want to see how I reverse
engineered the original Starcraft game
then check out this next video
Weitere ähnliche Videos ansehen
5.0 / 5 (0 votes)