ryancheung / FreeTypeSharp

A modern cross-platform managed FreeType2 library

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Problem moving to version v3.0.0

CalvinWilkinson opened this issue · comments

Hello!

First off, thanks for the library! I have been using this ever since I introduced text rendering into my game framework.
I greatly appreciate the work you have done with this!

I have run into an issue while moving to v3.0.0 of your library from version v2.2.0.

Here is what I have going on here. I have a class that acts as a thin wrapper for the most part around
your library. The reason that I do this is to act as a thin wrapper for testing and usage purposes.

With this "wrapper", I am calling the FT.FT_Set_Char_Size() method. When calling this method,
I am getting the FT_Err_Invalid_Size_Handle error.

Everything of course was working fine with version v2.2.0. Currently, I am making changes to accommodate
all of the API/breaking changes that are in v3.0.0. I have figured out most of these are
structs instead of pointers.

Due to this, I am thinking that I am doing something wrong. I will show you the before and after so you can see what
it was with version v2.2.0 compared to what I am trying to do now for version v3.0.0.

Below, this is the wrapper method that I have to wrap the FT.FT_Set_Char_Size() method. This is being invoked by another
class.

Method with version v2.2.0:

public void FT_Set_Char_Size(nint face, nint char_width, nint char_height, uint horz_resolution, uint vert_resolution)
{
    EnsureThat.PointerIsNotNull(face);
    EnsureThat.PointerIsNotNull(char_width);
    EnsureThat.PointerIsNotNull(char_height);

    var error = FT.FT_Set_Char_Size(face, char_width, char_height, horz_resolution, vert_resolution);

    if (error != FT_Error.FT_Err_Ok)
    {
        this.OnError?.Invoke(this, new FreeTypeErrorEventArgs(CreateErrorMessage(error.ToString())));
    }
}

Method with version v3.0.0:

public void FT_Set_Char_Size(ref FT_FaceRec_ face, nint char_width, nint char_height, uint horz_resolution, uint vert_resolution)
{
    EnsureThat.PointerIsNotNull(char_width);
    EnsureThat.PointerIsNotNull(char_height);

    unsafe
    {
        fixed (FT_FaceRec_* facePtr = &face)
        {
            EnsureThat.PointerIsNotNull((nint)facePtr);

            var error = FT.FT_Set_Char_Size(facePtr, char_width, char_height, horz_resolution, vert_resolution);

            if (error != FT_Error.FT_Err_Ok)
            {
                this.OnError?.Invoke(this, new FreeTypeErrorEventArgs(CreateErrorMessage(error.ToString())));
            }
        }
    }
}

The code that is calling my FT_Set_Char_Size() method are shown below:

Caller method with version v2.2.0:

public void SetFontSize(nint facePtr, uint sizeInPoints)
{
    if (sizeInPoints <= 0)
    {
        throw new ArgumentException("The font size must be larger than 0.", nameof(sizeInPoints));
    }

    var sizeInPointsPtr = (nint)(sizeInPoints << 6);

    this.freeTypeInvoker.FT_Set_Char_Size(
        facePtr,
        sizeInPointsPtr,
        sizeInPointsPtr,
        (uint)this.sysDisplayService.MainDisplay.HorizontalDPI,
        (uint)this.sysDisplayService.MainDisplay.VerticalDPI);
}

Caller method with version v3.0.0:

public void SetFontSize(FT_FaceRec_ face, uint sizeInPoints)
{
    if (sizeInPoints <= 0)
    {
        throw new ArgumentException("The font size must be larger than 0.", nameof(sizeInPoints));
    }

    var sizeInPointsPtr = (nint)(sizeInPoints << 6);

    this.freeTypeInvoker.FT_Set_Char_Size(
        ref face,
        sizeInPointsPtr,
        sizeInPointsPtr,
        (uint)this.sysDisplayService.MainDisplay.HorizontalDPI,
        (uint)this.sysDisplayService.MainDisplay.VerticalDPI);
}

I have been looking to see if there was a change to the original FreeType library to see if maybe
you upgraded to a later version of FreeType with your v3.0.0 release, but I had no luck. Maybe something was changing with the original C library that was causing issues.

Your release notes don't have any information about breaking changes, or versions that you might have moved to
so I could not get any information there either.

I tried to figure out what version of the original FreeType C lib you were pointing to, but it was quite
the trail due to you having a reference to the Fork of the GitHub mirror of the original lib from GitLab.

I also ran out of time trying to figure this out. 😀

My gut feeling here is that I am simply doing something wrong here in regards to the changes with version v3.0.0.

Also, if you are interested, my project Velaptor is open source so you can
take a look if you want to.

If you need more questions from me, please don't hesitate to ask!!

Thanks!!

void SetFontSize(FT_FaceRec_ face
You create a copy here, you should use the original pointer. Like void SetFontSize(FT_FaceRec_* face, ...
You should avoid copy the struct of FT_FaceRec_ and alwayse use struct pointers for it.

void SetFontSize(FT_FaceRec_ face You create a copy here, you should use the original pointer. Like void SetFontSize(FT_FaceRec_* face, ... You should avoid copy the struct of FT_FaceRec_ and always use struct pointers for it.

I did indeed catch that at first. What I did to prevent the face param of the SetFontSize() method from having to be an unsafe pointer just passed the face parameter to the freeTypeInvoker.FT_Set_Char_Size() method. This way it is the same pointer.

I verified that I was getting the same pointer inside of the FT_Set_Char_Size() method as I was inside of the SetFontSize() method and the pointers were the same.

I will of course keep playing around to see if I can get this to work but from what I am seeing, it should work.

I applied the same concept to some other methods that were being "copied" and I changed them to ref parameters to fix the issue and it worked. It should work for this as well.

Quick update, I took what you said and used a ref parameter instead of an unsafe pointer. I verified that the pointer so far in the call stack as it moves from method to method is the same and it is indeed the same.

The reason I am doing this is because I want to abstract away the unsafe pointers so they don't propagate out further into other parts of the code base. Regular pointers in v2.2.0 were fine, but now with version v3.0.0, I am being forced to use unsafe pointers unless I can come up with something different.

The error is raised at https://github.com/ryancheung/freetype/blob/0c2bdb01a2e1d24a3e592377a6d0822856e10df2/src/base/ftobjs.c#L3413
, means face->size is a null pointer.
And i tried the demo https://github.com/ryancheung/FreeTypeSharp/blob/main/FreeTypeSharp.SkiaDemo/Program.cs#L52 and it works fine.
It is weird in the case. You may check your code to ensure a fine face struct pointer.

The error is raised at https://github.com/ryancheung/freetype/blob/0c2bdb01a2e1d24a3e592377a6d0822856e10df2/src/base/ftobjs.c#L3413 , means face->size is a null pointer. And i tried the demo https://github.com/ryancheung/FreeTypeSharp/blob/main/FreeTypeSharp.SkiaDemo/Program.cs#L52 and it works fine. It is weird in this case. You can check your code to make sure a fine face struct pointer.

I will definitely keep working on it.

In the meantime, I have a quick question.
What does it mean by invalid size? Does it mean that it is a zero pointer? Does it mean that it is 32-bit instead of 64? I am not saying that is one of the issues, but I am just trying to understand what that means. This might help me figure out what is going on.

Thanks!!

Here is the method FT_Set_Char_Size(FT_FaceRec_* face, IntPtr char_width, IntPtr char_height, uint horz_resolution, uint vert_resolution). The type of the size parameter is IntPtr, you should not care of the type, but just give a a valid integer size. Normally you set the char_width to zero.
BTW, the IntPtr is used for size parameter is because the C type is 32bit in x86 binary and 64bit in amd64 binary.

Here is the method FT_Set_Char_Size(FT_FaceRec_* face, IntPtr char_width, IntPtr char_height, uint horz_resolution, uint vert_resolution). The type of the size parameter is IntPtr, you should not care of the type, but just give a a valid integer size. Normally you set the char_width to zero. BTW, the IntPtr is used for size parameter is because the C type is 32bit in x86 binary and 64bit in amd64 binary.

I apologize, but as of right now, I am not sure I understand what the InvalidSize error still means.

Note

I will admit that I have worked with pointers before and written some C++ in my day, but that was long ago.
Most of my years have been spent with C#, so I am not an expert in C. Due to this, I don't think I am going
to be able to get any useful info from the link you gave me to the source code of where the error is thrown.
Not only is it C, which I am not experienced with, but the file is 5869 lines long. 😔

I need more guidance, and I hope you can find the time to provide it.

This is what I have tried and done so far.

I have converted all of the methods associated with the FT_FaceRec_ parameters to ref parameters.
The files below are the files in my project that deal with font:

  1. FontService
  2. FontAtlasService
  3. FreeTypeInvoker
  4. Font

I have made changes in the WIP-4 Velaptor Branch if you are inclined to check out what I have going on.

I tried using 0 as the value in the FreeTypeInvoker.FT_Set_Char_Size() method, but it did not work.

public void FT_Set_Char_Size(ref FT_FaceRec_ face, nint char_width, nint char_height, uint horz_resolution, uint vert_resolution)
{
    EnsureThat.PointerIsNotNull(char_width);
    EnsureThat.PointerIsNotNull(char_height);

    unsafe
    {
        fixed (FT_FaceRec_* facePtr = &face)
        {
            EnsureThat.PointerIsNotNull((nint)facePtr);

-            var error = FT.FT_Set_Char_Size(facePtr, char_width, char_height, horz_resolution, vert_resolution);
+            var error = FT.FT_Set_Char_Size(facePtr, 0, char_height, horz_resolution, vert_resolution);

            if (error != FT_Error.FT_Err_Ok)
            {
                this.OnError?.Invoke(this, new FreeTypeErrorEventArgs(CreateErrorMessage(error.ToString())));
            }
        }
    }
}

What got me thinking about the size is maybe the char_width and char_height values are too large compared to the resolution parameters.

The incoming int size of the char_width and char_height parameters is 768.
The incoming horz_resolution and vert_resolution is 144.

My first thought was that maybe it was failing because the width and height of the character were larger than the resolution. However, I tested this theory by hard coding the values of the char_width and char_height to be 72, so they were less than the resolution, but it still failed.

I am taking the font size in points, which is 12, and bit shifting it to the left 6 places, converting it to a nint, and then using that value for the char_width and char_height parameters. This is the way I have always done it, and it worked just fine, so I don't think that is the issue either.

I am doing that in the FontService class:

public void SetFontSize(ref FT_FaceRec_ face, uint sizeInPoints)
{
    if (sizeInPoints <= 0)
    {
        throw new ArgumentException("The font size must be larger than 0.", nameof(sizeInPoints));
    }

    var sizeInPointsPtr = (nint)(sizeInPoints << 6);

    this.freeTypeInvoker.FT_Set_Char_Size(
        ref face,
        sizeInPointsPtr,
        sizeInPointsPtr,
        (uint)this.sysDisplayService.MainDisplay.HorizontalDPI,
        (uint)this.sysDisplayService.MainDisplay.VerticalDPI);
}

That is all I have for now. Hope to hear from you soon and thank you so much for the help you have provided so far. 😀

I apologize, but as of right now, I am not sure I understand what the InvalidSize error still means.

That said ((FT_FaceRec_ *)face)->size == 0. This does not means you size parameter passed to FT_Set_Char_Size is invalid.

Anyway let me take a look at your code.

https://github.com/KinsonDigital/Velaptor/blob/WIP-4/Testing/VelaptorTests/NativeInterop/FreeType/FreeTypeInvokerTests.cs#L157
Dude, you can't get null pointer of FT_FaceRec_ by FT_FaceRec_ face = default, it's a zero-allocated struct on the stack.

And you can't get null pointer in method like FT_Done_Face(ref FT_FaceRec_ face), ref FT_FaceRec_ face means face is always a allocated struct passed.

I suggest you use IntPtr for FT_FaceRec_ parameter, like FT_Set_Char_Size(nint face, nint char_width, nint char_height, uint horz_resolution, uint vert_resolution), if you want to avoid use unsafe keyword in consumer side.

In your FreeTypeInvoker class, you could use ((FT_FaceRec_*)face) to get data from face struct.
And remember don't try to copy the face pointer to stack-allocated struct, you shall use pointer to get data always to make sure you won't get something weird.

Found the bug code: https://github.com/KinsonDigital/Velaptor/blob/WIP-4/Velaptor/NativeInterop/FreeType/FreeTypeInvoker.cs#L119

    public void FT_New_Face(string filepathname, int face_index, ref FT_FaceRec_ face)
    {
        unsafe
        {
            fixed (FT_FaceRec_* facePtr = &face)
            {
                var error = FT.FT_New_Face(this.library.Native, (byte*)Marshal.StringToHGlobalAnsi(filepathname), face_index, &facePtr);

                if (error != FT_Error.FT_Err_Ok)
                {
                    this.OnError?.Invoke(this, new FreeTypeErrorEventArgs(CreateErrorMessage(error.ToString())));
                }
            }
        }
    }

You won't get a valid FT_FaceRec_* like this. Call method FT_New_Face(FT_LibraryRec_* library, byte* filepathname, nint face_index, FT_FaceRec_** aface) we get a unmanaged pointer stored in aface parameter, the correct way is like:

FT_FaceRec_* facePtr;
FT_New_Face(&lib, filename, 0, &facePtr);

In all other method calls requires facePtr, you give them facePtr which is a pointer address. If you wan't avoid unsafe keyword, just convert it to IntPtr, like nint face = (nint)facePtr.

Remember, you can't do things like FT_FaceRec_ faceStruct = *facePtr, and use ref faceStruct anywhere in your code. Because it will change the original pointer address return by FT_New_Face.

Found the bug code: https://github.com/KinsonDigital/Velaptor/blob/WIP-4/Velaptor/NativeInterop/FreeType/FreeTypeInvoker.cs#L119

    public void FT_New_Face(string filepathname, int face_index, ref FT_FaceRec_ face)
    {
        unsafe
        {
            fixed (FT_FaceRec_* facePtr = &face)
            {
                var error = FT.FT_New_Face(this.library.Native, (byte*)Marshal.StringToHGlobalAnsi(filepathname), face_index, &facePtr);

                if (error != FT_Error.FT_Err_Ok)
                {
                    this.OnError?.Invoke(this, new FreeTypeErrorEventArgs(CreateErrorMessage(error.ToString())));
                }
            }
        }
    }

You won't get a valid FT_FaceRec_* like this. Call method FT_New_Face(FT_LibraryRec_* library, byte* filepathname, nint face_index, FT_FaceRec_** aface) we get a unmanaged pointer stored in aface parameter, the correct way is like:

FT_FaceRec_* facePtr;
FT_New_Face(&lib, filename, 0, &facePtr);

In all other method calls requires facePtr, you give them facePtr which is a pointer address. If you wan't avoid unsafe keyword, just convert it to IntPtr, like nint face = (nint)facePtr.

Remember, you can't do things like FT_FaceRec_ faceStruct = *facePtr, and use ref faceStruct anywhere in your code. Because it will change the original pointer address return by FT_New_Face.

Thanks @ryancheung!!

This worked!! I am up and running again. Thanks for your help. I greatly appreciate it more than you know.
I learned a little something new the past couple of days due to not working with pointers that much in regards
to structs being passed in and out of the methods.

It never really dawned on me due to the subtle fact that the structure is copied by value instead of by reference.
This is why I started passing the FT_FaceRec_ as a reference to make sure that it was the same pointer.

But indeed, the main issue was that one failed point in the method that you discovered.

Great library, by the way. Your library has been vital to what I am doing and has saved me a lot of time by not
having to build the interop library and code myself, and I thank you for that.

The only thing I would recommend that could be a huge help would be to make sure you document the bug fixed,
breaking changes and other related items in your release notes with any guidance so people can make their
changes much quicker.

This is what I do for Velaptor and all of my open-source projects.

Cheers!!

np :). It's actually a bit hassle to handle pointer well in .NET at the beginning. But since the library code is auto-generated, there's no any highlevel abstraction on it, library users must do the hard work to provide their own abstractions based on the library.