정보

오디오 프로그래밍에 대해서 – 4

오디오 플레이어, 오디오 디코더 구현해보면서 공부한 것 정리.

1. 오디오 데이터의 형태

오디오 데이터는 PCM 또는 IEEE 754 부동소수점 등의 방식으로 저장할 수 있다. 이 데이터를 샘플이라고 부르며, 아날로그 오디오를 디지털화 하는 것을 샘플링이라고 한다.

샘플의 데이터 크기를 결정짓는 것은 채널 갯수(Channels), 샘플당 비트 수(Bits per Sample), 샘플링 주기(Sample Rate 또는 Samples per Second)의 세 가지인데, 한 채널에는 1초 길이당 샘플링 주기 크기만큼의 데이터가 만들어진다. 이 샘플 데이터의 총 바이트 길이는 샘플링 주기 * ( 샘플당 비트 수 / 2 )이고, 1초 길이의 총 샘플의 바이트 크기는 결국 채널 갯수 * (샘플당 비트 수 / 8 ) * 샘플링 주기가 된다.

각 샘플은 채널에 따라 따로 존재하는 것이 아니라 번갈아 가면서 있는데, 이 때문에 한 샘플 블럭 정렬(Block Alignment)의 크기는 ( 샘플당 비트 수 / 8 ) * 채널 갯수가 된다(단, MP3 파일은 채널에 따라 데이터를 따로 보관하고 있어 디코딩할 때 합쳐야 한다).

PCM 샘플 데이터를 IEEE 754 부동소수점 샘플 데이터로 변환하는 방법은 간단한데, 각각의 샘플을 2^샘플당 비트 수-1한 크기로 나눈 부동소수점 숫자가 샘플이 된다. 반대로 IEEE 754 부동소수점 샘플 데이터를 PCM 샘플 데이터로 변환하려면 각각의 샘플을 2^샘플당 비트 수-1한 크기로 곱하면 된다.

샘플당 비트 수는 8, 16, 24, 32가 허용된다. 일반적으로 MP3, Ogg Vorbis, AAC 등의 음원은 16비트를 사용하지만 WAV, FLAC 등의 음원은 다른 비트 수도 사용할 수 있다.

어떤 음원이 2채널, 16비트, 44100Hz 샘플링 주기를 가지고, 총 3분 20초 길이라면 이 음원의 블록 크기는 2 * (16 / 8 ) = 4바이트, 초당 바이트 길이(Bytes per Second 또는 Byte Rate)는 2 * (16 / 8 ) * 44100 = 176400바이트,  PCM 총 길이는 176400 * 200 = 35280000바이트 = 약 33.64MB가 된다.

1-1. WAVEFORMATEX

위에서 말한 채널 갯수, 샘플당 비트 수, 샘플링 주기, 블럭 정렬, 초당 바이트 길이, 그리고 각 샘플의 형태를 담는 구조체가 Windows API에서 기본 제공되는데, 이 구조체의 이름은 WAVEFORMATEX이다.

typedef struct {
WORD  wFormatTag;
WORD  nChannels;
DWORD nSamplesPerSec;
DWORD nAvgBytesPerSec;
WORD  nBlockAlign;
WORD  wBitsPerSample;
WORD  cbSize;
} WAVEFORMATEX;

wFormatTag에는 이 오디오 데이터가 PCM 데이터냐 IEEE 부동소수점이냐를 담고 있는데, 이 구조체가 확장 구조체인가를 담기도 하며, 이 경우 PCM 또는 IEE 부동소수점 데이터인지에 대한 정보는 해당당 확장 구조체를 뜯어야 알 수 있다.

그 외에 nChannels = 채널 갯수, nSamplesPerSec = 샘플링 주기, nAvgBytesPerSec = 초당 바이트 길이, nBlockAlign = 블럭 정렬, wBitsPerSample = 샘플당 비트 수, cbSize = 구조체 총 크기를 뜻한다.

이 구조체가 확장 구조체인 경우 WAVEFORMATEXTENSIBLE로 변환하면 뜯어볼 수 있다. 단, WAVEFORMATEX 구조체 변수가 포인터 변수였을 때에 한정하여 변환할 수 있다.

typedef struct WAVEFORMATEXTENSIBLE {
WAVEFORMATEX Format;
union {
WORD wValidBitsPerSample;
WORD wSamplesPerBlock;
WORD wReserved;
} Samples;
DWORD        dwChannelMask;
GUID         SubFormat;
}  *PWAVEFORMATEXTENSIBLE;

이 구조체 정보에는 위의 WAVEFORMATEX 구조체와 함께 채널 마스크와 서브 포맷을 담고 있는데, 채널 마스크는 멀티채널 오디오일 때 각 채널의 출력 스피커 번호를 명시할 수 있는 플래그를 담는다. 서브 포맷은 이 오디오가 PCM이냐 IEEE 부동소수점이냐를 담고 있다.

 

2. 오디오 코덱과 컨테이너

샘플링 주기가 크면 클 수록, 그리고 샘플당 비트 수가 크면 클 수록 정밀도가 높은 오디오 음원을 보관할 수 있기 때문에 일반적으로 16비트에 44100Hz 주기 이상을 사용하고 있는데, 이 경우 3~5분 내외의 음원을 보관할 때 매우 큰 파일이 생성된다.

이러한 큰 파일을 좀 더 작게 보관하기 위해 인코더를 이용해 압축하는데, 이 때 데이터 처리 방식에 따라 비손실 압축 코덱과 손실 압축 방식으로 나뉘어진다.

비손실 압축은 원본 데이터를 모두 살려서 보관하는 대신 압축률이 작다. 경우에 따라 음원을 압축하지 않은 WAV 파일과 비교했을 때 음원에 따라 무의미한 크기로 압축되기도 한다.

손실 압축은 시간 도메인의 데이터(PCM 또는 IEEE 754 부동소수점 샘플 데이터)를 푸리에 변환(DFT)이나 이산 코사인 변환(DCT) 등을 통해 주파수 도메인 데이터로 변환 후 필요 없는 주파수 대역의 데이터를 양자화를 이용해 모두 삭제하여 남은 데이터를 비손실 압축 방식으로 압축한다. 보통 이 주파수 대역을 결정하는 데에는 비트레이트를 이용하는데, 비트레이트가 작으면 작을 수록 손실되는 주파수 대역도 많아진다.

압축이 됐든 되지 않았든 이러한 오디오 데이터를 그냥 나열한다고 파일이 만들어지는 것은 아니고, 컨테이너 포맷을 이용해 재구성을 해야 하는데, 일반적으로는 AVI 또는 WAV에 사용되는 RIFF 컨테이너, MP4 또는 3GP 등에 사용되는 ISOBMFF, MKA 또는 WEBM 등에 사용되는 Matroska, Vorbis 또는 Opus 등에 사용되는 Ogg 등이 주로 사용된다.

MP3는 컨테이너가 따로 사용되지 않는다. 각 블록을 그냥 시간 순서대로 나열해놓은 원시적인 형태이지만 이 덕분에 ID3 태그 등의 비표준 블록을 추가하더라도 큰 문제가 없다. 또한 각 블럭의 초기 4바이트가 해당 블럭의 오디오 정보를 모두 담고 있기 때문에 따로 컨테이너가 필요 없기도 하다. 물론 MP3 데이터를 MP4 ISOBMFF 등의 다른 컨테이너에 보관하는 것도 가능하다.

 

3. 오디오 API

오디오 데이터를 스피커나 헤드폰 등으로 출력하려면 오디오 API를 사용해야 한다. 보통 이런 API들은 HAL(Hardware Abstraction Layer) 구조를 이용해 HAL의 하위 레이어인 드라이버만 잘 구현하면 이용할 수 있도록 되어 있다.

오디오 API의 종류로는 Windows Audio Session API(WASAPI), DirectSound 8, XAudio 2, OpenAL 등이 존재한다. 모두 저수준 API이며, 때문에 디코딩을 알아서 해주진 않기 때문에 압축된 데이터는 디코더를 이용해 PCM 또는 IEEE 754 부동소수점 데이터로 변환해주어야 한다.

물론 압축을 해제하면 MB 단위의 큰 데이터가 나오기 때문에 메모리를 아끼기 위해 조금씩 쪼개서 출력하기도 하는데 이런 방식을 스트리밍이라고 한다. 메모리 관리 문제도 있지만 사실 디코딩 과정이 오래 걸리는 문제도 있어서 초기 재생 딜레이를 줄이기 위한 방법이기도 하다.

이런 복잡한 구조를 쉽게 사용할 수 있도록 만들어진 고수준 API도 존재하는데, 일반적으로는 FMOD, BASS, NAudio, CSCore 등이 사용된다. 이런 API들은 파일명 입력해주고 재생 함수만 딱 호출해주면 알아서 재생해준다. 다만 API에 따라 다른 파일 포맷은 지원을 못하는 경우도 있음.

정보

크로스플랫폼 게임 개발을 위한 기초 지식

1. 각 플랫폼 별 기본 C/C++ 플랫폼 종속 API

  • Windows Desktop: C 계열 Windows API 클래스/함수들, C++ COM 객체들, .NET 객체들
  • Windows Store Apps/UWP: C++/CX 클래스 객체들, C 계열 WRL 함수들, C++ COM 객체들
  • macOS/iOS: Objective-C 또는 Swift 클래스/함수들 및 C 계열 UNIX 함수들 등
  • UNIX/Linux: C 계열 UNIX, GLX 함수들 등
  • Android: C 계열 UNIX, Android 함수들 등
  • WebGL: EMScripten 함수들 등

2. 저수준 API

2.1. 3D 그래픽 렌더링 API

  • Windows Desktop: DirectX 2~12, OpenGL, OpenGL ES(Compatibility Layer), Vulkan
  • Windows Store Apps: DirectX 11
  • UWP: DirectX 11/12
  • macOS: OpenGL, Metal
  • iOS: OpenGL ES 1.0~3.0, Metal
  • UNIX/Linux: OpenGL, OpenGL ES(Compatibility Layer), Vulkan
  • Android: OpenGL ES 1.0~3.0, Vulkan
  • HTML5: WebGL

2.2. 오디오 입출력 API

  • Windows Desktop: DirectSound, XAudio 2, OpenAL
  • Windows Store Apps/UWP: XAudio 2
  • macOS/iOS/UNIX/Linux/Android: OpenAL
  • HTML5: Low-Level: Not support, Only use <audio> or <video> tag.

2.3. GPGPU API

  • Windows Desktop: DirectCompute, C++ AMP, CUDA, OpenCL, Vulkan
  • Windows Store Apps/UWP: DirectCompute, C++ AMP
  • macOS: CUDA, OpenCL, Metal
  • iOS: Metal
  • UNIX/Linux: CUDA, OpenCL, Vulkan
  • Android: OpenCL, Vulkan
  • HTML5: WebCL(Firefox only)

2.4. 입력장치 API

  • Windows Desktop: Windows API, DirectInput, XInput
  • Windows Store Apps/UWP: C++/CX API, XInput
  • macOS/iOS/UNIX/Linux/Android/HTML5: Platform Dependent APIs

3. 범용 게임 통계 및 도전과제 서비스

  • Windows Desktop: GFWL(Deprecated), Steam(3rd party), …
  • Windows Store Apps/UWP: Xbox Live, …
  • macOS/iOS: GameKit, …
  • UNIX/Linux: Steam(3rd party), …
  • Android: Google Play(Google), …
  • HTML5: Facebook(3rd party), …

4. 플랫폼 종속 결제 서비스

  • Windows Desktop: Steam(3rd party), …
  • Windows Store Apps/UWP: Store, IAP API
  • macOS/iOS: StoreKit
  • UNIX/Linux: Steam(3rd party), …
  • Android: Google Play(Google), One Store(3rd party), …

5. 광고 서비스

  • Windows Store Apps/UWP: Windows Advertising SDK, …
  • macOS/iOS: iAd, …
  • Windows Desktop/UNIX/Linux/HTML5: 3rd party Advertisement services
  • Android: AdMob(Google), …

※참고: 게임에 자주 쓰이는 크로스플랫폼 서드파티 광고 서비스로 Unity Ads가 있다.

6. 각 컴퓨터 언어별 개발 가능 플랫폼

  • C/C++: Windows Desktop/Linux/macOS(Deprecated 32-bit Carbon API only)/Android/HTML5(EMScripten or WebAssembly)
  • C++/CX: Windows Desktop/Windows Store Apps/UWP
  • Objective-C, Objective-C++: macOS/iOS/tvOS/watchOS/Windows Desktop(Command Prompt Program only)/Linux(Command Prompt Program only)/UWP(Windows Bridge for iOS)
  • Swift: OS X/iOS/Linux(Command Prompt Program only)
  • C#: Windows Desktop/Windows Store Apps/UWP/Linux/macOS(.NET Framework or Xamarin)/iOS(Xamarin)/tvOS(Xamarin)/watchOS(Xamarin)/Android(Xamarin)/HTML5(SharpJS or C#/XAML for HTML5 or JSIL)
  • Java: Windows Desktop/Linux/macOS/Android
정보

C++ 컴파일 타임에 컴파일러 확인

1. Visual C++ 컴파일러

Visual C++ 컴파일러는 _MSC_VER 전처리기가 등록되었는지 확인하면 되며, 컴파일러 버전은 _MSC_VER 전처리기의 값을 체크하면 된다. 이 문서 참고.

2. GNU Compiler Collection 컴파일러

GCC 컴파일러는 __GNUC__ 전처리기가 등록되었는지 확인하면 되며, 컴파일러 버전은 __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__ 전처리기의 각 값을 체크하면 된다. 이 문서 참고.

3. LLVM Clang 컴파일러

Clang 컴파일러는 __clang__ 전처리기가 등록되었는지 확인하면 되며, 컴파일러 버전은 __clang_major__, __clang_minor__, __clang_patchlevel__ 전처리기의 각 값을 체크하면 되며 한꺼번에 체크하고 싶다면 __clang_version__을 체크하면 된다. 이 문서 참고.

정보

C++ 컴파일 타임에 CPU 아키텍처 확인

1. IA32

IA32 아키텍처(Intel 32-bit Architecture)는 C++ 컴파일러에서 _M_IX86, __i386__ 등의 전처리기 중 하나 이상을 자동으로 등록하며, 컴파일러에 따라 많이 다르지만 보통 Visual C++ 컴파일러와 GCC/Clang 컴파일러는 각각 _M_IX86, __i386__을 많이 사용하는 듯 하다. 이 외의 컴파일러에 대해서는 5번 항목 참조.

2. AMD64

AMD64(x86-64, x64, …)는 C++ 컴파일러에서 _M_AMD64, __amd64__ 등의 전처리기 중 하나 이상을 자동으로 등록하며, 컴파일러에 따라 다르지만 보통 Visual C++ 컴파일러와 GCC/Clang 컴파일러는 각각 _M_AMD64, __amd64__을 많이 사용하는 듯 하다. 이 외의 컴파일러에 대해서는 5번 항목 참조.

3. ARM

ARM은 C++ 컴파일러에서 _M_ARM, __arm__ 등의 전처리기 중 하나 이상을 자동으로 등록한다. 이 외의 컴파일러에 대해서는 5번 항목 참조.

4. ARM64

ARM64는 C++ 컴파일러에서 __aarch64__ 전처리기를 자동으로 등록하며, Visual C++ 컴파일러에서는 아직 지원하지 않기 때문에 전처리기를 등록하지 않는다. 이 외의 컴파일러에 대해서는 5번 항목 참조.

5. 그 외

그 외의 아키텍처에 대해서는 이 문서를 참조하면 된다. Alpha, HP/PA RISC, IA64, Motorola 68k, MIPS, PowerPC 등 다양한 아키텍처에 대한 전처리기를 안내하고 있다.

정보

C++ 컴파일 타임에 플랫폼 확인

1. Windows 계열

Windows 계열은 C++ 컴파일러에서 _WINDOWS나 _WIN32, _WIN64, WIN32, WIN64 중 하나의 전처리기를 자동으로 등록한다. 이 중 하나라도 적용된 것이 있다면 Windows 계열용으로 컴파일한다는 말.

세부적인 플랫폼 체크는 Windows.h 헤더파일을 포함한 후 가능하다.

1.1. Windows Desktop

Windows Desktop는 WINAPI_FAMILY_PARTITION 매크로를 이용해 WINAPI_PARTITION_DESKTOP이 검출됐거나 WINAPI_FAMILY_DESKTOP_APP의 정의가 되지 않았다면 해당된다.

1.2. Windows RT(Windows 8(.1) Store App, Universal Windows App)

Windows App은 Windows Desktop으로 검출이 되지는 않았으나 WINAPI_FAMILY_PARTITION 매크로를 이용해 WINAPI_FAMILY_PC_APP이 검출됐다면 해당된다.

1.3. Windows Phone(Windows Phone 8(.1) App, Universal Windows App Mobile)

Windows Phone은 Windows Desktop으로 검출이 되지는 않았으나 WINAPI_FAMILY_PARTITION 매크로를 이용해 WINAPI_FAMILY_PHONE_APP이 검출됐다면 해당된다.

2. EMScripten

EMScripten은 C++ 컴파일러에서 EMSCRIPTEN 전처리기를 자동으로 등록한다.

3. Apple OS

Apple의 모든 OS는 C/Objective-C/Objective-C++ 컴파일러에서 __APPLE__ 전처리기를 자동으로 등록한다.

세부적인 플랫폼 체크는 TargetConditionals.h 헤더파일을 포함한 후 가능하다.

3.1. macOS(Mac OS X, OS X)

macOS는 TARGET_OS_MAC이 1이고 TARGET_OS_IOS나 TARGET_OS_SIMULATOR 등 다른 요소가 0이면 해당된다.

3.2. iOS(iPhoneOS)

iOS는 TARGET_OS_IOS나 TARGET_OS_SIMULATOR가 1이면 해당된다.

3.3. watchOS

watchOS는 TARGET_OS_WATCH가 1이면 해당된다.

3.4. tvOS

tvOS는 TARGET_OS_TV가 1이면 해당된다.

4. Android

Android는 C/C++ 컴파일러에서 __ANDROID__ 전처리기를 자동으로 등록한다.

5. UNIX(Linux)

UNIX 계열 OS는 C/C++ 컴파일러에서 __unix__, __linux__ 등의 전처리기를 하나 이상 자동으로 등록하며, 이 중 Android는 __ANDROID__ 전처리기가 같이 포함되기 때문에 __ANDROID__가 등록되지 않은 경우 PC용 UNIX 계열 OS라고 볼 수 있다.

6. 최종적으로 내가 원하는 코드

#if ( defined ( _WINDOWS ) || defined ( _WIN32 ) || defined ( _WIN64 ) || defined ( WIN32 ) || defined ( WIN64 ) )
# include <Windows.h>
# if WINAPI_FAMILY_PARTITION ( WINAPI_PARTITION_DESKTOP ) || !defined ( WINAPI_FAMILY_DESKTOP_APP )
# define NBPlatformWindowsNT 1
# define NBPlatformWindowsRT 0
# elif WINAPI_FAMILY_PARTITION ( WINAPI_FAMILY_PC_APP )
# define NBPlatformWindowsNT 0
# define NBPlatformWindowsRT 1
# include <wrl.h>
# include <wrl/client.h>
# endif
#else
# define NBPlatformWindowsNT 0
# define NBPlatformWindowsRT 0
#endif
#if defined ( EMSCRIPTEN )
# include <emscripten/emscripten.h>
# include <emscripten/html5.h>
# define NBPlatformWeb 1
#else
# define NBPlatformWeb 0
#endif
#if defined ( __APPLE__ )
# include <TargetConditionals.h>
# define NBPlatformOSX TARGET_OS_MAC && !( TARGET_OS_IOS || TARGET_OS_SIMULATOR )
# define NBPlatformiOS TARGET_OS_IOS || TARGET_OS_SIMULATOR
# import <Foundation/Foundation.h>
# if ( NBPlatformOSX )
# import <Cocoa/Cocoa.h>
# else
# import <UIKit/UIKit.h>
# endif
#else
# define NBPlatformOSX 0
# define NBPlatformiOS 0
#endif
#if defined ( __ANDROID__ )
# include <jni.h>
# include <android/api-level.h>
# include <android/native_activity.h>
# include <android/native_window.h>
# include <android/input.h>
# include <android/window.h>
# include <android/configuration.h>
# include <android/asset_manager.h>
# include <android/obb.h>
# include <android/looper.h>
# include <android/keycodes.h>
# include <android/sensor.h>
# include <android/storage_manager.h>
# include <android/log.h>
# define NBPlatformAndroid 1
#else
# define NBPlatformAndroid 0
#endif
#if ( defined ( __unix__ ) || defined ( __linux__ ) ) && !defined ( __ANDROID__ )
# define NBPlatformUNIX 1
#else
# define NBPlatformUNIX 0
#endif
정보

System.Numerics.Vectors에 대하여

.NET Framework 4.6에 대한 큰 변화 중 하나는 SIMD 명령어를 지원한다는 점이다. 비록 제한적이지만 System.Numerics.Vectors 라이브러리를 사용하여 벡터나 행렬을 사용하면 SIMD 명령어로 동작한다는 얘기였다.

이를 철썩같이 믿고 있었는데 System.Numerics.Vectors의 실제 구현물을 봤다. JitIntrinsicAttribute를 메서드 등에 적용하면 .NET Framework 4.6 런타임에서는 SIMD로, 그게 아니면 SISD로 동작하는 구조. JinIntrinsicAttribute에는 내용이 없다. 여기까지는 문제가 없다. 하위 호환도 맞추고 있고 이를 그대로 다른 플랫폼에 포팅하기도 쉽다.

문제는 행렬에는 JitIntrinsic이 적용된 게 없다. 행렬 계산에 있어서, 특히나 행렬의 곱셈에 있어서 SIMD를 적용하는 것은 계산 속도의 향상을 할 수 있는 마지막 보루인 셈인데 이쪽에 적용된게 없다니…

정보

킹덤 도전과제 공략

Steam에서 판매되고 있는 게임인 Kingdom. 전략적으로 성벽을 짓고 궁수와 건축가들을 모으고 도시를 지켜나가며 몬스터가 소환되는 네 개의 포탈을 제거하는 게임이다.

최근에 도전과제 깨려고 삽질을 한 결과, 모든 도전과제를 깼다. 그에 대한 해결책을 간단하게 정리해본다.

 

On the First Day I Built an Army
첫 날 안에 8명의 궁수를 모으면 클리어. 보통 원주민 캠프가 오른쪽에 많이 있으므로 첫 날에 오른쪽 포탈 나오기 전까지 있는 모든 원주민 캠프의 인원을 끌어들여 궁수만 만들면 된다. 건축가는 만들지 말고, 오른쪽으로 가서 사람을 모으면 캠프 앞 두 명까지 모두 8명이 되니 바로 깰 수 있다. 다만 원주민 캠프에서 네 명까지밖에 못 모으는 경우가 있는데, 이 경우 빠르게 왼쪽으로 가서 그쪽 원주민 캠프의 사람들을 모아 밤이 지나기 전에 궁수를 8명으로 만들어야 한다.
돈이 부족할 수 있으니 시작하자마자 왼쪽으로 가서 동전 상자를 구하는 것도 좋은 방법이다. 물론 동전 상자가 안 나오면 첫 날에 8명의 궁수를 만들 시간이 없으므로 재시작을 해야 된다.
On the Second Day I Got a Gift
둘째 날에 캠프를 세 단계까지 업그레이드하고 건축가가 이에 따른 무료 성벽을 지으면 클리어. 첫 날에 돈을 많이 벌어놔야 클리어가 쉽다. 첫 날 시작할 때 캠프 짓고, 한 명만 건축가로 하고 나머지는 궁수로 하면서 돈을 모아두고, 둘째 날 시작하자마자 캠프 업그레이드, 날이 지나기 전에 한번 더 업그레이드 하면 무료 성벽이 생긴다.
On the Third Day I Lit a Fire
셋째 날이 오기 전까지 캠프를 지으면 안 된다. 따라서 내 왕관을 지켜줄 궁수도, 성벽을 지을 건축가도 없으므로 몬스터의 공격에 대비하여 동전이 많이 필요한데, 궁수나 농부 없이 동전을 모을 수 있는 방법은 맨 처음 시작하고 캠프 앞에 놓인 동전을 먹거나, 맵 안에 랜덤으로 존재하는 하나의 동전 상자를 뒤적이는 수 밖에 없다. 첫째 날 밤이 시작되기 전에 이 상자를 찾아 동전을 먹는 것이 안전하다.
On the Fourth Day We Had a Feast
넷째 날이 지나기 전에 20마리의 사슴을 잡아야 되는데, 은근히 어렵다. 시간날 때마다 닥치는대로 숲에 들어가서 사슴을 몰아와야 한다. 그러면서도 사슴을 한두방에 잡을 수 있을 정도로 충분한 궁수도 모아야 한다. 말의 폐활량이 거지같기 때문에 충분히 쉬어주면서 몰자. 일단 말의 뛰기 속도가 사슴의 뛰기 속도보다 빠르기 때문에 사슴을 따라잡아서 한참 앞으로 간 뒤, 다시 뒤로 돌아 몰아오면 된다.
평범하게 궁수만 풀어놓고 사슴을 잡으라고 하면 공터로는 사슴이 잘 나오지 않기 때문에 깨기 어렵다.
For Five Days I Turned the Other Cheek
6일이 오기 전까지 아무도(토끼나 사슴도 포함) 죽이면 안 된다. 따라서 6일 전에 궁수를 만들면 안 되며, 어떤 공략에는 동전을 많이 모아두라고 하지만 동전만 가지고 깨는건 한계가 있기 때문에 성벽을 많이 짓는 것이 깨기 쉽다. 레드문은 6일째에 시작되기 때문에 몬스터 러쉬를 걱정할 필요는 없다. 5일째까지 하루에 많아봐야 양 옆에서 네 마리의 몬스터가 나오므로 밤이 지나기 전까지 성벽이 버텨주면 되는데, 돌 성벽을 세 개 정도 지어두면 매우 안전하다. 캠프에서 하루마다 동전 상자도 주고, 건축가에게 나무를 베라고 하면 일정 확률로 한 개의 동전을 추가로 얻을 수 있기 때문에 건축가를 두 세명 모아두는 것이 깨는데 용이하다.
By Day Six I Was Rich
6일이 지나기 전에 동전 주머니에서 동전이 넘치면 된다. 동전을 많이 얻으려면 궁수를 적정량 만들면서 공터에 토끼숲이 파괴되지 않도록 성벽을 적절한 곳에 지으면 궁수들이 동전을 많이 얻을 수 있다. 거기에 사슴 몰이도 하고, 맵 어딘가에 있는 동전 상자도 찾으면 4일쯤 되는 날에 동전 주머니에서 동전이 넘치는 경험을 할 수 있을 것이다.
On the Eigth Day I Fumbled
이 도전과제가 가장 어려운데, 내가 한 번 먹은 동전을 땅에 닿지 않도록 해야 한다. 단, 공터에서 화살표 아래 키나 S키를 눌러 동전을 의도적으로 버리는 행위가 아닌 경우에는 상관이 없는 듯 하다. 오늘 시도하다가 이렇게 해서 실수로 땅에 떨궜는데 도전과제가 깨졌다. 원주민 캠프에서 사람을 모을 때와 동전 주머니에서 동전이 넘치는 것만 조심하면 된다는 얘긴데, 사람을 모을 때 동전을 떨구려면 동전을 주고 싶은 사람 앞에서 멈춰설 때, 내 캐릭터의 발이 원주민과 겹치는 상태에서 주면 높은 확률로 안 떨구고 원주민을 시민으로 만들 수 있다.
On the Ninth Day I First Ran
게임 시작하고 한 번도 달리지 않은 상태로 9일째가 되면 달려야 한다. 현재 날짜가 어떻게 되는지를 주의하면서 걷기만 하자.
The Tenth Day We Fought Back
정확히 어떻게 깨는지 기억이 안 나는데 내 기억이 맞다면 10일이 지나기 전에 포탈 하나를 부수면 된다. 궁수를 최대한 많이 얻어서 돈을 많이 벌어놓고, 나무를 베면서 한쪽으로 확장을 해나가면서 6일이 지나기 전에 캠프를 세 단계까지 올려놓은 뒤 6일 후부터 차근차근 캠프를 마지막 단계까지 업그레이드해서 9일째 쯤에 기사를 두 명 만들어 공격을 나가면 된다. 확실하게 깨부수려면 활 버프를 받아놓는 것도 좋다.
Day V
Day X
Day XV
Day XX
Day XXV
Day XXX
Day XXXV
Day XL
Day XLV
Day L
Day LV
Day LX
Day LXV
Day LXX
Day LXXV
Day LXXX
Day LXXXV
Day XC
Day XCV
Day C
각각 5의 배수만큼 날짜가 지나면 클리어된다. 한번에 100일까지 버티면 되는데, 아마도 초심자라면 40일쯤 되면 포탈을 두 개쯤 깼을 것이다. 100일이 되기 전에 포탈을 하나만 남겨놓은 뒤에 101일째에 마지막 포탈을 깨면 도전과제도 깨고 게임도 클리어할 것이다.
By the Seventh Day I Cleared an Acre
7일이 되기 전에 나무를 많이 베어 일정 크기의 공터를 만들면 된다. 중간에 원주민 캠프가 있다고 이를 남겨놓으면 안 되고, 원주민 캠프까지 싹 밀어야 된다. 왼쪽이든 오른쪽이든 닥치는대로 나무를 베자. 성실하게 나무를 베면 7일이 되기 전에 클리어할 것이다.
Safe in 25
Safe in 30
Safe in 35
Safe in 40
각각 25일 안에, 30일 안에, 35일 안에, 40일 안에 게임을 클리어하면 도전과제가 깨지며, 25일 안에 게임을 클리어하면 나머지도 다 깨지니 25일 안에만 깨면 나머지는 도전할 필요 없다. 의외로 25일 안에 깨는건 매우 쉬운데, 궁수만 많이 모았다면 10일 안에 포탈 두 개 깨는 것은 매우 쉬우며, 이후 궁수를 악착같이 모으고 투석기도 갖춰놓으면 23~25일쯤엔 나머지 포탈도 모두 깰 수 있다.
정보

파일의 Flush는 되도록 적게 하자

3년 전 쯤에 후배의 과제를 구경한 적이 있다. 코드 내 알고리즘 치곤 너무 느려서 한번 디버깅을 도와줬는데, 문제의 원인은 파일 저장 작업에서 flush 함수를 너무 많이 호출한 것. 거의 한 단어당 한 번씩 호출한 것이나 다름이 없는 정도로 매우 많이 호출했었다.

flush를 할 때마다 느려지는 원인은 바로 파일 입출력 기능에 있다. 주 기억 장치, 보조 기억 장치할것 없이 대부분의 장치는 쓰기 작업이 읽기 작업에 비해 느리다. 특히나 보조 기억 장치는 그것이 더한 편이다. 그 느린 보조 기억 장치의 쓰기 기능 중에서도 HDD의 쓰기 기능은 단연 톱으로 느리다. 물론 SSD나 USB 메모리의 기반이 되는 플래시 메모리 자체도 느리지만(기본적으로 HDD보다 느림) SSD는 내부적으로 플래시 메모리를 RAID로 묶어서 속도가 나오는 편. 따라서 SSD에 파일 쓰기 작업을 할 때 flush를 하면 그래도 속도가 많이 빠르다.

하드웨어적인 쓰기 기능 결함도 있지만 이와 연관된 소프트웨어적 결함도 있는데, flush를 할 때 동기 작업을 수행한다는 점이다. flush를 한다는 것은 곧 주 기억 장치에 작성했던 데이터를 보조 기억 장치로 보내고 주 기억 장치에 작성했던 데이터를 비운다는 것인데, 이 과정에서 flush가 동작하고 있으면서 flush를 완료하기 전에 데이터를 쓰게 되면 문제가 발생할 수 있다. 따라서 보통은 flush는 동기적으로 작업을 수행하며, 따라서 소프트웨어는 이에 속도를 맞춰서 동작한다. 물론 C#은 특정 버전부터(기억하기론 async-await이 추가된 5.0부터다) Flush 함수의 비동기 버전을 지원하지만 모든 언어가 이를 지원하진 않는다. 게다가 이를 공식적으로 지원하지 않는 언어나 플랫폼에서 어설프게 직접 비동기 기능을 작성한다면 문제가 발생할 소지도 있다.

Flush 많이 한다고 좋지 않다. 오히려 HDD의 경우 디스크 돌아가는 일이 늘어나고, SSD나 플래시 메모리의 경우 셀 수명이 단축될 수 있는 위험성도 존재한다. 물론 flush를 지속적으로 수행하면서 파일에 쓰기 작업을 하면 오류가 나서 프로그램이 종료가 돼도 그때까지 작성했던 데이터는 파일에 남는다는 장점은 있지만 쓰다가 만 파일이 얼마나 유용할지는 모르겠다.

Flush를 할거면 두 가지 중에 하나다. 청크 당 한 번 또는 파일 쓰기 마지막에 한 번 하자. 청크 당 한 번이면 그나마 쓰기 작업한 것이 유용하기 때문에 중간에 오류가 나도 사용이 가능한 데이터가 뽑힌다. 중간에 오류날 일도 없고, 어떻게 파일을 쓰든 오류 나서 프로그램이 꺼지면 파일도 날아간다고 봐야 되는 경우엔 중간중간 flush 쓴다고 좋은 것도 아니다. 웬만하면 flush는 자제하자.

정보

DirectX 11을 이용한 멀티스레드 렌더링

DirectX 11에는 Immediate Context와 Deferred Context라고 하는 두 종류의 컨텍스트가 존재한다.

Immediate Context는 메인 스레드에서 렌더링하기 위해 사용하는 녀석으로, 서브리소스 매핑, 서브리소스 업데이트 등의 작업부터 시작하여 명령 리스트 실행도 담당하는 어떻게 보면 중앙 집중형 컨텍스트이다. 이 녀석이 없다면 실질적인 렌더링을 수행할 수 없다.

Deferred Context는 서브 스레드에서 렌더링하기 위해 사용하는 녀석으로, 다른 작업은 다 할 수 있지만 서브리소스 편집을 수행할 수 없다. 다만 동적 리소스의 경우 Map 작업은 수행할 수 있다. Deferred Context에서 작업한 결과는 당장 렌더링되지는 않는다. FinishCommandList 작업을 통해 먼저 지금까지 Deferred Context에서 작업했던 내용을 명령 리스트로 뽑아낸 뒤 Immediate Context에서 ExecuteCommandList를 수행해주어야 한다.

Deferred Context는 한 스레드 당 하나 씩만 만들 수 있으며, Immediate Context와 중복된 스레드에서는 생성되지 않는다. 즉, 메인 스레드에서는 생성되지 않는다. 모든 렌더링 오브젝트를 패러렐하게 루프를 돌면서 렌더링을 시킬 생각이라면 Deferred Context는 사용할 수 없거나 오버헤드가 싱글스레드 렌더링할 때보다 더 커질 수 있다. + (2016. 05. 11) 최근에 테스트 해보니 C# 기준으로 패러렐하게 루프를 돌리면 기본적으로 CPU 코어 개수 + 1개의 스레드가 패러렐 루프가 끝날 때까지 유지된다. 따라서 패러렐하게 루프를 돌리되, 객체를 나누는 기준을 설정하는 것이 중요할 것 같다.

8개 스레드를 생성하여 Deferred Context를 사용해 출력한다고 해도 8배 더 좋은 성능을 내지는 못한다. 이름에서 유추할 수 있듯 Deferred Context는 어디까지나 가장 중요한 작업만 지연된 작업을 하고 덜 중요한 작업은 멀티스레드 프로그래밍 할 수 있도록 하는 컨텍스트이고, 실질적인 모든 렌더링 작업은 Immediate Context에서 수행하기 때문이다. 8스레드 CPU에서 GTX 970으로 삼각형 렌더링을 수행한 결과 1.8배 성능까지는 나오는 것 같다. ※(2016. 02. 25.) 다른 사람들에게 말한 결과, 이보다 더 속도가 빨라져야 된다고 한다. 내 코드에 뭔가 미스가 있는 모양.

DirectX 11로 해볼 수 있는 간단한 작업들은 오늘 작업해본 Deferred Context 사용으로 다 해본 것 같다. 이제 같은 코드를 DirectX 12에서 구동했을 때 얼마나 성능 향상이 있는지 확인하기 위해 또 공부를 해봐야지…

정보

꼭 필요한 경우가 아니라면 SQLite 사용을 자제하자

SQLite는 단일 파일 구성의 데이터베이스 파일이다. Microsoft Office Access 파일과 비슷하다고 볼 수도 있지만 이 파일은 엔드 유저(End User)가 사용하는 파일이고, SQLite 파일은 개발자가 사용하는 파일이라고 볼 수 있다. 즉, 개발자가 개발한 앱에서 사용하는 파일이다. 문제는 SQLite는 파일의 수정을 위해서 트랜잭션을 위해 저널링 파일 생성을 수행한다는 점에 있다.

요즘 사용되는 거의 대부분의 파티션 포맷은 저널링 파일 시스템이다. Microsoft Windows 계열(Windows, Windows Phone, Xbox 360/One)에서 사용하는 NTFS, Apple OS X 및 Apple iOS에서 사용하는 Mac OS Extended(HFSX), Linux에서 사용하는 ext(ext3, ext4) 등이 전부 저널링 파일 시스템인데, 거의 대부분의 파일 시스템이 저널링 파일 시스템인 이유는 오류가 발생했을 때 데이터를 복구하기 용이하기 때문이다.

SQLite에서는 자신의 저널링을 위해 저널링 파일 시스템에 두어번의 저널링을 더 시킨다는 점에 있다. 저널링 파일을 생성하고 삭제하기 때문인데, HDD를 사용하던 시절에는 이런 방법을 사용해도 저널링 파일 시스템에서는 약간 느리다는 단점 외에는 큰 문제가 없었다. 하지만 최근 많이 사용되는 SSD나 USB 메모리, 스마트폰과 같은 플래시메모리 사용 장치에서 문제가 발생한다. 쓰기 작업이 많아지면 플래시메모리의 셀 수명이 단축되기 때문이다.

스마트폰 앱 중에서 데이터를 기록하는 앱은 대부분 SQLite를 사용하고 있다. 특히 CoreData를 사용하고 있는 iOS 앱이라면 대부분 사용하고 있다(최근에는 Realm으로 백엔드를 대체하기도 하는 모양이기에 ‘대부분’. 다만 Realm도 같은 문제를 가지고 있을 수 있으니 자세히 알아봐야 할 듯). Android도 많은 입문서적에서 SQLite를 사용하는 방법을 기록하고 있는 것으로 보아 알게 모르게 많은 앱들이 SQLite를 사용하고 있을 것이다.

PC야 SSD 수명이 다 하거나 USB 플래시메모리 수명이 다 하면 쉽게 교체하면 되니 미리 데이터만 백업한다면 큰 금전적 손실 없이 이용할 수 있다지만 스마트폰이나 태블릿, 그리고 SSD 일체형 노트북(맥북 에어나 맥북 프로 레티나같은 랩탑 컴퓨터)은 좀 다르다. 메모리 수명이 다 해버리면 A/S를 하는 방법밖에 없는데, 보통은 그정도가 되면 A/S 기간이 끝난 상태라 유상 A/S 또는 A/S 거절이고, 유상 A/S도 해당 플래시메모리만 뜯어서 교체하는 방법이 없으므로 리퍼나 재구입밖에 방법이 없다.

단순 데이터 저장 정도만 하는 앱이라면 SQLite 사용을 자제하자. 저장한 데이터를 쉽게 활용하는 방법은 SQLite가 아니더라도 방법은 많다.

SQLite의 Journaling of Journal anomaly에 대해서 더 자세하게 설명한 학술 자료가 있다. 해결법도 제시되어 있으니 꼭 SQLite를 사용해야겠다면 SQLite의 소스 수정을 좀 하는 방법도 좋을 것이다.