切換
舊版
前往
大廳
主題

為了即將登場的Vulkan,使用Mantle來熱身

Lumi | 2015-08-11 08:46:29 | 巴幣 0 | 人氣 1197

某些網站推測Vulkan將會在八月初的SIGGRAPH或GDC Europe發表,但目前看來SIGGRAPH是無望了。Khronos的老大只保守地說會在年底前推出,如果像我一樣實在等不及了,不如乾脆先用相似的Mantle來做預習,以便將來能接近無縫接軌。首先準備一張AMD Radeon HD 7xxx以上顯卡、Win7以上作業系統、取得好心人士製作的header,再把API reference放在手邊。由於Mantle較靠近底層,光是想畫出簡單圖形就得費一番功夫,有了心理準備後我們就開始吧。


取得debug訊息
首先我們使用grDbgRegisterMsgCallback()註冊一個用來取得debug訊息的callback。省去每次得自己解讀錯誤回傳值的麻煩,讓mantle明確告訴我們錯在哪,例如以下錯誤訊息:

Insufficient amount of memory was provided to retrieve data in grGetGpuInfo. Need 64 bytes, but only 32 bytes were provided.

mantle抱怨我們呼叫grGetGpuInfo()時給太少記憶體,以致無法取得data。這裡有個小技巧,先呼叫一次grGetGpuInfo(),並且將data的指標設為null。取得data大小後就能預先準備好記憶體,然後再呼叫一次grGetGpuInfo()取得資訊。


初始化Mantle runtime
下一步是呼叫grInitAndEnumerateGpus()初始化Mantle,同時取得GPU的數量。此處要傳入應用程式的資訊,例如程式名稱和版本編號。Mantle會拿這些資訊做最佳化,不過既然我們不是甚麼Mantle會注意到的大廠,這部分可隨便填。如果沒有記憶體配置器,pAllocCb參數可直接設為null。grInitAndEnumerateGpus()沒有對應的釋放用函式。


取得command queue資訊
Mantle預設了兩種command queue,其一為universal queue,可以接收繪圖與計算的指令;其二為compute queue,只能接收計算的指令。其他種類的queue是屬於extension範圍,顯卡不一定會支援,但支援Mantle的顯卡一定至少會有一個universal queue。

將指令送進queue後,顯卡便會在背景執行指令。眾所周知Mantle一開始就設計成能搭配multithread運作,若不在多個thread中共用同一個queue,我們便可省下上鎖的功夫。但是有個壞消息,我的R9 380只支援universal queue、compute queue、DMA queue、Timer queue各一個,queue的數量根本不夠分配給每個thread,所以還是只能替每個queue添個鎖。使用grGetGpuInfo()函式可取出顯卡支援那些queue,和該種類queue的數量,收集完queue的資訊後就可以進行下一步。


確認extension
接著呼叫grGetExtensionSupport()來確認顯卡是否支援想用的extension,其中最重要的是"GR_WSI_WINDOWS",之後的presentation步驟需要此extension。因為上一步驟中我得知顯卡支援DMA與Timer queue,因此我還檢查了"GR_DMA_QUEUE"與"GR_TIMER_QUEUE"這兩個extension。


建立device

收集完以上資訊後,就可以呼叫grCreateDevice()。我們必須指定想取得哪個gpu的device、使用哪些queue和extension。在這邊至少也要指定一個universal queue,否則之後就沒得玩了。

為了防止不小心使用到某個extension的函式,未指明extension的情形下,呼叫extension的函式會失敗。另外,在這裡還可設定Mantle的Validation層級,讓Mantle告訴我們哪裡做錯了:

GR_DEVICE_CREATE_INFO deviceInfo = {};
deviceInfo.maxValidationLevel = GR_VALIDATION_LEVEL_4;
deviceInfo.flags |= GR_DEVICE_CREATE_VALIDATION;

為了追求效率,在release版本可把多餘的檢查拿掉。
接著使用grGetDeviceQueue()取得command queue handle,之後送出指令時會用到。程式結束時不需要釋放queue handle,但需要呼叫grDestroyDevice()來釋放device。


準備render target
接著得準備一張專門用來顯示在螢幕上的image,最終我們想畫的東西都得輸出到這張image (render target)上。在mantle中稱作presentable image,必須使用extension提供的函式來取得:

GR_WSI_WIN_PRESENTABLE_IMAGE_CREATE_INFO imageCreateInfo = {};
imageCreateInfo.format = {
GR_CH_FMT_R8G8B8A8,
GR_NUM_FMT_UNORM
};
imageCreateInfo.usage = GR_IMAGE_USAGE_COLOR_TARGET;
imageCreateInfo.extent = { width, height };

GR_IMAGE image;
GR_GPU_MEMORY imageMemory;
grWsiWinCreatePresentableImage(device, &imageCreateInfo, &image, &imageMemory);

在Mantle中,texture被切割為image與memory兩種物件。memory用於控制資料的儲存空間,圖片內容會存放在此。image則儲存關於圖片格式的資訊,並指向memory物件。

與一般image物件不同,grWsiWinCreatePresentableImage()所回傳的image物件已經幫我們設定好記憶體了,所以不可再將image指向別的memory物件。同時此函式回傳的memory物件也不可對它使用grFreeMemory(),此memory物件會隨著image物件一起被釋放。

釋放image物件可呼叫grDestroyObject(),除了physical GPU、device、queue還有memory等物件之外,都是使用此函式來釋放。


建立Command buffers
在mantle中的指令(command)函式都會以grCmd開頭,我們需要先將這些指令塞進cmd buffer內,然後將整個buffer送進cmd queue裡面。建立buffer可用grCreateCommandBuffer()函式,此時須指定此buffer是對應哪種queue。將指令塞進buffer則用下列方式:

grBeginCommand(cmdBuffer, 0);

// commands

grEndCommandBuffer(cmdBuffer);

Cmd buffer沒有像queue那樣有數目限制,可以在多個thread替不同的cmd buffer塞指令。而且buffer可以選擇不清空,如此可反覆執行相同的指令,和OpenGL古早的display list功能一樣。釋放cmd buffer時,需確認此buffer沒有正在被GPU執行或等待被執行。

建立一個新的cmd buffer後,前往下一步驟。


State transition與清除image內容
GPU存取memory或image時會預期這些資源處於特定的狀態,mantle將確保狀態正確的責任交給了我們。另外,在Mantle中大部分對資源的存取動作並非照順序執行,我們必須確保GPU不會對某資源同時進行讀取和寫入的動作(或同時寫入與寫入),Mantle依靠狀態設定來分隔不同的存取動作。在此之前我們先設定image的資訊:

GR_IMAGE_SUBRESOURCE_RANGE imageSubresRange= {};
imageSubresRange.aspect = GR_IMAGE_ASPECT_COLOR;
imageSubresRange.baseMipLevel = 0;
imageSubresRange.mipLevels = 1;
imageSubresRange.baseArraySlice = 0;
imageSubresRange.arraySize = 1;

上面程式碼中,aspect參數指出此image的種類是color、depth或stencil,在這裡我們設定只有一層mipmap。

剛拿到的presentable image預設是uninitialized(未初始化)狀態,我們可藉由呼叫grCmdPrepareImages()改變狀態。下列程式碼先將image轉為可清除內容的狀態,接著把Image"
清"成紅色,最後轉換成可顯示的狀態:

grBeginCommandBuffer(cmdBuffer, 0);

GR_IMAGE_STATE_TRANSITION transition= {};
transition.image = image;
transition.oldState = GR_IMAGE_STATE_UNINITIALIZED;
transition.newState = GR_IMAGE_STATE_CLEAR;
transition.subresourceRange = imageSubresRange;
grCmdPrepareImages(cmdBuffer, 1, &transition);

float clearColor[] = { 1.0, 0.0, 0.0, 1.0 };
grCmdClearColorImage(cmdBuffer, presentableImage_, clearColor, 1, &imageSubresRange);

transition.oldState = GR_IMAGE_STATE_CLEAR;
transition.newState = GR_WSI_WIN_IMAGE_STATE_PRESENT_WINDOWED;
grCmdPrepareImages(cmdBuffer, 1, &transition);

grEndCommandBuffer(cmdBuffer);


送出command buffers
在將cmd buffer送出之前,我們還得提供這些指令所用到的記憶體清單。在這裡不直接使用GR_GPU_MEMORY物件,而是用GR_MEMORY_REF將memory物件包住。確保每個記憶體可用是應用程式的責任,所以memory物件不可為GR_NULL_HANDLE。

GR_MEMORY_REF imageMemoryRef = { image, 0 };
grQueueSubmit(universalQueue, 1, &cmdBuffer, 1, &imageMemoryRef, 0);

注意送進queue的memory ref陣列中不可有重複的memory,且一次能送進queue的memory ref數量有上限,此上限可用grGetGpuInfo()調查。如果要確認指令何時完成,可在函式最後一個參數傳入GR_FENCE物件,然後用grWaitForFences()函式確認指令完成與否。


Image view
Shader不會直接使用Image物件,而是透過ImageView物件存取Image。Image也不能直接用作render target,得透過GR_COLOR_TARGET_VIEW物件:

GR_COLOR_TARGET_VIEW_CREATE_INFO colorTargetViewCreateInfo = {};
colorTargetViewCreateInfo.image = image;
colorTargetViewCreateInfo.arraySize = 1;
colorTargetViewCreateInfo.baseArraySlice = 0;
colorTargetViewCreateInfo.mipLevel = 0;
colorTargetViewCreateInfo.format.channelFormat = GR_CH_FMT_R8G8B8A8;
colorTargetViewCreateInfo.format.numericFormat = GR_NUM_FMT_UNORM;

GR_COLOR_TARGET_VIEW colorTargetView;
grCreateColorTargetView(device, &colorTargetViewCreateInfo, &colorTargetView);

至於depth與stencil target則是使用GR_DEPTH_STENCIL_VIEW物件與grCreateDepthStencilView()函式,但我們現在不需要。


Shader
Mantle不直接支援HLSL,得使用CodeXL附的shader analyzer將HLSL編譯成AMD Intermediate Language。不過新版本把編譯功能拿掉了 ,不得已只好拿別人已經編譯好的來用…
將編譯好的binary讀到記憶體中,送進grCreateShader()建立shader物件。

以下Shader code是從這裡複製過來的:

struct VOut {
float4 position : SV_POSITION;
float4 color : COLOR;
};

Buffer<float4> positions : register(t0);
Buffer<float4> colors : register(t1);

VOut VShader(uint id : SV_VertexID) {
VOut output;

output.position = positions.Load(id);
output.color = colors.Load(id);

return output;
}

取出頂點資訊的方式和以往大不相同,現在得靠內建的vertex id來取出buffer內的資料,這個vertex id代表我們正在處理第幾個座標點。至於pixel shader則和以前一樣:

float4 PShader(float4 position : SV_POSITION, float4 color : COLOR) : SV_TARGET {
return color;
}


Graphics pipeline



上圖是一個完整的pipeline,其中虛線的方框代表此步驟可省略。對於只想畫三角形的我們來說,只需設定IA (Input assembler)、VS (Vertex shader)、PS (Pixel shader)和CB (Color blender)。

GR_GRAPHICS_PIPELINE_CREATE_INFO pipelineCreateInfo = {};
pipelineCreateInfo.iaState.topology = GR_TOPOLOGY_TRIANGLE_STRIP;

pipelineCreateInfo.cbState.logicOp = GR_LOGIC_OP_COPY;
pipelineCreateInfo.cbState.target[0].blendEnable = GR_FALSE;
pipelineCreateInfo.cbState.target[0].channelWriteMask = 0xF; // RGBA bits
pipelineCreateInfo.cbState.target[0].format.channelFormat = GR_CH_FMT_R8G8B8A8;
pipelineCreateInfo.cbState.target[0].format.numericFormat = GR_NUM_FMT_UNORM;

接著將之前從grCreateShader()得到的shader物件填入:

pipelineCreateInfo.vs.shader = vertexShader;
pipelineCreateInfo.vs.dynamicMemoryViewMapping.slotObjectType = GR_SLOT_UNUSED;

pipelineCreateInfo.ps.shader = pixelShader;
pipelineCreateInfo.ps.dynamicMemoryViewMapping.slotObjectType = GR_SLOT_UNUSED;

利用descriptor slots指定shader所使用的資料,由於我們使用的vertex shader需要頂點座標與顏色,因此需要兩個slot分別表示這兩種資料。

GR_DESCRIPTOR_SLOT_INFO vsDescriptorSlots[2];
pipelineCreateInfo.vs.descriptorSetMapping[0].descriptorCount= 2;
pipelineCreateInfo.vs.descriptorSetMapping[0].pDescriptorInfo = vsDescriptorSlots;

vsDescriptorSlots[0].shaderEntityIndex = 0;    //對應shader中的t0
vsDescriptorSlots[1].shaderEntityIndex = 1;    //對應shader中的t1

vsDescriptorSlots[0].slotObjectType = GR_SLOT_SHADER_RESOURCE;
vsDescriptorSlots[1].slotObjectType = GR_SLOT_SHADER_RESOURCE;


pixel shader的descriptor slot數量需和vertex shader相同,即使我們沒用到也得設定:

GR_DESCRIPTOR_SLOT_INFO psDescriptorSlots[2];
pipelineCreateInfo.ps.descriptorSetMapping[0].descriptorCount= 2;
pipelineCreateInfo.ps.descriptorSetMapping[0].pDescriptorInfo = psDescriptorSlots;

psDescriptorSlots[0].slotObjectType = GR_SLOT_UNUSED;
psDescriptorSlots[1].slotObjectType = GR_SLOT_UNUSED;

和OpenGL相比,設定pipeline的過程簡化成只需填入Mantle提供的structure,很容易將設定值用LUA之類的方式儲存。設定完就能建立pipeline物件了:

GR_PIPELINE pipeline;
grCreateGraphicsPipeline(device, &createInfo, &pipeline);


Pipeline memory
之前我們建立的許多mantle object都會自動配置自己需要的記憶體,但這裡我們得手動配置。呼叫grGetObjectInfo()可取出pipeline object需要多少記憶體,同時告訴我們pipeline可以被放進哪些heap內:

GR_MEMORY_REQUIREMENTS memReqs = {};
GR_SIZE memReqsSize = sizeof(GR_MEMORY_REQUIREMENTS);
grGetObjectInfo(pipeline, GR_INFO_TYPE_MEMORY_REQUIREMENTS, &memReqsSize, &memReqs);

在我的R9 380上grGetObjectInfo()傳回了三個heap id,這時可用grGetMemoryHeapInfo()調查各heap的特性。例如CPU可否存取、GPU的寫入/讀取速度、heap大小與page大小。
    
GR_MEMORY_HEAP_PROPERTIES heapProps = {};
GR_SIZE heapPropsSize = sizeof(GR_MEMORY_HEAP_PROPERTIES);
grGetMemoryHeapInfo(device, memReqs.heaps[0], GR_INFO_TYPE_MEMORY_HEAP_PROPERTIES, &heapPropsSize, &heapProps);

向GPU要求配置的記憶體大小必須是page大小的倍數,在R9 380上page大小為64K,但pipeline物件只需要320 byte。這意味著我們必須手動幫GPU做記憶體管理,否則每個物件都配置64K的話,很快記憶體就用光了。在這裡我們偷懶直接替pipeline配置一整個page:

GR_GPU_MEMORY memory;
GR_MEMORY_ALLOC_INFO allocInfo = {};
allocInfo.size = heapProps.pageSize;
allocInfo.alignment = 0;
allocInfo.memPriority = GR_MEMORY_PRIORITY_HIGH;
allocInfo.heapCount = 1;
allocInfo.heaps[0] = memReqs.heaps[0];
grAllocMemory(device, &allocInfo, &memory);

最後我們將pipeline和memory綁在一起:

grBindObjectMemory(pipeline, memory, 0);

在程式結束時記得要先鬆綁再刪除pipeline物件:

grBindObjectMemory(pipeline, GR_NULL_HANDLE, 0);
grFreeMemory(memory);
grDestroyObject(pipeline);


Dynamic State Object
graphics pipeline物件裡儲存許多繪圖設定,但還有更多是存在dynamic state物件裡面。這些dynamic state物件有五種:raster state、viewport state、color blend state、depth stencil state、MSAA state。

製造這五種dynamic state物件的函式分別為:grCreateRasterState()、grCreateViewportState()、grCreateColorBlendState()、grCreateDepthStencilState()與grCreateMsaaState(),其設定值若是有寫過OpenGL應不會遇到太大困難,另外這邊也能用LUA簡單搞定設定值的儲存問題。

Mantle不像OpenGL每個設定都有預設值,所以我們必須準備好這五種dynamic state物件。每個cmd buffer的設定值都互相獨立,不同cmd buffer的pipeline與dynamic state不會互相干擾。不過這也表示我們必須為每個包含draw call的cmd buffer設定狀態,否則不會有東西被畫出來。在最後一小節我們會用上這些物件。


上傳Vertex data
在上傳多邊形座標前我們得先用上一節提到的grAllocMemory()準備好記憶體,此時必須選擇允許CPU存取的heap。
下面是以triangle strip排列方式儲存的四邊形座標,注意座標向量的w軸設為1。白色對應四邊形左下角,紅色對應右下,綠色對應左上,藍色對應右上角:

float vertices[] = {
// positions
-0.8f, -0.8f, 0, 1,  
0.8f, -0.8f, 0, 1,
-0.8f,  0.8f, 0, 1,
0.8f,  0.8f, 0, 1,

// colors
1,  1, 1, 0,  
1,  0, 0, 0,
0,  1, 0, 0,
0,  0, 1, 0,
};

接下來將座標資訊複製到剛才所取得的gpu記憶體中:

void* buffer = nullptr;
grMapMemory(vertexMemory, 0, &buffer);
memcpy(buffer , vertices, sizeof(vertices));
grUnmapMemory(vertexMemory);

還記得之前我們得呼叫grCmdPrepareImages()轉換圖片狀態嗎?這邊我們又得再來一次,只是改成呼叫grCmdPrepareMemoryRegions()。預設的記憶體狀態是data transfer,我們需要將它改成graphics shader read only:

grCreateCommandBuffer(device, &bufferCreateInfo, &cmdBuffer)

GR_MEMORY_STATE_TRANSITION dataTransition = {};
dataTransition.mem = vertexMemory;
dataTransition.oldState = GR_MEMORY_STATE_DATA_TRANSFER;
dataTransition.newState = GR_MEMORY_STATE_GRAPHICS_SHADER_READ_ONLY;
dataTransition.offset = 0;
dataTransition.regionSize = sizeof(vertices);

grBeginCommandBuffer(cmdBuffer, 0);
grCmdPrepareMemoryRegions(cmdBuffer, 1, &dataTransition);
grEndCommandBuffer(cmdBuffer);

將以上指令送給cmd queue後還沒完,記得之前設定pipeline時我們準備了兩個插槽(slot),分別接受position和color資料。現在我們得準備插頭:

GR_DESCRIPTOR_SET descriptorSet;
GR_DESCRIPTOR_SET_CREATE_INFO descriptorCreateInfo = {};
descriptorCreateInfo.slots = 2;
grCreateDescriptorSet(device, &descriptorCreateInfo, &descriptorSet);

注意descriptor set物件同樣需要特別為它配置記憶體,然後我們得設定vertex記憶體中哪個部份
對應哪一個slot,比較特別的地方是必須以grBeginDescriptorSetUpdate()和grEndDescriptorSetUpdate()兩函式將設定動作包起來。
    
grBeginDescriptorSetUpdate(descriptorSet);

GR_MEMORY_VIEW_ATTACH_INFO memoryViewAttachInfo = {};
memoryViewAttachInfo.mem = vertexMemory;
memoryViewAttachInfo.offset = 0;
memoryViewAttachInfo.stride = sizeof(float) * 4;
memoryViewAttachInfo.range = sizeof(float) * 16;
memoryViewAttachInfo.format.channelFormat = GR_CH_FMT_R32G32B32A32;
memoryViewAttachInfo.format.numericFormat = GR_NUM_FMT_FLOAT;
memoryViewAttachInfo.state = GR_MEMORY_STATE_GRAPHICS_SHADER_READ_ONLY;
grAttachMemoryViewDescriptors(descriptorSet, 0, 1, &memoryViewAttachInfo);

memoryViewAttachInfo.offset = sizeof(float) * 16;
grAttachMemoryViewDescriptors(descriptorSet, 1, 1, &memoryViewAttachInfo);

grEndDescriptorSetUpdate(descriptorSet);


Render loop
我們總算準備好所有材料,再撐著點,我們快搞定了。我們還得在繪圖迴圈裡:
  1. 轉換presentable image的狀態。
    從GR_WSI_WIN_IMAGE_STATE_PRESENT_WINDOWED轉換成GR_IMAGE_STATE_TARGET_RENDER_ACCESS_OPTIMAL。
  2. 設定render target為presentable image,指定我們要將圖形畫到這張image上。
    GR_COLOR_TARGET_BIND_INFO colorTargetBindInfo;
    colorTargetBindInfo.view = colorTargetView;
    colorTargetBindInfo.colorTargetState = GR_IMAGE_STATE_TARGET_RENDER_ACCESS_OPTIMAL;
    grCmdBindTargets(drawBuffer, 1, &colorTargetBindInfo, nullptr);
  3. 設定dynamic state object與graphics pipeline。
    grCmdBindStateObject(drawBuffer, GR_STATE_BIND_MSAA, msaaState);
    grCmdBindStateObject(drawBuffer, GR_STATE_BIND_VIEWPORT, viewportState);
    grCmdBindStateObject(drawBuffer, GR_STATE_BIND_COLOR_BLEND, colorBlendState);
    grCmdBindStateObject(drawBuffer, GR_STATE_BIND_DEPTH_STENCIL, depthStencilState);
    grCmdBindStateObject(drawBuffer, GR_STATE_BIND_RASTER, rasterState);
    grCmdBindPipeline(drawBuffer, GR_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
  4. 設定descriptor set,指定我們要使用什麼vertex資料。
    grCmdBindDescriptorSet(drawBuffer, GR_PIPELINE_BIND_POINT_GRAPHICS, 0, descriptorSet, 0);
  5. 繪圖。告知mantle我們有四個座標點,只畫出一次。
    grCmdDraw(drawBuffer, 0, 4, 0, 1);
  6. 轉換presemtable image的狀態。
    從GR_IMAGE_STATE_TARGET_RENDER_ACCESS_OPTIMAL轉換回GR_WSI_WIN_IMAGE_STATE_PRESENT_WINDOWED。

我們可以將以上指令保存進一個cmd buffer內,之後不須每個frame都重新設定此cmd buffer。記得我們還得準備好一個memory ref陣列,包含所有上面用到物件的記憶體。
為了得知GPU什麼時候完成這個frame,我們必須準備一個fence物件,隨著上面的cmd buffer與memory ref陣列一起送進cmd queue裡面,接著將畫好的東西顯示到畫面上:

GR_WSI_WIN_PRESENT_INFO presentInfo = {};
presentInfo.hWndDest = GetActiveWindow();
presentInfo.srcImage = presentableImage;
presentInfo.presentMode = GR_WSI_WIN_PRESENT_MODE_WINDOWED;
grWsiWinQueuePresent(universalQueue, &presentInfo);

最後我們用grWaitForFences()確認這個frame的指令已經執行完,這樣繪圖迴圈就到達終點,可以回頭進行下一個frame。



結語
有沒有覺得比起OpenGL麻煩到爆炸呢?除了設定pipeline變的較簡單,還有更容易整合進multithread環境之外,其他部份寫起來有點惱人。無法編譯shader使我沒辦法嘗試許多常用功能,所以我的新繪圖底層目前只完成一個骨架。
根據這份投影片,Vulkan的API與Mantle極為相似,而且已經能看到部分改良。但是沒規格書沒真相,只能祈禱Vulkan趕快推出了。


參考資料
Implementing hello triangle in mantle
Mantle Programming Guide and API Reference

創作回應

=星之卡比=
在一個論壇上看到相關人士說如果沒有意外的話,Vulkan會在二月中公布的樣子。真是令人期待~
不過我覺得奇怪的是,為甚麼要等到測試過了再公布Spec,畢竟也不會再更動了不是嗎@@
2016-01-27 21:23:48
Lumi
http://www.phoronix.com/scan.php?page=news_item&px=NVIDIA-Vulkan-Dev-Day
謠言說三月...[e42]
2016-01-28 18:45:01
Lumi
現在看來還真的有可能在台灣時間2月19日左右推出...
2016-02-12 19:44:26
=星之卡比=
恩恩,大大也是因為官網說的那個Webinar嗎XD
我是有在逛一個大陸的論壇,裡面有知情人士是說他自己得到的消息目前在18號沒錯。(不過好像16號就會有動作,但我看不太懂那張圖的表示方式XDlll)
2016-02-14 12:06:35
Lumi
似乎再過兩三個小時就會公布了, 看看這次究竟是真的還是又是謠言...
2016-02-16 21:16:35
=星之卡比=
是真的了~看來這次那個洩漏出的訊息沒錯~可以睡覺時拿來看了~
2016-02-16 22:01:19

更多創作