像素缓冲区对象(PBO) 的Streaming-Texture上传 源码解析

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 像素缓冲区对象(PBO) 的Streaming-Texture上传 源码解析

接这篇文章 OpenGL深入探索——像素缓冲区对象 (PBO)(附完整工程代码地址)

原理示意图:

image.png

首选检查显卡是否支持 PBO :

#if defined(_WIN32)
    // check PBO is supported by your video card
  // 检查显卡是否支持 PBO
    if (glInfo.isExtensionSupported("GL_ARB_pixel_buffer_object"))
    {
        // get pointers to GL functions
        glGenBuffersARB = (PFNGLGENBUFFERSARBPROC)wglGetProcAddress("glGenBuffersARB");
        glBindBufferARB = (PFNGLBINDBUFFERARBPROC)wglGetProcAddress("glBindBufferARB");
        glBufferDataARB = (PFNGLBUFFERDATAARBPROC)wglGetProcAddress("glBufferDataARB");
        glBufferSubDataARB = (PFNGLBUFFERSUBDATAARBPROC)wglGetProcAddress("glBufferSubDataARB");
        glDeleteBuffersARB = (PFNGLDELETEBUFFERSARBPROC)wglGetProcAddress("glDeleteBuffersARB");
        glGetBufferParameterivARB = (PFNGLGETBUFFERPARAMETERIVARBPROC)wglGetProcAddress("glGetBufferParameterivARB");
        glMapBufferARB = (PFNGLMAPBUFFERARBPROC)wglGetProcAddress("glMapBufferARB");
        glUnmapBufferARB = (PFNGLUNMAPBUFFERARBPROC)wglGetProcAddress("glUnmapBufferARB");
        // check once again PBO extension
        if (glGenBuffersARB && glBindBufferARB && glBufferDataARB && glBufferSubDataARB &&
                glMapBufferARB && glUnmapBufferARB && glDeleteBuffersARB && glGetBufferParameterivARB)
        {
            pboSupported = true;
            pboMode = 1;    // using 1 PBO
            cout << "Video card supports GL_ARB_pixel_buffer_object." << endl;
        }
        else
        {
            pboSupported = false;
            pboMode = 0;    // without PBO
            cout << "Video card does NOT support GL_ARB_pixel_buffer_object." << endl;
        }
    }
    // Query the system memory page size and update the default value
    SYSTEM_INFO si;
    GetSystemInfo(&si);
    if (si.dwPageSize > 0)
    {
        systemPageSize = si.dwPageSize;
    }
#elif defined (__gnu_linux__)
    // for linux, do not need to get function pointers, it is up-to-date
    if (glInfo.isExtensionSupported("GL_ARB_pixel_buffer_object"))
    {
        pboSupported = true;
        cout << "Video card supports GL_ARB_pixel_buffer_object" << endl;
    }
    else
    {
        cout << "Video card does NOT support GL_ARB_pixel_buffer_object" << endl;
    }
    if (glInfo.isExtensionSupported("GL_AMD_pinned_memory"))
    {
        amdSupported = true;
        cout << "Video card supports GL_AMD_pinned_memory" << endl;
    }
    else
    {
        cout << "Video card does NOT support GL_AMD_pinned_memory" << endl;
    }
    // Query the system memory page size and update the default value
    if (sysconf(_SC_PAGE_SIZE) > 0)
    {
        systemPageSize = sysconf(_SC_PAGE_SIZE);
    }
#endif

创建PBO / 调整 PBO个数 的代码:

// 创建 count 个 PBO
void setPboCount(int count)
{
    if (!pboSupported)
        return;
  // 如果 count 大于 当前的 PBO 数
    if (count > pboCount)
    {
        if (pboMethod != AMD)
        {
            // Generate each Pixel Buffer object and allocate memory for it(生成 PBO,并为其分配内存)
            // Hopefully, PBOs will get allocated in VRAM
            glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); // Unbind any buffer object previously bound(释放之前所绑定的 PBO)
            for (int i = pboCount; i < count; ++i)
            {
                GLuint pboId;
                glGenBuffers(1, &pboId); // Generate new Buffer Object ID
                glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboId); // Create a zero-sized memory Pixel Buffer Object and bind it
                glBufferData(GL_PIXEL_UNPACK_BUFFER, DATA_SIZE, NULL, GL_STREAM_DRAW); // Reserve the memory space for the PBO
                glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); // Release the PBO binding
                pboIds.push_back(pboId); // Update our list of PBO IDs
                pboFences.push_back(NULL);
                cout << "Created PBO buffer #" << i << " of size: " << DATA_SIZE << endl;
            }
            pboCount = pboIds.size();
            assert(GL_NO_ERROR == glGetError());
        }
    // 特殊的 DMA 模式,需要自己手动分配对齐的内存,并提供内存指针
        else
        {
            // Generate each Pixel Buffer object and allocate memory for it
            // PBOs will get allocated in System RAM, and GPU will access it through DMA
            glBindBuffer(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, 0); // Unbind any buffer object previously bound
            for (int i = pboCount; i < count; ++i)
            {
                GLuint pboId;
                glGenBuffers(1, &pboId); // Generate new Buffer Object ID
                glBindBuffer(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, pboId); // Create a zero-sized memory Pixel Buffer Object and bind it
                assert(GL_NO_ERROR == glGetError());
                // Memory alignment functions are compiler-specific
                GLubyte *ptAlignedBuffer = (GLubyte *)alignedMalloc(systemPageSize, DATA_SIZE);
                if (NULL == ptAlignedBuffer)
                {
                    cout << "ERROR [setPboCount] (alignedMalloc) size: " << DATA_SIZE << " alignment: " << systemPageSize << endl;
                    break;
                }
                cout << "Created memory buffer #" << i << " of size: " << DATA_SIZE << " alignment: " << systemPageSize << endl;
                glBufferData(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, DATA_SIZE, ptAlignedBuffer, GL_STREAM_DRAW); // Take control of the memory space for the PBO
                GLenum error = glGetError();
                if (GL_NO_ERROR != error)
                {
                    cout << "ERROR [setPboCount] (glBufferData): " << (char *)gluErrorString(error) << endl;
                    alignedFree(ptAlignedBuffer);
                    cout << "Freed memory buffer #" << i << endl;
                    break;
                }
                glBindBuffer(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, 0); // Release the PBO binding
                assert(GL_NO_ERROR == glGetError());
                pboIds.push_back(pboId); // Update our list of PBO IDs
                pboFences.push_back(NULL);
                alignedBuffers.push_back((GLubyte *)ptAlignedBuffer);
                cout << "Created PBO buffer #" << i << endl;
            }
            pboCount = pboIds.size();
            assert(GL_NO_ERROR == glGetError());
        }
    }
  // 如果 count 小于当前的 PBO 数
    else if (count < pboCount)
    {
        if (pboMethod != AMD)
        {
            glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); // Unbind any buffer object previously bound
            for (int i = pboCount - 1; i >= count; --i)
            {
                glDeleteSync(pboFences.back());
                pboFences.pop_back();
                GLuint pboId = pboIds.back();
                glDeleteBuffers(1, &pboId);
                pboIds.pop_back(); // Update our list of PBO IDs
                cout << "Deleted PBO buffer #" << i << endl;
            }
            pboCount = pboIds.size();
            assert(GL_NO_ERROR == glGetError());
        }
        else
        {
            glBindBuffer(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, 0); // Unbind any buffer object previously bound
            for (int i = pboCount - 1; i >= count; --i)
            {
                glDeleteSync(pboFences.back());
                pboFences.pop_back();
                GLuint pboId = pboIds.back();
                glDeleteBuffers(1, &pboId);
                pboIds.pop_back(); // Update our list of PBO IDs
                cout << "Deleted PBO buffer #" << i << endl;
        // 手动释放自己分配的内存
                alignedFree(alignedBuffers.back());
                alignedBuffers.pop_back();
                cout << "Freed memory buffer #" << i << endl;
            }
            pboCount = pboIds.size();
            assert(GL_NO_ERROR == glGetError());
        }
    }
    cout << "PBO Count: " << pboCount << endl;
}

最关键的显示回调方法:

void displayCB()
{
    if (pboMethod == NONE)
    {
        /*
        * Update data in System Memory.
        */
        t1.start();
        updatePixels(imageData, DATA_SIZE); // 更新 imageData 的像素
        t1.stop();
        updateTime = t1.getElapsedTimeInMilliSec();
        /*
        * Copy data from System Memory to texture object. (将 imageData 的内容从内存拷贝到纹理当中)
        */
        t1.start();
        glBindTexture(GL_TEXTURE_2D, textureId);
        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, IMAGE_WIDTH, IMAGE_HEIGHT, PIXEL_FORMAT, GL_UNSIGNED_BYTE, (GLvoid *)imageData);
        t1.stop();
        copyTime = t1.getElapsedTimeInMilliSec();
    }
    else
    {
        /*
        * Update buffer indices used in data upload & copy.
        *
        * "uploadIdx": index used to upload pixels to a Pixel Buffer Object.(上传像素至 uploadIdx 指定的 PBO)
        * "copyIdx": index used to copy pixels from a Pixel Buffer Object to a GPU texture.(拷贝 cpyIdx 指定的 PBO 的像素到 GPU 纹理)
        *
        * When (pboCount > 1), this will allow to perform(当 pboCount 数大于1时,就允许使用备用buffer来同时进行上传和拷贝)
        * simultaneous upload & copy, by using alternative buffers.
        * That is a good thing, unless the double buffering is being already
        * done somewhere else in the code.
        */
        static int copyIdx = 0;
        copyIdx = (copyIdx + 1) % pboCount;
        int uploadIdx = (copyIdx + 1) % pboCount;
        /*
        * Upload new data to a Pixel Buffer Object.
        */
        t1.start();
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[uploadIdx]); // Access the Pixel Buffer Object and bind it
    // pboMethod 表示不同的纹理流机制(Texture Stream methods)
        if (pboMethod == ORPHAN)
        {
      // GL_STREAM_DRAW 表示每次渲染都会更新该 PBO的像素数据
      // GL_DYNAMITC_DRAW 表示每帧都会更新该 Buffer
      // GL_STATIC_DRAW 表示几乎或从不更新该 Buffer
            glBufferDataARB(GL_PIXEL_UNPACK_BUFFER_ARB, DATA_SIZE, NULL, GL_STREAM_DRAW_ARB);
      // 获得 PBO 的映射 Buffer 指针,以待写入操作
            GLubyte *ptr = (GLubyte *)glMapBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, GL_WRITE_ONLY_ARB);
            if (NULL == ptr)
            {
                cout << "ERROR [displayCB] (glMapBufferARB): " << (char *)gluErrorString(glGetError()) << endl;
                return;
            }
            else
            {
                // update data directly on the mapped buffer(在映射的 Buffer 上直接更新 PBO 的像素数据)
                updatePixels(ptr, DATA_SIZE);
                // release pointer to mapping buffer(释放映射 Buffer 的指针)
                if (!glUnmapBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB))
                {
                    cout << "ERROR [displayCB] (glUnmapBufferARB): " << (char *)gluErrorString(glGetError()) << endl;
                }
            }
        }
    // 异步模式
        else if (pboMethod == UNSYNCH_ORPHAN || pboMethod == UNSYNCH_FENCES)
        {
            if (pboMethod == UNSYNCH_FENCES)
            {
        // 检查索引的pboFences 是否是同步对象(Sync Object)
                if (glIsSync(pboFences[uploadIdx]))
                {
          // 阻塞并 wait 同步对象,等待被 signal
                    GLenum result = glClientWaitSync(pboFences[uploadIdx], 0, GL_TIMEOUT_IGNORED);
                    switch (result)
                    {
                    case GL_ALREADY_SIGNALED:
                        // Transfer was already done when trying to use buffer
            // (说明此时传输已经完成)
                        cout << "DEBUG (glClientWaitSync): ALREADY_SIGNALED (good timing!) uploadIdx: " << uploadIdx << endl;
                        break;
                    case GL_CONDITION_SATISFIED:
                        // This means that we had to wait for the fence to synchronize us after using all the buffers,
                        // which implies that the GPU command queue is full and that we are GPU-bound (DMA transfers aren't fast enough).
            // (说明我们不得不等待 fence 的同步,即 我们所绑定的 GPU 命令队列已满[DMA传输还不够快])
                        cout << "WARNING (glClientWaitSync): CONDITION_SATISFIED (had to wait for the sync) uploadIdx: " << uploadIdx << endl;
                        break;
                    case GL_TIMEOUT_EXPIRED:
                        cout << "WARNING (glClientWaitSync): TIMEOUT_EXPIRED (DMA transfers are too slow!) uploadIdx: " << uploadIdx << endl;
                        break;
                    case GL_WAIT_FAILED:
                        cout << "ERROR (glClientWaitSync): WAIT_FAILED: " << (char *)gluErrorString(glGetError()) << endl;
                        break;
                    }
          // 删除同步对象
                    glDeleteSync(pboFences[uploadIdx]);
                    pboFences[uploadIdx] = NULL;
                }
            }
      // 注意和 ORPHAN 的区别
            else if (pboMethod == UNSYNCH_ORPHAN)
            {
        // Buffer 需要重新指定
                glBufferData(GL_PIXEL_UNPACK_BUFFER, DATA_SIZE, NULL, GL_STREAM_DRAW); // Buffer re-specification (orphaning)
            }
      // 获得 PBO 的映射 Buffer 指针,以待写入操作
            GLubyte *ptr = (GLubyte *)glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, DATA_SIZE, GL_MAP_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BIT);
            if (NULL == ptr)
            {
                cout << "ERROR [displayCB] (glMapBufferRange): " << (char *)gluErrorString(glGetError()) << endl;
                return;
            }
            else
            {
                updatePixels(ptr, DATA_SIZE); // Update data directly on the mapped buffer(直接更新映射 Buffer 的数据)
                if (!glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER))
                {
                    cout << "ERROR [displayCB] (glUnmapBuffer): " << (char *)gluErrorString(glGetError()) << endl;
                }
            }
        }
        else if (pboMethod == AMD)
        {
      // 同样要进行手动同步
            if (glIsSync(pboFences[uploadIdx]))
            {
                GLenum result = glClientWaitSync(pboFences[uploadIdx], 0, GL_TIMEOUT_IGNORED);
                switch (result)
                {
                case GL_ALREADY_SIGNALED:
                    // Transfer was already done when trying to use buffer
                    //cout << "DEBUG (glClientWaitSync): ALREADY_SIGNALED (good timing!) uploadIdx: " << uploadIdx << endl;
                    break;
                case GL_CONDITION_SATISFIED:
                    // This means that we had to wait for the fence to synchronize us after using all the buffers,
                    // which implies that the GPU command queue is full and that we are GPU-bound (DMA transfers aren't fast enough).
                    //cout << "WARNING (glClientWaitSync): CONDITION_SATISFIED (had to wait for the sync) uploadIdx: " << uploadIdx << endl;
                    break;
                case GL_TIMEOUT_EXPIRED:
                    cout << "WARNING (glClientWaitSync): TIMEOUT_EXPIRED (DMA transfers are too slow!) uploadIdx: " << uploadIdx << endl;
                    break;
                case GL_WAIT_FAILED:
                    cout << "ERROR (glClientWaitSync): WAIT_FAILED: " << (char *)gluErrorString(glGetError()) << endl;
                    break;
                }
                glDeleteSync(pboFences[uploadIdx]);
                pboFences[uploadIdx] = NULL;
            }
      // alignedBuffers 存储的是手动分配的一块对齐过的内存指针
            updatePixels(alignedBuffers[uploadIdx], DATA_SIZE); // Update data directly on the mapped buffer
        }
        t1.stop();
        updateTime = t1.getElapsedTimeInMilliSec();
        /*
        * Protect each Pixel Buffer Object against being overwritten.(防止 PBO 被重复写入)
        *
        * Tipically the data upload will be slower than our main loop, so this
        * function will be called again before the previous frame was uploaded
        * and processed. The main bottleneck is the PCI bus transfer speed,
        * which limits how fast the DMA (System Memory --> VRAM) can work.
    * 
        * 通常数据上传将会慢于主循环,意味着在先前的帧被上传处理之前,该方法将会被再次调用。
    * 主要的性能瓶颈在于 PCI 总线的传输速度, 它限制了 DMA 的传输速度(内存到显存)。
    *
        * OpenGL Sync Fences will block until the PBO is released.(GL 同步对象将会阻塞主线程,直到 PBO 被释放为止)
        */
        if (pboMethod == UNSYNCH_FENCES || pboMethod == AMD)
        {
      // 创建一个同步对象,并将其加入 GL 的命令流中(❤ 具体请参看第八版红宝书的 P589 第11章 Memory ❤)
            pboFences[uploadIdx] = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
        }
        /*
        * Copy data from a Pixel Buffer Object to a GPU texture.
        * glTexSubImage2D() will copy pixels to the corresponding texture in the GPU.
    * 传输 PBO 中的数据到 GPU 纹理
        */
        t1.start();
        glBindTexture(GL_TEXTURE_2D, textureId); // Bind the texture
        glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, pboIds[copyIdx]); // Access the Pixel Buffer Object and bind it
        // Use offset instead of pointer(由于使用了 PBO,所以传递的是偏移量)
        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, IMAGE_WIDTH, IMAGE_HEIGHT, PIXEL_FORMAT, GL_UNSIGNED_BYTE, 0);
        t1.stop();
        copyTime = t1.getElapsedTimeInMilliSec();
        // it is good idea to release PBOs with ID 0 after use.
        // Once bound with 0, all pixel operations behave normal ways.
        glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, 0);
    }
    // clear buffer
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    // save the initial ModelView matrix before modifying ModelView matrix
    glPushMatrix();
    // tramsform camera
    glTranslatef(0, 0, -cameraDistance);
    glRotatef(cameraAngleX, 1, 0, 0); // pitch
    glRotatef(cameraAngleY, 0, 1, 0); // heading
    // draw a point with texture
    glBindTexture(GL_TEXTURE_2D, textureId);
    glColor4f(1, 1, 1, 1);
    glBegin(GL_QUADS);
    glNormal3f(0, 0, 1);
    glTexCoord2f(0.0f, 0.0f);
    glVertex3f(-1.0f, -1.0f, 0.0f);
    glTexCoord2f(1.0f, 0.0f);
    glVertex3f(1.0f, -1.0f, 0.0f);
    glTexCoord2f(1.0f, 1.0f);
    glVertex3f(1.0f, 1.0f, 0.0f);
    glTexCoord2f(0.0f, 1.0f);
    glVertex3f(-1.0f, 1.0f, 0.0f);
    glEnd();
    // unbind texture
    glBindTexture(GL_TEXTURE_2D, 0);
    // draw info messages
    showInfo();
    //showTransferRate();
    printTransferRate();
    glPopMatrix();
    glutSwapBuffers();
}

运行结果对比:


【未使用 PBO 的情况】

image.png

【Orphaning模式,PBO 个数 = 1】

image.png

【异步 Orphaning模式, PBO个数 = 1】

image.png

【Fences 的 同步模式, PBO个数 = 1】

image.png【Orphaning模式, PBO个数=3】

image.png

【异步的 Orphaning 模式, PBO个数=3】

image.png

可见使用了 PBO 的确比未使用 PBO 性能要略优,但一味增加 PBO 的数量,并不能显著提高性能。

本例中 不同的 PBO 模式性能差别不大,但是 Orphan 模式的写法最简单,不需要自己手动同步和创建额外对齐的内存。

目录
相关文章
|
3天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
3天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
3天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是"将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。创建型模式分为5种:单例模式、工厂方法模式抽象工厂式、原型模式、建造者模式。
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
27天前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
54 12
|
22天前
|
PyTorch Shell API
Ascend Extension for PyTorch的源码解析
本文介绍了Ascend对PyTorch代码的适配过程,包括源码下载、编译步骤及常见问题,详细解析了torch-npu编译后的文件结构和三种实现昇腾NPU算子调用的方式:通过torch的register方式、定义算子方式和API重定向映射方式。这对于开发者理解和使用Ascend平台上的PyTorch具有重要指导意义。
|
4天前
|
安全 搜索推荐 数据挖掘
陪玩系统源码开发流程解析,成品陪玩系统源码的优点
我们自主开发的多客陪玩系统源码,整合了市面上主流陪玩APP功能,支持二次开发。该系统适用于线上游戏陪玩、语音视频聊天、心理咨询等场景,提供用户注册管理、陪玩者资料库、预约匹配、实时通讯、支付结算、安全隐私保护、客户服务及数据分析等功能,打造综合性社交平台。随着互联网技术发展,陪玩系统正成为游戏爱好者的新宠,改变游戏体验并带来新的商业模式。
|
1月前
|
SQL Java 数据库连接
canal-starter 监听解析 storeValue 不一样,同样的sql 一个在mybatis执行 一个在数据库操作,导致解析不出正确对象
canal-starter 监听解析 storeValue 不一样,同样的sql 一个在mybatis执行 一个在数据库操作,导致解析不出正确对象
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
77 2
|
2月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
81 0
|
2月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
67 0

热门文章

最新文章

推荐镜像

更多