精华内容
下载资源
问答
  • 在文章缓冲技术中提到无论是单缓冲、双缓冲或循环缓冲,均仅是进程专属缓冲配备,而一旦考虑到操作系统的分时并行特性,任一时刻只有一个进程的缓冲体系在工作之中,而其他进程的缓冲体系并不在工作(要么是迁移到...

    在文章缓冲技术中提到无论是单缓冲、双缓冲或循环缓冲,均仅是进程专属缓冲配备,而一旦考虑到操作系统的分时并行特性,任一时刻只有一个进程的缓冲体系在工作之中,而其他进程的缓冲体系并不在工作(要么是迁移到swap cache外设磁盘上要么是占据物理内存,显著减少可用物理内存容量),所以采用公用缓冲池架构为多个并发进程提供缓冲服务是当前主流的手段(操作系统、数据库)。

    由于完整地提供操作系统级别的缓冲池涉及到较多的内核知识,复杂度较高,出于演示说明的目的,所以本文通过一个为单个进程中的多线程提供缓冲服务的程序demo进行分析


    0. 可扩展数组实现的队列模板

    对于缓冲池而言,其主要由3种不同类型的缓冲区构成:空闲缓冲区、装满输入数据的缓冲区和装满输出数据的缓冲区,分别通过3个队列进行管理:空缓冲区队列emq, 装满输入数据的缓冲区队列inq, 装满输出数据的缓冲区队列outq。故而合理的队列实现是构成缓冲池管理各buffer块的基础,这里通过可扩展容量的数组实现队列模板。

    queue.h

    #pragma once
    #include <cassert>
    #include <cstring>
    
    namespace _tool
    {
        template <typename T>
        class queue
        {
         protected:
            int mFront,mEnd,mCapacity; //分别定义该队列的头(第一个有效数据Buffer的位置),尾(下一个待插入的空闲位置),mCapacity(当前队列的最大容量)
            T *mpData; //指向Buffer块连续数组的数组指针
    
            /*扩展当前buffer块数组的容量2倍*/
            virtual void double_resize(void)
            {
                T *tmp = new T[this->mCapacity*2];
                int size = this->size();
                for (int i=0; i<size; i++)
                {
                    tmp[i] = this->mpData[(this->mFront+i)%this->mCapacity];
                }
                delete[] mpData;
                this->mpData = tmp;
                this->mCapacity*=2;
                this->mEnd = size;
                this->mFront = 0;
            }
    
        public:
        /*默认初始化函数*/
            queue(void)
            {
                const int INIT_SIZE = 10;
                this->mCapacity = INIT_SIZE;
                this->mFront = this->mEnd = 0;
                this->mpData = new T[this->mCapacity];
            }
       /*“引用传递”初始化函数*/
            queue(const queue &from)
            {
                this->mCapacity = from.mCapacity;
                this->mFront = from.mFront;
                this->mEnd = from.mEnd;
                this->mpData = new T[this->mCapacity];
                for (int i=0; i<this->mCapacity; i++) 
                {
                    this->mpData[i] = from.mpData[i];//是否是深拷贝依旧取决于T数据类型的具体实现
                } 
            }
    
            virtual ~queue(void)
            {
                if (this->mpData)
                {
                    delete[] this->mpData;
                }
                this->mpData = 0;
            }
    
           /*往队列中压入新数据*/
            virtual void push_back(T value)
            {
                if (this->size() >= this->mCapacity-1)
                {
                    this->double_resize();
                }
                this->mpData[this->mEnd++] = value;
                this->mEnd %= this->mCapacity;
            }
    
           /*取出队列的首部元素*/
            virtual const T pop_front(void)
            {
                assert(!this->empty()); //输出断言,如果队列是空,则弹出断言,如果非空,则弹出队列首部
                T tmp = this->mpData[this->mFront++];
                this->mFront %= this->mCapacity;
                return tmp;
            }
    
           /*获取队列的首部元素,但不弹出*/
            virtual const T &get_front(void) const
            {
                assert(!this->empty());
                return this->mpData[this->mFront];
            }
    
           /*获取队列中当有效数据规模size*/
            virtual int size(void) const
            {
                return (this->mEnd+this->mCapacity-this->mFront) % this->mCapacity;
            }
    
            virtual bool empty(void) const
            {
                return this->size()==0;
            }
    
            const queue &operator =(const queue &from) //引用传递
            {
                if (this->mpData)
                {
                    delete[] this->mpData;
                }
                this->mpData = 0;
                this->mCapacity = from.mCapacity;
                this->mFront = from.mFront;
                this->mEnd = from.mEnd;
                this->mpData = new T[this->mCapacity];
                for (int i=0; i<this->mCapacity; i++)
                {
                    this->mpData[i] = from.mpData[i]; //此处可以看出队列的深拷贝还浅拷贝是由子元素自己实现的操作符函数的实现决定的
                }
                return *this;
            }
        };
    
    /*fixed_queue是在queue基础上继承而来的子类,fixed_queue子类并不支持queue的动态可扩展*/
        template <typename T>
        class fixed_queue : public queue<T>
        {
        public:
            fixed_queue(int size = 10)
            {
                this->mCapacity = size+1;
                this->mFront = this->mEnd = 0;
                this->mpData = new T[this->mCapacity];
            }
    
            /*引用传递初始化函数*/
            fixed_queue(const fixed_queue &from)
            {
                this->mCapacity = from.mCapacity;
                this->mFront = from.mFront;
                this->mEnd = from.mEnd;
                this->mpData = new T[this->mCapacity];
                for (int i=0; i<this->mCapacity; i++)
                {
                    this->mpData[i] = from.mpData[i];
                }
            }
    
            virtual ~fixed_queue(void)
            {
                if (this->mpData)
                {
                    delete[] this->mpData;
                }
                this->mpData = 0;
            }
    
            virtual void push_back(T value)
            {
                assert(!this->full());
                this->mpData[this->mEnd++] = value;
                this->mEnd %= this->mCapacity;
            }
            virtual bool full(void)
            {
                return (this->size() >= this->mCapacity-1);
            }
    
            const fixed_queue &operator =(const fixed_queue &right)
            {
                if (this->mpData)
                {
                    delete[] this->mpData;
                }
                this->mpData = 0;
                this->mCapacity = right.mCapacity;
                this->mFront = right.mFront;
                this->mEnd = right.mEnd;
                this->mpData = new T[this->mCapacity];
                for (int i=0; i<this->mCapacity; i++)
                {
                    this->mpData[i] = right.mpData[i];
                }
                return *this;
            }
    
            /*如果fixed_queue实在容量不够,那么必须显式指明新的size参数*/
            void resize(int size)
            {
                if (this->mpData)
                {
                    delete[] this->mpData;
                }
                this->mpData = 0;
                this->mCapacity = size+1;
                this->mFront = this->mEnd = 0;
                this->mpData = new T[this->mCapacity];
    
            }
        /*修改从父类queue中继承的double_resize()动态扩展函数*/
        protected:
            void double_resize(void)
            {
                assert(!"Don't call me!!");
            }
        };
    }


    1. BufferPool对外提供的API和具体实现

    缓冲池的主要构成是三种队列:空闲缓冲块队列EM、装满输入数据的缓冲块队列IN、装满输出数据待处理的缓冲块队列OUT
    缓冲池的工作方式主要是4种:收容输入、提取输入、收容输出、提取输出;
    BufferPool.h

    #pragma once
    #include "queue.h"
    #include <windows.h>
    
    class IOBuffer
    {
    public:
    /*通过枚举数据结构事先声明三种不同的队列,分别是空闲缓冲块队列、输入数据缓冲块队列、输出数据缓冲块队列*/
        enum eBuffType
        {
            BT_EM = 0,
            BT_IN = 1,
            BT_OUT = 2,
        };
    
        IOBuffer(int buffer_pool_capacity, int each_buffer_size);
        ~IOBuffer(void);
    
        //4个对外的函数
        int accept_in(void);//为了方便实现,只返回一个缓冲区,需要fill_in装入输入缓冲队列,和fill_in一起构成收容输入
        int accept_out(void);//为了方便实现,只返回一个缓冲区,需要fill_out装入输出缓冲队列,和fill_out一起构成收容输出
        void fill_in(int number);//将缓冲块buffer装载输入数据后放入输入缓冲区队列
        void fill_out(int number);//将缓冲区buffer装载要输出的数据然后放入输出缓冲区队列
    
        //2个提取装满数据的缓冲块的函数
        void distill_in(void);//对应提取输入
        void distill_out(void);//对应提取输出
    
        _tool::fixed_queue<char> &thebuffer(int number); //返回指定index的buffer的引用
    
    private:
        IOBuffer(const IOBuffer &from);
    
        int get_buff(int type);//已处理互斥问题
        void put_buf(int type, int work_buf);//已处理互斥问题
    
        /*为了简化操作,直接在当前进程空间的堆中申明一块buffer_pool_capacity * each_buffer_size大小的空间
        作为缓冲池的物理空间使用,并用mpBuffers指针指向该空间的首地址*/
        _tool::fixed_queue<char> *mpBuffers;
    
        //3个队列,之所以模板类采用int具象化,是因为和后续的设计配合在一起,我们是通过数组批量划分缓冲空间的,
        //故而每个缓冲块都对应缓冲空间数组中的具体索引Index,出于简化管理,之所以管理各缓冲块的Index即可
        _tool::queue<int> mqEM; 
        _tool::queue<int> mqIN;
        _tool::queue<int> mqOUT;
    
        HANDLE mhEM;//队列容量的信号量
        HANDLE mhIN;//队列容量的信号量
        HANDLE mhOUT;//队列容量的信号量
        HANDLE mhoEM;//队列访问权的信号量
        HANDLE mhoIN;//队列访问权的信号量
        HANDLE mhoOUT;//队列访问权的信号量
        int mBufferID;
        static int mstaTotalBuffers;
    };
    

    BufferPool.cpp

    #include "BufferPool.h"
    #include <sstream>
    #include <iostream>
    
    using namespace _tool;
    
    extern HANDLE OUTPUT_MUTEX;
    int IOBuffer::mstaTotalBuffers = 0;
    
    IOBuffer::IOBuffer(int buffer_pool_capacity, int each_buffer_size)
    {
        /*初始化缓冲池,缓冲池中buffer缓冲块数目总共有buffer_pool_capacity参数指定,
        每个缓冲块buffer含有each_buffer_size个字节*/
        this->mpBuffers = new fixed_queue<char>[buffer_pool_capacity];
        for (int i=0; i<buffer_pool_capacity; i++)
        {
            this->mpBuffers[i].resize(each_buffer_size);
        }
    
        //记录下该缓冲池的编号
        this->mBufferID = ++IOBuffer::mstaTotalBuffers;
        std::stringstream ss;//设置string对象,用于后续提供内核的唯一性命名
        ss << "BufferPool NO." << IOBuffer::mstaTotalBuffers;
    
        /*设置以下3个互斥量,3各信号量,每个队列对应一个Mutex和semaphore,其中Mutex对应队列的具体操作权限,比如弹出队列首,或压入新元素,显然每个时刻只能有一个线程获取具体的操作权限。
        而semaphore对应具体队列中的当前可用缓冲块数目,故而应该先获取semaphore,然后再或获取操作权限,双层内核加锁保证最后的线程互斥*/
        this->mhEM = CreateSemaphore(NULL, buffer_capacity, buffer_capacity, (ss.str() + "EM").c_str());
        this->mhIN = CreateSemaphore(NULL, 0, buffer_capacity, (ss.str() + "IN").c_str());
        this->mhOUT = CreateSemaphore(NULL, 0, buffer_capacity, (ss.str() + "OUT").c_str());
        this->mhoEM = CreateMutex(NULL, false, (ss.str() + "oEM").c_str());
        this->mhoIN = CreateMutex(NULL, false, (ss.str() + "oIN").c_str());
        this->mhoOUT = CreateMutex(NULL, false, (ss.str() + "oOUT").c_str());
    
        //初始化空闲缓冲区队列,将当前所有的空闲缓冲块的索引按照顺序依次加入到空闲队列中,这也是
        for (int i=0; i<buffer_capacity; i++)
        {
            this->mqEM.push_back(i);
        }
    }
    
    /*引用传递的初始化函数设计为无操作,你也可以自己实现,不过在这里不是很有意义*/
    IOBuffer::IOBuffer(const IOBuffer &from)
    {
    }
    
    IOBuffer::~IOBuffer(void)
    {
        //删除缓冲区
        delete[] this->mpBuffers;
        //清除信号量
        CloseHandle(this->mhEM);
        CloseHandle(this->mhIN);
        CloseHandle(this->mhOUT);
        CloseHandle(this->mhoEM);
        CloseHandle(this->mhoIN);
        CloseHandle(this->mhoOUT);
    }
    
    /*返回指定index的缓冲块buffer的引用*/
    fixed_queue<char> &IOBuffer::thebuffer(int number)
    {
        return this->mpBuffers[number];
    }
    
    int IOBuffer::get_buff(int type)
    {
        queue<int> *pq; //临时队列指针,指向将要操作的缓冲块的首地址
        HANDLE handle=NULL,ohandle=NULL;//临时内核对象,用来获取对应队列的mutex和semaphore
        //根据类型选择不同的队列和控制句柄
        switch (type)
        {
        case BT_EM:
            pq = &this->mqEM;
            handle = this->mhEM; //获取空闲队列的准入权限
            ohandle = this->mhoEM;//获取空闲队列的操作权限,
            break;
    
        case BT_IN:
            pq = &this->mqIN;
            handle = this->mhIN;
            ohandle = this->mhoIN;
            break;
    
        case BT_OUT:
            pq = &this->mqOUT;
            handle = this->mhOUT;
            ohandle = this->mhoOUT;
            break;
    
        default:
            assert(!"unknown type");
            break;
        }
        //申请一个空间,等待信号量
        WaitForSingleObject(handle, INFINITE); //先获取具体队列的准入权限,该操作会将该队列的可用资源数减1
        int tmp;
        //队列操作要互斥
        WaitForSingleObject(ohandle, INFINITE); //获取具体队列的操作权限
        tmp = pq->pop_front();//将该队列的可用缓冲块的Index弹出
        ReleaseMutex(ohandle);//释放具体队列的操作权限
        return tmp;
    }
    
    void IOBuffer::put_buf(int type, int work_buf) 
    {
        queue<int> *pq;
        HANDLE handle=NULL,ohandle=NULL;
        //根据类型选择不同的队列和控制句柄
        switch (type)
        {
        case BT_EM:
            pq = &this->mqEM;
            handle = this->mhEM;
            ohandle = this->mhoEM;
            break;
    
        case BT_IN:
            pq = &this->mqIN;
            handle = this->mhIN;
            ohandle = this->mhoIN;
            break;
    
        case BT_OUT:
            pq = &this->mqOUT;
            handle = this->mhOUT;
            ohandle = this->mhoOUT;
            break;
    
        default:
            assert(!"unknown type");
            break;
        }
        //释放一个空间,释放信号量
        WaitForSingleObject(ohandle, INFINITE);//获取该队列的操作权限
        pq->push_back(number);//将number指代的缓冲块加入到该队列的尾部
        ReleaseMutex(ohandle);
        ReleaseSemaphore(handle, 1, NULL);//将该队列的可用资源计数加1
    }
    
    /*为inq队列装入新的数据,先从空闲缓冲块队列中提取一个可用的空闲缓冲块*/
    int IOBuffer::accept_in(void)
    {
        return this->get_buff(BT_EM);
    }
    
    int IOBuffer::accept_out(void)
    {
        return this->get_buff(BT_EM);
    }
    /*将number号装载号了有效数据的输入缓冲块加入到inq输入缓冲块等待队列中,等待distill_in()函数提取处理*/
    void IOBuffer::fill_in(int number)
    {
        this->put_buf(BT_IN, number);
    }
    
    void IOBuffer::fill_out(int number)
    {
        this->put_buf(BT_OUT, number);
    }
    
    /*提取输入数据缓冲队列的函数*/
    void IOBuffer::distill_in(void)
    {
        //拿到一个已满的输入buffer
        int number = this->get_buff(BT_IN); //从输入缓冲块队列中提取首部缓冲块,get_buff()函数已经设计好了互斥机制
        WaitForSingleObject(OUTPUT_MUTEX, INFINITE); //获取stdout标准输出的输出权限
        std::cout << "*****flushing buffer " << number << " to input*****" << std::endl;
        std::cout << "##INPUT> ";
        //依次将number号缓冲块中存储的字节序列数据依次输出到stdout
        while (!this->mpBuffers[number].empty())
        {
            std::cout << this->mpBuffers[number].pop_front();
        }
        std::cout << std::endl;
        ReleaseMutex(OUTPUT_MUTEX);
        //释放掉buffer
        this->put_buf(BT_EM, number); //将number号缓冲块加入到空闲队列中,put_buff()函数已经设计号了互斥机制
    }
    
    /*提取输出数据缓冲队列的函数*/
    void IOBuffer::distill_out(void)
    {
        //拿到一个已满的输出buffer
        int number = this->get_buff(BT_OUT);
        WaitForSingleObject(OUTPUT_MUTEX, INFINITE);
        std::cout << "flushing buffer " << number << " to output" << std::endl;
        std::cout << "##OUTPUT";
        while (!this->mpBuffers[number].empty())
        {
            std::cout << this->mpBuffers[number].pop_front();
        }
        std::cout << std::endl;
        ReleaseMutex(OUTPUT_MUTEX);
        //释放掉buffer
        this->put_buf(BT_EM, number);
    }


    2. BufferPool测试程序

    test_main.cpp

    #include "queue.h"
    #include <iostream>
    #include "IOBuffer.h"
    #include <windows.h>
    
    HANDLE OUTPUT_MUTEX;//使用标准输出设备stdout的互斥锁,任一时刻只有一个线程可以输出,防止把输出搞乱
    IOBuffer afxBuff(10, 10);//全局的一个缓冲池,一共有10个buffer,每个buffer空间为10
    using namespace _tool;//使用定义在_tool命名空间中的queue,不然回合std中的queue冲突
    
    DWORD WINAPI distill_in(void*);//提取输入,出于显示目的,只设计一个提取输入线程
    DWORD WINAPI distill_out(void*);//提取输出,出于显示目的,只设计一个提取输入线程
    DWORD WINAPI accept_in(void *para);//收容输入
    DWORD WINAPI accept_out(void *para);//收容输出
    
    int main(void)
    {
        OUTPUT_MUTEX = CreateMutex(NULL, false, "OUTPUT");//先创建一个MUTEX控制输出,FALSE意味着该mutex并不为创建的宿主线程占用,一开始便处于激活状态,可介绍任何线程的声明占用
    
        HANDLE hfuncs[22];//创建22个线程,1各提取输入线程,1个提取输出线程,收容输入和收容输出线程各10个
        hfuncs[0] = CreateThread(NULL, 0, distill_in, NULL, 0, NULL);//创建一个提取输入进程
        hfuncs[1] = CreateThread(NULL, 0, distill_out, NULL, 0, NULL);//创建一个提取输出进程
        //创建10个收容输入进程
        for (int i=0; i<10; i++)
        {
            char c[11]="abcdefghij";
            hfuncs[2+i] = CreateThread(NULL, 0, accept_in, c+i, 0, NULL);
        }
        //创建10个收容输出进程,为了区别收容输入,输出数据全部是用大写
        for (int i=0; i<10; i++)
        {
            char c[11]="ABCDEFGHIJ";
            hfuncs[12+i] = CreateThread(NULL, 0, accept_out, c+i, 0, NULL);
        }
        WaitForMultipleObjects(22, hfuncs, true, 10000);//10s自动结束阻塞等待,否则该程序将一直输出
        return 0;
    }
    
    DWORD WINAPI distill_in(void*)//不断地检测输入缓冲区队列是否非空,有则输入
    {
        while (1)
        {
            Sleep(700);
            afxBuff.distill_in();
        }
    }
    
    DWORD WINAPI distill_out(void*)//不断地检测输出缓冲区队列是否非空,有则输出
    {
        while (1)
        {
            Sleep(800);
            afxBuff.distill_out();
        }
    }
    
    DWORD WINAPI accept_in(void *para)//不断检测是否有缓冲区可用,有则输入
    {
        char c = *((char*)para); //提取输入的单个字符
        while (1)
        {
            Sleep(900);
            int number = afxBuff.accept_in();
            WaitForSingleObject(OUTPUT_MUTEX,INFINITE);
            std::cout << "filling input buffer " << number << " with " << c << std::endl;
            ReleaseMutex(OUTPUT_MUTEX);
            //将该缓冲块用该字符填满
            while (!afxBuff.thebuffer(number).full())
            {
                afxBuff.thebuffer(number).push_back(c);
            }
            WaitForSingleObject(OUTPUT_MUTEX,INFINITE);
            std::cout << "put buffer " << number << " into input buffer queue" << std::endl;
            ReleaseMutex(OUTPUT_MUTEX);
            afxBuff.fill_in(number); //将装满的输入缓冲块装入in输入队列中
        }
    }
    
    DWORD WINAPI accept_out(void *para)//不断检测是否有缓冲区可用,有则输出
    {
        char c = *((char*)para);
        while (1)
        {
            Sleep(600);
            int number = afxBuff.accept_out();
            WaitForSingleObject(OUTPUT_MUTEX,INFINITE);
            std::cout << "filling output buffer " << number << " with " << c << std::endl;
            ReleaseMutex(OUTPUT_MUTEX);
            while (!afxBuff.thebuffer(number).full())
            {
                afxBuff.thebuffer(number).push_back(c);
            }
            WaitForSingleObject(OUTPUT_MUTEX,INFINITE);
            std::cout << "put buffer " << number << " into output buffer queue" << std::endl;
            ReleaseMutex(OUTPUT_MUTEX);
            afxBuff.fill_out(number);
        }
    }

    在Windows下VC编译器运行该程序,得到结果如下
    这里写图片描述
    这里的缓冲池技术实现的较为简单,实际的缓冲池实现涉及到诸多的内核知识,诸如系统中断、内存管理等,本文只是出于演示的目的,力求通过分析该简单的程序讲解缓冲池技术实现的重点。

    展开全文
  • 缓冲技术双缓冲是将图片在显示到DC前,现在要内存建一个DC,也就是用于存储这张图片的内存区,然后在将这部分update到你要显示的地方这样,可以防止画面抖动很大这样和你说吧,如果要实现你要的效果,你必须用指针访问...

    双缓冲技术

    双缓冲是将图片在显示到DC前,现在要内存建一个DC,也就是用于存储这张图片的内存区,然后在将这部分update到你要显示的地方

    这样,可以防止画面抖动很大

    这样和你说吧,如果要实现你要的效果,你必须用指针访问内存

    比如,把程序声明成unsafe的,然后按照上面的操作进行

     

    this.clear(this.BackColor)不行的 invalidate(),闪的厉害 所以不行

     

    我再来详细解释一下刚才实现双缓冲的具体步骤:

     

    1、   在内存中建立一块“虚拟画布”:

    Bitmap   bmp   =   new   Bitmap(600,   600);

    2、   获取这块内存画布的Graphics引用:

    Graphics   g   =   Graphics.FromImage(bmp);

    3、   在这块内存画布上绘图:

    g.FillEllipse(brush,   i   *   10,   j   *   10,   10,   10);

    4、将内存画布画到窗口中

    this.CreateGraphics().DrawImage(bmp,   0,   0);

    重点:

     现在的cpu飞快,其实数学计算一般很快,cpu大部分时间是在处理绘图,而绘图有三种境界:1>每次重绘整体Invalidate()

     2>每次局部绘制Invalidate(Rect);

          3>有选择的局部绘制。

      不能说,一定是第三种方式好,得视情况,境界高程序肯定就复杂,如果对效率要求不高或者绘图量小当然直接用第一种方式。然而,稍微专业点的绘图程序,第一第二种方式肯定满足不了要求,必须选用第三种方式。而第三种方式的手段多样,也得根据实际情况拿相应的解决之道。这里讲解一般的三种手段,他们可以联合使用。

    1. 缓存——Bitmap或者DoubleBuffer。缓存就是先把绘制的图形绘制到一张内存位图上,然后在一次性的贴位图,他可以提高绘图速度,也能避免闪烁。DoubleBuffer=true是C#窗体的属性,设置了此属性估计系统本身会起用无效区的内存位图缓存,而不需要程序员Bitmap处理。

    2. 合理利用无效区域。无效区域就是系统保存当前变化需要重绘的区域,可以在OnPaint()中,e.ClipRectangle(e.ClipRectangle.X)直接获得,也可以通过其他方式获得。Windows系统只会重绘无效区域内的绘图信息,然而我们用户的绘制代码一般是绘制整个区域的,很多时候无效区域只是一小部分区域,虽然执行了所有的绘图代码,但是Windows系统只会重新更新无效区域内的绘图。这里有两个利用点:

    1>用户请求重绘时,只请求重绘指定区域的,而不是整个区域,如Invalidate(Rect);

    2>在用户绘图代码

    Graphics g; g.DrawLine\g.DrawString\g.FillRectangle...前,先判断绘图的内容是否在无效区域,如果不是就不直接g.Draw...绘图代码。

    3. 直接贴图。一般绘图或者重绘是Windows根据无效区域绘制的,如果在鼠标移动时需要重绘通过Windows系统处理Paint消息,有时满足不了要求,

    比如①鼠标移动绘制十字测量线就得用异或线而不是Paint消息,

    又比如②鼠标移动绘制跟随的信息提示框需要频繁擦除上次覆盖的背景,

    又比如③台球滚动时台球与球桌背景的关系。

    类似的这些问题如何解决?首先肯定不能利用Windows原来的绘图机制。其中一种解决方式是,不断的帧间变化区域贴内存位图——②中的信息框每次鼠标位置变化时可以重新g.Draw...或者贴早生成的信息框内存位图,②中被信息框覆盖的背景应该把本来的大背景截取此需要擦除区域的位置大小位图贴回来就是擦除背景了。由于每次大背景发生变化时,都应会重新生成大背景内存位图,所以可以是变化的背景。

      这三种方式可以一起使用,应该可以解决中等的绘图项目的效率问题。中大型的绘图,必须记住两点1>只绘制电脑屏幕能

    显示的部分;2>只绘制变化的部分。

     

     

    C#GDI+双缓冲高效绘图

    Rectangle rectangle =  e.ClipRectangle;//取出次窗体或者画布的有效区的矩形区域

    BufferedGraphicsContext GraphicsContext = BufferedGraphicsManager.Current;//获取程序住缓冲区域的BufferedGraphicsContext(双缓存类,此类用于提供双缓冲的功能)对象

    BufferedGraphics myBuffer = GraphicsContext.Allocate(e.Graphics, e.ClipRectangle);//获取缓冲区

    Graphics g = myBuffer.Graphics;

    指定在呈现期间像素偏移的方式。

    g.PixelOffsetMode = PixelOffsetMode.HighQuality;//高质量低速度呈现

    指定是否将平滑处理(消除锯齿)应用于直线、曲线和已填充区域的边缘。

    g.SmoothingMode = SmoothingMode.HighQuality;// 指定高质量、低速度呈现。

    g.Clear(BackColor);//或者使用invalidate方法==有效区的擦除

    Pen bluePen2 = new Pen(Color.Blue);

    LineDrawRoutine(g, bluePen2);

     

    myBuffer.Render(e.Graphics);  //将图形缓冲区的内容写入指定的 Graphics 对象。

    g.Dispose();

    myBuffer.Dispose();

     

     

    其实在C#里如果是在Form中绘图的话直接把Form的DoubleBuffered = true就可以了(利用winfrom窗体的默认双缓冲)

     

    把所有的绘图放在一个picturebox里面绘制,

    不要直接再在form里面绘

    SetStyle(ControlStyles.UserPaint, true);

      SetStyle(ControlStyles.ResizeRedraw, true);

      SetStyle(ControlStyles.AllPaintingInWmPaint, true);

      SetStyle(ControlStyles.OptimizedDoubleBuffer, true);

      SetStyle(ControlStyles.Selectable, true);

     

     

     

    如果你在Form中绘图的话,不论是不是采用的双缓存,都会看到图片在更新的时候都会不断地闪烁,解决方法就是在这个窗体的构造函数中增加以下三行代码:

    请在构造函数里面底下加上如下几行:

    SetStyle(ControlStyles.UserPaint, true);

    SetStyle(ControlStyles.AllPaintingInWmPaint, true);   //   禁止擦除背景.

    SetStyle(ControlStyles.DoubleBuffer, true);   //   双缓冲

    参数说明:

    UserPaint  

    如果为true,控件将自行绘制,而不是通过操作系统来绘制。此样式仅适用于派生自   Control的类。

    AllPaintingInWmPaint  

    如果为true,控件将忽略 WM_ERASEBKGND窗口消息以减少闪烁。仅当UserPaint   位设置为true时,才应当应用该样式。    

    DoubleBuffer  

    如果为true,则绘制在缓冲区中进行,完成后将结果输出到屏幕上。双重缓冲区可防止由控件重绘引起的闪烁。要完全启用双重缓冲,还必须将UserPaint和AllPaintingInWmPaint样式位设置为   true。  

     

    GDI+的双缓冲问题

     

    我想有很多搞图形方面的朋友都会用到双缓冲技术的时候,而且有的时候她的确是个头疼的问题。最近我也要用双缓冲技术,程序怎么调试都不合适,当要对图形进行移动时,总是会出现闪烁抖动。在网上找了些资料,说得都不清不楚的,折腾了一晚上也没弄出来。第二天觉定自己研究一下。现在把自己的一些想法拿出来跟大家分享一下。

     

    双缓冲的基本原理:

     

    一直以来的误区:.net1.1 和 .net 2.0 在处理控件双缓冲上是有区别的。

    .net 1.1中,使用:this.SetStyle(ControlStyles.DoubleBuffer, true);

    .net 2.0中,使用:this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);

     

    要知道,图元无闪烁的实现和图元的绘制方法没有多少关系,只是绘制方法可以控制图元的刷新区域,使双缓冲性能更优!

     

    导致画面闪烁的关键原因分析:

     

    一、绘制窗口由于大小位置状态改变进行重绘操作时

     

    绘图窗口内容或大小每改变一次,都要调用Paint事件进行重绘操作,该操作会使画面重新刷新一次以维持窗口正常显示。刷新过程中会导致所有图元重新绘制,而各个图元的重绘操作并不会导致Paint事件发生,因此窗口的每一次刷新只会调用Paint事件一次。窗口刷新一次的过程中,每一个图元的重绘都会立即显示到窗口,因此整个窗口中,只要是图元所在的位置,都在刷新,而刷新的时间是有差别的,闪烁现象自然会出现。所以说,此时导致窗口闪烁现象的关键因素并不在于Paint事件调用的次数多少,而在于各个图元的重?%@B?。

    根据以上分析可知,当图元数目不多时,窗口刷新的位置也不多,窗口闪烁效果并不严重;当图元数目较多时,绘图窗口进行重绘的图元数量增加,绘图窗口每一次刷新都会导致较多的图元重新绘制,窗口的较多位置都在刷新,闪烁现象自然就会越来越严重。特别是图元比较大绘制时间比较长时,闪烁问题会更加严重,因为时间延迟会更长。

     

    解决上述问题的关键在于:窗口刷新一次的过程中,让所有图元同时显示到窗口。

    二、进行鼠标跟踪绘制操作或者对图元进行变形操作时

     

    当进行鼠标跟踪绘制操作或者对图元进行变形操作时,Paint事件会频繁发生,这会使窗口的刷新次数大大增加。虽然窗口刷新一次的过程中所有图元同时显示到窗口,但也会有时间延迟,因为此时窗口刷新的时间间隔远小于图元每一次显示到窗口所用的时间。因此闪烁现象并不能完全消除!所以说,此时导致窗口闪烁现象的关键因素在于Paint事件发生的次数多少。

    解决此问题的关键在于:设置窗体或控件的几个关键属性。

     

    下面讲具体的实现方法:(转)

     

    1、在内存中建立一块“虚拟画布”:Bitmap bmp = new Bitmap(600, 600);

    2、获取这块内存画布的Graphics引用:Graphics g = Graphics.FromImage(bmp);

    3、在这块内存画布上绘图:如画线g.DrawLine(添加参数);

    4、将内存画布画到窗口中:this.CreateGraphics().DrawImage(bmp, 0, 0);

     

    在构造函数中加如下代码

     

    代码一:

    SetStyle(ControlStyles.UserPaint, true);

    SetStyle(ControlStyles.AllPaintingInWmPaint, true); // 禁止擦除背景.

    SetStyle(ControlStyles.DoubleBuffer, true); // 双缓冲

    或代码二:

    this.SetStyle(ControlStyles.DoubleBuffer | ControlStyles.UserPaint |

    ControlStyles.AllPaintingInWmPaint, true);

    this.UpdateStyles();

     

    上述方式适合直接在窗体上绘制图形,并且很容易做到。但有时我们需要在某个控件上绘制图形,那该怎么办呢?原理跟直接在窗体上绘制图形采用双缓冲是一样的,也要在控件的构造函数里设置上述代码一或代码二。那么又怎么设置呢?

    在Microsoft Visual Studio 2005环境下的,用的C#语言,并采用GDI+。目标是实现简单的鼠标拖动画线,并且要把之前画过的线都重新画出来。

    整个程序使用了三个控件:一个SplitContainer控件、一个自定义的Panel控件和一个VS自带的Panel控件。SplitContainer控件的大小设置成窗体宽、半窗体高并定位在窗体的下半部分。自定义的Panel控件和VS自带的Panel控件都是通过设置它们的Dock属性使它们绑定到SplitContainer控件的Panel1和Panel2上。附录中会说到自定义的Panel控件是怎么定义的。

    窗体的上半部分采用双缓冲。自定义的Panel控件采用了双缓冲,是通过在自定义Panel控件时设置样式来做到的(设置方法与窗体的双缓冲设置方法一样,如下面三条语句),这不能够在面板的Paint方法里直接设置,因为SetStyle()在Panel类中不是public方法。VS自带的Panel控件没有采用双缓冲。

     

    SetStyle(ControlStyles.AllPaintingInWmPaint, true);

    SetStyle(ControlStyles.UserPaint, true);

    SetStyle(ControlStyles.OptimizedDoubleBuffer, true);

    把三者结合

     

    有两种方式来创建Graphics对象:第一是在内存上创建一块和显示区域或控件相同大小的画布,在这块画布上创建Graphics对象。接着所有的图元都在这块画布上绘制,绘制完成以后再使用该画布覆盖显示控件的背景,从而达到“显示一次仅刷新一次”的效果!第二是直接在内存上创建Graphics对象。

     

     

    使用双缓冲的图形可以减少或消除重绘显示图面时产生的闪烁。使用双缓冲时,更新的图形首先被绘制到内存的缓冲区中,然后,此缓冲区的内容被迅速写入某些或所有显示的图面中。显示图形的重写相对简短,这通常可以减少或消除有时在更新图形时出现的闪烁。

     

     

    双缓冲技术(C# GDI)

    c#如何实现防窗体闪烁的功能。大家都会想到运用双缓冲技术,那么在c#中是如何做的?

    1、 利用默认双缓冲

    (1)在应用程序中使用双缓冲的最简便的方法是使用 .NET Framework 为窗体和控件提供的默认双缓冲。通过将 DoubleBuffered 属性设置为 true。

    this.DoubleBuffered=true;

    (2)使用 SetStyle 方法可以为 Windows 窗体和所创作的 Windows 控件启用默认双缓冲。

    SetStyle(ControlStyles.OptimizedDoubleBuffer, true);

    2、 手工设置双缓冲

    .netframework提供了一个类BufferedGraphicsContext负责单独分配和管理图形缓冲区。每个应用程序域都有自己的默认 BufferedGraphicsContext 实例来管理此应用程序的所有默认双缓冲。大多数情况下,每个应用程序只有一个应用程序域,所以每个应用程序通常只有一个默认 BufferedGraphicsContext。默认 BufferedGraphicsContext 实例由 BufferedGraphicsManager 类管理。通过管理BufferedGraphicsContext实现双缓冲的步骤如下:

    (1)获得对 BufferedGraphicsContext 类的实例的引用。

    (2)通过调用 BufferedGraphicsContext.Allocate 方法创建 BufferedGraphics 类的实例。

    (3)通过设置 BufferedGraphics.Graphics 属性将图形绘制到图形缓冲区。

    (4)当完成所有图形缓冲区中的绘制操作时,可调用 BufferedGraphics.Render 方法将缓冲区的内容呈现到与该缓冲区关联的绘图图面或者指定的绘图图面。

    (5)完成呈现图形之后,对 BufferedGraphics 实例调用释放系统资源的 Dispose 方法。

    完整的例子,在一个400*400的矩形框内绘制10000个随机生成的小圆。

    BufferedGraphicsContext current = BufferedGraphicsManager.Current; //(1)

    BufferedGraphics bg = current.Allocate(this.CreateGraphics(),this.DisplayRectangle); //(2)

    Graphics g = bg.Graphics;//(3)

    //随机 宽400 高400

    System.Random rnd = new Random();

    int x,y,w,h,r,i;

    for (i = 0; i < 10000; i++)

    {

    x = rnd.Next(400);

    y = rnd.Next(400);

    r = rnd.Next(20);

    w = rnd.Next(10);

    h = rnd.Next(10);

    g.DrawEllipse(Pens.Blue, x, y, w, h);

    }

    bg.Render();//(4)

    //bg.Render(this.CreateGraphics());

    bg.Dispose();//(5)

    BufferedGraphicsContext current = BufferedGraphicsManager.Current; //(1)

    BufferedGraphics bg;

    bg = current.Allocate(this.CreateGraphics(),this.DisplayRectangle); //(2)

    Graphics g = bg.Graphics;//(3)

    //随机 宽400 高400

    System.Random rnd = new Random();

    int x,y,w,h,r,i;

    for (i = 0; i < 10000; i++)

    {

    x = rnd.Next(400);

    y = rnd.Next(400);

    r = rnd.Next(20);

    w = rnd.Next(10);

    h = rnd.Next(10);

    g.DrawEllipse(Pens.Blue, x, y, w, h);

    }

    bg.Render();//(4)

    //bg.Render(this.CreateGraphics());

    bg.Dispose();//(5)

    3、 自己开辟一个缓冲区(如一个不显示的Bitmap对象),在其中绘制完成后,再一次性显示。

    完整代码如下:

    view plaincopy to clipboardprint?

    Bitmap bt = new Bitmap(400, 400);

    Graphics bg = Graphics.FromImage(bt);

    System.Random rnd = new Random();

    int x, y, w, h, r, i;

     

    for (i = 0; i < 10000; i++)

    {

    x = rnd.Next(400);

    y = rnd.Next(400);

    r = rnd.Next(20);

    w = rnd.Next(10);

    h = rnd.Next(10);

    bg.DrawEllipse(Pens.Blue, x, y, w, h);

    }

    this.CreateGraphics().DrawImage(bt, new Point(0, 0));

    BufferedGraphicsContext的构造函数

    • BufferedGraphicsContext

    初始化BufferedGraphicsContext 类的新实例。

    BufferedGraphicsContext的方法

    • Allocate(Graphics, Rectangle)

    使用指定的Graphics 的像素格式,创建指定大小的图形缓冲区。

    • Allocate(IntPtr, Rectangle)

    使用指定的Graphics 的像素格式,创建指定大小的图形缓冲区。

    • Dispose

    Releases all resources used by theBufferedGraphicsContext.

    • Equals(Object)

    确定指定的Object 是否等于当前的Object。(继承自Object。)

    • Finalize

    允许Object 在“垃圾回收”回收Object 之前尝试释放资源并执行其他清理操作。(继承自Object。)

    • GetHashCode

    用作特定类型的哈希函数。 (继承自Object。)

    • GetType

    获取当前实例的Type。(继承自Object。)

    • Invalidate

    如果某个缓冲区已被分配但尚未释放,则释放当前的图形缓冲区。

    • MemberwiseClone

    创建当前Object 的浅表副本。(继承自Object。)

    • ToString

    返回表示当前Object 的String。(继承自Object。)

    BufferedGraphicsContext的属性

    • MaximumBuffer

    获取或设置要使用的缓冲区的最大大小。

    双缓冲技术绘图  

    本文主要介绍 .Net 框架的基本绘图技术。通过简要的介绍和示例程序来探讨绘图技术的
    优势、劣势以及其它相关注意事项。

      简介

      幸运的是当编写一个典型的Windows 窗体程序时,窗体和控件的绘制、效果等操作是
    不需要特别加以考虑的。这是为什么呢?因为通过使用 .Net 框架,开发人员可以拖动一
    系列的控件到窗体上,并书写一些简单的与事件相关联的代码然后在IDE中按F5,一个完完
    全全的窗体程序就诞生了!所有控件都将自己绘制自己,窗体或者控件的大小和缩放都调
    整自如。在这里经常会用到的,且需要引起一点注意的就是控件效果。游戏,自定义图表
    控件以及屏幕保护程序的编写会需要程序员额外撰写用于响应 Paint 事件的代码。

      本文针对那些Windows 窗体开发人员并有助于他们在应用程序编制过程中使用简单的
    绘图技术。首先,我们会讨论一些基本的绘图概念。到底谁在负责进行绘制操作?Window
    s 窗体程序是如何知道何时该进行绘制的?那些绘制代码究竟被放置在哪里?之后,还将
    介绍图像绘制的双重缓冲区技术,你将会看到它是怎样工作的,怎样通过一个方法来实现
    缓存和实际显示的图像间的交替。最后,我们将会探讨”智能无效区域”,实际就是仅仅
    重绘或者清除应用程序窗体上的无效部分,加快程序的显示和响应速度。希望这些概念和
    技术能够引导读者阅读完本文,并且有助于更快和更有效的开发Windows 窗体程序。

      Windows 窗体使用GDI+图像引擎,在本文中的所有绘图代码都会涉及使用托管的.Net
    框架来操纵和使用Windows GDI+图像引擎。

      尽管本文用于基本的窗体绘图操作,但是它同样提供了快速的、有效的且有助于提高
    程序性能的技术和方法。所以,在通读本文之前建议读者对.Net框架有个基本的了解,包
    括Windows 窗体事件处理、简单的GDI+对象譬如Line,Pen和Brush等。熟悉Visual Basic
    .Net或者C#编程语言。
    概念

      Windows 应用程序是自己负责绘制的,当一个窗体”不干净”了,也就是说窗体改变
    了大小,或者部分被其它程序窗体遮盖,或者从最小化状态恢复时,程序都会收到需要绘
    制的信息。Windows把这种”不干净”状态称为”无效的(Invalidated)”状态,我们理解为:需要重绘,当Windows 窗体程序需要重绘窗体时它会从Windows消息队列中获取绘制的信息。这个信息经过.Net框架封装然后传递到窗体的 PaintBackground 和 Paint 事件中去,在上述事件中适当的书写专门用于绘制的代码即可。
    简单的绘图示例如下:
    using System;
    using System.Drawing;
    using System.Windows.Forms;
    public class BasicX : Form {

     public BasicX() {
      InitializeComponent();
     }

     private void BasicX_Paint(object sender, PaintEventArgs e) {
      Graphics g = e.Graphics;
      Pen p = new Pen(Color.Red);
      int width = ClientRectangle.Width;
      int height= ClientRectangle.Height;
      g.DrawLine(p, 0,0, width, height);
      g.DrawLine(p, 0, height, width, 0);
      p.Dispose();
     }

     private void InitializeComponent() {
      this.SetStyle(ControlStyles.ResizeRedraw, true);
      this.ClientSize = new System.Drawing.Size(300, 300);
      this.Text = "BasicX";
      this.Paint += new PaintEventHandler(this.BasicX_Paint);
     }

     [System.STAThreadAttribute()]
     public static void Main() {
      Application.Run(new BasicX());
     }
    }
    上述代码分成两个基本的步骤来创建示例程序。首先 InitializeComponent 方法包含
    一些属性的设置和附加窗体 Paint 事件的处理过程。注意,在方法中控件的样式也同时被
    设置,设置控件的样式也是自定义Windows 窗体及控件行为的一种有效途径,譬如:控件
    的"ResizeRedraw"属性指示当窗体的大小变化发生以后需要对其完全进行重绘,也就是说
    重绘时总是需要对整个窗体的客户区域进行重绘。窗体的“客户区域”是指除了标题栏和
    边框的所有窗体区域。可以进行一个有趣的试验,取消该控件的属性然后再运行程序,我
    们可以很明显的看出为什么该属性会被经常的设置,因为窗体调整大小后的无效区域根本
    不会被重绘。

      好了,我们需要注意一下BasicX_Paint方法,正如先前所提到的,Paint 事件在程序
    需要重绘时被激活,程序窗体利用Paint事件来负责回应需要重绘的系统消息,BasicX_Pa
    int方法的调用需要一个对象 sender 和一个PaintEventArgs类型的变量,PaintEventArg
    s类的实例或称之为变量 e 封装了两个重要的数据,第一个就是窗体的 Graphics 对象,
    该对象表示窗体可绘制的表面也称之为画布用于绘制诸如线、文本以及图像等,第二个数
    据就是ClipRectangle,该Rectangle对象表示窗体上无效的的矩形范围,或者说就是窗体
    需要重绘的区域。记住,当窗体的ResizeRedDraw设置后,调整大小后该ClipRectangle的
    大小实际就等于窗体整个客户区域的大小,或者是被其它程序窗体遮盖的那部分剪切区域
    。关于部分剪切区域的用处我们会在智能重绘章节作更详细的阐述。
    双重缓冲区绘图技术

      双重缓冲区技术能够使程序的绘图更加快速和平滑,有效减少绘制时的图像闪烁。该
    技术的基本原理是先将图像绘制到内存中的一块画布上,一旦所有的绘制操作都完成了,
    再将内存中的画布推到窗体的或者控件的表面将其显示出来。通过这种操作后的程序能使
    用户感觉其更加快速和美观。

      下面提供的示例程序能够阐明双重缓冲区的概念和实现方法,这个示例所包含的功能
    已相当完整,且完全可以在实际应用中使用。在该章节后面还会提及该技术应该配合控件
    的一些属性设置才能达到更好的效果。

      要想领略双重缓冲区绘图技术所带来的好处就请运行SpiderWeb示例程序吧。程序启动
    并运行后对窗口大小进行调整,你会发现使用这种绘图算法的效率不高,并且在调整大小
    的过程中有大量的闪烁出现。
    纵观程序的源码你会发现在程序Paint事件激活后是通过调用LineDrawRoutine方法来实现
    线的绘制的。LineDrawRoutine方法有两个参数,第一个是Graphics对象是用于绘制线条的
    地方,第二个是绘图工具Pen对象用来画线条。代码相当简单,一个循环语句,LINEFREQ常量等,程序从窗体表面的左下一直划线到其右上。请注意,程序使用浮点数来计算在窗体
    上的绘制位置,这样做的好处就是当窗体的大小发生变化时位置数据会更加精确。

    private void LineDrawRoutine(Graphics g, Pen p) {
     float width = ClientRectangle.Width;
     float height = ClientRectangle.Height;
     float xDelta = width / LINEFREQ;
     float yDelta = height / LINEFREQ;

     for (int i = 0; i < LINEFREQ; i++) {
      g.DrawLine(p, 0, height - (yDelta * i), xDelta * i, 0);
     }
    }

      撰写很简单的用于响应Paint事件SpiderWeb_Paint的代码,正如前面所提到的,Grap
    hics对象就是从Paint事件参数PaintEventArgs对象中提取出来的表示窗体的绘制表面。这
    个Graphics对象连同新创建Pen对象一起传递给LineDrawRoutine方法来画出蜘蛛网似的线
    条,使用完Graphics对象和Pen对象后释放其占用的资源,那么整个绘制操作就完成了。

    private void SpiderWeb_Paint(object sender, PaintEventArgs e) {
     Graphics g = e.Graphics;
     Pen redPen = new Pen(Color.Red);
     //call our isolated drawing routing
     LineDrawRoutine(g, redPen);
     redPen.Dispose();
     g.Dispose();
    }

      那么到底作怎么样的改动才能使上面的SpiderWeb程序实现简单的双重缓冲区技术呢?
    原理其实相当简单,就是将应该画到窗体表面的绘制操作改成先画到内存中的位图上,Li
    neDrawRoutine向这个在内存中隐藏的画布执行同样的蜘蛛网绘制操作,等到绘制完毕再通
    过调用Graphics.DrawImage方法将隐藏的画布上内容推到窗体表面来显示出来,最后,再
    加上一些小的改动一个高性能的绘图窗体程序就完成了。

      比较下面双重缓冲区绘图事件与前面介绍的简单绘图事件间的区别:

    private void SpiderWeb_DblBuff_Paint(object sender, PaintEventArgs e) {
     Graphics g = e.Graphics;
     Pen bluePen = new Pen(Color.Blue);
     //create our offscreen bitmap
     Bitmap localBitmap = new Bitmap(ClientRectangle.Width,ClientRectangle.Height
    );
     Graphics bitmapGraphics = Graphics.FromImage(localBitmap);
     //call our isolated drawing routing
     LineDrawRoutine(bitmapGraphics, bluePen);
     //push our bitmap forward to the screen
     g.DrawImage(localBitmap, 0, 0);
     bitmapGraphics.Dispose();

     bluePen.Dispose();
     localBitmap.Dispose();
     g.Dispose();
    }

    上面的示例代码创建了内存位图对象,它的大小等于窗体的客户区域(就是绘图表面)
    的大小,通过调用Graphics.FromImage将内存中位图的引用传递给Graphics对象,也就是
    说后面所有对该Graphics对象的操作实际上都是对内存中的位图进行操作的,该操作在C+
    +中等同于将位图对象的指针复制给Graphics对象,两个对象使用的是同一块内存地址。现
    在Graphics对象表示的是屏幕后方的一块画布,而它在双重缓冲区技术中起到至关重要的
    作用。所有的线条绘制操作都已经针对于内存中的位图对象,下一步就通过调用DrawImag
    e方法将该位图复制到窗体,蜘蛛网的线条就会立刻显示在窗体的绘制表面而且丝毫没有闪
    烁出现。
    这一系列的操作完成后还不是特别有效,因为我们先前提到了,控件的样式也是定义Wind
    ows 窗体程序行为的一条途径,为了更好的实现双重缓冲区必须设置控件的Opaque属性,
    这个属性指明窗体是不负责在后台绘制自己的,换句话说,如果这个属性设置了,那么必
    须为清除和重绘操作添加相关的代码。具备双重缓冲区版本的SpiderWeb程序通过以上的设
    置在每一次需要重绘时都表现良好,窗体表面用其自己的背景色进行清除,这样就更加减
    少了闪烁的出现。

    public SpiderWeb_DblBuff() {
     SetStyle(ControlStyles.ResizeRedraw | ControlStyles.Opaque, true);
    }

    private void SpiderWeb_DblBuff_Paint(object sender, PaintEventArgs e) {
     //create our offscreen bitmap
     Bitmap localBitmap = new Bitmap(ClientRectangle.Width, ClientRectangle.Heigh
    t);
     Graphics bitmapGraphics = Graphics.FromImage(localBitmap);
     bitmapGraphics.Clear(BackColor);
     //call our isolated drawing routing
     LineDrawRoutine(bitmapGraphics, bluePen);
    }

      结果怎么样?图像的绘制平滑多了。从内存中将蜘蛛网的线条推到前台以显示出来是
    完全没有闪烁的,但是我们还是稍微停顿一下,先将内存中的位图修整一下再显示出来,
    可以添加一行代码以便使线条看上去更加平坦。

    bitmapGraphics.SmoothingMode = SmoothingMode.AntiAlias;

      在将内存中的位图对象赋给Graphics后通过放置这行代码,我们在画布上所画的每一
    个线条都使用了反锯齿,使凹凸不平的线条显得更加平坦。
    完成了简单的双重缓冲区应用后有两个问题需要向读者阐明,.Net中的某些控件例如
    :Button、PictureBox、Label还有PropertyGrid都已经很好的利用了该技术!这些控件在
    默认状态下会自动启用双重缓冲区技术,用户可以通过对“DoubleBuffer”属性的设置来
    就可以实现双重缓冲区技术。所以,用户若使用PictureBox来绘制蜘蛛网将会更有效率一
    些,而且也使程序变得更加简单了。

      我们在这里讨论的双重缓冲区技术既不是完全被优化但也没有什么太大的负面影响。
    双重缓冲区技术是减少Windows 窗体绘制时闪烁的一条重要途径,但是它也确实消耗不少
    内存,因为它将会使用双倍的内存空间:应用程序所显示的图像和屏幕后方内存中的图像
    。每次Paint事件被激活时都会动态的创建位图对象,这种机制会相当耗费内存。而自带双
    重缓冲区技术的控件在使用DoubleBuffer属性后执行起来的优化程度则会更好一些。

      使用GDI+的DIB(与设备无关的位图)对象来实现这种画面以外的内存缓冲,自带双重缓
    冲区机制的控件则能好的利用该位图对象。DIB是底层Win32的对象用于高效的屏幕绘制。
    同样,值得注意的是GDI+的第一个版本GDI中仅与硬件加速有关以及一些简单功能可以直接
    使用,由于这样的限制,像反锯齿和半透明等屏幕绘制方法执行起来的速度则相当慢。尽
    管双重缓冲区机制消耗了一些内存但是它的使用不容置疑的增强了程序的执行性能。

    智能重绘,在绘制前需要斟酌一下

    “智能无效”(智能重绘)就是在暗示程序员应该明白仅应对程序中无效的区域进行重
    绘,对Regions对象所对应的无效区域进行重绘可以提高绘制性能,使用Regions对象你可
    以仅排除或绘制控件和窗体的部分区域已获得更好的性能。我们现在就开始来看一下Basi
    cClip示例程序,这个程序使用保存在PaintEventArgs对象的ClipRectangle对象,之前我
    们已经提及,无论何时当程序的大小发生变化时Paint事件都会被激活。BasicClip示例程
    序用红和蓝两种颜色填充剪切的矩形区域,利用不同的速度调整窗体的大小几次以后,你
    会发现绘制的矩形区域其实就是窗体的无效区域(包括大于原始窗体大小的区域部分和缩少
    了的区域部分),示例程序的Paint事件代码如下:

    private void BasicClip_Paint(object sender, PaintEventArgs e) {
     Graphics g = e.Graphics;
     //swap colors
     if (currentBrush.Color == Color.Red)
      currentBrush.Color = Color.Blue;
     else
      currentBrush.Color = Color.Red;
      g.FillRectangle(currentBrush, e.ClipRectangle);
      g.Dispose();
    }

      该示例程序的唯一目的就是演示怎样仅针对部分区域进行图形绘制。
     Regions是一种被用来定义Windows 窗体或者控件区域的对象,调整窗体大小后所获得
    的Regions就是窗体重绘的最小区域。当程序需要进行绘制的时候仅绘制感兴趣的特殊区域
    ,这样绘制更小的区域就会使程序的运行速度更快。

      为了更好的演示Regions的用法,请查看TextCliping示例程序。该程序重载了OnPain
    tBackground和OnPaint方法,直接重载这些方法比侦听事件更能保证代码在其它的绘制操
    作之前被调用,而且对于自定义控件的绘制也更加有效。为了清楚起见,示例程序提供了
    一个Setup方法,该方法定义了全局的Graphics对象。

    private void Setup() {
     GraphicsPath textPath = new GraphicsPath();
    textPath.AddString(displayString, FontFamily.GenericSerif,
    0, 75, new Point(10, 50), new StringFormat());
     textRegion = new Region(textPath);
     backgroundBrush = new TextureBrush(new Bitmap("CoffeeBeanSmall.jpg"),
    WrapMode.Tile);
     foregroundBrush = new SolidBrush(Color.Red);
    }

      上面的Setup方法首先定义一个空的GraphicsPath对象变量textPath,下一步字符串“
    Windows Forms”的边界被添加到该路径中,围绕这个轮廓创建Region。这样,一个被绘制
    在窗体表面的以字符串轮廓为区域的Region就被创建了。最后,Setup方法创建以材质刷子
    为背景和以实色刷子为前景来绘制窗体。

    protected override void OnPaintBackground(PaintEventArgs e) {
     base.OnPaintBackground(e);
     Graphics bgGraphics = e.Graphics;
     bgGraphics.SetClip(textRegion, CombineMode.Exclude);
     bgGraphics.FillRectangle(backgroundBrush, e.ClipRectangle);
     bgGraphics.Dispose();
    }

      上面定义的OnPaintBackground方法先立刻调用基类方法,这能够保证所有底层绘制的
    代码都能够被执行。下一步,从PaintEventArgs中获得Graphics对象,再将Graphics对象
    的剪切区域定义为textRegion对象。通过指定CombineMode.Exclude参数,明确无论在哪里
    绘制或怎样绘制Graphics对象都不绘制textRegion区域内部。

    protected override void OnPaint(PaintEventArgs e) {
     base.OnPaint(e);
     Graphics fgGraphics = e.Graphics;
     fgGraphics.FillRegion(foregroundBrush, textRegion);
     fgGraphics.Dispose();
    }

      最后,OnPaint事件负责精确的绘制出字符串。可以很容易的通过调用Graphics的Fil
    lRegion方法来实现。通过指定的前景刷子foregroundBrush和textRegion且仅是该区域被
    绘制。结果,Windows 窗体程序在运行之前确实“思考”该怎样进行绘制。

    TextClipping示例程序,通过Region定义的Windows Forms字符串。能够使程序在绘制时避
    开一个区域。

      适当的组合使用区域和智能重绘你可以编写出运行速度快且不会引起闪烁的绘制代码
    ,并且比单独使用双重缓冲区绘制还要节省内存的消耗。

      结论

      如果你的程序确定要进行绘制操作,使用几种技术可以增强绘制性能。确保争取设置
    控件属性以及适当的Paint事件处理是编写健壮程序的开始。在权衡好利弊后可以使用双重
    缓冲区技术产生非常“保护视力”的结果。最后,在实际绘制前进行思考到底哪些客户区
    域或Region需要被绘制将非常有益。

    展开全文
  • C# GDI+双缓冲技术

    千次阅读 2018-05-18 16:55:15
    http://blog.sina.com.cn/s/blog_4e7453df0100d2fy.html (2009-04-11 23:17:23)转载▼ 标签: 休闲分类: CSharp GDI+的双缓冲问题 我想有很多搞图形方面的朋...

    http://blog.sina.com.cn/s/blog_4e7453df0100d2fy.html

    (2009-04-11 23:17:23)
    标签:

    休闲

    分类: CSharp

                                        GDI+的双缓冲问题

        我想有很多搞图形方面的朋友都会用到双缓冲技术的时候,而且有的时候她的确是个头疼的问题。最近我也要用双缓冲技术,程序怎么调试都不合适,当要对图形进行移动时,总是会出现闪烁抖动。在网上找了些资料,说得都不清不楚的,折腾了一晚上也没弄出来。第二天觉定自己研究一下。现在把自己的一些想法拿出来跟大家分享一下。

    双缓冲的基本原理:(转)

        一直以来的误区:.net1.1 和 .net 2.0 在处理控件双缓冲上是有区别的。
        .net 1.1中,使用:this.SetStyle(ControlStyles.DoubleBuffer, true);
        .net 2.0中,使用:this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
    怪不说老是提示参数无效,一直也不知道是这个问题,呵呵
     
        要知道,图元无闪烁的实现和图元的绘制方法没有多少关系,只是绘制方法可以控制图元的刷新区域,使双缓冲性能更优!
     
    导致画面闪烁的关键原因分析:

         一、绘制窗口由于大小位置状态改变进行重绘操作时

         绘图窗口内容或大小每改变一次,都要调用Paint事件进行重绘操作,该操作会使画面重新刷新一次以维持窗口正常显示。刷新过程中会导致所有图元重新绘制,而各个图元的重绘操作并不会导致Paint事件发生,因此窗口的每一次刷新只会调用Paint事件一次。窗口刷新一次的过程中,每一个图元的重绘都会立即显示到窗口,因此整个窗口中,只要是图元所在的位置,都在刷新,而刷新的时间是有差别的,闪烁现象自然会出现。所以说,此时导致窗口闪烁现象的关键因素并不在于Paint事件调用的次数多少,而在于各个图元的重绘。

         根据以上分析可知,当图元数目不多时,窗口刷新的位置也不多,窗口闪烁效果并不严重;当图元数目较多时,绘图窗口进行重绘的图元数量增加,绘图窗口每一次刷新都会导致较多的图元重新绘制,窗口的较多位置都在刷新,闪烁现象自然就会越来越严重。特别是图元比较大绘制时间比较长时,闪烁问题会更加严重,因为时间延迟会更长。

         解决上述问题的关键在于:窗口刷新一次的过程中,让所有图元同时显示到窗口。
         二、进行鼠标跟踪绘制操作或者对图元进行变形操作时

         当进行鼠标跟踪绘制操作或者对图元进行变形操作时,Paint事件会频繁发生,这会使窗口的刷新次数大大增加。虽然窗口刷新一次的过程中所有图元同时显示到窗口,但也会有时间延迟,因为此时窗口刷新的时间间隔远小于图元每一次显示到窗口所用的时间。因此闪烁现象并不能完全消除!所以说,此时导致窗口闪烁现象的关键因素在于Paint事件发生的次数多少。
         解决此问题的关键在于:设置窗体或控件的几个关键属性。

    下面讲具体的实现方法:(转)

        1、在内存中建立一块“虚拟画布”:Bitmap bmp = new Bitmap(600, 600);
      2、获取这块内存画布的Graphics引用:Graphics g = Graphics.FromImage(bmp);

      3、在这块内存画布上绘图:如画线g.DrawLine(添加参数);

      4、将内存画布画到窗口中:this.CreateGraphics().DrawImage(bmp, 0, 0);

    在构造函数中加如下代码

    代码一:
          SetStyle(ControlStyles.UserPaint, true);
          SetStyle(ControlStyles.AllPaintingInWmPaint, true); // 禁止擦除背景.
          SetStyle(ControlStyles.DoubleBuffer, true); // 双缓冲

    或代码二:
       this.SetStyle(ControlStyles.DoubleBuffer | ControlStyles.UserPaint |

    ControlStyles.AllPaintingInWmPaint, true);
       this.UpdateStyles();
    (转载结束)

     

        上述方式适合直接在窗体上绘制图形,并且很容易做到。但有时我们需要在某个控件上绘制图形,那该怎么办呢?原理跟直接在窗体上绘制图形采用双缓冲是一样的,也要在控件的构造函数里设置上述代码一或代码二。那么又怎么设置呢?我是通过阅读MSDN,找到自定义控件的方法,并在控件的构造函数里设置。在后面的附录里,我会说明怎么做。

     

        在Microsoft Visual Studio 2005环境下的,用的C#语言,并采用GDI+。目标是实现简单的鼠标拖动画线,并且要把之前画过的线都重新画出来。
        整个程序使用了三个控件:一个SplitContainer控件、一个自定义的Panel控件和一个VS自带的Panel控件。SplitContainer控件的大小设置成窗体宽、半窗体高并定位在窗体的下半部分。自定义的Panel控件和VS自带的Panel控件都是通过设置它们的Dock属性使它们绑定到SplitContainer控件的Panel1和Panel2上。附录中会说到自定义的Panel控件是怎么定义的。
        窗体的上半部分采用双缓冲。自定义的Panel控件采用了双缓冲,是通过在自定义Panel控件时设置样式来做到的(设置方法与窗体的双缓冲设置方法一样,如下面三条语句),这不能够在面板的Paint方法里直接设置,因为SetStyle()在Panel类中不是public方法。VS自带的Panel控件没有采用双缓冲。

               SetStyle(ControlStyles.AllPaintingInWmPaint, true);
                SetStyle(ControlStyles.UserPaint, true);
                SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
          
        我把三者结合在一块,我想你一定能够弄明白双缓冲的原理、实现以及效果了吧,如图。如果朋友你不是很清楚,可以给我留言,咱们讨论一下。

    C# <wbr>GDI+双缓冲技术

        有两种方式来创建Graphics对象:第一是在内存上创建一块和显示区域或控件相同大小的画布,在这块画布上创建Graphics对象。接着所有的图元都在这块画布上绘制,绘制完成以后再使用该画布覆盖显示控件的背景,从而达到“显示一次仅刷新一次”的效果!第二是直接在内存上创建Graphics对象。

        第一种方式具体代码如下:

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
    using System.Drawing.Drawing2D;

    namespace PanelLib2Test
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();

                //激活窗体的双缓冲技术,可以注释掉看看是什么效果
                SetStyle(ControlStyles.AllPaintingInWmPaint, true);
                SetStyle(ControlStyles.UserPaint, true);
                SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
            }

            private Point startP, endP, curP = new Point();//定义线段的起始点,终止点,鼠标当前位置

            //用在自定义面板上的变量,采用双缓冲
            private bool mdFlag = false;//标志左键是否按下
            private Point[][] lines = new Point[1000][];//存储已经画过的线段
            private int k = 0;//线段数组当前下标,表示已经画过的线段数

            //用在系统面板上的变量,不采用双缓冲
            private bool mdFlag2 = false;
            private Point[][] lines2 = new Point[1000][];
            private int k2 = 0;

            //用在自定义面板上的变量,采用双缓冲
            private bool mdFlag3 = false;
            private Point[][] lines3 = new Point[1000][];
            private int k3 = 0;

            private void Form1_Load(object sender, EventArgs e)
            {
                //初始化各线段数组
                for (int i = 0; i < 1000; i++)
                {
                    lines[i] = new Point[2];
                }

                for (int i = 0; i < 1000; i++)
                {
                    lines2[i] = new Point[2];
                }

                for (int i = 0; i < 1000; i++)
                {
                    lines3[i] = new Point[2];
                }
               
                //将分割控件定位在窗体的下半部分
                splitContainer1.Location = new Point(0, ClientSize.Height / 2);
                splitContainer1.Size = new Size(ClientSize.Width, ClientSize.Height / 2);
            }

            private void panelLib21_Paint(object sender, PaintEventArgs e)
            {
                Graphics g = e.Graphics;
                //Graphics g = panelLib21.CreateGraphics();用这个不行,可以试试是什么效果
                Bitmap bmp = new Bitmap(panelLib21.Width, panelLib21.Height);
                Graphics bufg = Graphics .FromImage(bmp);
                //g.Clear(panelLib21.BackColor);//可用可不用,视具体情况
                //bufg.Clear(panelLib21.BackColor);//可用可不用

                bufg.DrawLine(new Pen(Color.Black, 1), startP, endP);
                for (int i = 0; i <= k; i++)
             {
                    bufg.DrawLine(new Pen(Color.Black, 1), lines[i][0], lines[i][1]);
             }
                bufg.DrawString("Custom Panel DoubleBufferd", new Font("verdana", 16), new

    SolidBrush(Color.Red), 0, 0);
                g.DrawImage(bmp, 0, 0);

                bufg.Dispose();
                bmp.Dispose();
                //g.Dispose();//注意这个地方的绘图面g是窗体的Paint事件中的,不能被释放,否则出现”

    Application.Run(new Form1());“参数无效错误
            }

            private void panelLib21_MouseDown(object sender, MouseEventArgs e)
            {
                if (e.Button == MouseButtons.Left)
                {
                    startP = e.Location;
                    endP = e.Location;
                    mdFlag = true;
                    panelLib21.Capture = true;//捕获panelLib21上鼠标按下
                }
            }

            private void panelLib21_MouseMove(object sender, MouseEventArgs e)
            {
                curP = e.Location;
                if (mdFlag)
                {
                    if (endP.X != curP.X || endP.Y != curP.Y)
                    {
                        endP = curP;
                        panelLib21.Invalidate();//刷新panelLib21
                    }
                }
            }

            private void panelLib21_MouseUp(object sender, MouseEventArgs e)
            {
                lines[k][0] = startP;
                lines[k][1] = endP;
                k++;

                mdFlag = false;
                panelLib21.Capture = false;//释放panelLib21上鼠标按下
            }

            private void panel1_Paint(object sender, PaintEventArgs e)
            {
                Graphics g = e.Graphics;
                // Graphics g = this.CreateGraphics();
                Bitmap bmp = new Bitmap(panelLib21.Width, panelLib21.Height);
                Graphics bufg = Graphics.FromImage(bmp);
                //g.Clear(this.BackColor);//可用可不用,视具体情况
                //bufg.Clear(this.BackColor);//可用可不用
                bufg.DrawLine(new Pen(Color.Black, 1), startP, endP);

                for (int i = 0; i <= k2; i++)
                {
                    bufg.DrawLine(new Pen(Color.Black, 1), lines2[i][0], lines2[i][1]);
                }
                bufg.DrawString("System Panel Un-DoubleBufferd", new Font("verdana", 16), new

    SolidBrush(Color.Red), 0, 0);
                g.DrawImage(bmp, 0, 0);
               
                bufg.Dispose();
                bmp.Dispose();
            }

            private void panel1_MouseDown(object sender, MouseEventArgs e)
            {
                if (e.Button == MouseButtons.Left)
                {
                    startP = e.Location;
                    endP = e.Location;
                    mdFlag2 = true;
                    panel1.Capture = true;
                }
            }

            private void panel1_MouseMove(object sender, MouseEventArgs e)
            {
                curP = e.Location;
                if (mdFlag2)
                {
                    if (endP.X != curP.X || endP.Y != curP.Y)
                    {
                        endP = curP;
                        panel1.Invalidate();
                    }
                }
            }

            private void panel1_MouseUp(object sender, MouseEventArgs e)
            {
                lines2[k2][0] = startP;
                lines2[k2][1] = endP;
                k2++;

                mdFlag2 = false;
                panel1.Capture = false;
            }

            private void Form1_Paint(object sender, PaintEventArgs e)
            {
                Graphics g = e.Graphics;
                // Graphics g = this.CreateGraphics();
                Bitmap bmp = new Bitmap(ClientSize .Width, ClientSize .Height - splitContainer1

    .Height );//定义窗体宽、半窗体高的缓冲位图
                Graphics bufg = Graphics.FromImage(bmp);
                //g.Clear(this.BackColor);//可用可不用,视具体情况
                //bufg.Clear(this.BackColor);//可用可不用
                bufg.DrawLine(new Pen(Color.Black, 1), startP, endP);

                for (int i = 0; i <= k3; i++)
                {
                    bufg.DrawLine(new Pen(Color.Black, 1), lines3[i][0], lines3[i][1]);
                }
                bufg.DrawString("From DoubleBufferd", new Font("verdana", 16), new SolidBrush

    (Color.Red), 0, 0);
                g.DrawImage(bmp, 0, 0);

                bufg.Dispose();
                bmp.Dispose();
            }

            private void Form1_MouseDown(object sender, MouseEventArgs e)
            {
                if (e.Button == MouseButtons.Left)
                {
                    startP = e.Location;
                    endP = e.Location;
                    mdFlag3 = true;
                    Capture = true;
                }
            }

            private void Form1_MouseMove(object sender, MouseEventArgs e)
            {
                curP = e.Location;
                if (mdFlag3)
                {
                    if (endP.X != curP.X || endP.Y != curP.Y)
                    {
                        endP = curP;
                        Invalidate(new Rectangle(0, 0, ClientSize.Width, ClientSize.Height -

    splitContainer1.Height));//只刷新窗体的上半部分
                    }
                }
            }

            private void Form1_MouseUp(object sender, MouseEventArgs e)
            {
                lines3[k3][0] = startP;
                lines3[k3][1] = endP;
                k3++;

                mdFlag3 = false;
                Capture = false;
            }

            private void Form1_SizeChanged(object sender, EventArgs e)
            {
                splitContainer1.Location = new Point(0, ClientSize.Height / 2);
                splitContainer1.Size = new Size(ClientSize.Width, ClientSize.Height / 2);
            }
        }
    }

    第二种方式只需将Form1_Paint方法里改成如下代码:

     private void Form1_Paint(object sender, PaintEventArgs e)
     {
                BufferedGraphicsContext currentContext = BufferedGraphicsManager.Current;
                BufferedGraphics bufg = currentContext .Allocate (e.Graphics, new Rectangle(0, 0,

    ClientSize.Width, ClientSize.Height - splitContainer1.Height));
                Graphics g = bufg.Graphics;
                g.Clear(this.BackColor);
               
                g.DrawLine(new Pen(Color.Black, 1), startP, endP);
                for (int i = 0; i <= k3; i++)
                {
                  
                    g.DrawLine(new Pen(Color.Black, 1), lines3[i][0], lines3[i][1]);
                }
                g.DrawString("From DoubleBufferd", new Font("verdana", 16), new SolidBrush

    (Color.Red), 0, 0);
               
                bufg.Render(e.Graphics);
                g.Dispose();
                bufg .Dispose ();
     }

        你可以在panel1_Paint方法中设置如下代码来证明SetStyle的那三句话的重要性:

     private void panel1_Paint(object sender, PaintEventArgs e)
            {
                BufferedGraphicsContext currentContext = BufferedGraphicsManager.Current;
                BufferedGraphics bufg = currentContext.Allocate(e.Graphics, new Rectangle(0, 0,

    panel1.Width, panel1.Height));
                Graphics g = bufg.Graphics;
                g.Clear(panel1.BackColor);

                g.DrawLine(new Pen(Color.Black, 1), startP, endP);
                for (int i = 0; i <= k2; i++)
                {
                    g.DrawLine(new Pen(Color.Black, 1), lines2[i][0], lines2[i][1]);
                }
                g.DrawString("System Panel Un-DoubleBufferd", new Font("verdana", 16), new

    SolidBrush(Color.Red), 0, 0);
               
                bufg.Render(e.Graphics);
                g.Dispose();
                bufg.Dispose();
     }

    附录:自定义设置双缓冲的Panel面板控件

    1.创建项目
        创建新项目时,应指定其名称以便设置根命名空间、程序集名称和项目名称,并确保默认组件位于正确的命名空间中。

        创建 panelLib2 控件库和 panelLib2 控件在“文件”菜单上,指向“新建”,然后单击“项目”打开“新建项目”对话框。

        在 Visual C# 项目列表中,选择“Windows 控件库”项目模板,然后在“名称”框中键入 panelLib2。

        在“解决方案资源管理器”中右击“UserControl1.cs”,再从快捷菜单中选择“重命名”。将文件名更改为 panelLib2.cs。系统询问是否要重命名对“UserControl1”代码元素的所有引用时,单击“是”按钮。

        在“解决方案资源管理器”中右击“panelLib2.cs”,再选择“查看代码”。

        找到 class 语句行 public partial class panelLib2,并将此控件继承的类型从 UserControl 更改为Panel。这允许您所继承的控件继承 Panel 控件的所有功能。

        在“解决方案资源管理器”中打开“panelLib2.cs”节点,以显示设计器生成的代码文件“panelLib2.Designer.cs”。在“代码编辑器”中打开此文件。

        找到 InitializeComponent 方法并删除分配 AutoScaleMode 属性的行。在 Panel 控件中并不存在此属性。

        从“文件”菜单中,选择“全部保存”来保存项目。

    注意
    可视化设计器不再可用。由于 Panel 控件自行绘制,因此您无法在设计器中修改其外观。除非在代码中进行修改,否则它的可视化表示形式将与它所继承的类(即 Panel)的可视化表示形式完全一样。但您仍然可以向设计器图面添加不含 UI 元素的组件。
     
    2.将属性添加到继承的控件中
    继承的 Windows 窗体控件的可能用途之一是创建与标准 Windows 窗体控件的外观相同、但公开自定义属性的控件。在本节中,您将向控件中设置双缓冲属性。

        在“解决方案资源管理器”中,右击“panelLib2.cs”,然后从快捷菜单中单击“查看代码”。

    找到 class 语句。紧接在 { 后面键入下列代码:

                SetStyle(ControlStyles.AllPaintingInWmPaint, true);
                SetStyle(ControlStyles.UserPaint, true);
                SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
     
        从“文件”菜单中,选择“全部保存”来保存项目。

    3.测试控件
        控件不是独立的项目,它们必须寄宿在某个容器中。若要测试控件,必须提供一个运行该控件的测试项目。还必须通过生成(编译)该控件使其可由测试项目访问。在本节中,将生成控件并在 Windows 窗体中测试它。

    4.生成控件
        在“生成”菜单上单击“生成解决方案”。生成应该成功,没有任何编译器错误或警告。

    5.创建测试项目
        在“文件”菜单上,指向“添加”,然后单击“新项目”打开“添加新项目”对话框。

        在“Visual C#”节点下选择“Windows”节点,再单击“Windows 应用程序”。

        在“名称”框中键入 panelLib2Test。

        在“解决方案资源管理器”中,右击测试项目的“引用”节点,然后从快捷菜单上选择“添加引用”以显示“添加引用”对话框。

        单击标记为“项目”的选项卡。panelLib2 项目将在“项目名称”下列出。双击项目以添加对测试项目的引用。

        在“解决方案资源管理器”中右击“测试”,再选择“生成”。
    6.将控件添加到窗体
        在“解决方案资源管理器”中,右击“Form1.cs”,然后从快捷菜单中选择“视图设计器”。

        在“工具箱”中单击“panelLib2 组件”。双击“panelLib2”。窗体上显示一个“panelLib2”。

        在“解决方案资源管理器”中,右击“测试”,然后从快捷菜单中选择“设为启动项目”。

        从“调试”菜单中选择“启动调试”。将显示 Form1。

     

        以上只是说了如何自定义一个设置为双缓冲属性的Panel控件,我想其它两个控件的添加是比较容易的,这就不赘述了,有问题请留言

    展开全文
  • D3D12渲染技术之常量缓冲

    千次阅读 2018-09-16 13:45:56
    常量缓冲区是GPU资源(ID3D12Resource)的示例,其数据内容可以在着色器程序中引用。 正如我们将在博客中学到的,纹理和其他类型的缓冲区资源也可以在着色器程序中引用。顶点着色器具有以下代码: cbuffer ...

    常量缓冲区是GPU资源(ID3D12Resource),其数据内容可以在着色器程序中引用,正如我们将在博客中学到的,纹理和其他类型的缓冲区资源也可以在着色器程序中引用。顶点着色器具有以下代码:

    cbuffer cbPerObject : register(b0)
    {
      float4x4 gWorldViewProj; 
    };

    此代码引用名为cbPerObject的cbuffer对象(常量缓冲区),在此案例中,常量缓冲区存储一个名为gWorldViewProj的4×4矩阵,表示用于将点从局部空间转换为齐次裁剪空间的组合世界:视图和投影矩阵。 在HLSL中,内置的float4x4类型声明了一个4×4矩阵; 例如,要声明一个3×4矩阵和2×4矩阵,我们将分别使用float3x4和float2x2类型。
    与顶点和索引缓冲区不同,常量缓冲区通常由CPU每帧更新一次, 例如,如果摄像机每帧移动,则需要使用每帧的新视图矩阵更新常量缓冲区。 因此,我们在上传堆而不是默认堆中创建常量缓冲区,以便我们可以从CPU更新内容。
    常量缓冲区还具有特殊的硬件要求,即它们的大小必须是最小硬件分配大小(256字节)的倍数。
    通常我们需要多个相同类型的常量缓冲区, 例如,上面的常量缓冲区cbPerObject存储每个对象不同的常量,因此如果我们有n个对象,那么我们将需要n个这种类型的常量缓冲区。 以下代码显示了我们如何创建一个缓冲区来存储NumElements许多常量缓冲区:

    struct ObjectConstants
    {
      DirectX::XMFLOAT4X4 WorldViewProj = MathHelper::Identity4x4();
    };
    UINT elementByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
    
    ComPtr<ID3D12Resource> mUploadCBuffer;
    device->CreateCommittedResource(
      &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
      D3D12_HEAP_FLAG_NONE,
      &CD3DX12_RESOURCE_DESC::Buffer(mElementByteSize * NumElements),
      D3D12_RESOURCE_STATE_GENERIC_READ,
      nullptr,
      IID_PPV_ARGS(&mUploadCBuffer));

    我们可以将mUploadCBuffer视为存储ObjectConstants类型的常量缓冲区数组(使用填充来生成256字节的倍数)。 当绘制对象时,我们只需将常量缓冲区视图(CBV)绑定到缓冲区的子区域,该区域存储该对象的常量。 请注意,我们经常将缓冲区mUploadCBuffer称为常量缓冲区,因为它存储了一个常量缓冲区数组。
    程序函数d3dUtil :: CalcConstantBufferByteSize执行算术以将缓冲区的字节大小舍入为最小硬件分配大小的倍数(256字节):

    UINT d3dUtil::CalcConstantBufferByteSize(UINT byteSize)
    {
      // Constant buffers must be a multiple of the minimum hardware
      // allocation size (usually 256 bytes). So round up to nearest
      // multiple of 256. We do this by adding 255 and then masking off
      // the lower 2 bytes which store all bits < 256.
      // Example: Suppose byteSize = 300.
      // (300 + 255) & ˜255
      // 555 & ˜255
      // 0x022B & ˜0x00ff
      // 0x022B & 0xff00
      // 0x0200
      // 512
      return (byteSize + 255) & ˜255;
    }

    注意,即使我们以256的倍数分配常量数据,也没有必要在HLSL结构中显式填充相应的常量数据,因为它是隐式完成的:

    // Implicitly padded to 256 bytes.
    cbuffer cbPerObject : register(b0)
    {
      float4x4 gWorldViewProj; 
    };
    
    // Explicitly padded to 256 bytes.
    cbuffer cbPerObject : register(b0)
    {
      float4x4 gWorldViewProj; 
      float4x4 Pad0;
      float4x4 Pad1;
      float4x4 Pad1;
    };

    为了避免将常量缓冲区元素舍入为256字节的倍数,可以显式填充所有常量缓冲区结构,使其始终为256字节的倍数。
    Direct3D12引入了着色器模型5.1。 Shader模型5.1引入了另一种HLSL语法,用于定义如下所示的常量缓冲区:

    struct ObjectConstants
    {
      float4x4 gWorldViewProj;
      uint matIndex; 
    };
    ConstantBuffer<ObjectConstants> gObjConstants : register(b0);

    这里,常量缓冲区的数据元素只是在一个单独的结构中定义,然后从该结构创建一个常量缓冲区。 然后使用数据成员语法在着色器中访问常量缓冲区的字段:

    uint index = gObjConstants.matIndex;

    因为使用堆类型D3D12_HEAP_TYPE_UPLOAD创建了常量缓冲区,所以我们可以将数据从CPU上传到常量缓冲区资源。 要做到这一点,我们首先必须获得一个指向资源数据的指针,这可以使用Map方法完成:

    ComPtr<ID3D12Resource> mUploadBuffer;
    BYTE* mMappedData = nullptr;
    mUploadBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mMappedData));

    第一个参数是标识要映射的子资源的子资源索引, 对于缓冲区,唯一的子资源是缓冲区本身,因此我们将其设置为0,第二个参数是指向D3D12_RANGE结构的可选指针,该结构描述要映射的内存范围;指定null映射整个资源。 第二个参数返回指向映射数据的指针,要将数据从系统内存复制到常量缓冲区,我们可以执行memcpy:

    memcpy(mMappedData, &data, dataSizeInBytes);

    当我们完成一个常量缓冲区时,我们应该在释放内存之前取消映射它:

    if(mUploadBuffer != nullptr)
      mUploadBuffer->Unmap(0, nullptr);
    
    mMappedData = nullptr;

    Unmap的第一个参数是一个子资源索引,用于标识要映射的子资源,缓冲区为0。 Unmap的第二个参数是指向D3D12_RANGE结构的可选指针,该结构描述了要取消映射的内存范围; 指定null取消映射整个资源。

    我们在UploadBuffer.h中定义了以下类,以便更轻松地使用上传缓冲区, 它为我们处理上传缓冲区资源的构造和销毁,处理映射和取消映射资源,并提供CopyData方法来更新缓冲区中的特定元素。 当我们需要从CPU更改上传缓冲区的内容时(例如,当视图矩阵改变时),我们使用CopyData方法。 请注意,此类可用于任何上传缓冲区,不一定是常量缓冲区。 但是,如果我们将它用于常量缓冲区,我们需要通过isConstantBuffer构造函数参数来指示。 如果它存储一个常量缓冲区,那么它将自动填充内存,使每个常量缓冲区成为256字节的倍数。

    template<typename T>
    class UploadBuffer
    {
    public:
      UploadBuffer(ID3D12Device* device, UINT elementCount, bool isConstantBuffer) : 
        mIsConstantBuffer(isConstantBuffer)
      {
        mElementByteSize = sizeof(T);
    
        // Constant buffer elements need to be multiples of 256 bytes.
        // This is because the hardware can only view constant data 
        // at m*256 byte offsets and of n*256 byte lengths. 
        // typedef struct D3D12_CONSTANT_BUFFER_VIEW_DESC {
        // UINT64 OffsetInBytes; // multiple of 256
        // UINT  SizeInBytes;  // multiple of 256
        // } D3D12_CONSTANT_BUFFER_VIEW_DESC;
        if(isConstantBuffer)
        mElementByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(T));
    
        ThrowIfFailed(device->CreateCommittedResource(
          &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
          D3D12_HEAP_FLAG_NONE,
          &CD3DX12_RESOURCE_DESC::Buffer(mElementByteSize*elementCount),
             D3D12_RESOURCE_STATE_GENERIC_READ,
          nullptr,
          IID_PPV_ARGS(&mUploadBuffer)));
    
        ThrowIfFailed(mUploadBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mMappedData)));
    
        // We do not need to unmap until we are done with the resource.
        // However, we must not write to the resource while it is in use by
        // the GPU (so we must use synchronization techniques).
      }
       UploadBuffer(const UploadBuffer& rhs) = delete;
      UploadBuffer& operator=(const UploadBuffer& rhs) = delete;
      ˜UploadBuffer()
      {
        if(mUploadBuffer != nullptr)
          mUploadBuffer->Unmap(0, nullptr);
    
        mMappedData = nullptr;
      }
    
      ID3D12Resource* Resource()const
      {
        return mUploadBuffer.Get();
      }
    
      void CopyData(int elementIndex, const T& data)
      {
        memcpy(&mMappedData[elementIndex*mElementByteSize], &data, sizeof(T));
      }
    
    private:
      Microsoft::WRL::ComPtr<ID3D12Resource> mUploadBuffer;
      BYTE* mMappedData = nullptr;
    
      UINT mElementByteSize = 0;
      bool mIsConstantBuffer = false;
    };

    通常,对象的世界矩阵在移动/旋转/缩放时将改变,当摄像机移动/旋转时视图矩阵改变,并且当窗口调整大小时投影矩阵改变。 在本篇的案例中,我们允许用户使用鼠标旋转和移动摄像机,并且我们在Update功能的每一帧中使用新视图矩阵更新组合的世界视图投影矩阵:

    void BoxApp::OnMouseMove(WPARAM btnState, int x, int y)
    {
      if((btnState & MK_LBUTTON) != 0)
      {
        // Make each pixel correspond to a quarter of a degree.
        float dx = XMConvertToRadians(0.25f*static_cast<float> (x - mLastMousePos.x));
        float dy = XMConvertToRadians(0.25f*static_cast<float> (y - mLastMousePos.y));
    
        // Update angles based on input to orbit camera around box.
        mTheta += dx;
        mPhi += dy;
         // Restrict the angle mPhi.
        mPhi = MathHelper::Clamp(mPhi, 0.1f, MathHelper::Pi - 0.1f);
      }
      else if((btnState & MK_RBUTTON) != 0)
      {
        // Make each pixel correspond to 0.005 unit in the scene.
        float dx = 0.005f*static_cast<float>(x - mLastMousePos.x);
        float dy = 0.005f*static_cast<float>(y - mLastMousePos.y);
    
        // Update the camera radius based on input.
        mRadius += dx - dy;
    
        // Restrict the radius.
        mRadius = MathHelper::Clamp(mRadius, 3.0f, 15.0f);
      }
    
      mLastMousePos.x = x;
      mLastMousePos.y = y;
      }
    
    void BoxApp::Update(const GameTimer& gt)
    {
      // Convert Spherical to Cartesian coordinates.
      float x = mRadius*sinf(mPhi)*cosf(mTheta);
      float z = mRadius*sinf(mPhi)*sinf(mTheta);
      float y = mRadius*cosf(mPhi);
    
      // Build the view matrix.
      XMVECTOR pos = XMVectorSet(x, y, z, 1.0f);
      XMVECTOR target = XMVectorZero();
      XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
    
      XMMATRIX view = XMMatrixLookAtLH(pos, target, up);
      XMStoreFloat4x4(&mView, view);
    
      XMMATRIX world = XMLoadFloat4x4(&mWorld);
      XMMATRIX proj = XMLoadFloat4x4(&mProj);
       XMMATRIX worldViewProj = world*view*proj;
    
      // Update the constant buffer with the latest worldViewProj matrix.
      ObjectConstants objConstants;
      XMStoreFloat4x4(&objConstants.WorldViewProj,  XMMatrixTranspose(worldViewProj));
      mObjectCB->CopyData(0, objConstants);
    }

    回想一下,我们通过描述符对象将资源绑定到渲染管道, 到目前为止,我们已经使用描述符/视图来渲染目标,深度/模板缓冲区以及顶点和索引缓冲区。 我们还需要描述符来将常量缓冲区绑定到管道, 常量缓冲区描述符位于D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV类型的描述符堆中。 这样的堆可以存储常量缓冲区,着色器资源和无序访问描述符的混合。 要存储这些新类型的描述符,我们需要创建一个这种类型的新描述符堆:

    D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc;
    cbvHeapDesc.NumDescriptors = 1;
    cbvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
    cbvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
    cbvHeapDesc.NodeMask = 0;
    
    ComPtr<ID3D12DescriptorHeap> mCbvHeap
    md3dDevice->CreateDescriptorHeap(&cbvHeapDesc,
      IID_PPV_ARGS(&mCbvHeap));

    此代码类似于我们创建渲染目标和深度/模板缓冲区描述符堆的方式, 但是,一个重要的区别是我们指定D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE标志以指示着色器程序将访问这些描述符。 在演示中,我们没有SRV或UAV描述符,我们只会绘制一个对象; 因此,我们在这个堆中只需要1个描述符来存储1个CBV。
    通过填写D3D12_CONSTANT_BUFFER_VIEW_DESC实例并调用来创建常量缓冲区视图

    ID3D12Device::CreateConstantBufferView:
    // Constant data per-object.
    struct ObjectConstants
    {
      XMFLOAT4X4 WorldViewProj = MathHelper::Identity4x4();
    };
    // Constant buffer to store the constants of n object.
    std::unique_ptr<UploadBuffer<ObjectConstants>> mObjectCB = nullptr;
    mObjectCB = std::make_unique<UploadBuffer<ObjectConstants>>(
      md3dDevice.Get(), n, true);
    
    UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
    
    // Address to start of the buffer (0th constant buffer).
    D3D12_GPU_VIRTUAL_ADDRESS cbAddress = mObjectCB->Resource()->GetGPUVirtualAddress();
    
    // Offset to the ith object constant buffer in the buffer.
    int boxCBufIndex = i;
    cbAddress += boxCBufIndex*objCBByteSize;
    
    D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
    cbvDesc.BufferLocation = cbAddress;
    cbvDesc.SizeInBytes = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
    
    md3dDevice->CreateConstantBufferView(
      &cbvDesc,
      mCbvHeap->GetCPUDescriptorHandleForHeapStart());
    // Texture resource bound to texture register slot 0.
    Texture2D  gDiffuseMap : register(t0);
    
    // Sampler resources bound to sampler register slots 0-5.
    SamplerState gsamPointWrap        : register(s0);
    SamplerState gsamPointClamp       : register(s1);
    SamplerState gsamLinearWrap       : register(s2);
    SamplerState gsamLinearClamp      : register(s3);
    SamplerState gsamAnisotropicWrap  : register(s4);
    SamplerState gsamAnisotropicClamp : register(s5);
    
    // cbuffer resource bound to cbuffer register slots 0-2
    cbuffer cbPerObject : register(b0)
    {
      float4x4 gWorld;
      float4x4 gTexTransform;
    };
    
    // Constant data that varies per material.
    cbuffer cbPass : register(b1)
    {
      float4x4 gView;
      float4x4 gProj;
      […] // Other fields omitted for brevity.
    };
    
    cbuffer cbMaterial : register(b2)
    {
      float4  gDiffuseAlbedo;
      float3  gFresnelR0;
      float  gRoughness;
      float4x4 gMatTransform;
    };
    
    // Root parameter can be a table, root descriptor or root constants.
    CD3DX12_ROOT_PARAMETER slotRootParameter[1];
    
    // Create a single descriptor table of CBVs.
    CD3DX12_DESCRIPTOR_RANGE cbvTable;
    cbvTable.Init(
      D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 
      1, // Number of descriptors in table
      0);// base shader register arguments are bound to for this root parameter
    
    slotRootParameter[0].InitAsDescriptorTable(
      1,     // Number of ranges
      &cbvTable); // Pointer to array of ranges
    
    // A root signature is an array of root parameters.
    CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(1, slotRootParameter, 0, nullptr, 
      D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
    
    // create a root signature with a single slot which points to a 
    // descriptor range consisting of a single constant buffer.
    ComPtr<ID3DBlob> serializedRootSig = nullptr;
    ComPtr<ID3DBlob> errorBlob = nullptr;
    HRESULT hr = D3D12SerializeRootSignature(&rootSigDesc, 
      D3D_ROOT_SIGNATURE_VERSION_1,
      serializedRootSig.GetAddressOf(), 
      errorBlob.GetAddressOf());
    
    ThrowIfFailed(md3dDevice->CreateRootSignature(
      0,
      serializedRootSig->GetBufferPointer(),
      serializedRootSig->GetBufferSize(),
      IID_PPV_ARGS(&mRootSignature)));
    CD3DX12_ROOT_PARAMETER slotRootParameter[1];
    
    CD3DX12_DESCRIPTOR_RANGE cbvTable;
    cbvTable.Init(
      D3D12_DESCRIPTOR_RANGE_TYPE_CBV, // table type
      1, // Number of descriptors in table
      0);// base shader register arguments are bound to for this root parameter
      cbvDesc.SizeInBytes = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
    
    md3dDevice->CreateConstantBufferView(
      &cbvDesc,
      mCbvHeap->GetCPUDescriptorHandleForHeapStart());

    D3D12_CONSTANT_BUFFER_VIEW_DESC结构描述了要绑定到HLSL常量缓冲区结构的常量缓冲区资源的子集。 如上所述,通常一个常量缓冲区存储n个对象的每个对象常量数组,但是我们可以通过使用BufferLocation和SizeInBytes来获取第i个对象常量数据的视图。 由于硬件要求,D3D12_CONSTANT_BUFFER_VIEW_DESC :: SizeInBytes和D3D12_CONSTANT_BUFFER_VIEW_DESC :: OffsetInBytes成员必须是256字节的倍数。 例如,如果指定了64,那么将收到以下调试错误:
    D3D12 ERROR: ID3D12Device::CreateConstantBufferView: SizeInBytes of 64 is invalid. Device requires SizeInBytes be a multiple of 256.

    D3D12 ERROR: ID3D12Device:: CreateConstantBufferView: OffsetInBytes of 64 is invalid.
    设备要求OffsetInBytes是256的倍数。
    通常,不同的着色器程序期望在执行绘制调用之前将不同的资源绑定到呈现管道, 资源绑定到特定的寄存器槽,可以通过着色器程序访问它们。 例如,前一个顶点和像素着色器只需要一个常量缓冲区绑定到寄存器b0。 我们会在后面使用的一组更高级的顶点和像素着色器需要将几个常量缓冲区,纹理和采样器绑定到各种寄存器槽:

    // Texture resource bound to texture register slot 0.
    Texture2D  gDiffuseMap : register(t0);
    
    // Sampler resources bound to sampler register slots 0-5.
    SamplerState gsamPointWrap        : register(s0);
    SamplerState gsamPointClamp       : register(s1);
    SamplerState gsamLinearWrap       : register(s2);
    SamplerState gsamLinearClamp      : register(s3);
    SamplerState gsamAnisotropicWrap  : register(s4);
    SamplerState gsamAnisotropicClamp : register(s5);
    
    // cbuffer resource bound to cbuffer register slots 0-2
    cbuffer cbPerObject : register(b0)
    {
      float4x4 gWorld;
      float4x4 gTexTransform;
    };
    
    // Constant data that varies per material.
    cbuffer cbPass : register(b1)
    {
      float4x4 gView;
      float4x4 gProj;
      […] // Other fields omitted for brevity.
    };
    
    cbuffer cbMaterial : register(b2)
    {
      float4  gDiffuseAlbedo;
      float3  gFresnelR0;
      float  gRoughness;
      float4x4 gMatTransform;
    };

    根签名定义了在执行绘制调用之前应用程序将绑定到呈现管道的资源以及这些资源被映射到着色器输入寄存器的位置。 根签名必须与它将使用的着色器兼容(即,根签名必须提供着色器期望在执行绘制调用之前绑定到渲染管道的所有资源); 这将在创建管道状态对象时验证后面介绍。 不同的绘制调用可以使用不同的着色器程序集,这将需要不同的根签名。
    注意,如果我们将着色器程序视为一个函数,并将着色器期望的输入资源视为函数参数,那么根签名可以被认为是定义函数签名(因此名称为root签名)。 通过将不同的资源绑定为参数,着色器输出将是不同的。 因此,例如,顶点着色器将取决于输入到着色器的实际顶点,以及绑定的资源。
    ID3D12RootSignature接口在Direct3D中表示根签名,它由一组根参数定义,这些参数描述着色器对绘制调用所期望的资源。 根参数可以是根常量,根描述符或描述符表。 我们将在下一篇博客讨论根常量和根描述符; 在本篇中,我们将只使用描述符表, 描述符表指定描述符堆中的连续描述符范围。
    下面的代码创建了一个根签名,它有一个根参数,它是一个足以存储一个CBV(常量缓冲区视图)的描述符表:

    // Root parameter can be a table, root descriptor or root constants.
    CD3DX12_ROOT_PARAMETER slotRootParameter[1];
    
    // Create a single descriptor table of CBVs.
    CD3DX12_DESCRIPTOR_RANGE cbvTable;
    cbvTable.Init(
      D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 
      1, // Number of descriptors in table
      0);// base shader register arguments are bound to for this root parameter
    
    slotRootParameter[0].InitAsDescriptorTable(
      1,     // Number of ranges
      &cbvTable); // Pointer to array of ranges
    
    // A root signature is an array of root parameters.
    CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(1, slotRootParameter, 0, nullptr, 
      D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
    
    // create a root signature with a single slot which points to a 
    // descriptor range consisting of a single constant buffer.
    ComPtr<ID3DBlob> serializedRootSig = nullptr;
    ComPtr<ID3DBlob> errorBlob = nullptr;
    HRESULT hr = D3D12SerializeRootSignature(&rootSigDesc, 
      D3D_ROOT_SIGNATURE_VERSION_1,
      serializedRootSig.GetAddressOf(), 
      errorBlob.GetAddressOf());
    
    ThrowIfFailed(md3dDevice->CreateRootSignature(
      0,
      serializedRootSig->GetBufferPointer(),
      serializedRootSig->GetBufferSize(),
      IID_PPV_ARGS(&mRootSignature)));

    我们将描述
    CD3DX12_ROOT_PARAMETER和CD3DX12_DESCRIPTOR_RANGE在下一篇中有更多内容,但现在只需了解代码:

    CD3DX12_ROOT_PARAMETER slotRootParameter[1];
    
    CD3DX12_DESCRIPTOR_RANGE cbvTable;
    cbvTable.Init(
      D3D12_DESCRIPTOR_RANGE_TYPE_CBV, // table type
      1, // Number of descriptors in table
      0);// base shader register arguments are bound to for this root parameter
    
    slotRootParameter[0].InitAsDescriptorTable(
      1,     // Number of ranges
      &cbvTable); // Pointer to array of ranges

    创建一个根参数,该参数期望1 CBV的描述符表被绑定到常量缓冲寄存器0(即HLSL代码中的寄存器(b0))
    我们在本篇博客中的根签名示例非常简单, 我们将在本系列博客中看到许多根签名的例子,并且它们将根据需要增加复杂性。
    根签名仅定义应用程序将绑定到呈现管道的资源; 它实际上没有做任何资源绑定。 使用命令列表设置根签名后,我们使用ID3D12GraphicsCommandList :: SetGraphicsRootDescriptorTable将描述符表绑定到管道:

    void ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable( 
      UINT RootParameterIndex,
      D3D12_GPU_DESCRIPTOR_HANDLE BaseDescriptor);

    RootParameterIndex:我们正在设置的根参数的索引。
    BaseDescriptor:处理堆中的描述符,该描述符指定正在设置的表中的第一个描述符。 例如,如果根签名指定此表有五个描述符,则堆中的BaseDescriptor和接下来的四个描述符将被设置为此根表。
    以下代码将根签名和CBV堆设置为命令列表,并设置描述符表以标识要绑定到管道的资源:

    mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
    ID3D12DescriptorHeap* descriptorHeaps[] = { mCbvHeap.Get() };
    mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps); 
    
    // Offset the CBV we want to use for this draw call.
    CD3DX12_GPU_DESCRIPTOR_HANDLE cbv(mCbvHeap ->GetGPUDescriptorHandleForHeapStart());
    cbv.Offset(cbvIndex, mCbvSrvUavDescriptorSize);
    
    mCommandList->SetGraphicsRootDescriptorTable(0, cbv);

    注意,为了提高性能,请使根签名尽可能小,并尝试最小化每个渲染帧更改根签名的次数。
    当内容的任何部分在绘制/调度调用之间发生变化时,应用程序绑定的根签名(描述符表,根常量和根描述符)的内容会自动获得D3D12驱动程序的版本控制。 因此,每个绘制/分派都会获得一组唯一的Root Signature状态。
    如果更改根签名,则会丢失所有现有绑定。 也就是说,您需要将所有资源重新绑定到新根签名所需的管道。

    展开全文
  • Java非对称加密源码实例 1个目标文件 摘要:Java源码,算法相关,非对称加密 Java非对称加密源程序代码实例,本例中使用RSA加密技术,定义加密算法可用 DES,DESede,Blowfish等。 设定字符串为“张三,你好,我是李四”...
  • Java基础知识面试题(2020最新版)

    万次阅读 多人点赞 2020-02-19 12:11:27
    Java面试总结(2021优化版)已发布在个人微信公众号【技术人成长之路】,优化版首先修正了读者反馈的部分答案存在的错误,同时根据最新面试总结,删除了低频问题,添加了一些常见面试题,对文章进行了精简优化,欢迎...
  • 队列1-环形缓冲

    千次阅读 2019-01-27 16:58:34
    本篇为队列的第一篇文章,介绍基于数组结构的一个环形缓冲区队列。我觉得没有必要再从数组来写起,毕竟对于数组本身来说,我觉得是没有太多可说的,但是基于数组的数据结构就有的说了。 什么是环形缓冲区 环形缓冲...
  • (至少两个例子:x86 和arm)(1)ROP在X86指令集上的实例(2)ROP在ARM上的可行性2、举例描述一个远程缓冲区溢出,思考和本地溢出的区别,攻击难点在哪里?3、描述arm和x64下的缓冲区溢出,比较和x86的差异。 1、...
  • java IO流 [缓冲技术] [装饰设计模式]

    千次阅读 2013-01-06 20:48:38
    IO流流的分类字符流的由来乱码的出现读写流流的读写操作WriterReader缓冲技术IO异常装饰设计模式InputStreamReader IO流  IO流---input 和 output 的简写,用来处理设备之间的数据传输 如内存 硬盘 网络等
  • 缓冲区溢出是一种非常普遍、非常危险的漏洞,在各种操作系统、应用软件中广泛存在。利用缓冲区溢出攻击,可以导致程序运行失败、系统宕机、重新启动等后果。更为严重的是,可以利用它执行非授权指令,甚至可以取得...
  • Java面试题大全(2020版)

    万次阅读 多人点赞 2019-11-26 11:59:06
    引用类型:比较的是引用是否相同; 代码示例: String x = "string"; String y = "string"; String z = new String("string"); System.out.println(x==y); // true System.out.println(x==z); // false System.out....
  • 缓冲区溢出漏洞分析技术研究进展 一.缓冲区溢出攻击步骤: 注入攻击代码 跳转到攻击代码(核心环节) 执行攻击代码 二.缓冲区溢出按照所攻击对象的不同可分为 3 类 破坏栈数据 ​ 改变 RETADDR 的值,使其存放已经...
  • 环形缓冲

    万次阅读 多人点赞 2018-10-20 15:11:46
     环形缓冲区是一项很好的技术,不用频繁的分配内存,而且在大多数情况下,内存的反复使用也使得我们能用更少的内存块做更多的事。    在网络IO线程中,我们会为每一个连接都准备一个环形缓冲区,用于临时存放...
  • IoT上的缓冲区溢出漏洞

    千次阅读 2018-09-18 07:01:53
    在过去N年里,缓冲区溢出一直是网络攻击中最常被利用的漏洞。 看一下缓冲区是如何创建的,就能知道原因所在。 下面是C语言的一个例子: 第一步,程序员使用 malloc 函数并定义缓冲区内存的数量(例如32位) 第二步,...
  •  我想有很多搞图形方面的朋友都会用到双缓冲技术的时候,而且有的时候她的确是个头疼的问题。最近我也要用双缓冲技术,程序怎么调试都不合适,当要对图形进行移动时,总是会出现闪烁抖动。在网上找了些资料,说得都...
  • 专栏作品利用GDI+的双缓冲技术来提高绘图效率卢彦 前言进入.NET时代,Windows的绘图技术也从GDI升级到了GDI+,从名字就能知道GDI+是对以前传统GDI绘图技术的一次升级,不过在微软几乎把所有的新技术都冠之.NET的情况...
  • 2021年前端面试题及答案

    万次阅读 多人点赞 2020-02-11 19:29:34
    前端面试汇总(2020年) 一 大纲 1、前言 2、前端工程化 3、前端设计模式 ...9、前端技术栈问题 前言 由于新冠肺炎疫情,现在成天呆在家里,加上也要准备面试,就在家里看面试题...
  • pb 缓冲区 rowsdiscard()

    千次阅读 2019-04-18 11:23:46
    浅议PB中数据窗口缓冲区...--引用 摘 要:文章描述了PowerBuilder中数据窗口缓冲区,详细介绍了数据窗口中行与列的修改状态以及PowerBuilder提供的相关函数,并给出了几则应用实例。 关键词:PowerBuilder ...
  • Java引用对象

    千次阅读 2018-12-11 10:09:03
    事实上,JVM使用被称为“标记-清除”的技术。标记-清除算法的思路很简单:所有不能被程序访问到的对象都是垃圾,都可以被收集。 标记-清除算法有以下阶段: 阶段一:标记 垃圾回收器从“root”引用开始,...
  • JDK

    千次阅读 2019-09-17 16:51:38
    逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术 逃逸分析的 JVM 参数如下: 开启逃逸分析:-XX:+DoEscapeAnalysis ...
  • 软件测试面试题汇总

    万次阅读 多人点赞 2018-09-27 12:31:09
    转载自: ... 软件测试面试题汇总 测试技术面试题 ...........................................................................................................
  • 引用:http://blog.163.com/danshiming@126/blog/static/109412748201632895137788/ 这几天看《OPenGL 编程指南》中的模板缓冲区,一头雾水,根本就没怎么理解,然后网上找了一些例子,先将自己理解的记录如下: ...
  • 测试开发笔记

    万次阅读 多人点赞 2019-11-14 17:11:58
    ★软件测试的目的、意义:(怎么做好软件测试) 7 3.软件生命周期: 7 第二章 测试过程 8 1.测试模型 8 H模型: 8 V模型 9 2.内部测试 10 3外部测试: 10 验收测试:(在系统测试之后) 11 回归测试: 11 4.测试过程...
  • JAVA上百实例源码以及开源项目

    千次下载 热门讨论 2016-01-03 17:37:40
    FTP的目标是:(1)提高文件的共享性(计算机程序和/或数据),(2)鼓励间接地(通过程序)使用远程计算机,(3)保护用户因主机之间的文件存储系统导致的变化,(4)为了可靠和高效地传输,虽然用户可以在终端上...
  • Cachelab 高速缓冲器模拟

    千次阅读 2019-01-14 17:54:31
    高速缓冲器模拟  专 业 计算机科学与技术 csim.c和trans.c代码见文章末尾 目 录 第1章 实验基本信息... - 3 - 1.1 实验目的... - 3 - 1.2 实验环境与工具... - 3 - 1.2.1 硬件环境... -...
  • C++面试题

    千次阅读 多人点赞 2019-01-07 17:06:10
    如果连接程序不能在所有的库和目标文件内找到所引用的函数、变量或标签,将产生此错误消息。 一般来说,发生该错误的原因有两个:一是所引用的函数、变量不存在、拼写不正确或者使用错误;其次可能使用了不同版本...
  • 下面一起来探究“缓冲区溢出和注入”问题(主要是关心程序的内存映像)。 进程的内存映像 永远的Hello World,太熟悉了吧, #include int main(void) { printf("Hello Worldn"); return 0; } 如果要用...
  • 内存绘图、双缓冲绘图

    万次阅读 2013-05-22 19:52:51
    写在前面:这两天在分析DUILIB代码,遇到GDI绘图,现在对内存绘图和双缓冲绘图还不是很清楚,写转两篇文章以留备用,等学好了再给大家讲吧。 内存绘图 转载地址:http://redbox.blogbus.com/logs/2299466.html...
  • 缓冲区溢出攻击实验(一)

    万次阅读 2015-11-18 20:43:27
    无聊之余,想弄一下缓冲区溢出实验,之前一直听说这个,也没有亲自动手 做一下,发现真正弄起来的时候还是没那么简单的,其实学到的东西还是不少的。特此记下学习的过程。 一、基础知识 这一部分主要是关于程序内存...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 62,309
精华内容 24,923
关键字:

引用缓冲技术的主要目的