ffmpeg開發(fā)播放器學(xué)習(xí)筆記 - Metal 渲染YUV

該節(jié)是ffmpeg開發(fā)播放器學(xué)習(xí)筆記的第五節(jié)《Metal 渲染YUV》

Metal是Apple開發(fā)的適用于iOS/macOS/iPadOS平臺的圖形渲染與硬件加速庫召调。Metal 提供對圖形處理器 (GPU) 的接近直接訪問膨桥,使您能最大程度地發(fā)揮 iOS、macOS 和 Apple tvOS app 中的圖形和計算潛能唠叛。Metal 構(gòu)建于易用的低開銷架構(gòu)之上只嚣,而且提供預(yù)編譯的 GPU 著色器和精細(xì)的資源控制,并支持多線程處理艺沼。相對于OpenGL,它是采用了面向?qū)ο蟮脑O(shè)計更易于使用,在Apple的系統(tǒng)平臺上可發(fā)揮更大的性能優(yōu)勢册舞。

image

? 第一節(jié) - Hello FFmpeg
? 第二節(jié) - 軟解視頻流,渲染 RGB24
? 第三節(jié) - 認(rèn)識YUV
? 第四節(jié) - 硬解碼,OpenGL渲染YUV
?? 第五節(jié) - Metal 渲染YUV
?? 第六節(jié) - 解碼音頻,使用AudioQueue 播放
?? 第七節(jié) - 音視頻同步
?? 第八節(jié) - 完善播放控制
?? 第九節(jié) - 倍速播放
?? 第十節(jié) - 增加視頻過濾效果
?? 第十一節(jié) - 音頻變聲

該節(jié) Demo 地址: https://github.com/czqasngit/ffmpeg-player/releases/tag/Metal-YUV
實例代碼提供了Objective-CSwift兩種實現(xiàn),為了方便說明,文章引用的是Objective-C代碼,因為Swift代碼指針看著不簡潔。

該節(jié)最終效果如下圖:

image

image

目標(biāo)

  • 了解Metal基本使用流程
  • 初始化Metal
  • 了解metal小程序
  • 了解Metal計算線程分布
  • 利用Metal渲染YUV

了解Metal基本使用流程

下面這張流程圖大致展示了Metal的工程原理:


image

1.獲取GPU設(shè)備實例

要使用Metal進行計算或渲染,首先需要獲取到當(dāng)前系統(tǒng)支持的GPU實例,后續(xù)所有的操作都必須建立在這個GPU實例的計算上進行障般。值得注意的是macOS平臺可能會有多個GPU實例调鲸。

2.初始化計算管線

Metal使用.metal文件來編寫小程序,它的風(fēng)格類似C++。編寫好的.metal文件會在編譯時統(tǒng)一生成default.metallib資源文件挽荡。通過字符串查找到需要使用的小程序并最終生成計算管理實例藐石。

3.創(chuàng)建指令隊列

Metal的計算是通過計算隊列實例來管理的,它的目標(biāo)就是合理的調(diào)度GPU計算資源按提交的計算指令一個一個的計算。

4.創(chuàng)建指令緩沖對象

Metal的每一次計算都需要創(chuàng)建一個新的指令緩沖對象,Metal的計算目的是GPU,GPU不能直接訪問內(nèi)存數(shù)據(jù),緩沖對象分配好當(dāng)前計算所需要的顯存,在計算時Metal直接讀取顯存數(shù)據(jù)進行計算定拟。

5.將內(nèi)存數(shù)據(jù)發(fā)送到顯存

Metal計算所需要的數(shù)據(jù)需要在提交計算前從內(nèi)存發(fā)送到顯存,并在緩沖區(qū)中保存贯钩。

6.將指令緩沖對象提交到緩沖隊列計算

一切數(shù)據(jù)準(zhǔn)備好之后就可以將指令緩沖對象提交到指令隊列,等待指令隊列的調(diào)度并完成計算。

初始化Metal

1.獲取MTLDevice

id<MTLDevice> device = MTLCreateSystemDefaultDevice();

通過上面的代碼可以獲取到一個最優(yōu)的GPU實例,如果你需要獲取所有的GPU實例你可以使用如下代碼:

 NSArray<id<MTLDevice>> devices = MTLCopyAllDevices();

2.創(chuàng)建MTLComputePipelineState

創(chuàng)建MTLLibrary

id<MTLLibrary> library = [device newDefaultLibrary];

MTLLibrary實例是所有的metal小程序的集合,它可以理解成metal小程序庫办素。

從MTLLibrary中獲取MTLFunction

id<MTLFunction> function = [library newFunctionWithName:@"yuv420ToRGB"];

MTLFunction是一個具體的小程序

生成MTLComputePipelineState實例

id<MTLComputePipelineState> computePipline = [device newComputePipelineStateWithFunction:function error:&error];

指定計算管線計算時需要調(diào)用的GPU小程序函數(shù)

3.創(chuàng)建MTLCommandQueue

id<MTLCommandQueue> commandQueue = [device newCommandQueue];

到此,Metal使用的初始化工作就完成了

了解metal小程序

metal小程序采用了類似C++風(fēng)格的編碼方式,函數(shù)申明使用kernel關(guān)鍵字角雷。

#include <metal_stdlib>
using namespace metal;

kernel void foo(texture2d<float, access::read> texture [[ texture(0) ]],
                constant uint2 &byteSize [[ buffer(1) ]],
                texture2d<float, access::write> outTexture [[ texture(2) ]],
                uint2 gid [[ thread_position_in_grid ]]) {
        
}

textture變量是一個texture2d二維紋理對象,它的數(shù)據(jù)格式是float。方括號里表示它在Metal框架中的對應(yīng)的內(nèi)存中的數(shù)據(jù)類型是texture,變量位于位置0,只有讀取權(quán)限性穿。

byteSize變量是一個uint2類型的數(shù)據(jù),uint2即包含了兩個uint的結(jié)構(gòu)體類型勺三。它對應(yīng)內(nèi)存中的Buffer數(shù)據(jù)類型(即一維數(shù)據(jù)),變量位于位置1,只有讀取權(quán)限。

outTexture變量是一個texture2d二維紋理對象,它的數(shù)據(jù)格式是float需曾。它在Metal框架中的對應(yīng)的內(nèi)存中的數(shù)據(jù)類型是texture,變量位于位置2,只有寫入權(quán)限吗坚。

gid則是Metal框架計算時帶入的變量,它表示當(dāng)前計算的線程號,通過線程號可以獲取到具體要計算的數(shù)據(jù)祈远。這會在稍后進行更詳細(xì)的說明。

以上是一個基礎(chǔ)的函數(shù)申明,申明的只讀取變量即是從內(nèi)存發(fā)送到顯存的數(shù)據(jù),只寫變量則是計算完成后輸出到內(nèi)存的數(shù)據(jù)商源。

更多詳細(xì)的Metal Language可參考: https://developer.apple.com/metal/Metal-Shading-Language-Specification.pdf

了解Metal計算線程

Metal在執(zhí)行每一個Command Buffer的時候车份,都將其模擬成一個網(wǎng)格,每一個網(wǎng)格都有一個單獨的線程執(zhí)行牡彻。
GPU執(zhí)行程序的邏輯與CPU最大的不同就是并行執(zhí)行大量相互不干擾的邏輯,迸發(fā)線程數(shù)量比CPU多很多,這也是硬件加速的本質(zhì)扫沼。
網(wǎng)格看上去類似這樣的:

image

這里繪制的示意圖為了更貼近本節(jié)的內(nèi)容,所以繪制成了二維網(wǎng)格。需要注意的是,Metal還可以執(zhí)行一維網(wǎng)格與三維網(wǎng)格,原理都是一樣的庄吼。

圖中藍(lán)色的部分是實際需要計算的數(shù)據(jù),橙色的部分則是無數(shù)據(jù)的網(wǎng)格缎除。在設(shè)定Metal最終計算的風(fēng)格大小的時候往往會多設(shè)置一部分?jǐn)?shù)據(jù),防止某些邊緣界線的數(shù)據(jù)被遺漏了。這些無數(shù)據(jù)的網(wǎng)格需要在小程序里進行過濾总寻。

Metal在計算的時候不是以單一的網(wǎng)格作為計算基礎(chǔ),而是將一組網(wǎng)格作為一個計算基礎(chǔ)器罐,一次性一組網(wǎng)絡(luò)。它看上去是這樣的:

image

這里模擬一組網(wǎng)格是4x4,表明Metal在調(diào)度執(zhí)行的時候一次性計算16個網(wǎng)格渐行。這一組執(zhí)行完了再執(zhí)行下一組,因為GPU的線程并發(fā)數(shù)量也是有限的,配置一個最優(yōu)的執(zhí)行大小對計算速度也是有影響的轰坊。

利用Metal渲染YUV

1.初始化CVMetalTextureCacheRef

初始化CVMetalTextureCacheRef只需要執(zhí)行一次

CVMetalTextureCacheRef metalTextureCache = NULL;
CVReturn ret = CVMetalTextureCacheCreate(kCFAllocatorDefault, NULL, device, NULL, &metalTextureCache);

CVMetalTextureCacheRef用于緩存后期創(chuàng)建CVMetalTextureRef與CVPixelBufferRef之間的映射,如果有同樣的CVPixelBufferRef實例被再次創(chuàng)建時,可直接使用緩存的CVMetalTextureRef實例。

2.AVFrame轉(zhuǎn)換成CVPixelBufferRef

需要得到用于表達Metal中紋理資源對象實例,需要將AVFrame轉(zhuǎn)換成CVPixelBufferRef

- (BOOL)setupCVPixelBufferIfNeed:(AVFrame *)frame {
    if(!pixelBufferPoolRef) {
        NSMutableDictionary *pixelBufferAttributes = [[NSMutableDictionary alloc] init];
        if(frame->color_range == AVCOL_RANGE_MPEG) {
            pixelBufferAttributes[_CFToString(kCVPixelBufferPixelFormatTypeKey)] = @(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange);
        } else {
            pixelBufferAttributes[_CFToString(kCVPixelBufferPixelFormatTypeKey)] = @(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange);
        }
        pixelBufferAttributes[_CFToString(kCVPixelBufferMetalCompatibilityKey)] = @(TRUE);
        pixelBufferAttributes[_CFToString(kCVPixelBufferWidthKey)] = @(frame->width);
        pixelBufferAttributes[_CFToString(kCVPixelBufferHeightKey)] = @(frame->height);
        /// bytes per row(alignment)
        pixelBufferAttributes[_CFToString(kCVPixelBufferBytesPerRowAlignmentKey)] = @(frame->linesize[0]);
//        pixelBufferAttributes[_CFToString(kCVPixelBufferIOSurfacePropertiesKey)] = @{};
        CVReturn cvRet = CVPixelBufferPoolCreate(kCFAllocatorDefault,
                                NULL,
                                (__bridge  CFDictionaryRef)pixelBufferAttributes,
                                &(self->pixelBufferPoolRef));
        if(cvRet != kCVReturnSuccess) {
            NSLog(@"create cv buffer pool failed: %d", cvRet);
            return NO;
        }
    }
    return YES;
}
if(![self setupCVPixelBufferIfNeed:frame]) return NULL;
CVPixelBufferRef _pixelBufferRef;
CVReturn cvRet = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pixelBufferPoolRef, &_pixelBufferRef);
if(cvRet != kCVReturnSuccess) {
    NSLog(@"create cv buffer failed: %d", cvRet);
    return NULL;
}
CVPixelBufferLockBaseAddress(_pixelBufferRef, 0);
/// copy y
size_t yBytesPerRowSize = CVPixelBufferGetBytesPerRowOfPlane(_pixelBufferRef, 0);
void *yBase = CVPixelBufferGetBaseAddressOfPlane(_pixelBufferRef, 0);
memcpy(yBase, frame->data[0], yBytesPerRowSize * frame->height);
/// copy uv
void *uvBase = CVPixelBufferGetBaseAddressOfPlane(_pixelBufferRef, 1);
size_t uvBytesPerRowSize = CVPixelBufferGetBytesPerRowOfPlane(_pixelBufferRef, 1);
memcpy(uvBase, frame->data[1], uvBytesPerRowSize * frame->height / 2);
CVPixelBufferUnlockBaseAddress(_pixelBufferRef, 0);
return _pixelBufferRef;

3.編寫小程序

#include <metal_stdlib>
using namespace metal;
///y_inTexture: Y
///uv_inTexture: UV
///byteSize: Y的寬高
///outTexture: RGBA
///gid: 執(zhí)行線程所在的Grid位置
kernel void yuv420ToRGB(texture2d<float, access::read> y_inTexture [[ texture(0) ]],
                        texture2d<float, access::read> uv_inTexture [[ texture(1) ]],
                        constant uint2 &byteSize [[ buffer(2) ]],
                        texture2d<float, access::write> outTexture [[ texture(3) ]],
                        uint2 gid [[ thread_position_in_grid ]]) {
    /// 超出實際紋理寬高的網(wǎng)格不計算,直接返回
    if(gid.x > byteSize.x || gid.y > byteSize.y) return;
//    if(gid.x % 2 == 0 || gid.y % 2 == 0 || gid.x % 3 == 0 || gid.y % 3 == 0) {
//        outTexture.write(float4(0, 0, 0, 1.0), gid);
//        return;
//    }
    /// 獲取y分量數(shù)據(jù),由于在創(chuàng)建MetalTexture的時候在方法CVMetalTextureCacheCreateTextureFromImage
    /// 中指定了歸一化的格式,所以這里得到的y值范圍是[0, 1]
    float4 yFloat4 = y_inTexture.read(gid);

    /// Y與UV在YUV420P格式下的比例是4:1
    /// YUV420P垂直與水平分別是2:1的比例
    /// gid是包含X祟印,Y坐標(biāo),所以這里gid/2實際上是縮小了4倍衰倦,符合YUV420P中Y與UV的比例
    /// 每4個Y共享一組UV
    float4 uvFloat4 = uv_inTexture.read(gid/2);
    float y = yFloat4.x;
    float cb = uvFloat4.x;
    float cr = uvFloat4.y;
    
    /// 按YCbCr轉(zhuǎn)RGB的公式進行數(shù)據(jù)轉(zhuǎn)換
    float r = y + 1.403 * (cr - 0.5);
    float g = y - 0.343 * (cb - 0.5) - 0.714 * (cr - 0.5);
    float b = y + 1.770 * (cb - 0.5);
    outTexture.write(float4(r, g, b, 1.0), gid);
        
}

4.獲取Metal紋理資源對象MTLTexture

在Metal中所有的資源對象都是MTLResource,常用的兩種類型則是MTLTexture與MTLBuffer,它們都是MTLResource子類型。MTLTexture是紋理(2維或者3維)資源對象,MTLBuffer是連續(xù)存儲數(shù)據(jù)的資源對象旁理。以下實例以是yTexture分量的MTLTexture對象:

size_t yWidth = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0);
size_t yHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0);
CVMetalTextureRef yMetalTexture;
CVReturn ret = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
                                                         self->metalTextureCache,
                                                         pixelBuffer,
                                                         NULL,
                                                         MTLPixelFormatR8Unorm,
                                                         yWidth,
                                                         yHeight,
                                                         0,
                                                         &yMetalTexture);

pixelBuffer是CVPixelBufferRef實例,它保存了具體的數(shù)據(jù)樊零。

MTLPixelFormatR8Unorm指定了數(shù)據(jù)格式y(tǒng)Texture存儲的數(shù)據(jù)是一個8位無符號整形數(shù)據(jù),它的存儲范圍是[0, 255],歸一化后對應(yīng)的小程序取值范圍是[0, 1]。在metal小程序執(zhí)行時,讀取MTLTexture數(shù)據(jù)一次讀取一個8位作為并歸一化處理孽文。

得到CVMetalTextureRef即可獲取MTLTexture實例

id<MTLTexture> yTexture = CVMetalTextureGetTexture(yMetalTexture);

由于當(dāng)前CVPixelBufferRef實例中存儲的是NV12格式的數(shù)據(jù),所以除了Y分量數(shù)據(jù),還需要創(chuàng)建UV分量的MTLTexture驻襟。代碼如下:

size_t uvWidth = CVPixelBufferGetWidthOfPlane(pixelBuffer, 1);
size_t uvHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, 1);
CVMetalTextureRef uvMetalTexture;
ret = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
                                                self->metalTextureCache,
                                                pixelBuffer,
                                                NULL,
                                                MTLPixelFormatRG8Unorm,
                                                uvWidth,
                                                uvHeight,
                                                1,
                                                &uvMetalTexture);
if(ret != kCVReturnSuccess) return;
id<MTLTexture> uvTexture = CVMetalTextureGetTexture(uvMetalTexture);
if(!uvTexture) return;

MTLPixelFormatRG8Unorm格式指定了由兩個8位無符號整形的數(shù)據(jù)格式。在metal小程序執(zhí)行時,讀取MTLTexture數(shù)據(jù)一次讀取兩個8位作為并歸一化處理芋哭。

5.創(chuàng)建MTLCommandBuffer

每一個指令執(zhí)行都有單獨的運行時數(shù)據(jù),這些數(shù)據(jù)需要開辟GPU顯存來存在

id<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];

6.創(chuàng)建MTLComputeCommandEncoder

內(nèi)存中的數(shù)據(jù)需要發(fā)送到顯存,需要通過MTLComputeCommandEncoder來完成

id<MTLComputeCommandEncoder> commandEncoder = [commandBuffer computeCommandEncoder];

設(shè)置指令緩沖對象執(zhí)行時的計算管線

[commandEncoder setComputePipelineState:self.computePipline];

設(shè)置小程序的紋理參數(shù)

[commandEncoder setTexture:yTexture atIndex:0];
[commandEncoder setTexture:uvTexture atIndex:1];

設(shè)置Metal執(zhí)行時的實際網(wǎng)格大小

simd_uint2 byteSize = simd_make_uint2((uint32_t)yWidth, (uint32_t)yHeight);
[commandEncoder setBytes:&byteSize length:sizeof(simd_uint2) atIndex:2];

由于Metal只能訪問MTLResource資源對象,這里設(shè)置的&byteSize相當(dāng)于最終也會轉(zhuǎn)化成MTLBuffer沉衣。也可以先創(chuàng)建MTLBuffer,配置好數(shù)據(jù)再設(shè)置到小程序?qū)?yīng)的變量位置。

設(shè)置輸出紋理對象

CAMetalLayer *layer = (CAMetalLayer *)self.layer;
id<CAMetalDrawable> drawable = [layer nextDrawable];
[commandEncoder setTexture:drawable.texture atIndex:3];

當(dāng)前繪制的視圖繼承至MetalKit提供的MTKView,它提供了可繪制RGBA的紋理資源對象减牺。

設(shè)置Metal執(zhí)行的線程組(一次性執(zhí)行多少個網(wǎng)格-線程)與線程組數(shù)量

NSUInteger threadExecutionWidth = self.computePipline.threadExecutionWidth;
NSUInteger maxTotalThreadsPerThreadgroup = self.computePipline.maxTotalThreadsPerThreadgroup;
MTLSize threadgroupSize = MTLSizeMake(threadExecutionWidth,
                                      maxTotalThreadsPerThreadgroup / threadExecutionWidth,
                                      1);
MTLSize threadgroupCount = MTLSizeMake((yWidth  + threadgroupSize.width -  1) / threadgroupSize.width,
                                       (yHeight + threadgroupSize.height - 1) / threadgroupSize.height,
                                       1);
[commandEncoder dispatchThreadgroups:threadgroupCount threadsPerThreadgroup:threadgroupSize];

MTLComputePipelineState提供了當(dāng)前GPU實例的最大的一次性執(zhí)行線程的最大寬與一次性可以執(zhí)行的最大線程數(shù)豌习。通過這兩個數(shù)據(jù)可以計算出一次性執(zhí)行線程組的大小,也可以自行設(shè)置大小,但不能超過。

這里將threadgroupCount設(shè)置成超出了實際紋理寬高的大小,防止邊界遺漏拔疚。

紋理寬度使用Y分量的寬高,UV分量的寬高為Y分量的1/4肥隆。

提交并顯示RGBA紋理

[commandEncoder endEncoding];
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> _Nonnull buffer) {
    CVBufferRelease(yMetalTexture);
    CVBufferRelease(uvMetalTexture);
}];
[commandBuffer presentDrawable:drawable];
[commandBuffer commit];




到此,利用Metal渲染YUV就完成。

總結(jié):

  • 了解Metal基本使用流程,Metal基于面向?qū)ο?相對于OpenGL更方便使用稚失。Metal還提供了計算管線,可輕松利用GPU能力實現(xiàn)大并發(fā)無依賴的數(shù)據(jù)計算栋艳。
  • 初始化Metal使用環(huán)境,了解了MTLDevice,MTLComputePipelineState等對象的作用
  • 了解metal小程序,編寫了將YUV轉(zhuǎn)換成RGB的metal小程序
  • 了解Metal計算線程的執(zhí)行流程與邏輯
  • 利用Metal與MetalKit完成了YUV的渲染

更多內(nèi)容請關(guān)注微信公眾號<<程序猿搬磚>>

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市句各,隨后出現(xiàn)的幾起案子吸占,更是在濱河造成了極大的恐慌晴叨,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件矾屯,死亡現(xiàn)場離奇詭異兼蕊,居然都是意外死亡,警方通過查閱死者的電腦和手機件蚕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門孙技,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人骤坐,你說我怎么就攤上這事绪杏∠掠” “怎么了纽绍?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長势似。 經(jīng)常有香客問我拌夏,道長,這世上最難降的妖魔是什么履因? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任障簿,我火速辦了婚禮,結(jié)果婚禮上栅迄,老公的妹妹穿的比我還像新娘站故。我一直安慰自己,他們只是感情好毅舆,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布西篓。 她就那樣靜靜地躺著,像睡著了一般憋活。 火紅的嫁衣襯著肌膚如雪岂津。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天悦即,我揣著相機與錄音吮成,去河邊找鬼。 笑死辜梳,一個胖子當(dāng)著我的面吹牛粱甫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播作瞄,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼魔种,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了粉洼?” 一聲冷哼從身側(cè)響起节预,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤叶摄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后安拟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蛤吓,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年糠赦,在試婚紗的時候發(fā)現(xiàn)自己被綠了会傲。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡拙泽,死狀恐怖淌山,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情顾瞻,我是刑警寧澤泼疑,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站荷荤,受9級特大地震影響退渗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蕴纳,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一会油、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧古毛,春花似錦翻翩、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至颖低,卻和暖如春絮吵,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背忱屑。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工蹬敲, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人莺戒。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓伴嗡,卻偏偏與公主長得像,于是被迫代替她去往敵國和親从铲。 傳聞我的和親對象是個殘疾皇子瘪校,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353