• 输入系统:进程间双向通信(socketpair+binder)


     一、双向通信(socketpair)

     socketpair()函数用于创建一对无名的、相互连接的套接子,如果函数成功,则返回0,创建好的套接字分别是sv[0]和sv[1];否则返回-1,错误码保存于errno中。

    • socketpair()函数的声明:
        #include <sys/types.h>
        #include <sys/socket.h>
        int socketpair(int domain, int type, int protocol, int sv[2]);
    • 参数说明:

    参数1(domain):表示协议族,在Linux下只能为AF_LOCAL或者AF_UNIX。(自从Linux 2.6.27后也支持SOCK_NONBLOCK和SOCK_CLOEXEC)
    参数2(type):表示协议,可以是SOCK_STREAM或者SOCK_DGRAM。SOCK_STREAM是基于TCP的,而SOCK_DGRAM是基于UDP的
    参数3(protocol):表示类型,只能为0
    参数4(sv[2]):套节字柄对,该两个句柄作用相同,均能进行读写双向操作

    返回errno含义:

       EAFNOSUPPORT:本机上不支持指定的address。
    
       EFAULT: 地址sv无法指向有效的进程地址空间内。
    
       EMFILE: 已经达到了系统限制文件描述符,或者该进程使用过量的描述符。
    
       EOPNOTSUPP:指定的协议不支持创建套接字对。
    
       EPROTONOSUPPORT:本机不支持指定的协议。
    • 注意:

    1.该函数只能用于UNIX域(LINUX)下。
    2.只能用于有亲缘关系的进程(或线程)间通信。
    3.所创建的套节字对作用是一样的,均能够可读可写(而管道PIPE只能进行单向读或写)。
    4.这对套接字可以用于全双工通信,每一个套接字既可以读也可以写。例如,可以往sv[0]中写,从sv[1]中读;或者从sv[1]中写,从sv[0]中读;
    5.该函数是阻塞的,且如果往一个套接字(如sv[0])中写入后,再从该套接字读时会阻塞,只能在另一个套接字中(sv[1])上读成功;
    6. 读、写操作可以位于同一个进程,也可以分别位于不同的进程,如父子进程。如果是父子进程时,一般会功能分离,一个进程用来读,一个用来写。因为文件描述副sv[0]和sv[1]是进程共享的,所以读的进程要关闭写描述符, 反之,写的进程关闭读描述符。

     

    示例代码1(同一进程不同线程间通信):

    #include <pthread.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <sys/types.h>          /* See NOTES */
    #include <sys/socket.h>
    
    #define SOCKET_BUFFER_SIZE      (32768U)
    
    
    /* 参考:
     * frameworks
    ativelibsinputInputTransport.cpp
     */
    
    void *function_thread1 (void *arg)
    {
        int fd = (int)arg;
        char buf[500];
        int len;
        int cnt = 0;
        
        while (1)
        {
            /* 向 main线程发出: Hello, main thread  */
            len = sprintf(buf, "Hello, main thread, cnt = %d", cnt++);
            write(fd, buf, len);
    
            /* 读取数据(main线程发回的数据) */
            len = read(fd, buf, 500);
            buf[len] = '';
            printf("%s
    ", buf);
    
            sleep(5);
        }
        
        return NULL;
    }
    
    
    int main(int argc, char **argv)
    {
        int sockets[2];
    
        socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets);
    
        int bufferSize = SOCKET_BUFFER_SIZE;
        setsockopt(sockets[0], SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(bufferSize));
        setsockopt(sockets[0], SOL_SOCKET, SO_RCVBUF, &bufferSize, sizeof(bufferSize));
        setsockopt(sockets[1], SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(bufferSize));
        setsockopt(sockets[1], SOL_SOCKET, SO_RCVBUF, &bufferSize, sizeof(bufferSize));
        
        /* 创建线程1 */
        pthread_t threadID;
        pthread_create(&threadID, NULL, function_thread1, (void *)sockets[1]);
    
    
        char buf[500];
        int len;
        int cnt = 0;
        int fd = sockets[0];
    
        while(1)
        {
            /* 读数据: 线程1发出的数据 */
            len = read(fd, buf, 500);
            buf[len] = '';
            printf("%s
    ", buf);
            
            /* main thread向thread1 发出: Hello, thread1 */
            len = sprintf(buf, "Hello, thread1, cnt = %d", cnt++);
            write(fd, buf, len);
        }
    }

    参考Android源码:

    frameworks
    ativelibsinputInputTransport.cpp  (socketpair)
    调用过程:
    WindowManagerService.java
        InputChannel.openInputChannelPair(name)
            nativeOpenInputChannelPair(name);
                android_view_InputChannel_nativeOpenInputChannelPair
                    InputChannel::openInputChannelPair (InputTransport.cpp)

     

     示例代码2
    (父子进程间通信):

    #include <stdio.h> 
    #include <string.h> 
    #include <unistd.h> 
    #include <sys/types.h> 
    #include <error.h> 
    #include <errno.h> 
    #include <sys/socket.h> 
    #include <stdlib.h> 
     
    const char* str = "SOCKET PAIR TEST.";
     
    int main(int argc, char* argv[]){
        char buf[128] = {0};
        int socket_pair[2]; 
        pid_t pid; 
     
        if(socketpair(AF_UNIX, SOCK_STREAM, 0, socket_pair) == -1 ) { 
            printf("Error, socketpair create failed, errno(%d): %s
    ", errno, strerror(errno));
            return EXIT_FAILURE; 
        } 
     
        pid = fork(); //创建子进程
        if(pid < 0) {
            printf("Error, fork failed, errno(%d): %s
    ", errno, strerror(errno));
            return EXIT_FAILURE;
        } else if(pid > 0) { //父进程
            //关闭另外一个套接字
            close(socket_pair[1]);
            int size = write(socket_pair[0], str, strlen(str));
            printf("Write success, pid: %d
    ", getpid());
     
        } else if(pid == 0) { //子进程
            //关闭另外一个套接字
            close(socket_pair[0]);
            read(socket_pair[1], buf, sizeof(buf));        
            printf("Read result: %s, pid: %d
    ",buf, getpid());
        }
     
        for(;;) {
            sleep(1);
        }
     
        return EXIT_SUCCESS;    
    } 

    、任意进程间通信(socketpair_binder)

     Android系统中进程间通信方式主要为Binder,而Binder通信方式中,Client端可以主动发起请求与Server端通信,但Server端无法向Client端主动发起请求,基于socketpair + binder可以实现任意进程间的双向通信(通过binder将fd句柄传到另一个非亲缘关系的进程),代码实现如下:

    1.IHelloService.h (Hello服务接口定义)

    /* 参考: frameworksavincludemediaIMediaPlayerService.h */
    
    #ifndef ANDROID_IHELLOERVICE_H
    #define ANDROID_IHELLOERVICE_H
    
    #include <utils/Errors.h>  // for status_t
    #include <utils/KeyedVector.h>
    #include <utils/RefBase.h>
    #include <utils/String8.h>
    #include <binder/IInterface.h>
    #include <binder/Parcel.h>
    
    #define HELLO_SVR_CMD_SAYHELLO     1
    #define HELLO_SVR_CMD_SAYHELLO_TO  2
    #define HELLO_SVR_CMD_GET_FD       3
    
    
    namespace android {
    
    class IHelloService: public IInterface
    {
    public:
        DECLARE_META_INTERFACE(HelloService);
        virtual void sayhello(void) = 0;
        virtual int sayhello_to(const char *name) = 0;
        virtual int get_fd(void) = 0;
    };
    
    class BnHelloService: public BnInterface<IHelloService>
    {
    private:
        int fd;
    public:
        virtual status_t    onTransact( uint32_t code,
                                        const Parcel& data,
                                        Parcel* reply,
                                        uint32_t flags = 0);
    
        virtual void sayhello(void);
        virtual int sayhello_to(const char *name);
        virtual int get_fd(void);
    
        BnHelloService();
        BnHelloService(int fd);
    
    };
    }
    
    #endif

    2.IGoodbyeService.h (Goodbye服务接口定义)

    /* 参考: frameworksavincludemediaIMediaPlayerService.h */
    
    #ifndef ANDROID_IGOODBYEERVICE_H
    #define ANDROID_IGOODBYEERVICE_H
    
    #include <utils/Errors.h>  // for status_t
    #include <utils/KeyedVector.h>
    #include <utils/RefBase.h>
    #include <utils/String8.h>
    #include <binder/IInterface.h>
    #include <binder/Parcel.h>
    
    #define GOODBYE_SVR_CMD_SAYGOODBYE     1
    #define GOODBYE_SVR_CMD_SAYGOODBYE_TO  2
    
    
    namespace android {
    
    class IGoodbyeService: public IInterface
    {
    public:
        DECLARE_META_INTERFACE(GoodbyeService);
        virtual void saygoodbye(void) = 0;
        virtual int saygoodbye_to(const char *name) = 0;
    };
    
    class BnGoodbyeService: public BnInterface<IGoodbyeService>
    {
    public:
        virtual status_t    onTransact( uint32_t code,
                                        const Parcel& data,
                                        Parcel* reply,
                                        uint32_t flags = 0);
    
        virtual void saygoodbye(void);
        virtual int saygoodbye_to(const char *name);
    
    };
    }
    
    #endif

    3.BnHelloService.cpp (Hello服务本地类)

    /* 参考: frameworksavmedialibmediaIMediaPlayerService.cpp */
    
    #define LOG_TAG "HelloService"
    
    #include "IHelloService.h"
    
    
    namespace android {
    
    BnHelloService::BnHelloService()
    {
    }
    
    BnHelloService::BnHelloService(int fd)
    {
        this->fd = fd;
    }
    
    status_t BnHelloService::onTransact( uint32_t code,
                                    const Parcel& data,
                                    Parcel* reply,
                                    uint32_t flags)
    {
        /* 解析数据,调用sayhello/sayhello_to */
    
        switch (code) {
            case HELLO_SVR_CMD_SAYHELLO: {
                sayhello();
                reply->writeInt32(0);  /* no exception */
                return NO_ERROR;
            } break;
            
            case HELLO_SVR_CMD_SAYHELLO_TO: {
    
                /* 从data中取出参数 */
                int32_t policy =  data.readInt32();
                String16 name16_tmp = data.readString16(); /* IHelloService */
                
                String16 name16 = data.readString16();
                String8 name8(name16);
    
                int cnt = sayhello_to(name8.string());
    
                /* 把返回值写入reply传回去 */
                reply->writeInt32(0);  /* no exception */
                reply->writeInt32(cnt);
                
                return NO_ERROR;
            } break;
    
            case HELLO_SVR_CMD_GET_FD: {
                int fd = this->get_fd();
                reply->writeInt32(0);  /* no exception */
    
                /* 参考:
                 * frameworksasecorejniandroid_view_InputChannel.cpp
                 * android_view_InputChannel_nativeWriteToParcel
                 */
                reply->writeDupFileDescriptor(fd);
                return NO_ERROR;
            } break;
    
            
            default:
                return BBinder::onTransact(code, data, reply, flags);
        }
    }
    
    void BnHelloService::sayhello(void)
    {
        static int cnt = 0;
        ALOGI("say hello : %d
    ", ++cnt);
    
    }
    
    int BnHelloService::sayhello_to(const char *name)
    {
        static int cnt = 0;
        ALOGI("say hello to %s : %d
    ", name, ++cnt);
        return cnt;
    }
    
    int BnHelloService::get_fd(void)
    {
        return fd;
    }
    
    
    }

    4.BnGoodbyeService.cpp (Goodbye服务本地类)

    /* 参考: frameworksavmedialibmediaIMediaPlayerService.cpp */
    
    #define LOG_TAG "GoodbyeService"
    
    #include "IGoodbyeService.h"
    
    
    namespace android {
    
    status_t BnGoodbyeService::onTransact( uint32_t code,
                                    const Parcel& data,
                                    Parcel* reply,
                                    uint32_t flags)
    {
        /* 解析数据,调用saygoodbye/saygoodbye_to */
    
        switch (code) {
            case GOODBYE_SVR_CMD_SAYGOODBYE: {
            saygoodbye();
                reply->writeInt32(0);  /* no exception */
                return NO_ERROR;
            } break;
            
            case GOODBYE_SVR_CMD_SAYGOODBYE_TO: {
    
                /* 从data中取出参数 */
                int32_t policy =  data.readInt32();
                String16 name16_tmp = data.readString16(); /* IGoodbyeService */
                
                String16 name16 = data.readString16();
                String8 name8(name16);
    
                int cnt = saygoodbye_to(name8.string());
    
                /* 把返回值写入reply传回去 */
                reply->writeInt32(0);  /* no exception */
                reply->writeInt32(cnt);
                
                return NO_ERROR;
            } break;
            default:
                return BBinder::onTransact(code, data, reply, flags);
        }
    }
    
    void BnGoodbyeService::saygoodbye(void)
    {
        static int cnt = 0;
        ALOGI("say goodbye : %d
    ", ++cnt);
    
    }
    
    int BnGoodbyeService::saygoodbye_to(const char *name)
    {
        static int cnt = 0;
        ALOGI("say goodbye to %s : %d
    ", name, ++cnt);
        return cnt;
    }
    
    }

    5.BpHelloService.cpp (Hello服务代理类)

    /* 参考: frameworksavmedialibmediaIMediaPlayerService.cpp */
    
    #include "IHelloService.h"
    
    namespace android {
    
    class BpHelloService: public BpInterface<IHelloService>
    {
    public:
        BpHelloService(const sp<IBinder>& impl)
            : BpInterface<IHelloService>(impl)
        {
        }
    
        void sayhello(void)
        {
            /* 构造/发送数据 */
    
            Parcel data, reply;
            data.writeInt32(0);
            data.writeString16(String16("IHelloService"));
    
            remote()->transact(HELLO_SVR_CMD_SAYHELLO, data, &reply);
        }
        
        int sayhello_to(const char *name)
        {
            /* 构造/发送数据 */
            Parcel data, reply;
            int exception;
    
            data.writeInt32(0);
            data.writeString16(String16("IHelloService"));
    
            data.writeString16(String16(name));
    
            remote()->transact(HELLO_SVR_CMD_SAYHELLO_TO, data, &reply);
    
            exception = reply.readInt32();
            if (exception)
                return -1;
            else
                return reply.readInt32();
        }
    
        int get_fd(void)
        {
            /* 构造/发送数据 */
            Parcel data, reply;
            int exception;
    
            data.writeInt32(0);
            data.writeString16(String16("IHelloService"));
    
            remote()->transact(HELLO_SVR_CMD_GET_FD, data, &reply);
    
            exception = reply.readInt32();
            if (exception)
                return -1;
            else
            {
    
                /* 参考:
                 * frameworksasecorejniandroid_view_InputChannel.cpp
                 * android_view_InputChannel_nativeReadFromParcel
                 */
                int rawFd = reply.readFileDescriptor();
                return dup(rawFd); //复制句柄,reply中析构会关闭原始句柄
            }
        }
    
    
    };
    
    IMPLEMENT_META_INTERFACE(HelloService, "android.media.IHelloService");
    
    }

    6.BpGoodbyeService.cpp (Goodbye服务代理类)

    /* 参考: frameworksavmedialibmediaIMediaPlayerService.cpp */
    
    #include "IGoodbyeService.h"
    
    namespace android {
    
    class BpGoodbyeService: public BpInterface<IGoodbyeService>
    {
    public:
        BpGoodbyeService(const sp<IBinder>& impl)
            : BpInterface<IGoodbyeService>(impl)
        {
        }
    
        void saygoodbye(void)
        {
            /* 构造/发送数据 */
    
            Parcel data, reply;
            data.writeInt32(0);
            data.writeString16(String16("IGoodbyeService"));
    
            remote()->transact(GOODBYE_SVR_CMD_SAYGOODBYE, data, &reply);
        }
        
        int saygoodbye_to(const char *name)
        {
            /* 构造/发送数据 */
            Parcel data, reply;
            int exception;
    
            data.writeInt32(0);
            data.writeString16(String16("IGoodbyeService"));
            
            data.writeString16(String16(name));
    
            remote()->transact(GOODBYE_SVR_CMD_SAYGOODBYE_TO, data, &reply);
    
            exception = reply.readInt32();
            if (exception)
                return -1;
            else
                return reply.readInt32();
            }
    
    };
    
    IMPLEMENT_META_INTERFACE(GoodbyeService, "android.media.IGoodbyeService");
    
    }

    7.test_server.cpp

    /* 参考: frameworksavmediamediaserverMain_mediaserver.cpp */
    
    #define LOG_TAG "TestService"
    //#define LOG_NDEBUG 0
    
    #include <fcntl.h>
    #include <sys/prctl.h>
    #include <sys/wait.h>
    #include <binder/IPCThreadState.h>
    #include <binder/ProcessState.h>
    #include <binder/IServiceManager.h>
    #include <cutils/properties.h>
    #include <utils/Log.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <sys/socket.h>
    
    #include "IHelloService.h"
    #include "IGoodbyeService.h"
    
    #define SOCKET_BUFFER_SIZE      (32768U)
    
    using namespace android;
    
    /* 参考:
     * http://blog.csdn.net/linan_nwu/article/details/8222349
     */
    class MyThread: public Thread {  
    private:
        int fd;
    public:  
        MyThread() {}
        MyThread(int fd) { this->fd = fd; }
     
            
        //如果返回true,循环调用此函数,返回false下一次不会再调用此函数  
        bool threadLoop()
        {
            char buf[500];
            int len;
            int cnt = 0;
            
            while(1)
            {
                /* 读数据: test_client发出的数据 */
                len = read(fd, buf, 500);
                buf[len] = '';
                ALOGI("%s
    ", buf);
                
                /* 向 test_client 发出: Hello, test_client */
                len = sprintf(buf, "Hello, test_client, cnt = %d", cnt++);
                write(fd, buf, len);
            }
            
               return true;  
        }
      
    };  
    
    
    /* usage : test_server  */
    int main(void)
    {
    
        int sockets[2];
    
        socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets);
    
        int bufferSize = SOCKET_BUFFER_SIZE;
        setsockopt(sockets[0], SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(bufferSize));
        setsockopt(sockets[0], SOL_SOCKET, SO_RCVBUF, &bufferSize, sizeof(bufferSize));
        setsockopt(sockets[1], SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(bufferSize));
        setsockopt(sockets[1], SOL_SOCKET, SO_RCVBUF, &bufferSize, sizeof(bufferSize));
    
        /* 创建一个线程, 用于跟test_client使用socketpiar通信 */
        sp<MyThread> th = new MyThread(sockets[0]);
        th->run();  
    
    
        /* addService */
    
        /* while(1){ read data, 解析数据, 调用服务函数 } */
    
        /* 打开驱动, mmap */
        sp<ProcessState> proc(ProcessState::self());
    
        /* 获得BpServiceManager */
        sp<IServiceManager> sm = defaultServiceManager();
    
        sm->addService(String16("hello"), new BnHelloService(sockets[1])); //传入句柄,Hello服务类的构造函数中会保存
        sm->addService(String16("goodbye"), new BnGoodbyeService());
    
        /* 循环体 */
        ProcessState::self()->startThreadPool();
        IPCThreadState::self()->joinThreadPool();
    
        return 0;
    }

    8.test_client.cpp

    #define LOG_TAG "TestService"
    //#define LOG_NDEBUG 0
    
    #include <fcntl.h>
    #include <sys/prctl.h>
    #include <sys/wait.h>
    #include <binder/IPCThreadState.h>
    #include <binder/ProcessState.h>
    #include <binder/IServiceManager.h>
    #include <cutils/properties.h>
    #include <utils/Log.h>
    #include <unistd.h>
    
    #include "IHelloService.h"
    #include "IGoodbyeService.h"
    
    using namespace android;
    
    /* ./test_client <hello|goodbye>
     * ./test_client <readfile>
     * ./test_client <hello|goodbye> <name>
     */
    int main(int argc, char **argv)
    {
        int cnt;
        
        if (argc < 2){
            ALOGI("Usage:
    ");
            ALOGI("%s <readfile>
    ", argv[0]);
            ALOGI("%s <hello|goodbye>
    ", argv[0]);
            ALOGI("%s <hello|goodbye> <name>
    ", argv[0]);
            return -1;
        }
    
        /* getService */
        /* 打开驱动, mmap */
        sp<ProcessState> proc(ProcessState::self());
    
        /* 获得BpServiceManager */
        sp<IServiceManager> sm = defaultServiceManager();
    
        if (strcmp(argv[1], "hello") == 0)
        {
    
            sp<IBinder> binder =
                sm->getService(String16("hello"));
    
            if (binder == 0)
            {
                ALOGI("can't get hello service
    ");
                return -1;
            }
    
            /* service肯定是BpHelloServie指针 */
            sp<IHelloService> service =
                interface_cast<IHelloService>(binder);
    
    
            /* 调用Service的函数 */
            if (argc < 3) {
                service->sayhello();
                ALOGI("client call sayhello");
            }
            else {
                cnt = service->sayhello_to(argv[2]);
                ALOGI("client call sayhello_to, cnt = %d", cnt);
            }
        }
        else if (strcmp(argv[1], "readfile") == 0)
        {
    
            sp<IBinder> binder =
                sm->getService(String16("hello"));
    
            if (binder == 0)
            {
                ALOGI("can't get hello service
    ");
                return -1;
            }
    
            /* service肯定是BpHelloServie指针 */
            sp<IHelloService> service =
                interface_cast<IHelloService>(binder);
    
    
            /* 调用Service的函数 */
            int fd = service->get_fd();
    
            ALOGI("client call get_fd = %d", fd);
    
            char buf[500];
            int len;
            int cnt = 0;
            
            while (1)
            {
                /* 向 test_server 进程发出: Hello, test_server    */
                len = sprintf(buf, "Hello, test_server, cnt = %d", cnt++);
                write(fd, buf, len);
            
                /* 读取数据(test_server进程发回的数据) */
                len = read(fd, buf, 500);
                buf[len] = '';
                ALOGI("%s
    ", buf);
            
                sleep(5);
            }
        }
        else
        {
    
            sp<IBinder> binder =
                sm->getService(String16("goodbye"));
    
            if (binder == 0)
            {
                ALOGI("can't get goodbye service
    ");
                return -1;
            }
    
            /* service肯定是BpGoodbyeServie指针 */
            sp<IGoodbyeService> service =
                interface_cast<IGoodbyeService>(binder);
    
    
            /* 调用Service的函数 */
            if (argc < 3) {
                service->saygoodbye();
                ALOGI("client call saygoodbye");
            }
            else {
                cnt = service->saygoodbye_to(argv[2]);
                ALOGI("client call saygoodbye_to, cnt = %d", cnt);
            }
        }
        
        return 0;
    }

    9.Android.mk

    LOCAL_PATH:= $(call my-dir)
    
    include $(CLEAR_VARS)
    
    LOCAL_SRC_FILES:= 
        BnHelloService.cpp 
        BpHelloService.cpp 
        BnGoodbyeService.cpp 
        BpGoodbyeService.cpp 
        test_server.cpp
    
    LOCAL_SHARED_LIBRARIES := 
        libcutils 
        libutils 
        liblog 
        libbinder 
    
    
    LOCAL_MODULE:= test_server
    LOCAL_32_BIT_ONLY := true
    
    include $(BUILD_EXECUTABLE)
    
    include $(CLEAR_VARS)
    
    LOCAL_SRC_FILES:= 
        BpHelloService.cpp 
        BpGoodbyeService.cpp 
        test_client.cpp
    
    LOCAL_SHARED_LIBRARIES := 
        libcutils 
        libutils 
        liblog 
        libbinder 
    
    
    LOCAL_MODULE:= test_client
    LOCAL_32_BIT_ONLY := true
    
    include $(BUILD_EXECUTABLE)

    参考代码:

    frameworksbasecorejniandroid_view_InputChannel.cpp (用binder传文件句柄)
    server端写fd: android_view_InputChannel_nativeWriteToParcel
                          parcel->writeDupFileDescriptor
    client端读fd: android_view_InputChannel_nativeReadFromParcel
                        int rawFd = parcel->readFileDescriptor();
                        int dupFd = dup(rawFd);               
              
    frameworks
    ativelibsinderParcel.cpp



     -end-

  • 相关阅读:
    IDirect3DDevice9::SetTexture的stage参数
    c++ 返回对象的引用要小心
    c++ 头文件循环引用解法
    Real-Time Rendering.3rd,Radiance与距离无关 的解释
    0xffff0000颜色表示
    signed distance field 算法
    c++ abs与fabs
    unity, OnTriggerEnter2D不触发
    unity, particle play once and destroy
    装机人员工具
  • 原文地址:https://www.cnblogs.com/blogs-of-lxl/p/10542654.html
Copyright © 2020-2023  润新知