KnightOnline icon indicating copy to clipboard operation
KnightOnline copied to clipboard

Support cross-platform client builds with CMake

Open twostars opened this issue 2 weeks ago • 0 comments

Now that the server is cross-platform and we have our basic CMake build system, we can start looking at the rest of the code.

The client will be a massive undertaking, and one that requires a lot of thought and care.

Effectively we only care about the client; the tools only matter so far as their connection to N3Base, which is heavily tied to Windows-specific code right now.

More than anything, this issue should serve as a discussion for how this should be implemented going forward, before we introduce too many further changes to make our job harder.

Besides the obvious WinAPI-specific type or API call stuff, I want to talk more specifically about our larger blockers for this:

DirectX 9

The entire engine is built around DirectX 9 (well, technically it was built for 8). As far as I know, the API doesn't mesh with any modern alternative -- the closest would probably be OpenGL, aside from the right-handed vs left-handed coordinate systems. But that having said, DirectX 9 is also rapidly losing support. Many modern systems don't even support DirectX 9 natively anymore, which is why official KO quickly started updating for DirectX 11 (no doubt they would never have bothered unless their hand wasn't forced). Granted, it's usually (but not always) laptops/anything with integrated GPUs (such as with Intel Xe) rather than dedicated GPUs, but still.

The idea would be to update this but also allow for it to be cross-platform, and therein lies the issue - swapping between multiple. The simplest means would probably be to use complete separate builds for it (e.g. #ifdef DIRECTX, #ifdef VULKAN), producing separate binaries.

Of course this still feels like a massive mess.

Then of course there's the Linux alternative -- would it just be something like Vulkan? I don't personally know how well supported Vulkan actually is, but if its API flow is similar to say, DirectX 11, then it'd be ideal (I've never touched Vulkan so I couldn't say).

D3DXMath

This presently uses D3DXMath. Ideally this should use a more optimised SIMD implementation, but it needs to be cross-platform. We also need to be very careful, because D3DXMath implementations don't necessarily behave as you'd expect them to in other implementations. So a viable, cross-platform alternative needs to be found, and it needs to be tested against our D3DXMath implementations (#663).

DirectX types

As a special addendum to the above, the DX types in general are typically used in file formats. We have cases ranging from this like _D3DLIGHT9 being used in .n3light for instance, to the more mathy types (__Vector3, __Quaternion as off-handed recollections) for things like transformation data. Not to mention the extensive number of file formats reading vertices in varying formats directly.

Special care must be taken to ensure these files still get loaded and stored properly, even if they're, for example, using SIMD types. This may mean either manually reading by field, or building file-format specific structs for the express purpose of reading/writing to file.

Audio engine

This still uses the original DirectSound implementation. We've just added a quick one-time transcoding layer over the top via libmpg123.

While libmpg123 is cross-platform, DirectSound is obviously not, so the entire thing will probably end up being scrapped and the audio engine reworked, ideally around something that supports MP3.

Realistically I think the best we'll find is libogg, which is what official KO uses nowadays anyway -- this, of course, doesn't support MP3, which once again poses the problem of whether we stick to keeping the official client assets, or adding onto them with the newer .ogg assets for the sake of cross-platform compatibility (not to mention performance).

File I/O

The entire engine revolves around WinAPI's CreateFile(), ReadFile(), WriteFile(), SetFilePointer() calls.

While switching these out is very easy, performance on such a large scale is also a consideration when doing so. For example, we could replace with either FILE handles or std::istream refs, but the concern there is with the increased overhead on both fronts compared to the very direct ReadFile() calls on Windows.

Buffering may well prove it to not be an issue, but it should still be measured and taken into consideration when replacing, because there's a considerable amount of this code which is used throughout across all of the tools too, so we want to make sure we're not unnecessarily hurting performance here.

Windowing

In the client at least this is fairly simple (it's generally just a basic window for rendering), though there it should be noted that there are some nuances with input (for example when moving to the edge of the screen).

Originally this project had tried to use SDL 2, but in a very hamfisted way. The resulting windowing behaviour made it very clear that we need to be very, very careful to ensure things get replicated somewhat consistently, since it's not a given that they will be.

I don't necessarily love SDL, but it is technically an option. It doesn't do things like support window icons natively (it supports window icons, but on Windows for example, you're not going to preserve the same behaviour with having it loading an .ico file period, let alone from an embedded resource). Same with cursors.

It is possible that a more dedicated windowing library might be more useful. I think I'd rather ensure things remain working consistently, rather than lose support for compatibility, even if that means more platform-specific boilerplate code.

Input (i.e. DirectInput)

This largely ties in with windowing to an extent, especially if using something like SDL which integrates all of this. Naturally, DirectInput is not cross-platform.

Networking

We already implement asio for the server, so it makes sense to continue to use it for the client. So this is a non-issue.

Anything else

There's a lot of things that can just be replaced with C++ standard code. These aren't really much of a concern.

For example, a lot of the path logic can simply be replaced with std::filesystem::path, though care must be taken to ensure paths are still handled consistently (there are notably some official assets with null-terminators in paths which are ignored in this/the official implementation because it c_str()s it rather than using it directly).

Naturally, a lot of the Windows types can also be replaced with C++ standard types. Just so long as they're not currently used with a Windows-specific API.

I think this covers most of the main blockers that we'll encounter here. It's certainly worth discussing how we want to approach them before diving into the weeds of it.

twostars avatar Dec 12 '25 09:12 twostars