[Solved] custom cursor image

Started by idhan, March 19, 2015, 11:22:26

Previous topic - Next topic

idhan

Hi,

how can I set a custom cursor image with LWJGL 3?

SHC

Assuming you want to change the cursor i.e., show an image instead of the mouse arrow, you have to create a GLFW Cursor object and set it to the window. The following code does that.

// Create the cursor object
long cursor = GLFW.glfwCreateCursor(imageBuffer, 0, 0);

if (cursor == MemoryUtil.NULL)
    throw new RuntimeException("Error creating cursor");

// Set the cursor on a window
GLFW.glfwSetCursor(myWindow, cursor);

In the above code, the imageBuffer is the pixel data contained in an instance of GLFWimage struct class. This is how that is created. It is assumed that pixels is a ByteBuffer containing the pixels of the image you want to use as the cursor in RGBA format.

ByteBuffer imageBuffer = GLFWimage.malloc(width, height, pixels);

And that is how you create different cursors. Please note that this code is just my guess after reading the documentation of GLFW and LWJGL3 and I had never tried this code. If I have done anything wrong, please correct me.

EDIT: I have confirmed that this is working. See this screenshot for example.


idhan

that's exactly the example I was looking for!!

thank you SHC!

Waffles

I've been trying the same thing, but it seems to fail for me. Once my application reaches glfwCreateCursor, it immediately crashes back to desktop, with a fatal error in the console:


#
# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x00007ff9e0506bc0, pid=18720, tid=16700
#
# JRE version: Java(TM) SE Runtime Environment (8.0_25-b18) (build 1.8.0_25-b18)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.25-b02 mixed mode windows-amd64 compressed oops)
# Problematic frame:
# C  [lwjgl.dll+0x36bc0]
#
# Failed to write core dump. Minidumps are not enabled by default on client versions of Windows
#
# An error report file with more information is saved as:
# C:\Users\Zeno\My Development\5. Jade\10. Testing (BETA)\Core - Viewport2D Test\hs_err_pid18720.log
#
# If you would like to submit a bug report, please visit:
#   http://bugreport.sun.com/bugreport/crash.jsp
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.
#


I attached the generated log file to this post, just in case.

I figure it has something to do with how I create my byte buffer? The thing is, I use the same code to load image data into textures, and those definitely seem to work. Here it is:

public static ByteBuffer buffer(BufferedImage image)
{
   byte[] data = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
   ByteBuffer buffer = BufferUtils.createByteBuffer(data.length);
   return (ByteBuffer) buffer.put(data).flip();
}


When I use the GLFWimage.malloc method as posted here, I get a "Java Platform Binary has stopped working" dialog.

Concerning the image itself, as far as I can tell, it's in the correct format.

  • Filetype: png
  • Size: 32x32
  • Depth: 32-bit

spasi

SHC's description is correct. Here's an example from the LWJGL samples (uses STB for decoding the image).

Zeno, I'd be interested to see more of the code you're using.

SHC

I don't find any reason why that code fails, it looks correct to me. Maybe, just a guess, are you calling this from any other thread which is not main? If not, then I'm not sure. It would be helpful if you tell what OS version, and some code too.

Waffles

Information about my OS can be found at the very bottom of the log file I attached in my previous post.

I'm not sure what other code you'd want me to provide. I suppose the only important bit remaining would be how I load the image into memory? We're actually using a third-party library for that (JavaPNG, found here). The reason for this; back when we were still using a software renderer, we often ran into PNG images that were loaded incorrectly (garbled colors and whatnot). This library fixed all of our problems and we've been using it ever since.


So I went through the code from the link you provided. I copied the code and the required IOUtil class, and apparently, using these methods instead, my cursor loads just fine. First off, thank you very much, however if you don't mind, I do have a few questions with this approach...


  • The first part of the code calls the method IOUtil.ioResourceToByteBuffer. I take it the resulting buffer contains the raw byte data from the file? Why is the resulting byte buffer created with a size of (file channel size + 1)?
  • Next, it uses this buffer in the call to STBImage.stbi_load_from_memory. Am I correct in thinking that this method essentially disposes of all the data in the file that's not pure pixel byte data (like png header data) and perhaps deals with compression if necessary?
  • Lastly, this second buffer is used in the GLFWImage.malloc method to create a third, and final buffer. As far as I can tell, this does nothing more than ensure the resulting buffer has the correct dimensions?

Perhaps most interestingly, it seems I'm getting the same fatal error as in the previous post, if I omit the call to GLFWImage.malloc and return the second generated buffer instead.

EDIT: Last statement verified, if I use our own image-to-bytebuffer method as stated above, then apply GLFWImage.malloc to it, the cursor works. From what I've seen in the source code, this function basically appends width and height to the start of the buffer, right?

SHC

Instead of deciding on the size of the buffer before hand, I use a ByteArrayOutputStream to collect the data before hand and put that in the buffer.

public static ByteBuffer readToByteBuffer(InputStream inputStream)
{
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

    byte[] buffer = new byte[4096];

    try
    {
        while (true)
        {
            int n = inputStream.read(buffer);

            if (n < 0)
                break;

            outputStream.write(buffer, 0, n);
        }

        inputStream.close();
    }
    catch (Exception e) { SilenceException.reThrow(e); }

    byte[] bytes = outputStream.toByteArray();

    ByteBuffer byteBuffer = BufferUtils.createByteBuffer(bytes.length);
    byteBuffer.put(bytes).flip();

    return byteBuffer;
}


And this is the code I use to load the image buffer from the memory buffer.

ByteBuffer imageBuffer = FileUtils.readToByteBuffer(stream);

IntBuffer width = BufferUtils.createIntBuffer(1);
IntBuffer height = BufferUtils.createIntBuffer(1);
IntBuffer components = BufferUtils.createIntBuffer(1);

ByteBuffer image = stbi_load_from_memory(imageBuffer, width, height, components, 0);

if (image == null)
    throw new SilenceException("Failed to load image: " + stbi_failure_reason());


Now once you have got the image buffer, it's time to send it to GLFW. Once you have created the cursor and set it on the window object, you will have to free the image data from the memory.

stbi_image_free(image);


Hope this clarifies your questions.

Waffles

I'm sorry, but I don't quite follow how that relates to what I've said. But thanks for the tip on freeing the data from memory.

Kai

Quote from: SHC on July 06, 2015, 16:55:52
Instead of deciding on the size of the buffer before hand, I use a ByteArrayOutputStream to collect the data before hand and put that in the buffer.
Why? I would definitely prefer the solution with a preallocated ByteBuffer, since it does not require the destination array/buffer to be dynamically resized as you read data into it.
With a file size of N bytes, the default ByteArrayOutputStream would need `n = (log(N) - log(32)) / log(2)` enlarge (i.e. realloc) operations.
If the size of the file is only 64 kilobytes, this would already be 11 reallocations.

EDIT:
Just if anyone is interested how much memory is actually created as garbage when using a doubling reallocation as is used by the default ByteArrayOutputStream:
For this we need to compute the series `32 + 64 + 128 + ...`, which is: `Sum[i=0..n-1]{2^n} * 32`, where 'n' is the number of reallocations we need to do as given by the upper formula. This gives the sum of all memory that is thrown away as a result of a reallocation of the backing array.
Now this series is actually equal to `N - 32` and for increasing `n` converges to N.
So in general, with the ByteArrayOutputStream we always allocate the same amount of memory as garbage as we eventually need in the end (plus of course any internal JVM object headers/paddings, etc.)

EDIT2: The small program I used to calculate the produced garbage :)

public class MemoryAllocationCalculator {
  public static void main(String[] args) {
    int preallocated = 32; // might also set this to 4096
    int N = 64 * 1024; // the desired eventual size of the buffer/array
    int n = (int) Math.ceil((Math.log(N-1) - Math.log(preallocated)) / Math.log(2));
                                   // N-1 to account for floating-point imprecisions with log and ceiling
    int garbage = 0;
    int allocated = preallocated;
    for (int i = 0; i < n; i++) {
      garbage += allocated;
      System.err.println(allocated);
      allocated *= 2;
    }
    System.out.println("Reallocations:   " + n);
    System.out.println("Garbage(series): " + garbage);
    System.out.println("Garbage(simple): " + Math.max(0, N - preallocated));
    System.out.println("% garbage:       " + (float) garbage / N * 100.0);
  }
}


Also quite interesting is the fact that for values of `N` that are not exactly powers of 2, the garbage can actually be as high as `2 * N`. So, when N is 1025 bytes, then the actual garbage produced is 2016 bytes.

spasi

Quote from: Zeno on July 06, 2015, 16:45:55The first part of the code calls the method IOUtil.ioResourceToByteBuffer. I take it the resulting buffer contains the raw byte data from the file? Why is the resulting byte buffer created with a size of (file channel size + 1)?

First of all, that code is test/sample code. It is by no means production ready, etc.

The method ioResourceToByteBuffer is supposed to load a resource from disk, whether it's a plain file or inside a JAR (in the classpath). It is also supposed to do this with minimal memory overhead, i.e. as few allocations as possible. This is something I generally favor, memory efficiency over raw performance efficiency. With the way modern CPU caches work, being memory efficient usually results in better (overall) performance.

Anyway, the +1 is in the FileChannel path (the resource is a simple file) and it's there because it makes the following loop extremely simple:

while ( fc.read(buffer) != -1 );

Basically, I didn't bother making it clever. Removing the +1 will result in an infinite loop, read the Javadoc to see why.

Quote from: Zeno on July 06, 2015, 16:45:55Next, it uses this buffer in the call to STBImage.stbi_load_from_memory. Am I correct in thinking that this method essentially disposes of all the data in the file that's not pure pixel byte data (like png header data) and perhaps deals with compression if necessary?

Yes. The first buffer is the raw file data; the compressed PNG image + header/metadata. GLFW expects an uncompressed 32-bit RGBA image and stbi_load_from_memory does exactly that: it decompresses the image data (in the second buffer) and also returns some metadata (width/height/component count).

Quote from: Zeno on July 06, 2015, 16:45:55Lastly, this second buffer is used in the GLFWImage.malloc method to create a third, and final buffer. As far as I can tell, this does nothing more than ensure the resulting buffer has the correct dimensions?

The 3rd buffer is technically a struct: GLFWimage { int width, int height, unsigned char* pixels }. Note that the 3rd field is a pointer to the image buffer, not the image data itself. This struct is what the GLFW API expects in glfwCreateCursor and that's why directly using the image buffer there results in a crash.

I hope this explains everything.


Waffles

Yes, that does indeed explain everything, and then some. Thank you very much for all the help and information. To each of you, you are a gentleman and a scholar! :D