S-C-A-N / SCANsat

Real Scanning, Real Science, at Warp Speed!
220 stars 97 forks source link

Suggestion: Move away from using SetPixel. #49

Open Mihara opened 10 years ago

Mihara commented 10 years ago

Currently, SCANsat (and SCANsatRPM for that matter, because back when I started it I wanted it to just work first and optimize later, and there never is a later) is using SetPixel to draw the map pixel by pixel in the bitmap. This is known to be slow. (See Unity docs) You can't make SetPixel significantly faster.

Here's an idea: There are other ways to do it. I am not entirely certain if the method I'm about to describe is not going to create it's own performance bottlenecks, but if anything, those performance bottlenecks will mostly be on the GPU, not the CPU, and GPU in KSP is underutilized anyway.

Instead of using SetPixel, draw pixels using shaders and GL.Vertex the way SCANsatRPM draws orbits on top of everything. Working out the correct basic method to draw a pixel using this method will take some effort, but once it's done, it should be a seamless replacement, and SCANsat should become able to redraw the entire map during one frame if desired.

MOARdV commented 10 years ago

Shaders still need to know the color data, which has to be sent to the GPU somehow. If this were a C/C++ environment, I'd say the pixel height / color data could be set on the CPU-side in a buffer, and sent to the GPU with glTex* initialization routines. I don't know if there's an analogue in Unity/C#, but a single texture initialization call would be drastically more efficient than repeated SetPixel calls.

Mihara commented 10 years ago
GL.Color(color);
GL.Vertex(x,y);
GL.Vertex(x,y);

I'm not sure how efficient will that be, but I have a pretty good hunch it will, at least, work, and anything will be better than SetPixel. :)

A proper shader which could actually consume data from an array of elevations would be vastly more efficient than that, naturally, but who's got the expertise to write one?

DMagic1 commented 10 years ago

Currently the big map caches elevation data similar to how the small map does. It's a fairly dumb method that I came up with in a few days after looking at the small map method.

It looks up elevation data only when a location has been scanned, it requires that the cache be rebuilt every time the map is re-sized, and it requires three layers of data for the different map projections.

I think a much better solution might be to assign some arbitrary upper limit on map resolution (something like 2000-3000 pixels wide, theoretically only 8-18MB) then build up an elevation array for the entire planet, regardless of scan status, in the background (maybe over 100 or so frames). This would only need to be rebuilt when changing planets and would only require a single layer. Zooming in beyond a particular limit would obviously be a problem, more so with RPM than with the tiny standard zoom map.

This would be a much better source of data for a shader than the current system. It would probably be a substantial improvement over the current system even without any other changes.

I don't really know anything about shaders, but I understand the concept and agree that going through potentially millions of pixels, one-by-one, every time the map is changed is not a good idea.

I think you might still run into the issue of needing to rebuild the texture whenever the map size is changed, but at least changing map type/color/projection could be improved.

MOARdV commented 10 years ago

"I think you might still run into the issue of needing to rebuild the texture whenever the map size is changed, but at least changing map type/color/projection could be improved."

Actually, for map size, all you do is create the map as a texture with mipmaps. That way, you would only need to create it once at full resolution, and rendering would be as simple as drawing a quad. The GPU would handle scaling it for "free". If you set the max resolution at 2048 x 1024, then you'd have at least pixel-level fidelity on screen for most monitor resolutions. At that point, you wouldn't need custom shaders to draw the map.

technogeeky commented 10 years ago

Actually, for map size, all you do is create the map as a texture with mipmaps. That way, you would only need to create it once at full resolution, and rendering would be as simple as drawing a quad. The GPU would handle scaling it for "free". If you set the max resolution at 2048 x 1024, then you'd have at least pixel-level fidelity on screen for most monitor resolutions. At that point, you wouldn't need custom shaders to draw the map.

I have read this thread with great interest. I am currently (still) working on something else, but performance increases are my next task -- and this seems like the obvious place to start.

However, there are two things to remember about existing SCANsat code which may affect your arguments:

  1. The map currently has a fixed aspect ratio. Judging from SCANui.drawGrid(), it is 12:6 or 2:1. So your suggestion of 2048x1024 is fine in this regard. However, virtually none of the IVA monitors use this aspect ratio so maps are always clipped in the IVA context. I wish there were a way around this.
  2. I'm not sure how it's handled, but the high resolution altimetry has unlimited zoom (see spotmap in SCANui and SCANmap). I guess that using your method for the overall map will mean that the zoom functionality will need to remain using some other technique (including possibly SetPixel). Is this correct?

Also, if someone has some example code to get me started on this task, I would appreciate it.

MOARdV commented 10 years ago

"1.The map currently has a fixed aspect ratio. Judging from SCANui.drawGrid() , it is 12:6 or 2:1. So your suggestion of 2048x1024 is fine in this regard. However, virtually none of the IVA monitors use this aspect ratio so maps are always clipped in the IVA context. I wish there were a way around this."

Graphics.DrawTexture has a variant that allows specifying a section of the texture, so clipping is also covered. The GL namespace also allows immediate mode GL calls, so (with a little math), you can specify vertex/texture coordinate pairs that way as well. I've used both in RPM.

"2. I'm not sure how it's handled, but the high resolution altimetry has unlimited zoom (see spotmap in SCANui and SCANmap). I guess that using your method for the overall map will mean that the zoom functionality will need to remain using some other technique (including possibly SetPixel). Is this correct?"

Beyond a certain zoom level, you will need to do something if you want to maintain high quality zooms. One possibility would be to fall back to the existing SetPixel approach.

Another possibility would be to create a "zoomed map" texture that contains finer detail, but only for a subset of the map. That is, instead of drawing the whole world, you just redraw the region where the zoom is taking place. Once again, it'd be at a higher initial resolution than you're displaying, so that you can zoom a few times before you need to discard the texture and create yet a tighter zoom.

Note that I have not looked at the SCANsat code at all, so I don't know how easy any of this would be. I also don't know how one can create a procedural texture and load it into the GPU through C#/Unity. I've done that sort of thing lots of times in other environments, but not within the constraints of this engine.

If there is a sufficiently faster way to specify textures than SetPixel, you may not need to do anything fancy like this.

DMagic1 commented 10 years ago

Rendering the entire map and clipping it has been brought before in discussions about RPM. If we have a quick way to render the map it should work fine.

How far can SCANsatRPM zoom in, is it unlimited like the standard SCANsat spotmap? If there is some reasonably limit it might be feasible to create a big enough elevation data cache and just use that for zooming in in RPM.

The spotmap is small enough that it isn't really a problem to use a slower method of generating the texture, but the RPM would ideally use a quicker method. It might also be nice to make the spotmap bigger too.

nevercast commented 6 years ago

Sorry, I'm not a Unity developer. But it's C# yes? You can Unlock the bits of a C# Bitmap and write the bytes directly to the memory address. Then relock the bits and your color stuffed image is ready for use.

Someone would have to join the world of C# Bitmap and Unity together, like I said, I don't work with Unity.

Ruedii commented 6 years ago

Simply using image copy, and image display should work best, honestly.

You should store the maps as cached textures maps on disk, and just load them into memory.

As mentioned by others, you should be able to use a pixel shader to draw the textures.

It would be more efficient to use textures and shaders instead of bitmaps and array operations, due to the fact that GPU operations can be written in a manner to be massively parallelized.

nevercast commented 6 years ago

@Ruedii Is there a minimum feature set that KSP requires on the GPU to work, do you know which version of Pixel Shader for example is required? Within reason, it should be discouraged to increase the minimum system requirements of KSP. In my opinion. Obviously there is some system cost to modding, RAM, CPU, storage. GPU is usually up to the mod author though and it might be worth considering.

FalcoGer commented 4 years ago

Can you not assume that the entire map has been mapped and then delete the areas that have not actually been mapped? At the first game launch you can render the highest resolution images for each sensor and map type for each planet and then just dump the entire texture into the UI/RPM in one go and then blank out what isn't scanned. Sure it needs a bit more memory, but this should be considerably faster than rendering from scratch every time. Because the planets aren't going to change often, there is no need to re-render any of them unless some mod is installed or removed that alters planets in some way. If this can be detected, then you can re-render then, otherwise give the user a button that, once clicked will make their computer freeze for an hour because it renders all the maps again at best sensor resolutions. Or you could put it in a thread. You could even provide pre-rendered stock maps to begin with. If having a single, massive, high res texture is too much to load, you could have a bunch of smaller ones that load on demand and puzzle them together as required.