d3d 函数讲解 2d应用

原文地址:http://blog.csdn.net/weili_2007/article/details/1916422

 这批文章解决了我很多的遗憾,感谢作者!!!

这一章将讨论IDirect3DDevices9接口怎样应用到简单的两维应用程序里去。然而,接下来讨论的方法和接口的方法不仅仅只应用在两维应用程序里面。

   开始我们将看看IDirect3DSurface9接口,它用于存放像素数据。我们将看看怎样创建surfaces,怎样填充数据,并且执行像素拷贝操作。

   接下来,我们讨论IDirect3DSwapChain9接口管理back buffer集合。设备创建的时候都会携带一个默认的swap chain,但是也可以为window模式下多个视图创建新的swap chain。

   再接下来,我们将讨论Presentation,Present 也是IDirect3DDevice9提供的方法。

   即使Direct3D可以不使用GDI,但是他们也要处理发送到应用程序top-level窗口的消息。我们推荐使用Direct3D应用程序来处理窗口消息。DirectX 并没有提供直接方法来将GDI和Direct3D结合起来。但是,通常是在内存DC和流水线产生的结果像素数据上执行GDI操作。

   最后,我们将讨论流水线的Video scan out部分以及swap chain从back buffer到front back 的过程。video scan out 电流读取数据,使用cursor overlay,gamma校正以及像素数据转化成monitor的模拟信号。

 

Pixel Surface

 

   Pixel 面是像素数据的矩形集合。像素数据的内存layout是通过D3DFORMAT定义的。在设备上使用surface有几处地方:back buffer surfaces, depth/stencil buffer surfaces,纹理层surface,render target surface 以及图片surface。Direct3D 使用IDirect3DSurface9接口表示一个surface,CreateOffscreenPlainsurface方法可以创建一个图片surface,它能存在于scrath内存池,系统内存池和设备内存池。CreateDepthStencilSurface和CreateRenderTarget分别返回depth/stencil的surface和render target的surface。一个plain surface可能不是3D 渲染的目标,但是你能在plain surface和其他surface之间进行数据拷贝。

   HRESULT CreateOffscreenPlainSurface(UINT width,

                                                      UINT height,

                                                      D3DFORMAT format,

                                                      D3DPOOL pool,

                                                      IDirect3DSurface9 ** result,

                                                      HANDLE * unused);

 

如果设备不支持被请求面的各式,CreateOffscreenPlainSurface将会失败,或者是系统内存里面没有足够的存储空间。使用CheckDeviceFormat可以用来预先检查设备是否支持某种格式的surface。

interface IDirect3DSurface9 : IDirect3DResource9

{

HRESULT GetContainer(REFIID container_iid, void ** value);

HRESULT GetDC(HDC ** value);

HRESULT GetDesc(D3DSURFACE_DESC * value);

HRESULT LockRect(D3DLOCKED_RECT * data,  const RECT * locked_region,DWORD flags);

HRESULT Release(HDC context);

HRESULT UnlockRect();

}

   对于CreateOffscreenPlainSurface创建的surface,当container_iid是IID_Direct3DDevice9时,GetContainer将返回成功。GetDESC将返回D3DSURFACE_DESC结构体里面的像素数据的描述信息。

typedef struct _D3DSURFACE_DESC

{

D3DFORMAT Format;

D3DRESOURCETYPE Type;

DWORD Usage;

D3DPOOL Pool;

D3DMULTISAMPLE_TYPE MultiSampleType; //与render target surface一起使用的multisampling

DWORD MultiSampleQuality;

UINT Width;

UINT Height;

} D3DSURFACE_DESC;

 

Accessing Surface Pixel Data

 

    为了获取surface的像素数据,我们需要使用LockRect和UnlockRect方法。当lockRect方法的locked_region不为NULL的时候,就只lock住了surface的某个局部。参数flags告诉Direct3D一旦surface被锁住,数据将怎么办。Flags包括D3DLOCK_DISCARD, D3DLOCK_DONOTWAIT, D3DLOCK_NO_DIRTY_UPDATE,D3DLOCK_NOSYSLOCK, D3DLOCK_READONLY。

     D3DLOCK_DISCARD通知runtime整个locked区域将只被写入而不读取。使用discard flag, runtime不会提供一份surface数据的copy给应用程序读取。不使用discard flag,runtime可能被迫停掉所有未决的操作,然后再返回一个surface数据的copy给应用程序。注意:这个flag不能使用在子区域。

     D3DLOCK_DONOTWAIT 决定当lock的时候不会阻塞住runtime,等待未决的渲染操作完成。如果lock调用已经阻塞了,它将返回D3DERR_WASSTILLDRAWING。

    Direct3D为每个可管理的surface(注意这里只能是managed surface)维护一个dirty 区域表,用来最小化unlock数据的拷贝量。如果已经是使用了D3D_NO_DIRTY_UPDATE,locked区域将不会影响dirty 区域list。

   使用D3DLOCK_READONLY,应用程序保证对锁定区域没有任何的写操作。

   D3DLOCK_NOSYSLOCK只只能应用到video memory的surfaces。为了阻止video存储资源被锁时设备lost,Direct3D获取系统critical section来阻止设备丢失。它也能阻塞操作系统的其他部分执行,这样势必会影响系统的交互和反应灵敏度。 D3DLOCK_NOSYSLOCK 禁止获取系统的critical section。

   LockRect 方法返回D3DLOCKED_RECT结构体,它定义了surface的像素数据。它保证了surface数据在sanline上是连续的。

   typedef struct _D3DLOCKED_RECT

{

  int  pitch;  //定义了相邻两个scanlines的距离。

  void *pBits; //像素数据的指针。

}D3DLOCKED_RECT;

  当遍历surface的每个像素时,pitch 和像素的大小是很重要的。像素数据的大小是跟D3DFORMAT有关。

IDirect3DSurface9并没有提供方法用于从图片装载到surface,或者像素格式转换,D3DX却提供了很多这样的操作方法。

 

Using GDI On a Surface

 

GDI提供GetDC和ReleaseDC方法来操作与GDI兼容格式的surface。与GDI兼容格式包括D3DFMT_R5G6B5,D3DFMT_X1R5G5B5,D3DFMT_R8G8B8和D3DFMT_X8R8G8B8。

GDI device context可以使用GDI执行基于surface的渲染操作,然后立刻释放。一旦device context被创建,就会在Direct3D runtime里面建立一个lock。 这个lock保证runtime并不直接跟GDI rendering交互。因为这个lock的存在,应用程序应该尽可能快的释放GDI device context。而且, 只有device context释放以后,下面表里的方法才能调用。

InterfaceMethod
IDirect3DCubeTexture9LockRect
IDirect3DDevice9ColorFill
 Present
 StretchRect
 UpdateSurface
 UpdateTexture
IDirect3DSurface9LockRect
IDirect3DSwapChain9Present
IDirect3DTexture9LockRect

 

Swap Chain

   每个设备都包含一组默认的swap chains。GetNumberOfSwapChains返回设备swap chains的数目。GetSwapChain方法返回默认swap chain集合里的swap chain的接口。

   默认swap chain集合的属性定义在D3DPRESENT_PARAMETERS。Swap chain由1个,2个,3个back buffer surface 和一个front buffer surface。front buffer surface不能直接访问,但是它仍然参与到swap chain的presentation过程中。 当调用Present时back buffer surface显示在monitor。

  exclusive 模式的设备使用它默认的back buffer。window模式的设备可以使用多个swap chain,每个都可以将渲染结果绘制它自己的窗口。exclusive模式的adapter group 设备也可以渲染到多个monitor。

  CreateAdditionalSwapChain创建一个基于D3DPRESENT_PARAMETERS的swap chain,然后返回一个IDirect3DSwapChain9接口。注意一个Swap chain只能包含back buffer surface而不是depth/stencil surface。

  HRESULT CreateAdditionalSwapChain(D3DPRESENT_PARAMETERS * params, IDirect3DSwapChain9 ** result);

interface IDirect3DSwapChain9 : IUnknown

{

     HRESULT GetBackBuffer(UINT buffer, D3DBACKBUFFER_TYPE kind, IDirect3DSurface9 ** value);

     HRESULT GetDevice(IDirect3DDevice9  ** value);

     HRESULT GetDisplayMode(D3DDISPLAYMODE * value);

    HRESULT GetFrontBufferData(IDirect3DSurface9 * destination);

    HRESULT  GetPresentParameters(D3DPRESENT_PARAMETERS * value);

    HRESULT  GetRasterStatus(D3DRASTER_STATUS * value);

 

    HRESULT Present(CONST RECT * source, CONST RECT * destination, HWND override, CONST RGNDATA * dirty_region, DWORD flags);

}

    GetBackBuffer返回一个back buffer surface的接口指针。back buffer序号以0开始,present首先显示是back buffer 0里面的内容,然后再是back buffer 1, 依此下去...。 D3DBACKBUFFER_TYPE定义了back buffer的类型。DirecX 9.0c 并不支持stereo 渲染,,kind只能是D3DBACKBUFFERTYPE_MONO.

typedef enum _D3DBACKBUFFER_TYPE

{

   D3DBACKBUFFER_TYPE_MONO = 0,

   D3DBACKBUFFER_TYPE_LEFT = 1,

   D3DBACKBUFFER_TYPE_RIGHT = 2

}D3DBACKBUFFER_TYPE;

    Present跟device的Present方法一样,都执行同样的功能。flag的值包括D3DPRESENT_DONOTWAIT和D3DPRESENT_LINEAR_CONTENT。D3DPRESENT_DONOTWAIT表示如果presentation引起应用程序阻塞,它将立刻返回D3DERR_WASSTILLDRAWING。 D3DPRESENT_LINEAR_CONTENT 告诉设备presentation时源区域像素应该从线性颜色空间转化到sRGB颜色空间。

 

Presentation

   当调用了Present后,back buffer的内容将会被复制到front buffer,video scan out 电流读取front buffer的像素把他们显示在monitor上。如果D3DCAPS9::DevCaps的D3DDEVCAPS_CANRENDERAFTERFLIP被设置,present发生以后设备将继续queue渲染命令,也就是在设备与CPU之间有更多的并发,当前帧渲染的时候,下一帧已经queue。但是一个设备queue的帧数最多不能超过两帧。

  #define D3DDEVCAPS_CANRENDERAFTERFLIP 0x000000800L

   HRESULT Present(const RECT * source_rect, const RECT* dest_rect,HWND override_window, const RGNDATA * dirty_region);

   Present对Swap chain的行为使用D3DPRESENT_PARAMETERS的SwapEffect定义的。

   typedef enum _D3DSWAPEFFECT

{

    D3DSWAPEFFECT_DISCARD = 1,

    D3DSWAPEFFECT_FLIP  = 2,

    D3DSWAPEFFECT_COPY = 3

} D3DSWAPEFFECT;

  在window模式下面,所有的swap effec都是通过拷贝操作实现的。使用最近时间间隔创建的swap chain并不能让monitor的垂直扫描与拷贝操作同步。拷贝操作运行在在Video scan out过程中,这样导致了artifacts,也称作图像tearing。为了避免这种artifacts,我们需要同步video scan out与拷贝操作,以便于当video beam还停留在拷贝操作的目的地时,不能有拷贝操作发生。如果显卡并不支持video beam 位置信息,拷贝操作将会立刻发生。

  在exclusive模式下,presentation的频率是由D3DPRESENT_PARAMETERS的FullScreen_PresentationInterval指定的。默认的presentation inverval 对应adapter视频模式的刷新率。

  如果dest_window不是空,dest_window客户端区域就是present的Target。如果dest_window空而D3DPRESENT_PARAMETERS的hDeviceWindow不空,则hDeviceWindow就是present的Target。 如果两者都空,CreateDevice的focus_window参数将是present的target。

source和dest只能应用到拷贝操作方式,Flip 和Discard这两个参数必须是空。空值表示整个source surface 和目的surface。dirty_region也只能使用在拷贝操作下。

 

Lost Devices and Reset

 

   TestCooperativelevel检查设备的状态,Reset重启设备。

HRESULT Reset(D3DPRESENT_PARAMETERS * params);

HRESULT TestCooperativeLevel();

 

Video Scan out

    front buffer的内容来自于Present。GetDisplayMode返回了当前front buffer的显示模式。应用程序不能直接访问front buffer,但是可以通过GetFrontBufferData返回一份front buffer的拷贝。GetFrontBufferData的destination参数必须是一个存在的surface,它的维度等于adapter当前显示模式,并且像素格式是D3DFMT_A8R8G8B8。在拷贝过程中数据将从adapter的显示模式格式转化成surface格式。

    HRESULT GetDisplayMode(D3DDISPLAYMODE * value);

    HRESULT GetFrontBufferData(IDirect3DSurface* destination);

   如果D3DCAPS::Caps的D3DCAPS_READ_SCANLINE位被设置,设备将它的video scan out scanline和垂直blank状态。

   GetRasterStatus将使用D3DRASTER_STATUS结构体返回video scan out 状态。scanline 成员指定了rater beam的当前位置,0是最顶端的scanline。如果是自下而上的垂直扫描,InVBlank成员将为TRUE。

  HRESULT GetRasterStatus(D3DRASTER_STATUS * value);

   typedef struct _D3DRASTER_STATUS

{

  BOOL InVBlank;

  UINT    Scanline;

} D3DRASTER_STATUS;

  • Cursor

    在exclusive模式下,Direct3D管理着cursor的显示。在video scan out的过程中硬件Cursor可能会取代cursor图像。如果硬件cursor不可用,runtime会提供一个软件cursor对front buffer读,修改,写操作。在window模式下,应用程序可能会使用GDI cursor 或者Direct3D cursor。Direct3D cursor 能通过方法ShowCursor显示或者隐藏cursor。ShowCursor并不返回HRESULT,而是cursor的上一个隐藏状态。如果它是TRUE,cursor在调用ShowCursor之前是可见的。

    BOOL ShowCursor(BOOL Show);

     void    SetCursorPosition(UINT x, UINT y, DWORD flags);

    HRESULT  SetCursorProperties(UINT hot_spot_x, UINT hot_spot_y, IDirect3DSurface9 * image);

   cursor的位置是通过SetCursorPosition设定的,flags参数

  • Gamma Ramp

    在exclusive模式下,应用cursor以后, 在进行A/D转换之前将会对像素数据应用Gamma校正。在窗口模式下,应用程序也能使用GDI来做Gamma校正。如果D3DCAPS9::Caps的D3DCAPS2_FULLSCREENGAMMA设置,设备将会支持exclusive的Gamma Ramp。

    GetGammaRamp能读取Gamma Ramp的属性,它返回一个Gamma Ramp的结构体。

void GetGammaRamp(D3DGAMMARAMP * value);

void SetGammaRamp(DWORD Flags, const D3DGAMMARAMP * value);

typedef struct _D3DGAMMARAMP

{

    WORD red[256];

    WORD green[256];

    WORD  blue[256];

} D3DGAMARAMP;

    Gamma ramp属性通过SetGammaRamp进行设置,并且它能够立刻产生反应。Flags参数表示设备是否使用ramp的calibration,它包括D3DSGR_NO_CALIBRATION和D3DSGR_CALIBRATE。

     如果D3DCAPS9::Caps2的D3DCAPS2_CANCALIBRATEGAMMA位设置,设备将能够给Gamma Ramp指定一个calibration。

     下面例子显示了怎样计算ramp values。

void compute_ramp(D3DGAMARAMP & ramp, float gamma)

{

  for (UINT i = 0;i< 256;i++)

  {

    const  WORD val = static_cast<int> (65535 * pow(i/255.f, 1.f/gamma));

    ramp.red[i] = val;

    ramp.green[i] = val;

    ramp.blue[i] = val;

  }

}

 

2D Pixel Copies

   

    如果我们申请可锁住的back buffer,我们能锁住back buffer的矩形区域,然后使用软件直接对它读写操作。然而,back buffer surface是video memory的设备surface。通过CPU访问video memory将是一个昂贵的操作,我们应该避免。 但是存放系统内存池或者scratch内存池的图像surface能轻松的被CPU访问。

    拷贝像素操作一般包括:设备内存到设备内存, 系统内存到设备内存,设备内存到系统内存。StetchRect 能有效的进行设备内存之前的拷贝。UpdateSurface和UpdateTexture一般都是将数据从系统内存转移到设备内存里面。GetRenderTargetData用于从设备内存将像素数据转移到系统内存。

   当你想要在设备资源直接拷贝数据时,你可以使用StretchRect。当你需要更新默认内存池的某个资源(如图像surface或者纹理资源),你可以使用UpdateSurface和updateTexture,不过managed池里的资源会自动更新,当它的系统内存的数据变化时,它会自动更新到设备内存里面去)。

 

  • Pixel Copies Within Device Memory

    StretchRect能够将一个矩形区域的像素从设备内存的一个surface转移到另一个上面去。这两个surface通常会是同样的D3DFORMAT。在拷贝过程中,StretchRect也能执行有限的几张颜色转换。

    HRESULT StretchRect(IDirect3DSurface9 * source , const RECT * source_rect, IDirect3DSurface9 * destination, const RECT * dest_rect, D3DTEXTUREFILTERTYPE filter);

    当source_rect非空,整个source 面将会拷贝到目的surface。当source_rect非空,它指向source面的一个子区域。dest_rect也是一样。 当需要调整源像素区域大小来适应目的像素区域的时候,filter参数将指定调整的方式,它的值包括D3DTEXF_NONE, D3DTEXF_POINT,D3DTEXF_LINEAR。当缩小和放大源像素区域的时候,point和linear的过滤将会支持。D3DCAPS9结构的StretchRectFilterCaps各个位标记描述了StretchRect的过滤支持情况。

    #define D3DPTFILTERRCAPS_MAGFLINEAR 0x02000000L

   #define  D3DPTFILTERCAPS_MAGFPOINT  0x01000000L

   #define  D3DPTFILTERCAPS_MINFLINEAR  0x00000200L

   #define  D3DPTFILTERCAPS_MINFPOINT   0x00000100L

  如果D3DCAPS9::DevCaps的D3DDEVCAPS_CANBLTSYSTONONLOCAL设置,StretchRect能支持从系统内存到non-local视频存储(如AGP内存)转移数据。

设备像素拷贝的格式转换

   StretchRect也能支持拷贝过程中的颜色转换操作。支持的转换可以是高性能的YUV surface格式到高性能的RGB surface格式。具体支持什么样的格式转换,你可以通过CheckDeviceFormatConversion方法来查看。

   HRESULT CheckDeviceFormatConversion(UINT adapter, D3DDEVTYPE device_kind, D3DFORMAT source_fmt, D3DFORMAT target_fmt);

   source_fmt可能是FOURCC格式也可能是合理的back buffer格式。 Target格式将是下面中的一种:D3DFMT_A1R5G5B5  , D3DFMT_A8R8G8B8, D3DFMT_A16B16G16R16, D3DFMT_X1R5G5B5, D3DFMT_A8R8G8B8, D3DFMT_A16B16G16R16, D3DFMT_R5G6B5, D3DFMT_X8B8G8R8, D3DFMT_A32B32G32R32F,

D3DFMT_R8G8B8, D3DFMT_X8R8G8B8, D3DFMT_A2R10G10B10, D3DFMT_A2B10G10R10。

 

设备像素拷贝限制:

     因为StretchRect直接工作在设备内存上,它将面临很多限制。

     restrictions: 1. source和destination不能是相同的surface。 2. 不能从render target surface 到off screen surface。 3. 不能基于压缩格式stretch。 4.D3DDEVCAPS2_CAN_STRETCH_RECT_FROM_TEXTURES ,如果source是纹理surface。

    depth/stencil restrictions:1. 不能是纹理,2. 不能discardable;3. 必须是整个surface拷贝;4. source和destination必须是同样的尺寸。5.不支持filtering。6. 不能从一个场景里面被调用。

    downsampling multisample render target: 1. 创建multisample render target;2. 创建同样大小的non-multismapled render target; 3. 从non-MS RT拷贝到MS RT。

如下是source和destination的结合情况:

DX8 Driver no stretching    
SourceDestination   
 TextureRT TextureRTOff-screen Plain
TextureNONONONO
RT TextureNOYesYesNo
RTNOYesYesNo
Off-screen PlainYesYesYesYes
DX8 Driver no stretching    
 TextureRT TextureRTOff-screen Plain
TextureNoNoNoNo
RT TextureNoNoNoNo
RTNoYesYesNo
Off-screen PlainNoYesYesNo
DX9 Driver no stretching    
 TextureRT TextureRTOff-screen Plain
TextureNoYesYesNo
RT TextureNoYesYesNo
RTNoYesYesNo
Off-screen PlainNoYesYesYes
DX9 Driver  stretching    
 TextureRT TextureRTOff-screen Plain
TextureNoYesYesNo
RT TextureNoYesYesNo
RTNoYesYesNo
Off-screen PlainNoYesYesNo

   

  • Copies From System Memory To Device Memory

    通常你lock一个surface,然后直接使用CPU为它填充数据。但是并且每个surface都是lockable。设备内存里面的surface通常是不经常lock的,因为lock住以后访问速度是非常慢的。所以我们通常的办法是先使用CPU update系统内存的surface,然后使用updateSurface或者UpdateTexture将他们传给设备内存。

   HRESULT UpdateSurface(IDirect3DSurface9 * source, const RECT * source_rect, IDirect3DSurface9 * destination, CONST POINT * offset);

   offset参数指定了目的区域对应source 区域左上角像素的位置偏移。

   如果source或者destination的surface用于multisampling或者depth/stencil ,UpdateSurface也将失败。如下表是updateSurface的source和destinationa的结合情况。

SourceDestination   
 TextureRT TextureRTOff-screen Plain
TextureYESYESYESYES
RT TextureNONONONo
RTNONONONo
Off-screen PlainYesYesYesYes

 

    UpdateTexture与UpdateSurface相似,但是它对于整个纹理资源而不是单个surface。runtime 将维护一个dirty区域。

   HRESULT UpdateTexture(IDirect3DBaseTexture9 * source , IDirect3DBaseTexture9 * destination);

   当UpdateTexture被调用,所有累计的dirty区域将会值为level 0的纹理区域。对于mipmapped的纹理,每个mip level对应的区域同样也要考虑dirty。 source的levels不能小于destination level数,并且level 0的大小也必须相同。  

  • Copies From Device Memory To System Memory

    只有两种方法从设备里面读回被渲染的图片:一种是创建设备的时候允许锁住back buffer,另外一种是 GetRenderTargetData。锁住back buffer一般比较慢。

HRESULT GetRenderTargetData(IDirect3DSurface9 * source, IDirect3DSurface9 * destination);

    如果source 是multisamepled或者不是render target,它将会失败。

 

Filling Rectangles

 

     如果你想直接用某种solid的颜色填充surface的某个矩形区域,你可能直接使用colorFill方法,而不是先锁住再使用CPU填充。

   HRESULT  ColorFill(IDirect3DSurface9 * destination, CONST RECT * region, D3DCOLOR color);

 

Windows Messages

 

   在执行过程中CreateDevice和Reset方法可能会产生窗口消息。应用程序不应该在这些方法的执行过程调用设备方法来处理这些消息。 只有设备窗口完全构造完成后,设备才有方法调用。

WM_ACTIVATEAPP当active发生变化时. 中止或者恢复窗口重画。
WM_CLOSE发送出去,标记应用终止。释放设备上所有的对象,释放设备然后退出。
WM_COMPACTING标记当前内存很低。
WM_CONTEXTMENU当用户点击context button
WM_CREATE发送到一个被创建的窗口。WM_CREATE一般是在CreateWindow完成之前就发送出去了。你不应该在它里面创建设备,设备必须至少在::CreateWindow完成之后再创建。
WM_DISPLAYCHNANGE当桌面分辨率改变的时候。设备可能会reshape,你需要reshape swap chain。
WM_ENTERMENULOOP当进入一个modal菜单。暂停redraw.
WM_ENTERSIZEMOVE当开始一个窗口size 和移动操作时发送。
WM_ERASEBKGND当窗口北京需要删除是发送。
WM_EXITMENULOOP当退出modal菜单是发送。恢复连续的redraw
WM_EXITSIZEMOVE当窗口的size 和move操作完成,reshape swap chain
WM_GETMINMAXINFO当窗口的位置和大小信息将要变化的时候发送
WM_MOUSEMOVE鼠标移动。
WM_NCHITTEST当鼠标移动或者鼠标pressed或者release的时候发送。
WM_PAINT发送重画窗口damaged的部分。
WM_POWERBROADCAST电源管理事件
WM_SETCURSOR设置一个cursor。
WM_SHOWWINDOW当窗口将要显示或者隐藏的时候
WM_SIZE当窗口大小改变时
WM_SIZING当窗口正在resize是发送。
WM_SYSCOMMAND在系统命令下发送。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值