Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[rtextures] Fix HalfToFloat() and FloatToHalf() dereferencing issues with an union #4729

Merged
merged 3 commits into from
Jan 26, 2025

Conversation

asdqwe
Copy link

@asdqwe asdqwe commented Jan 26, 2025

Attempts to fix #4728 using @Not-Nik suggestion (#4728 (comment)) of using an union.

This change was tested with the following test on Linux Mint 22.0:

Both versions were tested side by side to confirm they would return the same results:

// NOTE: Compile this with -02.

#include <stdio.h>

// Snippet from 'rtextures.c' ------------------------------------------

    // Convert half-float (stored as unsigned short) to float
    // REF: https://stackoverflow.com/questions/1659440/32-bit-to-16-bit-floating-point-conversion/60047308#60047308
    static float aHalfToFloat(unsigned short x)
    {
        float result = 0.0f;

        const unsigned int e = (x & 0x7C00) >> 10; // Exponent
        const unsigned int m = (x & 0x03FF) << 13; // Mantissa
        const float fm = (float)m;
        const unsigned int v = (*(unsigned int *)&fm) >> 23; // Evil log2 bit hack to count leading zeros in denormalized format
        const unsigned int r = (x & 0x8000) << 16 | (e != 0)*((e + 112) << 23 | m) | ((e == 0)&(m != 0))*((v - 37) << 23 | ((m << (150 - v)) & 0x007FE000)); // sign : normalized : denormalized

        result = *(float *)&r;

        return result;
    }

    // Convert float to half-float (stored as unsigned short)
    static unsigned short aFloatToHalf(float x)
    {
        unsigned short result = 0;

        const unsigned int b = (*(unsigned int *) & x) + 0x00001000; // Round-to-nearest-even: add last bit after truncated mantissa
        const unsigned int e = (b & 0x7F800000) >> 23; // Exponent
        const unsigned int m = b & 0x007FFFFF; // Mantissa; in line below: 0x007FF000 = 0x00800000-0x00001000 = decimal indicator flag - initial rounding

        result = (b & 0x80000000) >> 16 | (e > 112)*((((e - 112) << 10) & 0x7C00) | m >> 13) | ((e < 113) & (e > 101))*((((0x007FF000 + m) >> (125 - e)) + 1) >> 1) | (e > 143)*0x7FFF; // sign : normalized : denormalized : saturate

        return result;
    }

// ---------------------------------------------------------------------

// This PR change on 'rtextures.c' -------------------------------------

    union floatUnsignedUnion {
        float fm;
        unsigned int ui;
    };

    // Convert half-float (stored as unsigned short) to float
    // REF: https://stackoverflow.com/questions/1659440/32-bit-to-16-bit-floating-point-conversion/60047308#60047308
    static float bHalfToFloat(unsigned short x)
    {
        float result = 0.0f;

        union floatUnsignedUnion uni;
        uni.fm = 0.0f;

        const unsigned int e = (x & 0x7C00) >> 10; // Exponent
        const unsigned int m = (x & 0x03FF) << 13; // Mantissa
        uni.fm = (float)m;
        const unsigned int v = uni.ui >> 23; // Evil log2 bit hack to count leading zeros in denormalized format
        uni.ui = (x & 0x8000) << 16 | (e != 0)*((e + 112) << 23 | m) | ((e == 0)&(m != 0))*((v - 37) << 23 | ((m << (150 - v)) & 0x007FE000)); // sign : normalized : denormalized

        result = uni.fm;

        return result;
    }

    // Convert float to half-float (stored as unsigned short)
    static unsigned short bFloatToHalf(float x)
    {
        unsigned short result = 0;

        union floatUnsignedUnion uni;
        uni.fm = x;

        const unsigned int b = uni.ui + 0x00001000; // Round-to-nearest-even: add last bit after truncated mantissa
        const unsigned int e = (b & 0x7F800000) >> 23; // Exponent
        const unsigned int m = b & 0x007FFFFF; // Mantissa; in line below: 0x007FF000 = 0x00800000-0x00001000 = decimal indicator flag - initial rounding

        result = (b & 0x80000000) >> 16 | (e > 112)*((((e - 112) << 10) & 0x7C00) | m >> 13) | ((e < 113) & (e > 101))*((((0x007FF000 + m) >> (125 - e)) + 1) >> 1) | (e > 143)*0x7FFF; // sign : normalized : denormalized : saturate

        return result;
    }

// ---------------------------------------------------------------------

int main(void) {

    float f;
    unsigned short us;

    printf("\ncurrent master branch:\n");

    f = 0.0f; us = aFloatToHalf(f); printf("aFloatToHalf(0.0f):%d\n", us);
    f = 100.0f; us = aFloatToHalf(f); printf("aFloatToHalf(100.0f):%d\n", us);
    f = 1000.0f; us = aFloatToHalf(f); printf("aFloatToHalf(1000.0f):%d\n", us);
    f = 10000.0f; us = aFloatToHalf(f); printf("aFloatToHalf(10000.0f):%d\n", us);

    us = 0; f = aHalfToFloat(us); printf("aHalfToFloat(0):%f\n", f);
    us = 22080; f = aHalfToFloat(us); printf("aHalfToFloat(22080):%f\n", f);
    us = 25552; f = aHalfToFloat(us); printf("aHalfToFloat(25552):%f\n", f);
    us = 28898; f = aHalfToFloat(us); printf("aHalfToFloat(28898):%f\n", f);

    printf("\nthis PR:\n");

    f = 0.0f; us = bFloatToHalf(f); printf("bFloatToHalf(0.0f):%d\n", us);
    f = 100.0f; us = bFloatToHalf(f); printf("bFloatToHalf(100.0f):%d\n", us);
    f = 1000.0f; us = bFloatToHalf(f); printf("bFloatToHalf(1000.0f):%d\n", us);
    f = 10000.0f; us = bFloatToHalf(f); printf("bFloatToHalf(10000.0f):%d\n", us);

    us = 0; f = bHalfToFloat(us); printf("bHalfToFloat(0):%f\n", f);
    us = 22080; f = bHalfToFloat(us); printf("bHalfToFloat(22080):%f\n", f);
    us = 25552; f = bHalfToFloat(us); printf("bHalfToFloat(25552):%f\n", f);
    us = 28898; f = bHalfToFloat(us); printf("bHalfToFloat(28898):%f\n", f);

    return 0;

}

Which produces the following results:

current master branch:
aFloatToHalf(0.0f):0
aFloatToHalf(100.0f):22080
aFloatToHalf(1000.0f):25552
aFloatToHalf(10000.0f):28898
aHalfToFloat(0):0.000000
aHalfToFloat(22080):100.000000
aHalfToFloat(25552):1000.000000
aHalfToFloat(28898):10000.000000

this PR:
bFloatToHalf(0.0f):0
bFloatToHalf(100.0f):22080
bFloatToHalf(1000.0f):25552
bFloatToHalf(10000.0f):28898
bHalfToFloat(0):0.000000
bHalfToFloat(22080):100.000000
bHalfToFloat(25552):1000.000000
bHalfToFloat(28898):10000.000000

Note: @Not-Nik If you can, could you please review this? I don't want to accidentaly break 16-Bit HDR.

@Not-Nik
Copy link
Contributor

Not-Nik commented Jan 26, 2025

Looks good to me, although a can't test test it right now.
For reference, this is where the type punning behaviour is described: https://en.cppreference.com/w/c/language/union#Explanation

src/rtextures.c Outdated Show resolved Hide resolved
@raysan5
Copy link
Owner

raysan5 commented Jan 26, 2025

@asdqwe @Not-Nik I'd really prefer to avoid defining an union to address this issue, is there no other alternative?

@Not-Nik
Copy link
Contributor

Not-Nik commented Jan 26, 2025

Unfortunately I dont think so, but the union can be anonymous, as done here: https://stackoverflow.com/a/44611722

@raysan5
Copy link
Owner

raysan5 commented Jan 26, 2025

@Not-Nik If there is no other option, I'd keep the union internal to the functions...

@orcmid
Copy link
Contributor

orcmid commented Jan 26, 2025

@raysan5 @Not-Nik

@Not-Nik If there is no other option, I'd keep the union internal to the functions...

Well, the functions are already static so that is some localization.

I am rather dismayed how many unchecked assumptions there are around the nature of short, float, and union without any reliance (or checking) on limits.h and float.h, (and rounding method for that matter).

I don't expect anything to be done about this. It's just dismaying with respect to the C Language ISO standards. I'm not invested enough to do anything about it but wring my hands at this stage of life.

@raysan5 raysan5 merged commit f6f31a9 into raysan5:master Jan 26, 2025
14 checks passed
@raysan5
Copy link
Owner

raysan5 commented Jan 26, 2025

@asdqwe @Not-Nik thanks for the review!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[rtextures] HalfToFloat() and FloatToHalf() dereferencing issues
4 participants