Thursday, June 26, 2008

Textures

Today I've added in the caustics warp for underwater surfaces. I've tried to keep it fairly subtle, so hopefully the effect won't distract you from doing what you're supposed to be doing - staying alive and getting out of wherever you are!

I've also had to go back and rework water warp updating owing to some bugs that surfaced in the course of this. It was fairly irritating to have to do this, but I've finally got a rock solid implementation. It's based on the FitzQuake framebuffer update, but with a few small modifications:

  • Doesn't need to scale the update texture to 2x the original size.
  • Capable of handling any texture size (up to the supported maximum (for more on this see further down)) - the FitzQuake version only did 64 x 64 textures.
  • Uses vertex arrays for rendering.
  • Capable of handling variable warp speeds - including negative/reverse warps (both the FitzQuake and the original GLQuake warps only did a speed of 8).
  • No longer uses the old gl_warpsin.h table (uses a custom sintable instead, capable of handling greater levels of detail).
  • Ensures that liquid textures are square.
  • Various small optimisations.
I don't know if I mentioned anything about the maximum texture size MHQuake supports, but this is quite interesting so I'm going to write a bit about it. When I calculate the max texture size at startup, I use the following procedure:
  1. Do a glGetIntegerv for the max size supported by the implementation.
  2. Reduce this to the lowest of the MHQuake window width and height.
  3. Reduce further to the highest power of 2 below this.
Some folks may kick and scream about this, but when you think about it it does make sense. There's no point in having a texture that will always be minified on-screen, you're only wasting video RAM because miplevel 0 never gets used. Some cards even cause texture corruption under these circumstances.

I'm going to need to revisit my resolution changing code, as right now this is one thing that a resolution change doesn't accomodate (the max texture size won't change, and current textures don't get updated to reflect what the new max size should be). I always needed to do this for more robust 3D card support (hello ATI!), so having the second reason is the kick in the pants I need for it.

When uploading textures I now follow this procedure:
  1. Calculate the scaled_width and scaled_height to the power of 2 equal or above the base size.
  2. Resample it up to this.
  3. Use mipmapping of width and height individually to bring it down to the max supported size.
  4. glTexImage2D it.
This might seem strange, but again it makes sense. Main memory is plentiful in supply, so I don't mind using a bit extra when I need to. Furthermore, my mipmapping functions have been modified to preserve colour balance better, so it makes sense to use them where they're of benefit.

Finally, my resampling function is a custom one of my own that lets me expand an 8 bit texture to 32 bit at the same time as I'm resampling it. This gives me a memory saving, so I gain as well as lose on this strategy.

Till next time!

Tuesday, June 24, 2008

Update

I've taken one of my regular breaks to do Other Stuff recently, but I've been back updating things again. The focus now is on finalizing the surface refresh. I have a number of different incarnations of this in place, so I'm just combining elements from them all to get a final take on it completed. The remaining elements are a small tweak on underwater fog (call me fussy, but I want fullbrights to shine through underwater fog, which I don't really have in my current implementation) and getting a good take on the needed 4-texture pass (light/caustic/fullbright/texture - in that order) with 2 and 3 TMU cards. At that point I'll consider the surface refresh largely "done".

The main remaining elements in the engine will be finishing up the particle system (which I've put on hold pending a satisfactory replacement explosion effect) and some tweaking in the menus (making them consistent and handling some ATI uselessness). Hopefully that will put me in a position where I can finally release, but I won't make any promises - I thought I was there 3 months ago!

Wednesday, June 11, 2008

Nothing done today

I didn't sleep well last night in the heat, so no updates, I'm turning in early.

Monday, June 9, 2008

Small but significant change to texture loading

I posted on QuakeOne about a BSP texture issue that affects all GLQuake engines, so I was intrigued enough to sit down and actually do something about it.

In software Quake, BSP textures are stored as 4 miplevels within the BSP itself. Each of these is restricted to the Quake palette, and the appropriate miplevel is used depending on distance from the view origin.

However, GLQuake does not use these. Instead, it generates miplevels through the GL_MipMap function for eveything but the first (miplevel 0). This means that the original colour balance of software Quake is totally lost in each and every GL engine.

Now, while this may be technically superior and more accurate, there is a lot to be said for reverting to the original software Quake method and using the stored mipmaps. Software Quake had a lot going for it that looked much better than GLQuake, and the colour balance (with 8 bit textures) was one of those things. This colour balance came from two factors: lighting and textures. Lighting is already fixed by restoring overbrights to GLQuake, but why not go the distance and also restore the original textures?

So I did. ;) MHQuake is now more accurate than any other engine out there for retaining the software Quake colour balance (it's less faithful in other ways, of course). It's a subtle difference, and you likely won't even see it unless you go looking for it, but it's there.

As a bonus, MHQuake is also capable of accurately reproducing map hacks that might exist which use a different image at each miplevel. I don't expect that there's too many of those, but you never know...

It's only a matter of time before other engines also get this feature (despite what I said above, I'd be shocked if DarkPlaces didn't already have it...), so here's my code (specific to my own engine, but should be easily adapted:


if (flags & TEX_MIPMAP)
{
int miplevel = 1;

// if it's an 8 bit texture coming from a BSP we handle it differently, as we want to take the first 4 (i.e.
// (next 3) miplevels from the BSP... this ensures that the colour balance correctly matches software quake
if ((flags & TEX_BSP) && !(flags & TEX_32BIT))
{
// BSP textures are ensured to be large enough to survive this...
for (; miplevel < 4; miplevel++)
{
// advance to next stored BSP mipmap
data += width * height;

// take down width and height
width >>= 1;
height >>= 1;

// take down scaled_width and scaled_height
scaled_width >>= 1;
scaled_height >>= 1;

// deal with texture resizing
if (width == scaled_width && height == scaled_height)
{
int i;
int size = width * height;

// upload is already set and valid, so we can copy it right in
for (i = 0; i < size; i++) upload[i] = d_8to24table[data[i]];
}
else
{
// resample the new data into upload (already set and valid here too), never a 32 bpp base
GL_ResampleTexture (data, width, height, upload, scaled_width, scaled_height, false);
}

// upload (don't increment miplevel for this one as the for loop does it for us)
glTexImage2D (GL_TEXTURE_2D, miplevel, GL_RGBA, scaled_width, scaled_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, upload);
}
}

// BSP mipmapping sets up so that it will fall through correctly to here...
while (scaled_width > 1 || scaled_height > 1)
{
// generate our mipmaps
GL_MipMap ((byte *) upload, scaled_width, scaled_height);

// take down to next level
scaled_width >>= 1;
scaled_height >>= 1;

// never go < 1
if (scaled_width < 1) scaled_width = 1;
if (scaled_height < 1) scaled_height = 1;

glTexImage2D (GL_TEXTURE_2D, miplevel++, GL_RGBA, scaled_width, scaled_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, upload);
}
}

[Video Startup] When is SetPixelFormat actually required?

This is an interesting question I've been pondering over (and experimenting with) in the course of my resolution changing code. Received wisdom appears to be that when changing resolution you should take the following steps (simplified list):

  • Delete all OpenGL objects.
  • Destroy your current OpenGL context and make the current context NULL.
  • Destroy your window.
  • Recreate your window.
  • Set a new pixel format.
  • Create a new context and make it current.
  • Reload all of your OpenGL objects (textures/display lists/etc).
It turns out that in reality this is not actually the case! Unfortunately, the documentation is of absolutely no help whatsoever in telling you this, all the information you'll get from MSDN will reveal is that ChoosePixelFormat and SetPixelFormat refer to the pixel format of a device context, but what it fails to give you is any practical real-world-use information (most MSDN documentation is like this, but that's another story...) OpenGL tutorials or documentation are just as bad, as they generally only deal with first-time startup.

If you're the cautious type you'll likely do the above anyway, and be happy to leave it at that. But just think: you could be creating a load of unnecessary work for yourself. There are plenty of GLUT examples where the window size changes but a Pixel Format switch is not required, and how is that really any different from a change of resolution? Likewise with when the scr_viewsize cvar changes.

So I decided to throw out caution and just change the window size (and resolution if required), without bothering to destroy and recreate anything. And guess what - it worked. Perfectly. First time. Everything survived a ChangeDisplaySettings call with no issues or corruptions. I've tested the following mode changes and everything just works perfectly fine:
  • Change from one Windowed mode to another.
  • Change from Fullscreen to Windowed.
  • Change from Windowed to Fullscreen.
  • Change from one Fullscreen mode to another.
  • Change from 16 bit to 32 bit.
  • Change from 32 bit to 16 bit.
Until I see some conclusive evidence that destroy/recreate is an absolute requirement because of 1... 2... 3... (in other words I want to see concrete definite reasons) I'm going to leave things be. I want a definite yes or no answer on this, not some vague description of what a function does. I have a feeling that this "accepted way of doing things" attitude may have been relevant in the days of 4 MB 3DFX cards, where you had limited memory and a Pixel Format switch may have been required as a higher resolution mode may have needed less bits in the depth or color buffers, but in 2008 when even an Intel Integrated card from over 2 years ago can easily support 1280 x 768 fullscreen with 32 bit colour and 32 bit depth, this is just silly.

So the answer to my initial question seems to be: SetPixelFormat is only required when you need to change the bit-depth of the color or depth buffers. Never any other time.

For the record, here's the procedure I use for switching resolution.
  • Determine if ChangeDisplaySettings is required by comparing the new mode with the current mode, and call it if required.
  • Call SetWindowLong to change the window style (title bar & borders vs. none).
  • Call AdjustWindowRectEx to set the client area to the desired window size (adjusting for title bar & borders, or lack thereof).
  • Call MoveWindow to position the window to the origin (not required, but hey!)
  • Call CenterWindow to center the window (required as the size will have changed).
  • Call InvalidateRect (NULL, NULL, TRUE); to force an immediate repaint of the desktop (required to refresh desktop areas that the window may have previously covered).
  • Call IN_ActivateMouse and IN_HideMouse to bring the mouse settings into sync with the new mode (if it's a windowed mode, the next GL_EndRendering will finish fixing things up).
  • Store the new mode settings back to the current mode settings, and do some other bookkeeping stuff.
Postscript: one other thing strikes me here. If we're running in a full-screen desktop-resolution mode, there doesn't seem to be much to stop us from using the desktop as our OpenGL window (by passing a HWND parameter of NULL to the relevant functions). I'd like to experiment with this sometime, but probably outside of the context of MHQuake as it would complicate things a bit too much. I've also got a feeling that regular Windows desktop repaints might mess things up a bit.

Post-postscript: If one was feeling a bit cautious but still wanted to try this method out, once could do a ChoosePixelFormat on the desktop DC (using GetDC (NULL);), then do SetPixelFormat on the window DC. This is based on the assumption that whatever pixel format is used for the desktop will also be valid for any window we care to create. I'll probably end up going down this route myself.

Thursday, June 5, 2008

Change of scene

Just worked on removing alias model caching. As a result, alias models will need to be reloaded at each map change; they're no longer taken from a persistent cache. This is part of my reworking of the entire memory subsystem, which I had done before, but which I am revisiting as I'm not happy with the previous work (some bugs). OK, it makes maps that little bit slower to load, but it's needed to make game changes viable. And in-game game changes really do go hand-in-hand with the in-game resolution changes I was previously working on, as textures will need to be reloaded for both of them (my current resolution changing doesn't destroy the window or reload textures, but this is not the right way to do it, and chances are some pixel format incompatibility problems will hit in the wild). The objective is to minimise the amount of things in persistent caches that will need to be reloaded (as reloading into a persistent cache is a little bit awkward).

Sounds will come next, which will result in the only persistent items being the texture cache (which was designed with reloading in mind anyway) and a few other startup bits and bobs.

Sorry about all the parenthesized stuff in this post, by the way (it's late, I'm a bit tired, and I need to concentrate to avoid writing like this all the time!)

The rest of this week is for Real Life stuff, so no updates for a while!

Tuesday, June 3, 2008

Well that didn't take long...

Just restored the command-line override for startup resolution. I can think of at least one place where it's useful - quick testing.

Video startup now works as follows:

  • Check the registry for an explicit mode request, fall back on desktop full screen if nothing specified.
  • Check the command-line for an explicit override. -width on it's own will also set a height at 4:3 aspect, to replicate how the original GLQuake did it.
  • If it's windowed, look for a best match.
  • If it's fullscreen, look for an exact match, fall back on the desktop if none found.
  • Set the mode and write it back to the registry.
It's probably worth mentioning how windowed modes are set. In short, any fullscreen mode is also a valid windowed mode, unless either of the width or height also match the desktop width or height, respectively. I'm not overly happy with this, as there is enormous scope for windowed modes of any aspect ratio, whereas this way locks them to aspects that are also supported by the fullscreen modes. I have a feeling that I'm going to rework this to allow for specific aspect ratios to be chosen, as I sometimes like to test in a widescreen windowed mode when I don't have a widescreen monitor to hand.

More video work (and thoughts on the Registry)

I've been doing some more cleanup of gl_vidnt.c - it's still an unholy mess, but it's getting less and less so by the day. This is mostly consisting of removing old software compatibility stuff, totally reworking video mode handling (so that it's actually marginally understandable) and removing a lot of the globals. Shortly going on to the video commands.

I see that Entar is crying out (?in jest?) against my use of the Registry. Muhahahahahah. Seriously though, some further comment on this does seem merited. It does make sense to use the Registry for this purpose; it's far cleaner and far more robust that pre-parsing a config.cfg file, and it is made for storing this kind of setting. For what it's worth, I only store the initial video mode settings in it, it uses HKEY_CURRENT_USER\Software\MHQuake, and it's been coded to fall back on the desktop mode if anything invalid is found in there.

I for one believe that video modes are also something that most people will only set once and then leave be forever after. They will also be used globally for all games, not separately for each game. I can't imagine that there are many people who prefer to play Hipnotic in 640 x 480 fullscreen but ID1 in 1152 x 864 windowed, for example (and I just know I'm asking for someone to come and prove me wrong here...) Having to set them individually in each config seems a bit of a silly thing, and remember: it's all about making things easier for the players at the end of the day.

The Registry has gotten a bit of a bad rep in the past, but this is primarily from the bad old days of Windows 9x, when if you so much as sneezed at it, things went wrong. It's a lot more robust and stable nowadays, and maybe late switchers to a proper NT-based OS (I was on Windows 2000 since the day it was released) just have a little bit of catching up to do in terms of confidence in it.

I can understand arguments both for and against the Registry vs. config files. I personally dislike having a multitude of config files scattered all over the place, but I can see them being useful for certain purposes (although I don't buy the "human-readable" argument: it's perfectly possible to create an unreadable mess of a config file, just as it's also possible to have a logical, clean and human-readable structure in the Registry). Believe me, one of the options I considered was a config file in Documents and Settings\Username\Local Settings\Application Data, but in the end that option lost. Why? Simply because there's a perfectly good and usable Registry API that already exists, that can handle multiple different types of data, and that is tried and trusted, tested for over a decade, debugged code.

Bear in mind that I'm not trying to justify a controversial choice here, certainly not having a pop at Entar (who's work I respect), and most definitely not trying to take sides in any holy wars (my previous paragraph should have made that clear!) I'm just outlining the reasoning and thinking behind my decision. If anyone still baulks at this, or refuses to use my engine because it uses the Registry for a mere 4 config settings, well so be it.

Finally, the bugs mentioned below have now been fixed.