Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 24개 있습니다.)
.NET Framework: 1129. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 인코딩 예제(encode_video.c)
; https://www.sysnet.pe.kr/2/0/12898

.NET Framework: 1134. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c)
; https://www.sysnet.pe.kr/2/0/12924

.NET Framework: 1135. C# - ffmpeg(FFmpeg.AutoGen)로 하드웨어 가속기를 이용한 비디오 디코딩 예제(hw_decode.c)
; https://www.sysnet.pe.kr/2/0/12932

.NET Framework: 1136. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 MP2 오디오 파일 디코딩 예제(decode_audio.c)
; https://www.sysnet.pe.kr/2/0/12933

.NET Framework: 1137. ffmpeg의 파일 해시 예제(ffhash.c)를 C#으로 포팅
; https://www.sysnet.pe.kr/2/0/12935

.NET Framework: 1138. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 멀티미디어 파일의 메타데이터를 보여주는 예제(metadata.c)
; https://www.sysnet.pe.kr/2/0/12936

.NET Framework: 1139. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 오디오(mp2) 인코딩하는 예제(encode_audio.c)
; https://www.sysnet.pe.kr/2/0/12937

.NET Framework: 1147. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 오디오 필터링 예제(filtering_audio.c)
; https://www.sysnet.pe.kr/2/0/12951

.NET Framework: 1148. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 오디오 필터 예제(filter_audio.c)
; https://www.sysnet.pe.kr/2/0/12952

.NET Framework: 1150. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/12959

개발 환경 구성: 637. ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) - 세 번째 이야기
; https://www.sysnet.pe.kr/2/0/12960

.NET Framework: 1151. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 프레임의 크기 및 포맷 변경 예제(scaling_video.c)
; https://www.sysnet.pe.kr/2/0/12961

.NET Framework: 1153. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 avio_reading.c 예제 포팅
; https://www.sysnet.pe.kr/2/0/12964

.NET Framework: 1157. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 muxing.c 예제 포팅
; https://www.sysnet.pe.kr/2/0/12971

.NET Framework: 1159. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 qsvdec.c 예제 포팅
; https://www.sysnet.pe.kr/2/0/12975

.NET Framework: 1161. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 resampling_audio.c 예제 포팅
; https://www.sysnet.pe.kr/2/0/12978

.NET Framework: 1166. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 filtering_video.c 예제 포팅
; https://www.sysnet.pe.kr/2/0/12984

.NET Framework: 1169. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 demuxing_decoding.c 예제 포팅
; https://www.sysnet.pe.kr/2/0/12987

.NET Framework: 1170. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 transcode_aac.c 예제 포팅
; https://www.sysnet.pe.kr/2/0/12991

.NET Framework: 1178. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 http_multiclient.c 예제 포팅
; https://www.sysnet.pe.kr/2/0/13002

.NET Framework: 1180. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 remuxing.c 예제 포팅
; https://www.sysnet.pe.kr/2/0/13006

.NET Framework: 1188. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 transcoding.c 예제 포팅
; https://www.sysnet.pe.kr/2/0/13023

.NET Framework: 1190. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 vaapi_encode.c, vaapi_transcode.c 예제 포팅
; https://www.sysnet.pe.kr/2/0/13025

.NET Framework: 1191. C 언어로 작성된 FFmpeg Examples의 C# 포팅 전체 소스 코드
; https://www.sysnet.pe.kr/2/0/13026




C# - ffmpeg(FFmpeg.AutoGen)로 하드웨어 가속기를 이용한 비디오 디코딩 예제(hw_decode.c)

지난 글에 이어,

C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c)
; https://www.sysnet.pe.kr/2/0/12924

이번에는 하드웨어 가속기를 이용한 디코딩 예제를 변환해 보겠습니다.

hw_decode.c
; https://ffmpeg.org/doxygen/trunk/hw_decode_8c-example.html

변환한 C# 코드의 전문은 다음과 같습니다.

using FFmpeg.AutoGen;
using FFmpeg.AutoGen.Example;
using System;
using System.IO;
using System.Runtime.InteropServices;

namespace FFmpegApp1
{
    internal unsafe class Program
    {
        static AVPixelFormat _hw_pix_fmt = AVPixelFormat.AV_PIX_FMT_NONE;
        static AVBufferRef* _hw_device_ctx = null;

        static void Main(string[] args)
        {
            FFmpegBinariesHelper.RegisterFFmpegBinaries();

#if DEBUG
            Console.WriteLine("Current directory: " + Environment.CurrentDirectory);
            Console.WriteLine("Running in {0}-bit mode.", Environment.Is64BitProcess ? "64" : "32");
            Console.WriteLine($"FFmpeg version info: {ffmpeg.av_version_info()}");
#endif
            Console.WriteLine();

            Console.WriteLine($"LIBAVFORMAT Version: {ffmpeg.LIBAVFORMAT_VERSION_MAJOR}.{ffmpeg.LIBAVFORMAT_VERSION_MINOR}");

            string outputPath = @"c:\temp\output";
            try
            {
                Directory.Delete(outputPath, true);
                Directory.CreateDirectory(outputPath);
            }
            catch { }

            string outputFilePath = Path.Combine(outputPath, "test_cs2.dat");

            /* hwdevice type: cuda, dxva2, d3d11va, opencl */
            video_decode_example("cuda", @"D:\video_sample\theade-i-was-young.mp4", outputFilePath);
        }

        static unsafe void video_decode_example(string hwdeviceName, string filename, string outputFileName)
        {
            AVFormatContext* input_ctx = null;
            AVStream* video = null;
            AVCodecContext* decoder_ctx = null;
            AVCodec* decoder = null;
            AVPacket* packet = null;

            // https://ffmpeg.org/doxygen/trunk/codec_8h_source.html#l00420
            int AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX = 0x01;
            int ret = 0;

            AVHWDeviceType type = AVHWDeviceType.AV_HWDEVICE_TYPE_NONE;

            Console.WriteLine("Available device types:");
            while ((type = ffmpeg.av_hwdevice_iterate_types(type)) != AVHWDeviceType.AV_HWDEVICE_TYPE_NONE)
            {
                Console.WriteLine($"\t{ffmpeg.av_hwdevice_get_type_name(type)}");
            }

            type = ffmpeg.av_hwdevice_find_type_by_name(hwdeviceName);
            if (type == AVHWDeviceType.AV_HWDEVICE_TYPE_NONE)
            {
                Console.WriteLine($"Device type {hwdeviceNae} is not supported.");
                return;
            }

            do
            {
                packet = ffmpeg.av_packet_alloc();
                if (packet == null)
                {
                    Console.WriteLine("Failed to allocate AVPacket");
                    break;
                }

                if (ffmpeg.avformat_open_input(&input_ctx, filename, null, null) < 0)
                {
                    Console.WriteLine($"Cannot open input file: {filename}");
                    break;
                }

                if (ffmpeg.avformat_find_stream_info(input_ctx, null) < 0)
                {
                    Console.WriteLine("Cannot find input stream information.");
                    break;
                }

                /* find the video stream information */
                ret = ffmpeg.av_find_best_stream(input_ctx, AVMediaType.AVMEDIA_TYPE_VIDEO, -1, -1, &decoder, 0);
                if (ret < 0)
                {
                    Console.WriteLine("Cannot find a video stream in the input file\n");
                    break;
                }

                int video_stream = ret;

                for (int i = 0; ; i++)
                {
                    AVCodecHWConfig* config = ffmpeg.avcodec_get_hw_config(decoder, i);
                    if (config == null)
                    {
                        string decoderName = Marshal.PtrToStringUTF8(new IntPtr(decoder->name));
                        Console.WriteLine($"Decoder {decoderName} does not support device type {ffmpeg.av_hwdevice_get_type_name(type)}");
                        break;
                    }

                    if ((config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX) == AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX &&
                            config->device_type == type)
                    {
                        _hw_pix_fmt = config->pix_fmt;
                        break;
                    }
                }

                if (_hw_pix_fmt == AVPixelFormat.AV_PIX_FMT_NONE)
                {
                    break;
                }

                decoder_ctx = ffmpeg.avcodec_alloc_context3(decoder);
                if (decoder_ctx == null)
                {
                    Console.WriteLine("Could not allocate video codec context");
                    break;
                }

                video = input_ctx->streams[video_stream];
                if (ffmpeg.avcodec_parameters_to_context(decoder_ctx, video->codecpar) < 0)
                {
                    break;
                }

                decoder_ctx->get_format = (AVCodecContext_get_format_func)get_hw_format;

                if (hw_decoder_init(decoder_ctx, type) < 0)
                {
                    break;
                }

                if ((ret = ffmpeg.avcodec_open2(decoder_ctx, decoder, null)) < 0)
                {
                    Console.WriteLine($"Failed to open codec for stream {video_stream}");
                    break;
                }

                using FileStream output_file = File.OpenWrite(outputFileName);

                while (ret >= 0)
                {
                    if ((ret = ffmpeg.av_read_frame(input_ctx, packet)) < 0)
                    {
                        break;
                    }

                    if (video_stream == packet->stream_index)
                    {
                        ret = decode_write(decoder_ctx, packet, output_file);
                    }
                }

                ffmpeg.av_packet_unref(packet);

                /* flush the decoder */
                ret = decode_write(decoder_ctx, null, output_file);

            } while (false);

            if (packet != null)
            {
                ffmpeg.av_packet_free(&packet);
            }

            if (decoder_ctx != null)
            {
                ffmpeg.avcodec_free_context(&decoder_ctx);
            }

            if (input_ctx != null)
            {
                ffmpeg.avformat_close_input(&input_ctx);
            }

            if (_hw_device_ctx != null)
            {
                fixed (AVBufferRef** ppRef = &_hw_device_ctx)
                {
                    ffmpeg.av_buffer_unref(ppRef);
                }
            }
        }

        static unsafe int decode_write(AVCodecContext* avctx, AVPacket* packet, FileStream output_file)
        {
            AVFrame* frame = null;
            AVFrame* sw_frame = null;
            AVFrame* tmp_frame = null;
            byte* buffer = null;
            int size = 0;
            int ret = 0;

            ret = ffmpeg.avcodec_send_packet(avctx, packet);
            if (ret < 0)
            {
                Console.WriteLine("Error during decoding");
                return ret;
            }

            while (true)
            {
                if ((frame = ffmpeg.av_frame_alloc()) == null || (sw_frame = ffmpeg.av_frame_alloc()) == null)
                {
                    Console.WriteLine("Can not alloc frame");
                    ret = ffmpeg.AVERROR(ffmpeg.ENOMEM);
                    goto fail;
                }

                ret = ffmpeg.avcodec_receive_frame(avctx, frame);
                if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN) || ret == ffmpeg.AVERROR_EOF)
                {
                    ffmpeg.av_frame_free(&frame);
                    ffmpeg.av_frame_free(&sw_frame);
                    return 0;
                }
                else if (ret < 0)
                {
                    Console.WriteLine("Error while decoding");
                    goto fail;
                }

                if (frame->format == (int)_hw_pix_fmt)
                {
                    /* retrieve data from GPU to CPU */
                    if ((ret = ffmpeg.av_hwframe_transfer_data(sw_frame, frame, 0)) < 0)
                    {
                        Console.WriteLine("Error transferring the data to system memory");
                        goto fail;
                    }

                    tmp_frame = sw_frame;
                }
                else
                {
                    tmp_frame = frame;
                }

                size = ffmpeg.av_image_get_buffer_size((AVPixelFormat)tmp_frame->format, tmp_frame->width,
                                                tmp_frame->height, 1);
                buffer = (byte *)ffmpeg.av_malloc((ulong)size);
                if (buffer == null)
                {
                    Console.WriteLine("Can not alloc buffer");
                    ret = ffmpeg.AVERROR(ffmpeg.ENOMEM);
                    goto fail;
                }

                FFmpeg.AutoGen.byte_ptrArray4* ptrFrameData = (FFmpeg.AutoGen.byte_ptrArray4*)&tmp_frame->data;
                FFmpeg.AutoGen.int_array4* ptrLineSize = (FFmpeg.AutoGen.int_array4*)&tmp_frame->linesize;

                ret = ffmpeg.av_image_copy_to_buffer(buffer, size,
                                              *ptrFrameData, *ptrLineSize,
                                      (AVPixelFormat)tmp_frame->format,
                                      tmp_frame->width, tmp_frame->height, 1);
                if (ret < 0)
                {
                    Console.WriteLine("Can not copy image to buffer");
                    goto fail;
                }

                ReadOnlySpan<byte> contents = new Span<byte>(buffer, size);
                output_file.Write(contents);

            fail:
                ffmpeg.av_frame_free(&frame);
                ffmpeg.av_frame_free(&sw_frame);
                ffmpeg.av_freep(&buffer);
                if (ret < 0)
                {
                    return ret;
                }
            }
        }

        static unsafe int hw_decoder_init(AVCodecContext* ctx, AVHWDeviceType type)
        {
            int err = 0;

            fixed (AVBufferRef** ptr = &_hw_device_ctx)
            {
                if ((err = ffmpeg.av_hwdevice_ctx_create(ptr, type, null, null, 0)) < 0)
                {
                    Console.WriteLine("Failed to create specified HW device.");
                    return err;
                }

                ctx->hw_device_ctx = ffmpeg.av_buffer_ref(*ptr);
            }

            return err;
        }

        static unsafe AVPixelFormat get_hw_format(AVCodecContext* ctx, AVPixelFormat* pix_fmts)
        {
            AVPixelFormat* p;

            for (p = pix_fmts; *p != AVPixelFormat.AV_PIX_FMT_NONE; p++)
            {
                if (*p == _hw_pix_fmt)
                {
                    return *p;
                }
            }

            Console.WriteLine("Failed to get HW surface format.");
            return AVPixelFormat.AV_PIX_FMT_NONE;
        }
    }
}

C#의 unsafe 구문이 없었으면 꿈도 못 꿀 변환입니다. ^^;




그나저나, 지난번 decode_video.c 사례에서는 정상적인 이미지 파일로 변환을 못했는데요, 이번에는 어떨까요? ^^

decode_write 함수에 av_image_copy_to_buffer의 buffer에 담긴 데이터가,

ret = ffmpeg.av_image_copy_to_buffer(buffer, size,
                                *ptrFrameData, *ptrLineSize,
                        (AVPixelFormat)tmp_frame->format,
                        tmp_frame->width, tmp_frame->height, 1);

이거저거 시도해 보니 해석이 됩니다. 가령, 다음과 같이 프레임 하나를 건져 format을 보니 AV_PIX_FMT_NV12로 나옵니다.

if (avctx->frame_number == 310)
{
    if (tmp_frame->format == (int)AVPixelFormat.AV_PIX_FMT_NV12)
    {
        // ...[생략]...
    }
}

AV_PIX_FMT_NV12에 대한 도움말을 보면,

//
// Summary:
//     planar YUV 4:2:0, 12bpp, 1 plane for Y and 1 plane for the UV components, which
//     are interleaved (first byte U and the following byte V)
AV_PIX_FMT_NV12 = 23,

12bit per plane인데, 다행히 이에 대한 해석을 stackoverflow에서 얻을 수 있습니다.

How to Convert NV12 to BGR(AVFrame to cv::Mat)
; https://stackoverflow.com/questions/53994735/how-to-convert-nv12-to-bgravframe-to-cvmat

따라서, 다음과 같이 코딩을 완성할 수 있습니다.

if (avctx->frame_number == 310)
{
    if (tmp_frame->format == (int)AVPixelFormat.AV_PIX_FMT_NV12)
    {
        OpenCvSharp.Mat rgbMat = new OpenCvSharp.Mat();
        OpenCvSharp.Mat nvMat = new OpenCvSharp.Mat(tmp_frame->height * 3 / 2, tmp_frame->width,
            OpenCvSharp.MatType.CV_8UC1, new IntPtr(buffer));

        OpenCvSharp.Cv2.CvtColor(nvMat, rgbMat, OpenCvSharp.ColorConversionCodes.YUV2BGR_NV12);
        OpenCvSharp.Cv2.ImShow("test", rgbMat);
        return ret;
    }
}

8UC1이라 8bit * 3 / 2 = 12bit가 왠지 절묘하게 맞는 것도 같습니다. ^^ 어쨌든, 저렇게 해서 변환된 rgbMat 변수를 OpenCvSharp.Cv2.ImShow로 보면 이미지가 정상적으로 잘 나옵니다. 실제로 아래의 화면에서 곰 플레이어로 본 영상(위)과 OpenCvSharp.Cv2.ImShow로 영상(아래)을 함께 캡처한 것입니다.

(사진은 유튜브 영상 "디에이드"의 "안다은" 님이고 사용을 허락받고 올립니다.)
hw_video_decode_1.png

(첨부 파일은 hw_decode.c 파일의 C#과 Visual C++로 포팅한 프로젝트 파일을 포함합니다.)
(이 글의 소스 코드는 github에 올려져 있습니다.)




그나저나, 정말 영상 압축의 힘이 대단한 것 같습니다. ^^ 13MB 짜리 h264 인코딩된 mp4 파일인데, 이것을 위의 hw_decode.c / decode_write 함수 내에서 해석한 raw 이미지들을 파일로 쓰면 그 용량이 거의 1.8GB에 달합니다.

그리고, AVPixelFormat에 정의된 enum 상수들은 보는 것만으로도 질리게 만듭니다. ^^; 게다가 비슷한 정의들도 있는데, 이번 글에서 언급한 AV_PIX_FMT_NV12 포맷과 AV_PIX_FMT_YUV420P의 포맷은 어떤 차이일까요? ^^ (혹시 아시는 분은 덧글 부탁드립니다.)

AV_PIX_FMT_YUV420P 	
planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples)

AV_PIX_FMT_NV12 
planar YUV 4:2:0, 12bpp, 1 plane for Y and 1 plane for the UV components, which are interleaved (first byte U and the following byte V)

아마도 AVPixelFormat의 상수를 모두 설명하는 것만으로 책 한 권 분량이 나오지 않을까 싶습니다.




이 예제에서 출력한 test.dat 파일은 다음의 명령어로 재생해 볼 수 있습니다.

ffplay -autoexit -f rawvideo -pixel_format nv12 -video_size 1920x1080 test.dat

참고로, 테스트를 위해 (repo의 Samples 디렉터리에 포함시킨 5.00초 분량의) file_example_MP4_1920_18MG.mp4 파일을 출력한 test.dat 파일을 ffplay로 재생해 보면 5.97초로 재생이 됩니다. 왜냐하면 ffplay는 기본적으로 25fps로 재생하기 때문인데요, 따라서 옵션에 30fps를 지정하면 됩니다.

ffplay -autoexit -f rawvideo -framerate 30 -pixel_format nv12 -video_size 1920x1080 test.dat

그럼 ffplay에 찍히는 재생 시간이 대략 4.96 ~ 4.98초 정도로 나옵니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 3/14/2022]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2022-01-21 10시14분
[이승준] https://learn.microsoft.com/ko-kr/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering
요기서 yv12랑 nv12를 보시면 됩니다. yv12가 yuv420p 입니다.
[guest]
2022-01-21 09시29분
@이승준 링크 정보 감사합니다. Microsoft 자료에 그렇게 있으리라고는 상상도 못했습니다. ^^ 암튼, 전문 분야는 아니지만 알아가는 재미가 있어서 좋은 것 같습니다.
정성태

1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13500정성태12/24/20232175디버깅 기술: 194. Windbg - x64 가상 주소를 물리 주소로 변환
13498정성태12/23/20232812닷넷: 2186. 한국투자증권 KIS Developers OpenAPI의 C# 래퍼 버전 - eFriendOpenAPI NuGet 패키지
13497정성태12/22/20232293오류 유형: 885. Visual Studiio - error : Could not connect to the remote system. Please verify your connection settings, and that your machine is on the network and reachable.
13496정성태12/21/20232306Linux: 66. 리눅스 - 실행 중인 프로세스 내부의 환경변수 설정을 구하는 방법 (gdb)
13495정성태12/20/20232316Linux: 65. clang++로 공유 라이브러리의 -static 옵션 빌드가 가능할까요?
13494정성태12/20/20232499Linux: 64. Linux 응용 프로그램의 (C++) so 의존성 줄이기(ReleaseMinDependency) - 두 번째 이야기
13493정성태12/19/20232541닷넷: 2185. C# - object를 QueryString으로 직렬화하는 방법
13492정성태12/19/20232261개발 환경 구성: 699. WSL에 nopCommerce 예제 구성
13491정성태12/19/20232231Linux: 63. 리눅스 - 다중 그룹 또는 사용자를 리소스에 권한 부여
13490정성태12/19/20232344개발 환경 구성: 698. Golang - GLIBC 의존을 없애는 정적 빌드 방법
13489정성태12/19/20232132개발 환경 구성: 697. GoLand에서 ldflags 지정 방법
13488정성태12/18/20232066오류 유형: 884. HTTP 500.0 - 명령행에서 실행한 ASP.NET Core 응용 프로그램을 실행하는 방법
13487정성태12/16/20232381개발 환경 구성: 696. C# - 리눅스용 AOT 빌드를 docker에서 수행 [1]
13486정성태12/15/20232193개발 환경 구성: 695. Nuget config 파일에 값 설정/삭제 방법
13485정성태12/15/20232088오류 유형: 883. dotnet build/restore - error : Root element is missing
13484정성태12/14/20232161개발 환경 구성: 694. Windows 디렉터리 경로를 WSL의 /mnt 포맷으로 구하는 방법
13483정성태12/14/20232295닷넷: 2184. C# - 하나의 resource 파일을 여러 프로그램에서 (AOT 시에도) 사용하는 방법파일 다운로드1
13482정성태12/13/20232812닷넷: 2183. C# - eFriend Expert OCX 예제를 .NET Core/5+ Console App에서 사용하는 방법 [2]파일 다운로드1
13481정성태12/13/20232265개발 환경 구성: 693. msbuild - .NET Core/5+ 프로젝트에서 resgen을 이용한 리소스 파일 생성 방법파일 다운로드1
13480정성태12/12/20232597개발 환경 구성: 692. Windows WSL 2 + Chrome 웹 브라우저 설치
13479정성태12/11/20232282개발 환경 구성: 691. WSL 2 (Ubuntu) + nginx 환경 설정
13477정성태12/8/20232465닷넷: 2182. C# - .NET 7부터 추가된 Int128, UInt128 [1]파일 다운로드1
13476정성태12/8/20232202닷넷: 2181. C# - .NET 8 JsonStringEnumConverter의 AOT를 위한 개선파일 다운로드1
13475정성태12/7/20232261닷넷: 2180. .NET 8 - 함수 포인터에 대한 Reflection 정보 조회파일 다운로드1
13474정성태12/6/20232133개발 환경 구성: 690. 닷넷 코어/5+ 버전의 ilasm/ildasm 실행 파일 구하는 방법 - 두 번째 이야기
13473정성태12/5/20232309닷넷: 2179. C# - 값 형식(Blittable)을 메모리 복사를 이용해 바이트 배열로 직렬화/역직렬화파일 다운로드1
1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...