• 在GBA上写光线追踪:定点数运算库


    这篇文章是关于我的GBA库lib_hl中数学库的定点数部分。


    定点数是什么?为什么要用定点数?

    在之前的文章中,我已经介绍了GBA的硬件,它的CPU竟然居然理所当然没有浮点数运算单元!

    我要写的是光线追踪程序,基本上都在做精确的数学运算,而这个CPU却连浮点数都不支持,那不是没得玩?

    当然是有方法的:

    1、使用软件浮点数,在软件层面模拟浮点数,但比起硬件浮点数慢了太多,光线追踪是运算密集型程序,这样肯定不行;

    2、使用定点数,在电脑普遍没有浮点运算单元时,大家都是用定点数代替小数运算。定点数运算速度只比整数运算慢几倍,还是可以接受的。

    定点数通过固定小数点位置,使用整数表示小数,与相比浮点数,定点数可表示范围比浮点数小,而且它的表示范围和精度不可兼得。

    关于定点数的详细原理,参见我的另一篇文章(不打算写了),可以百度

    hl_types.h

    最开始写的是一个.h头文件,里面包含了将用到的数据类型和一些常见操作的宏定义。

    无论是在什么程序中,对数据类型进行定义是非常必要的,因为int, long, long long这些类型在不同的编译器中长度是不同的,在32位/64位的情况下也是不同的,为了程序的强适应性,应该使用自己定义的长度可知的数据类型。

    基础数据类型 代码如下:

    typedef signed    char    s8;  //8位有符号整数
    typedef signed    short     s16;  //16位有符号整数
    typedef signed    int     s32;  //32位有符号整数
    typedef signed    long long s64;  //64位有符号整数
    
    typedef unsigned char       u8;   //8位无符号整数
    typedef unsigned short     u16;    //16位无符号整数
    typedef unsigned int       u32;    //32位无符号整数
    typedef unsigned long long u64;  //64位无符号整数
    
    typedef volatile signed      char     vs8;    //易变型8位有符号整数
    typedef volatile signed      short   vs16;    //易变型16位有符号整数
    typedef volatile signed      int     vs32;    //易变型32位有符号整数
    
    typedef volatile unsigned    char     vu8;    //易变型8位无符号整数
    typedef volatile unsigned    short   vu16;    //易变型16位无符号整数
    typedef volatile unsigned    int     vu32;    //易变型32位无符号整数

    然后是一些会随着32/64位系统变化的类型:

    #ifdef _X64
    typedef long long _stype;
    typedef unsigned long long _utype;
    #define _XLEN        8
    #else
    typedef int _stype;
    typedef unsigned int _utype;
    #define _XLEN        4
    #endif
    
    //通用指针类型
    typedef void *t_pointer, *t_ptr;
    //整型地址类型
    typedef _utype t_addr;

    通过在64位环境下预定义一个_X64的宏,可以使_utype在32位时是4字节,64位数时是8字节长。虽然我们的GBA肯定是32位的,但假如我们要把程序迁移到64位电脑上,就要注意指针类型和地址的长度变化。

    然后是一些常用的定义:

    //布尔类型
    typedef int Bool;
    
    #ifndef NULL
    #define NULL        0
    #endif
    
    #ifndef TRUE
    #define TRUE        1
    #endif
    
    #ifndef FALSE
    #define FALSE        0
    #endif
    
    /*内联函数声明*/
    #define _INLINE_ static inline
    
    /*获取元素相对结构体起始地址的偏移*/
    #define _OFFSET(_type,_element) ((t_addr)&(((_type *)0)->_element))
    ...
    #define BIT(n)    (1<<(n))        //第n比特为1 (2^n)
    ...
    #define SETFLAG(v,flag)        v=(v|flag)    //设定Flag
    #define HASFLAG(v,flag)        (v&flag)        //是否有Flag
    #define HASFLAGS(v,flags)    ((v&(flags))==(flags))    //是否有全部flags
    #define NOFLAG(v,flag)        ((v&flag)==0)    //没有Flag
    ...

    其他定义后续我们需要时在补上,现在我们可以开始写数学库了。

    hl_math.h

    文件开头加上:

    #pragma once
    #include <hl_system.h>

    #pragma once是写给编译器看的,意思是这段代码只编译一次。

    之所以要在头文件加这句话,是因为C中引用头文件,是通过直接把头文件的内存复制到#include的位置,如果在多个文件中都包含了同一个头文件,编译时就会导致宏、结构体等被多次定义,引起编译错误。

    另一种适合所有编译器的写法是:

    #ifndef _XXX_H 
    #define _XXX_H 
    代码 ...
    #endif

    定义定点数

    之后开始编写真正的代码,先定义定点数类型:

    //32位定点数
    typedef s32 fp32;
    
    //32位定点数的小数位数 20bit
    //整数大小 -2048-2047,小数精度 0.000001
    #define FP32_FBIT    20
    #define FP32_1 (1<<FP32_FBIT) //fp32 1f #define FP32_H5 (1<<(FP32_FBIT-1)) //fp32 0.5f #define FP32_LIMIT1 (FP32_1-1) //fp32 不到1f的最大值 #define FP32_MAX 2147483647 #define FP32_MIN (-2147483647-1) #define FP32_MAXINT ( (1<<(31-FP32_FBIT))-1) #define FP32_MININT (-(1<<(31-FP32_FBIT))) #define FP32_PI (1686629713>>(29-FP32_FBIT)) #define FP32_SQRT2 (1518500249>>(30-FP32_FBIT)) #define FP32_SQRT3 (1859775393>>(30-FP32_FBIT)) #define FP32_F2(n) (1<<(FP32_FBIT-(n))) //fp32 1/(2^n) //16位定点数 typedef s16 fp16; //16位定点数的小数位数 10bit //整数大小 -32-31,小数精度 0.001 #define FP16_FBIT   10
    #define FP16_1   (1<<FP16_FBIT) #define FP16_H5  (1<<(FP16_FBIT-1)) #define FP16_MAX 32767 #define FP16_MIN   -32768 #define FP16_MAXINT ( (1<<(15-FP16_FBIT))-1) #define FP16_MININT (-(1<<(16-FP16_FBIT)))

    可以看到我的定点数有32位的和16位的,32位叫fp32,主要用于精度要求比较高的大部分运算,16位的叫fp16,主要用于精度低的色彩等运算。

    fp32为了运算精度,给小数部分分配了20位(可以说是非常重视精度),这样小数的分度值是1/220 ,到小数点后6位的精度,而整数只有12位,除去符号位,可表示211=2048,范围就是-2048~2047。

    fp16的位长有效,给小数分配10位,也只有1/210=1/1024也就是0.001的精度,而整数只剩可怜的5位,范围是-32~31。

    除了定义小数位长FBIT,我还定义了一些常见数值的对应的定点数,例如1,0.5,π。可以看到,定点数的1就是1*220,0.5就是0.5*220,这就是定点数的原理。

    同样的原理我们可以写几个转换函数:

    //int -> fp32
    static inline fp32    fp32_int(int n) { return n << FP32_FBIT; }
    //float -> fp32
    //static inline fp32    fp32_float(float f) { return (fp32)(f * (1 << FP32_FBIT)); }
    //int/100 -> fp32
    static inline fp32    fp32_100f(int n) { return (((s64)n << FP32_FBIT) + 50) / 100; }
    //fp32 -> int
    static inline int    int_fp32(fp32 f) { return f >> FP32_FBIT; }

    看完代码对定点数的理解应该也深一些吧。

    所有函数前都加上了static inlineinline是声明这个函数是内联函数,也就是在编译时会被展开,避免函数调用开销,对于我们这种常用且短小的运算函数,当然要加。但inline只是向编译器提个建议,编译器可能不听,如果它觉得这个函数太大,内联不划算,就不内联了。这时这个函数就变成了定义在头文件的普通函数,这会带来一个问题,如果头文件被多次包含会导致函数重定义,所以加上static,声明为静态函数,只是在声明它的文件中可见,避免命名冲突。其实,规范地写,应该使用之前定义的_INLNE_,以防切换到不支持staic inline特性的编译器。

    定点数运算

    之后就是运算函数了。首先是加减运算,和整数运算并无两样。它的运算原理如下:

    假设:

    整数A是小数a的定点数形式,即 A = a*fs (fs = 1<<FBIT)

    整数B是小数b的定点数形式,即 B = b*fs (fs = 1<<FBIT)

    则 定点数A 加 定点数B 的公式是:

     A (+) B = a*fs (+) b*fs = (a+b)*fs = (A/fs+B/fs)*fs = A+B

    //fp32 + fp32 **事实上没有用的必要
    static inline fp32    fp32_add(fp32 a, fp32 b) { return a + b; }
    //fp32 - fp32 **事实上没有用的必要
    static inline fp32    fp32_sub(fp32 a, fp32 b) { return a - b; }

    然后是乘除法:

     先看代码,区别是乘完后需要缩小2FBIT,除完后需要放大2FBIT

    //fp32 * fp32 (64位安全运算)
    static inline fp32    fp32_mul64(fp32 a, fp32 b)
    { return (((s64)a) * b) >> FP32_FBIT; }
    
    //fp32 / fp32 (64位安全运算) *b<1仍可能溢出
    static inline fp32    fp32_div64(fp32 a, fp32 b)
    { return (((s64)a) << FP32_FBIT) / b; }

    定点数A乘定点数B的推导过程:

     A (x) B = (a*b)*fs = (A/fs)*(B/fs)*fs = (A*B)/fs

    定点数A除定点数B的推导过程:

     A (÷) B = (a/b)*fs = (A/fs)/(B/fs)*fs = (A/B)*fs

    不难理解,定点数是小数乘了2FBIT得到的,如果两个定点数相乘,两次2FBIT就累积了,所以要除去一次2FBIT

    之后是一些常用的函数:

    //fp32^2
    static inline fp32    fp32_pow2(fp32 a)
    { return (((s64)a) * a) >> FP32_FBIT; }
    //返回结果是u64
    static inline u64        fp32_pow2_64(fp32 a)
    { return (((s64)a) * a) >> FP32_FBIT; }
    
    static inline fp32    fp32_lerp(fp32 a, fp32 b, fp32 t)
    { return a + fp32_mul64(b - a, t); }

    下一部分 数学函数库 也会包含一些定点数常用函数,例如开方和三角函数。

    这里只列出小部分,其他若有需要请看源码。

  • 相关阅读:
    第二十五节:Java语言基础-面向对象基础
    第二十五节:Java语言基础-面向对象基础
    第二十五节:Java语言基础-面向对象基础
    第二十四节:Java语言基础-讲解数组的综合应用
    第二十四节:Java语言基础-讲解数组的综合应用
    第二十四节:Java语言基础-讲解数组的综合应用
    第二十三节:Java语言基础-详细讲解函数与数组
    第二十三节:Java语言基础-详细讲解函数与数组
    第二十三节:Java语言基础-详细讲解函数与数组
    第二十二节:Java语言基础-详细讲解位运算符与流程控制语句
  • 原文地址:https://www.cnblogs.com/h5l0/p/lib_hl_fixed-point.html
Copyright © 2020-2023  润新知