2018年10月29日 星期一

從OpenGL跨到OpenGLES的心得

從OpenGL跨到OpenGLES的心得

前言

  最近將C++的繪圖引擎跨到Android,PC端使用的是OpenGL4.6,Android端使用的是OpenGLES3.2,目前只完成初步的測試,就是只能用基本的Shader畫三角形。這次我做了個挑戰,由於手機有支援OpenGLES3.2,所以直接跨到OpenGLES3.2,果不其然碰到了不少問題,這裡做個紀錄。

內容

  從OpenGL跨到OpenGLES的差異這裡分成"Context的差異"與"API的差異"兩部分。Context的差異是必然的,因為PC端的OpenGL為Windows與Linux分別準備了WGL與GLX,自然也別期望android平台可以用一樣的方法,而API的差異部分比想像來的少,透過docs.gl可以查詢OpenGL與OpenGLES的差異。

  Context的差異的部分主要有兩大問題,"執行序(Thread)的問題"與"視窗可能被銷毀的問題",執行序(Thread)的問題主要是因為OpenGL與OpenGLES的Context都會認執行序,也就是用哪個執行序創建Context就只能用該執行序來執行繪圖,這個規則在PC時問題不大,但在Android時會有麻煩,由於Android的Activity不提供類似main()的Function,所以要自己做一個執行序(Thread)來當作main(),這時候就可能發生創建Context與繪圖的執行序(Thread)不是同一個,而且不幸的是這件事情發生時不會有任何的報錯!只會得到一個黑畫面而已,一定要確保創建Context、執行繪圖與銷毀Context都是用C++創建的執行序(Thread),接合Android的Activity的事件只能用來通知有事件發生,不能執行任何EGL與OpenGLES的API。視窗可能被銷毀的問題是Android跟Windows與Linux很大的不同,在Windows與Linux把視窗縮到最小是將視窗Hide的意思,但Android很不一樣,當進入休眠或跳出程式時,都會去銷毀視窗!還記得EGL創建Context時用的eglMakeCurrent()嗎?裡面的Surface參數會在進入休眠或跳出程式時失效,所以必須要對該事件做處理,處理的流程可以參考Android 前后台切换与OpenGL(EGL)创建销毁的周期,裡面有個ResetSurface()可以在事件發生時喚起。

  API的差異部分如下:
1.不支援glDepth(),請用glDepthf()來替代
2.不支援GL_TEXTURE_1D,如果需要1D的Texture請用2D替代
3.不支援glMapBuffer(),寫UBO的話請用glBufferSubData()替代
4.GLSL編譯的時候請將"#version 300 es"放在最開頭
5.Extension不互通,如在OpenGL判斷是否支援UBO可以透過Extension檢查,OpenGLES則是版本來決定(OpenGLES3以後就支援)

前4點都還好很容易克服,第五點就比較頭痛了,必須知道各個版本的規格,只能用經驗來克服了。

  最後再說個關於include的問題,如果查詢Android NDK Native APIs時會發現以下畫面
OpenGLES3.2的include
這個include是錯的!如果去NDK把"GLES3/gl3ext.h"打開的話,就會發現這個Header是個只有註解的空檔案,正確的include如下
#include "GLES3/gl32.h"
#include "GLES2/gl2.ext.h"

雖然看起來有些奇怪,但如果不這麼做的話,會沒辦法創建壓縮格式的Texture,如ASTC、PVR...等,因為Marco都定義在"GLES2/gl2ext.h",由於OpenGLES目前只有跨到Andoird,不知這是不是OpenGLES的標準用法,只能日後再確認了。

參考資料

Android NDK Native APIs
docs.gl
Android 前后台切换与OpenGL(EGL)创建销毁的周期

2018年10月22日 星期一

C++的IO在跨平台時的問題

C++的IO在跨平台時的問題

前言

  最近將C++的繪圖引擎從Windows轉到Linux,本想問題比較大的應該是OpenGL的部分,但結果是我花在OpenGL的部分的時間沒想像的多,反倒是其它的問題讓我花了很多時間Debug,而這個問題跟C++的IO有關,以下是Debug的過程,在此做個紀錄。

內容

  在繪圖引擎轉到Linux後,花了些時間解決OpenGL在跨平台的問題,接著本想就沒什麼大問題了,但實際上卻是Shader的部分卻在Run的時候初始化失敗,本想是程式的工作目錄的問題,但結果並不是,Debug後發現檔案是有讀到的,但Shader的初始化這段程式碼在Windows跑的時候都沒問題,難道Shader的初始化有分平台!?

  在此說明一下,在我的繪圖引擎的Shader初始化是將檔案讀入後,在經由Parser取得Shader的資訊後,再將shader code送進OpenGL的API。Debug後發現問題發生在Parser取得Shader的資訊的過程,由於這個Parser做得有點複雜,所有我花了大量的時間在Debug這個Parser,在Debug時候一直有個想法,不就是個Parser,為什麼Windows與Linux的結果會不一樣?最後我找到兇手了,兇手來自以下的程式碼
std::string shaderCode;
std::ifstream fileStream(shaderFilePath);
if(!fileStream)
  return false;
shaderCode.assign((std::istreambuf_iterator<char>(fileStream) ),std::istreambuf_iterator<char>() );
//start parse
//...


return true;

這段程式碼將檔案讀入後送到Parser,但我發現"shaderCode"的內容在Windows與Linux的結果是不一樣的!由於Shader檔案是在Windows的文字編輯器打的,所以換行的字元是"\r\n",這段程式碼在VC(visual c++)跑的時候會將'\r'過濾掉,但是在g++的時候卻不會,所以造成之後的Parser會Parse出不同的結果,造成初始化錯誤。

  所以要如何解決這個問題呢?解決的方法並不是改變這段讀取檔案的程式碼,因為如果這樣解決的話代表Parser本身不容許'\r'在字尾,所以比較好的解法是讓Parser不要單單使用Split的方式來取得,而是在每次Split後也同時做Trim的動作,這樣就可以讓Parser接受有\r'的內容了。

參考資料

How to read line by line or a whole text file at once?

2018年10月15日 星期一

在Android studio裡設定NDK

在Android studio裡設定NDK

前言

  最近打算讓C++的引擎支援Android,所以就安裝許久沒用的Android studio,裝完後發現NDK的用法跟我以前的用法差很多,所以想說可以換回以前的"ndk-build.cmd"嗎?答案是可以的,設定的過程不太好記,所以在此做個紀錄。

內容

  在設定NDK的第一步驟是安裝NDK,Android studio本來就有整合,只是預設不會裝。首先打開SDK Manager,如下圖
打開SDK Manager

接著選取安裝NDK,如下圖
安裝NDK

安裝完後請重新開啟Android studio。

  下一個步驟是設定External Tools,這裡會新增兩個工具,一個是javah,另一個是ndk-build。
打開設定,如下圖
打開設定

接著到External Tools新增工具,如下圖
External Tools新增工具

新增"javah",如下圖
新增"javah"
Program的參數
$JDKPath$\bin\javah.exe

Arguments的參數
-classpath "$Classpath$;" -d $ModuleFileDir$\src\main\jni $FileClass$

Working Directory的參數
$ModuleFileDir$\src\main\java\
接著新增ndk-build",如下圖
新增"ndk-build"

Program的參數
C:\Users\hosee\AppData\Local\Android\Sdk\ndk-bundle\ndk-build.cmd

Working Directory的參數
$ModuleFileDir$\src\main\
Program的參數的參數因為沒marcro,所以是個絕對位置,其實就是NDK的安裝位置,如果不要用Android studio裡的NDK,
可以把這裡的位置改掉。
接著設定打開專案結構,如下
打開專案結構

接著設定JDK,使用Android studio裡面的JDK,如下圖
設定JDK
設定的部分到此結束,接著是在專案中使用NDK。

  開一個新專案後,在gradle.properties新增以下
android.useDeprecatedNdk=true

如圖
修改gradle.properties

然後在build.gradle新增以下
ndk {
  moduleName "HelloNDK"
  ldLibs "android"
}
sourceSets.main {
  jni.srcDirs = []
  jniLibs.srcDir "src/main/libs"
}

如圖
修改build.gradle

修改完後,再來是新增jni的資料夾,如下圖
新增jni資料夾

新增後會跳出"New Android Component",直接Finish即可,如下圖
"New Android Component"視窗

  在專案的Activity裡新增以下
    static {
        System.loadLibrary("HelloNDK");
    }

    public static native String nativeHello();

如圖
新增NDK介面

介面加完後,使用"javah"來新增header檔。

  接著在jni資料夾新增source檔,檔案內容如下
#include "string.h"
#include "jni.h"
#include "com_example_hosee_hellondk_MainActivity.h"
JNIEXPORT jstring JNICALL Java_com_example_hosee_hellondk_MainActivity_nativeHello(JNI* pEnv,jclass jObj)
{
  return (pEnv)->NewStringUTF("Hello NDK!");
}

操作如圖
新增source檔

  接著在jni資料夾新增"Android.mk",內容如下
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := HelloNDK
LOCAL_SRC_FILES := com_example_hosee_hellondk_MainActivity.cpp
LOCAL_LDLIBS += -landroid
include $(BUILD_SHARED_LIBRARY)

操作如圖
新增"Android.mk"

  接著在jni資料夾新增"Application.mk",內容如下
APP_PLATFORM := android-15
APP_MODULES := HelloNDK
APP_ABI := all

操作如圖
新增"Application.mk"

  接著使用ndk-build產生so檔,如下圖
使用ndk-build產生so檔

最後在專案的Activity新增以下
TextView tv = (TextView) findViewById(R.id.TestTextView);
tv.setText(nativeHello() );

操作如圖
使用NDK的Function

這裡要注意TextView的ID要先去Layout裡面加完,加完後就可以使用了。


參考資料

超级简单的Android Studio jni 实现(无需命令行)

2018年10月8日 星期一

在Ubuntu裡切換GPU

在Ubuntu裡切換GPU

前言

  在筆電安裝Ubuntu後,發現Chrome跑個網頁相當吃力,本想可能是選到Intel的GPU,想做個交換GPU,發現根本沒選項,所以就來研究怎麼切換GPU,在此做個紀錄。

內容

  研究後發現Ubuntu根本沒安裝顯卡驅動,所以跑個網頁就相當吃力。怎麼安裝顯卡驅動呢?
在"系統設定值"裡有個"軟體與更新",如下圖
軟體與更新
進去後選取"額外的驅動程式"頁籤,如下圖
安裝驅動程式
可以看到下方有個列表,預設的驅動程式是在第三個選項,將它選到第一個選項進行安裝,安裝後再重新啟動。

  安裝完驅動程式後,可以開啟顯卡驅動所附帶的軟體來切換GPU,以nVidia的為例就是開啟"NVIDIA X Server Settings",如下圖
NVIDIA X Server Settings

開啟後,在左側選取選項"PRIME Profiles",如下圖
選取GPU
接著就可以在右側進行切換GPU了。

參考資料

Switch Between Intel and Nvidia Graphics Card in Ubuntu 16.04

2018年10月1日 星期一

使用python來備份SVN

使用python來備份SVN

前言

  備份SVN這件事以前都不是我會負責的事,但一定有做,最近添購了一台NAS,想想是個機會,就來研究一下,在此做個紀錄。

內容

  如果需要備份SVN的話就需要"svnadmin",所以請先確認該命令是否可以使用。備份SVN的命令如下
svnadmin dump c:\svnrepo --incremental > c:\backup\xxx.dump

紅色的部分是檔案庫的位置,藍色的則是輸出的備份檔,請依據需求更改。如果要用備份檔來還原檔案庫的話命令如下
svnadmin load c:\svnrepo < c:\backup\xxx.dump

紅色的部分是還原檔案庫的位置,藍色的則是使用的備份檔。整個過程用起來相當簡單,但備份這件事通常會希望是"自動執行"的,而不是需要用"手動執行"命令才動作,所以接著來介紹如何自動執行。

  排程這個功能在Windows與Linux都有這個功能,它可以用來每隔一段時間來時執行某個命令,幸運的,買來的NAS有視窗化的操作來編輯排程,所以這部份很快就完成了,至於Windows與Linux的排程實際上怎麼使用以後有機會再研究。

  使用Python來執行備份命令,為什麼不直接用bash呢?唯一的考量就是跨平台,加上Python使用起來比較靈活,所以就決定用Python了。執行的時候通常會希望可以依據備份的時間來分資料夾,所以我實作了以下的範例碼
import os
import time
backUpRootDir = '/homes/hosee/SVNBackUp/'
backUpFileName = 'svnBackUp.dump'
#
dirName = time.strftime("%Y%m%d_%H%M%S", time.localtime() )
os.system('mkdir -p '+backUpRootDir+dirName)

os.system('svnadmin dump /homes/hosee/svn/repo --incremental > '+backUpRootDir+dirName+'/'+backUpFileName)

這個範例碼會依據備份的時間製作資料夾,並在該資料夾儲存備份檔。範例碼的路徑請依據需要修改,如"backUpRootDir"與svnadmin的檔案庫位置。最後,就是只要用排程來啟動Python的script就完成了。

參考資料

subversion的備份hotcopy以及dump的使用