# 2020q3 Homework5 (render)
contributed by < `OscarShiang` >
## 測試環境
```shell
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="20.04 LTS (Focal Fossa)"
$ cat /proc/version
Linux version 5.4.0-47-generic (buildd@lcy01-amd64-014)
(gcc version 9.3.0 (Ubuntu 9.3.0-10ubuntu2))
```
## 作業要求
- [ ] 修正浮點數和定點數算繪程式展現的缺失,並提出改進 precision 及 accuracy 的方式
- [x] 輸出算繪過程的 [frame rate](https://en.wikipedia.org/wiki/Frame_rate),日後當我們進一步提升算繪程式的效率時,這會是效能評比的方式之一
- [x] 利用 [tools/precalculator.cpp](https://github.com/sysprog21/raycaster/blob/master/tools/precalculator.cpp) 產生運算表格,修改相關程式碼,使得程式碼在編譯時期才去產生運算表格,後者以標頭檔案 (generated header) 的形式存在並編譯進入主程式。換言之,檔案 [raycaster_tables.h](https://github.com/sysprog21/raycaster/blob/master/raycaster_tables.h) 應自 repository 移除,改用編譯時期產生
- [ ] 解說現有 fixed-point 實作機制,並探討前述表格產生的機制,需要提及其中的考量點
- [ ] 參照 [C 語言:物件導向程式設計篇](https://hackmd.io/@sysprog/c-oop),透過建立共用介面 (interface) 的手法,將 [raycaster](https://github.com/sysprog21/raycaster) 以 C99/C11 (或 gnu99/gnu11) 重寫,允許在執行時期載入 fixed-point 和 floating-point 為基礎的 renderer
## 輸出算繪過程的 frame rate
根據 [Frame rate - Wikipedia] 的描述
> Frame rate (expressed in frames per second or FPS) is the frequency (rate) at which consecutive images called frames appear on a display.
可以知道 FPS 就是每秒我們更新了多少次遊戲畫面。
而對應到 `raycaster` 中就是在一秒內,`main.cpp` 中的 game loop 跑了幾次。
所以我們可以先看到 `main.cpp:main` 函式中的實作:
```cpp
int main(int argc, char *args[])
{
<...>
while (!isExiting) {
floatRenderer.TraceFrame(&game, floatBuffer);
fixedRenderer.TraceFrame(&game, fixedBuffer);
DrawBuffer(sdlRenderer, fixedTexture, fixedBuffer, 0);
DrawBuffer(sdlRenderer, floatTexture, floatBuffer,
SCREEN_WIDTH + 1);
SDL_RenderPresent(sdlRenderer);
if (SDL_PollEvent(&event)) {
isExiting =
ProcessEvent(event, &moveDirection, &rotateDirection);
}
const auto nextCounter = SDL_GetPerformanceCounter();
const auto seconds = (nextCounter - tickCounter) /
static_cast<float>(tickFrequency);
tickCounter = nextCounter;
game.Move(moveDirection, rotateDirection, seconds);
}
<...>
}
```
在這個 while 迴圈之中,我們可以看到 `raycaster` 利用這個迴圈重複地進行算繪,並將 floating-point 與 fixed-point 的 buffer 利用 `SDL_RenderPresent` 顯示在視窗上,因此從這邊我們可以判斷這個迴圈就是 game loop
根據 SDL Wiki 上的說明,我們可以知道在這邊使用到的 `SDL_GetPerformanceCounter` 是用來取得 SDL 內建的 high resolution counter 的數值,而我們可以透過這個數值差來算出兩次 game loop 之間相差多久。
而在上述程式碼中可以看到在取得兩個 frame 之間的 counter 差值後,我們是透過 `tickFrequency` 來算出時間差的,而這個部分可以回推到 `main.cpp:93` 的部分
```cpp
const static auto tickFrequency = SDL_GetPerformanceFrequency();
```
`SDL_GetPerformanceFrequency` 這個函式在 SDL Wiki 中的描述是
> Returns a platform-specific count per second.
因此我們可以透過這個函式得知當前運行平台每秒對應到多少 count,透過這樣我們就可以從 `(nextCounter - tickCounter) /
static_cast<float>(tickFrequency)` 計算出產生這個 frame 所花費的時間。並從這邊推得 FPS 的數值。
在 FPS 的實作中,我使用兩個變數作為 counter,分別計算從上次更新 FPS 到目前的秒數以及產生的 frame 的數量
```cpp
frame_count++;
sec_counter += seconds;
```
當 `sec_counter` 大於 1 時,將 `frame_counter` 中的數值作為 FPS 輸出,並清空
```cpp
if (sec_counter >= 1.0f) {
cout << "FPS = " << frame_count;
sec_counter = 0;
frame_count = 0;
}
```
得到的執行結果如下:
```shell
$ ./main
FPS = 44
FPS = 61
FPS = 60
FPS = 61
FPS = 61
FPS = 61
<...>
```
為了方便在測試的過程中,查看 FPS 的數值,我將這個結果改以顯示在遊戲視窗的標題中
所以我們需要將程式碼進行以下的修改
```diff
frame_count++;
sec_counter += seconds;
if (sec_counter >= 1.0f) {
- cout << "FPS = " << frame_count;
+ stringstream title;
+ title << GAME_TITLE " FPS = " << frame_count;
+ SDL_SetWindowTitle(sdlWindow, title.str().c_str());
sec_counter = 0;
frame_count = 0;
```
而結果呈現如下紅框所示
![](https://i.imgur.com/0PLC9Qs.png)
## 利用 tools/precalculator.cpp 產生運算表格
因為 `raycaster_table.h` 中的數值是固定的,所以我們可以在編譯時期才產生
為了將在 `RayCasterPrecalculator::Precalculate` 計算出來的表格輸出到檔案中,我們需要修改其實作的回傳值
```diff
#pragma once
+#include <string>
class RayCasterPrecalculator
{
public:
RayCasterPrecalculator();
~RayCasterPrecalculator();
- static void Precalculate();
+ static std::string Precalculate();
};
```
接著實作 `table_generator` 將結果輸出到檔案中
```cpp
int main(int argc, char *argv[])
{
if (argc != 2) {
cout << "[ERR] Output filename is needed" << endl;
exit(-1);
}
string filename(argv[1]);
RayCasterPrecalculator calculator;
string data = calculator.Precalculate();
generate_table(filename, data);
return 0;
}
```
`generate_table` 的實作如下
```cpp
void generate_table(string filename, string data)
{
fstream out(filename, ios::out);
string macro_id;
for (int i = 0; i < filename.length(); i++) {
}
char marcro_id[filename.length() + 1];
for (int i = 0; i < filename.length(); i++) {
switch (filename[i]) {
case '.':
marcro_id[i] = '_';
break;
default:
marcro_id[i] = toupper(filename[i]);
}
}
marcro_id[filename.length()] = '\0';
string macro(marcro_id);
/* Add dependencies */
out << "#include \"raycaster.h\""
<< "\n\n";
/* Add protect marco */
out << "#ifndef " << macro << '\n';
out << "#define " << macro << "\n\n";
out << data;
out << "#endif /* " << macro << " */" << '\n';
out.close();
}
```
測試其產生的結果是否正確
```shell
$ ./table_generator raycaster_tables.h
$ cat raycaster_tables.h
#include "raycaster.h"
#ifndef RAYCASTER_TABLES_H
#define RAYCASTER_TABLES_H
const uint16_t LOOKUP_TBL g_tan[256] = {0,1,3,4,6,7,9,11,12,14,15,17,18,20,22,23,25,26,28,29,31,33,34,36,37,39,41,42,44,46,47,49,50,52,54,55,57,59,60,62,64,65,67,69,70,72,74,75,77,79,81,82,84,86,88,89,91,93,95,96,98,100,102,104,106,107,109,111,113,115,117,119,121,123,124,126,128,130,132,134,136,138,140,142,145,147,149,151,153,155,157,159,162,164,166,168,171,173,175,177,180,182,185,187,189,192,194,197,199,202,204,207,210,212,215,218,220,223,226,229,232,234,237,240,243,246,249,252,255,259,262,265,268,272,275,278,282,285,289,293,296,300,304,308,311,315,319,323,328,332,336,340,345,349,354,358,363,368,373,378,383,388,393,398,404,409,415,421,427,433,439,445,451,458,465,471,478,486,493,500,508,516,524,532,541,549,558,568,577,587,597,607,618,628,640,651,663,675,688,701,715,729,744,759,774,791,808,825,843,862,882,903,925,947,971,996,1022,1049,1077,1108,1140,1173,1209,1246,1286,1329,1374,1423,1475,1531,1591,1655,1725,1801,1884,1975,2075,2185,2308,2445,2599,2773,2972,3202,3470,3787,4166,4631,5210,5956,6950,8341,10428,13905,20859,41720};
const uint16_t LOOKUP_TBL g_cotan[256] = {0,41720,20859,13905,10428,8341,6950,5956,5210,4631,4166,3787,3470,3202,2972,2773,2599,2445,2308,2185,2075,1975,1884,1801,1725,1655,1591,1531,1475,1423,1374,1329,1286,1246,1209,1173,1140,1108,1077,1049,1022,996,971,947,925,903,882,862,843,825,808,791,774,759,744,729,715,701,688,675,663,651,640,628,618,607,597,587,577,568,558,549,541,532,524,516,508,500,493,486,478,471,465,458,451,445,439,433,427,421,415,409,404,398,393,388,383,378,373,368,363,358,354,349,345,340,336,332,328,323,319,315,311,308,304,300,296,293,289,285,282,278,275,272,268,265,262,259,256,252,249,246,243,240,237,234,232,229,226,223,220,218,215,212,210,207,204,202,199,197,194,192,189,187,185,182,180,177,175,173,171,168,166,164,162,159,157,155,153,151,149,147,145,142,140,138,136,134,132,130,128,126,124,123,121,119,117,115,113,111,109,107,106,104,102,100,98,96,95,93,91,89,88,86,84,82,81,79,77,75,74,72,70,69,67,65,64,62,60,59,57,55,54,52,50,49,47,46,44,42,41,39,37,36,34,33,31,29,28,26,25,23,22,20,18,17,15,14,12,11,9,7,6,4,3,1};
<...>
#endif /* RAYCASTER_TABLES_H */
```
並將 `table_generator` 整合到 `Makefile` 中
```diff
diff --git a/Makefile b/Makefile
index e554d48..67d0780 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,12 @@
BIN = main
+TBL_GEN = table_generator
CXXFLAGS = -std=c++11 -O2 -Wall -g
+LOOKUP_TBL := raycaster_tables.h
+
+TOOLS_DIR := tools
+
# SDL
CXXFLAGS += `sdl2-config --cflags`
LDFLAGS += `sdl2-config --libs`
@@ -18,7 +23,7 @@ endif
GIT_HOOKS := .git/hooks/applied
.PHONY: all clean
-all: $(GIT_HOOKS) $(BIN)
+all: $(GIT_HOOKS) $(LOOKUP_TBL) $(BIN)
$(GIT_HOOKS):
@scripts/install-git-hooks
@@ -32,14 +37,29 @@ OBJS := \
main.o
deps := $(OBJS:%.o=.%.o.d)
+TBL_GEN_OBJS := \
+ tools/precalculator.o \
+ tools/table_generator.o
+deps := $(TBL_GEN_OBJS:%.o=.%.o.d)
+
%.o: %.cpp
+ @mkdir -p .$(TOOLS_DIR)
$(VECHO) " CXX\t$@\n"
$(Q)$(CXX) -o $@ $(CXXFLAGS) -c -MMD -MF .$@.d $<
$(BIN): $(OBJS)
- $(Q)$(CXX) -o $@ $^ $(LDFLAGS)
+ $(Q)$(CXX) -o $@ $^ $(LDFLAGS)
+
+$(TBL_GEN): $(TBL_GEN_OBJS)
+ $(VECHO) " CXX\t$@\n"
+ $(Q)$(CXX) -o $@ $^
+
+$(LOOKUP_TBL): $(TBL_GEN)
+ $(VECHO) " GEN\t$@\n"
+ $(Q)eval "./$^ $@"
clean:
- $(RM) $(BIN) $(OBJS) $(deps)
+ $(RM) $(BIN) $(OBJS) $(TBL_GEN) $(TBL_GEN_OBJS) $(deps)
+ rm -rf .$(TOOLS_DIR)
-include $(deps)
```
在 `all` 這個 target 中調整順序就可以確保在編譯 raycaster 前會先將 `raycaster_tables.h` 這個表格產生出來後才開始進行編譯。
測試的結果如下
```shell
$ make
CXX tools/precalculator.o
CXX tools/table_generator.o
CXX table_generator
GEN raycaster_tables.h
CXX game.o
CXX raycaster_fixed.o
CXX raycaster_float.o
CXX renderer.o
CXX main.o
```
## 參考資料
###### tags: `sysprog2020`