From 76b38bebadda8456766a19f1865b2a9b493d1150 Mon Sep 17 00:00:00 2001 From: Gabriel Calero Date: Tue, 12 Dec 2017 15:34:02 -0300 Subject: [PATCH 01/17] Make Interface run in Android --- android/app/CMakeLists.txt | 46 ++-- android/app/build.gradle | 5 +- android/app/src/main/AndroidManifest.xml | 39 ++- android/app/src/main/cpp/main.cpp | 5 +- .../hifiinterface/InterfaceActivity.java | 178 +++++++++++++ .../hifiinterface/PermissionChecker.java | 130 ++++++++++ .../hifiinterface/WebViewActivity.java | 242 ++++++++++++++++++ .../main/res/drawable/ic_close_black_24dp.xml | 9 + .../src/main/res/layout/activity_web_view.xml | 34 +++ .../app/src/main/res/menu/web_view_menu.xml | 10 + android/app/src/main/res/values/dimens.xml | 12 + android/app/src/main/res/values/strings.xml | 7 +- android/app/src/main/res/values/styles.xml | 12 +- cmake/macros/TargetOpenGL.cmake | 5 +- interface/CMakeLists.txt | 59 ++--- interface/src/Application.cpp | 28 +- .../AssetMappingsScriptingInterface.cpp | 26 +- interface/src/scripting/LimitlessConnection.h | 4 +- interface/src/ui/Stats.cpp | 5 +- .../src/avatars-renderer/Avatar.cpp | 4 +- .../display-plugins/OpenGLDisplayPlugin.cpp | 12 +- libraries/entities-renderer/CMakeLists.txt | 3 +- .../script-engine/src/ArrayBufferClass.h | 1 + .../src/ConsoleScriptingInterface.cpp | 1 + libraries/ui/src/ui/OffscreenQmlSurface.cpp | 8 +- libraries/ui/src/ui/types/FileTypeProfile.cpp | 9 +- .../ui/types/FileTypeRequestInterceptor.cpp | 2 +- .../HFTabletWebEngineRequestInterceptor.h | 20 +- .../ui/src/ui/types/HFWebEngineProfile.cpp | 12 +- .../types/HFWebEngineRequestInterceptor.cpp | 2 +- libraries/ui/src/ui/types/RequestFilters.cpp | 2 +- 31 files changed, 831 insertions(+), 101 deletions(-) create mode 100644 android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java create mode 100644 android/app/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java create mode 100644 android/app/src/main/java/io/highfidelity/hifiinterface/WebViewActivity.java create mode 100644 android/app/src/main/res/drawable/ic_close_black_24dp.xml create mode 100644 android/app/src/main/res/layout/activity_web_view.xml create mode 100644 android/app/src/main/res/menu/web_view_menu.xml create mode 100644 android/app/src/main/res/values/dimens.xml diff --git a/android/app/CMakeLists.txt b/android/app/CMakeLists.txt index 4411b7b1bb..4260882018 100644 --- a/android/app/CMakeLists.txt +++ b/android/app/CMakeLists.txt @@ -1,26 +1,28 @@ set(TARGET_NAME native-lib) -setup_hifi_library(Gui Qml Quick) - -# Minimal dependencies for testing UI compositing -#link_hifi_libraries(shared networking gl ui) - -link_hifi_libraries( - shared networking octree - script-engine recording trackers - gl ktx image gpu gpu-gles render render-utils - physics - audio audio-client - ui midi controllers pointers - model model-networking fbx animation - entities entities-renderer - avatars avatars-renderer - ui-plugins input-plugins - # display-plugins - # auto-updater -) - - -target_link_libraries(native-lib android log m) +setup_hifi_library() +link_hifi_libraries(shared networking gl gpu gpu-gles image fbx render-utils physics entities octree) target_opengl() target_bullet() +set(INTERFACE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../interface") +add_subdirectory("${INTERFACE_DIR}" "libraries/interface") +include_directories("${INTERFACE_DIR}/src") + +target_link_libraries(native-lib android log m interface) + +set(GVR_ROOT "${HIFI_ANDROID_PRECOMPILED}/gvr/gvr-android-sdk-1.101.0/") +target_include_directories(native-lib PRIVATE "${GVR_ROOT}/libraries/headers") +target_link_libraries(native-lib "${GVR_ROOT}/libraries/libgvr.so") + +# finished libraries +# core -> qt +# networking -> openssl, tbb +# fbx -> draco +# physics -> bullet +# entities-renderer -> polyvox + +# unfinished libraries +# image -> nvtt (doesn't look good, but can be made optional) +# script-engine -> quazip (probably not required for the android client) + + diff --git a/android/app/build.gradle b/android/app/build.gradle index 066eb7da3d..343074047e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,10 +1,13 @@ apply plugin: 'com.android.application' +ext.RELEASE_NUMBER = project.hasProperty('RELEASE_NUMBER') ? project.getProperty('RELEASE_NUMBER') : '0' +ext.RELEASE_TYPE = project.hasProperty('RELEASE_TYPE') ? project.getProperty('RELEASE_TYPE') : 'DEV' +ext.BUILD_BRANCH = project.hasProperty('BUILD_BRANCH') ? project.getProperty('BUILD_BRANCH') : '' android { compileSdkVersion 26 defaultConfig { - applicationId "com.highfidelity.iface" + applicationId "io.highfidelity.hifiinterface" minSdkVersion 24 targetSdkVersion 26 versionCode 1 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2160f7b591..8e3b89d605 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ - + + @@ -19,18 +20,34 @@ android:icon="@mipmap/ic_launcher" android:launchMode="singleTop" android:roundIcon="@mipmap/ic_launcher_round"> - + + + + + + + + + android:launchMode="singleTop" + android:enableVrMode="com.google.vr.vrcore/com.google.vr.vrcore.common.VrCoreListenerService" + > - - + + @@ -43,4 +60,10 @@ - \ No newline at end of file + + + + + + + diff --git a/android/app/src/main/cpp/main.cpp b/android/app/src/main/cpp/main.cpp index b947323720..27d43e34aa 100644 --- a/android/app/src/main/cpp/main.cpp +++ b/android/app/src/main/cpp/main.cpp @@ -1,4 +1,6 @@ -#include +/* + The main function in this file overrides the one in interface library + #include #include #include @@ -151,3 +153,4 @@ int main(int argc, char* argv[]) timer.start(); return app.exec(); } +*/ \ No newline at end of file diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java b/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java new file mode 100644 index 0000000000..de3bcee88d --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java @@ -0,0 +1,178 @@ +// +// InterfaceActivity.java +// android/app/src/main/java +// +// Created by Stephen Birarda on 1/26/15. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +package io.highfidelity.hifiinterface; + +import android.content.Intent; +import android.content.res.AssetManager; +import android.net.Uri; +import android.os.Bundle; +import android.view.WindowManager; +import android.util.Log; +import org.qtproject.qt5.android.bindings.QtActivity; + +/*import com.google.vr.cardboard.DisplaySynchronizer; +import com.google.vr.cardboard.DisplayUtils; +import com.google.vr.ndk.base.GvrApi;*/ +import android.graphics.Point; +import android.content.res.Configuration; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.view.View; + +public class InterfaceActivity extends QtActivity { + + //public static native void handleHifiURL(String hifiURLString); + private native long nativeOnCreate(InterfaceActivity instance, AssetManager assetManager); + //private native void nativeOnPause(); + //private native void nativeOnResume(); + //private native void nativeOnStop(); + //private native void nativeOnStart(); + //private native void saveRealScreenSize(int width, int height); + //private native void setAppVersion(String version); + private native long nativeOnExitVr(); + + private AssetManager assetManager; + + private static boolean inVrMode; +// private GvrApi gvrApi; + // Opaque native pointer to the Application C++ object. + // This object is owned by the InterfaceActivity instance and passed to the native methods. + //private long nativeGvrApi; + + public void enterVr() { + //Log.d("[VR]", "Entering Vr mode (java)"); + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + inVrMode = true; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + + // Get the intent that started this activity in case we have a hifi:// URL to parse + Intent intent = getIntent(); + if (intent.getAction() == Intent.ACTION_VIEW) { + Uri data = intent.getData(); + +// if (data.getScheme().equals("hifi")) { +// handleHifiURL(data.toString()); +// } + } + +/* DisplaySynchronizer displaySynchronizer = new DisplaySynchronizer(this, DisplayUtils.getDefaultDisplay(this)); + gvrApi = new GvrApi(this, displaySynchronizer); + */ +// Log.d("GVR", "gvrApi.toString(): " + gvrApi.toString()); + + assetManager = getResources().getAssets(); + + //nativeGvrApi = + nativeOnCreate(this, assetManager /*, gvrApi.getNativeGvrContext()*/); + + Point size = new Point(); + getWindowManager().getDefaultDisplay().getRealSize(size); +// saveRealScreenSize(size.x, size.y); + + try { + PackageInfo pInfo = this.getPackageManager().getPackageInfo(getPackageName(), 0); + String version = pInfo.versionName; +// setAppVersion(version); + } catch (PackageManager.NameNotFoundException e) { + Log.e("GVR", "Error getting application version", e); + } + + final View rootView = getWindow().getDecorView().findViewById(android.R.id.content); + + // This is a workaround to hide the menu bar when the virtual keyboard is shown from Qt + rootView.getViewTreeObserver().addOnGlobalLayoutListener(new android.view.ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + int heightDiff = rootView.getRootView().getHeight() - rootView.getHeight(); + if (getActionBar().isShowing()) { + getActionBar().hide(); + } + } + }); + } + + @Override + protected void onPause() { + super.onPause(); + //nativeOnPause(); + //gvrApi.pauseTracking(); + } + + @Override + protected void onStart() { + super.onStart(); +// nativeOnStart(); + } + + @Override + protected void onStop() { + Log.d("[Background]","Calling nativeOnStop from InterfaceActivity"); +// nativeOnStop(); + super.onStop(); + } + + @Override + protected void onResume() { + super.onResume(); + //nativeOnResume(); + //gvrApi.resumeTracking(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + // Checks the orientation of the screen + if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){ +// Log.d("[VR]", "Portrait, forcing landscape"); + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + if (inVrMode) { + inVrMode = false; +// Log.d("[VR]", "Starting VR exit"); + nativeOnExitVr(); + } else { + Log.w("[VR]", "Portrait detected but not in VR mode. Should not happen"); + } + } + } + + public void openUrlInAndroidWebView(String urlString) { + Log.d("openUrl", "Received in open " + urlString); + Intent openUrlIntent = new Intent(this, WebViewActivity.class); + openUrlIntent.putExtra(WebViewActivity.WEB_VIEW_ACTIVITY_EXTRA_URL, urlString); + startActivity(openUrlIntent); + } + + /** + * Called when view focus is changed + */ + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) { + final int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + + getWindow().getDecorView().setSystemUiVisibility(uiOptions); + } + } + +} \ No newline at end of file diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java b/android/app/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java new file mode 100644 index 0000000000..34b087ca25 --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java @@ -0,0 +1,130 @@ +package io.highfidelity.hifiinterface; + +import android.Manifest; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.app.Activity; + +import android.content.DialogInterface; +import android.app.AlertDialog; + +import org.json.JSONException; +import org.json.JSONObject; +import android.util.Log; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.util.Arrays; +import java.util.List; + +public class PermissionChecker extends Activity { + private static final int REQUEST_PERMISSIONS = 20; + + private static final boolean CHOOSE_AVATAR_ON_STARTUP = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (CHOOSE_AVATAR_ON_STARTUP) { + showMenu(); + } + this.requestAppPermissions(new + String[]{ + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.RECORD_AUDIO, + Manifest.permission.CAMERA} + ,2,REQUEST_PERMISSIONS); + + } + + public void requestAppPermissions(final String[] requestedPermissions, + final int stringId, final int requestCode) { + int permissionCheck = PackageManager.PERMISSION_GRANTED; + boolean shouldShowRequestPermissionRationale = false; + for (String permission : requestedPermissions) { + permissionCheck = permissionCheck + checkSelfPermission(permission); + shouldShowRequestPermissionRationale = shouldShowRequestPermissionRationale || shouldShowRequestPermissionRationale(permission); + } + if (permissionCheck != PackageManager.PERMISSION_GRANTED) { + System.out.println("Permission was not granted. Ask for permissions"); + if (shouldShowRequestPermissionRationale) { + requestPermissions(requestedPermissions, requestCode); + } else { + requestPermissions(requestedPermissions, requestCode); + } + } else { + System.out.println("Launching the other activity.."); + launchActivityWithPermissions(); + } + } + + private void launchActivityWithPermissions(){ + finish(); + Intent i = new Intent(this, InterfaceActivity.class); + startActivity(i); + finish(); + } + + private void showMenu(){ + final List avatarOptions = Arrays.asList("\uD83D\uDC66\uD83C\uDFFB Cody","\uD83D\uDC66\uD83C\uDFFF Will","\uD83D\uDC68\uD83C\uDFFD Albert", "\uD83D\uDC7D Being of Light"); + final String[] avatarPaths = { + "http://mpassets.highfidelity.com/8c859fca-4cbd-4e82-aad1-5f4cb0ca5d53-v1/cody.fst", + "http://mpassets.highfidelity.com/d029ae8d-2905-4eb7-ba46-4bd1b8cb9d73-v1/4618d52e711fbb34df442b414da767bb.fst", + "http://mpassets.highfidelity.com/1e57c395-612e-4acd-9561-e79dbda0bc49-v1/albert.fst" }; + + final String pathForJson = "/data/data/io.highfidelity.hifiinterface/files/.config/High Fidelity - dev/"; + new AlertDialog.Builder(this) + .setTitle("Pick an avatar") + .setItems(avatarOptions.toArray(new CharSequence[avatarOptions.size()]),new DialogInterface.OnClickListener(){ + + @Override + public void onClick(DialogInterface dialog, int which) { + if(which < avatarPaths.length ) { + JSONObject obj = new JSONObject(); + try { + obj.put("firstRun",false); + obj.put("Avatar/fullAvatarURL", avatarPaths[which]); + File directory = new File(pathForJson); + + if(!directory.exists()) directory.mkdirs(); + + File file = new File(pathForJson + "Interface.json"); + Writer output = new BufferedWriter(new FileWriter(file)); + output.write(obj.toString().replace("\\","")); + output.close(); + System.out.println("I Could write config file expect to see the selected avatar"+obj.toString().replace("\\","")); + + } catch (JSONException e) { + System.out.println("JSONException something weired went wrong"); + } catch (IOException e) { + System.out.println("Could not write file :("); + } + } else { + System.out.println("Default avatar selected..."); + } + launchActivityWithPermissions(); + } + }).show(); + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + int permissionCheck = PackageManager.PERMISSION_GRANTED; + for (int permission : grantResults) { + permissionCheck = permissionCheck + permission; + } + if ((grantResults.length > 0) && permissionCheck == PackageManager.PERMISSION_GRANTED) { + launchActivityWithPermissions(); + } else if (grantResults.length > 0) { + System.out.println("User has deliberately denied Permissions. Launching anyways"); + launchActivityWithPermissions(); + } + } + + +} diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/WebViewActivity.java b/android/app/src/main/java/io/highfidelity/hifiinterface/WebViewActivity.java new file mode 100644 index 0000000000..4d706248d8 --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/WebViewActivity.java @@ -0,0 +1,242 @@ +// +// WebViewActivity.java +// interface/java +// +// Created by Cristian Duarte and Gabriel Calero on 8/17/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +//package hifiinterface.highfidelity.io.mybrowserapplication; +package io.highfidelity.hifiinterface; + +import android.app.ActionBar; +import android.app.Activity; +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.net.http.SslError; +import android.os.Bundle; +import android.util.Log; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.webkit.SslErrorHandler; +import android.webkit.WebChromeClient; +import android.webkit.WebResourceError; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.ProgressBar; +import android.widget.Toast; +import android.widget.Toolbar; +import android.os.Looper; +import java.lang.Thread; +import java.lang.Runnable; + +import java.net.MalformedURLException; +import java.net.URL; + +public class WebViewActivity extends Activity { + + public static final String WEB_VIEW_ACTIVITY_EXTRA_URL = "url"; + + private native void nativeProcessURL(String url); + + private WebView myWebView; + private ProgressBar mProgressBar; + private ActionBar mActionBar; + private String mUrl; + + enum SafenessLevel { + NOT_ANALYZED_YET(""), + NOT_SECURE(""), + SECURE("\uD83D\uDD12 "), + BAD_SECURE("\uD83D\uDD13 "); + + String icon; + SafenessLevel(String icon) { + this.icon = icon; + } + } + + private SafenessLevel safenessLevel = SafenessLevel.NOT_ANALYZED_YET; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_web_view); + + setActionBar((Toolbar) findViewById(R.id.toolbar_actionbar)); + mActionBar = getActionBar(); + mActionBar.setDisplayHomeAsUpEnabled(true); + + mProgressBar = (ProgressBar) findViewById(R.id.toolbarProgressBar); + + mUrl = getIntent().getStringExtra(WEB_VIEW_ACTIVITY_EXTRA_URL); + myWebView = (WebView) findViewById(R.id.web_view); + myWebView.setWebViewClient(new HiFiWebViewClient()); + myWebView.setWebChromeClient(new HiFiWebChromeClient()); + WebSettings webSettings = myWebView.getSettings(); + webSettings.setJavaScriptEnabled(true); + webSettings.setBuiltInZoomControls(true); + webSettings.setDisplayZoomControls(false); + myWebView.loadUrl(mUrl); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + // Check if the key event was the Back button and if there's history + if ((keyCode == KeyEvent.KEYCODE_BACK) && myWebView.canGoBack()) { + myWebView.goBack(); + return true; + } + // If it wasn't the Back key or there's no web page history, bubble up to the default + // system behavior (probably exit the activity) + return super.onKeyDown(keyCode, event); + } + + private void showSubtitleWithUrl(String url) { + try { + mActionBar.setSubtitle(safenessLevel.icon + new URL(url.toString()).getHost()); + } catch (MalformedURLException e) { + Toast.makeText(WebViewActivity.this, "Error loading page: " + "bad url", Toast.LENGTH_LONG).show(); + Log.e("openUrl", "bad url"); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.web_view_menu, menu); + return true; + } + + private String intentUrlOrWebUrl() { + return myWebView==null || myWebView.getUrl()==null?mUrl:myWebView.getUrl(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item){ + switch (item.getItemId()) { + // Respond to the action bar's Up/Home/back button + case android.R.id.home: + finish(); + break; + case R.id.action_browser: + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(intentUrlOrWebUrl())); + startActivity(i); + return true; + case R.id.action_share: + Intent is = new Intent(Intent.ACTION_SEND); + is.setType("text/plain"); + is.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.web_view_action_share_subject)); + is.putExtra(Intent.EXTRA_TEXT, intentUrlOrWebUrl()); + startActivity(Intent.createChooser(is, getString(R.string.web_view_action_share_chooser))); + return true; + } + return super.onOptionsItemSelected(item); + } + + class HiFiWebViewClient extends WebViewClient { + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + mProgressBar.setVisibility(View.GONE); + if (safenessLevel!=SafenessLevel.BAD_SECURE) { + if (url.startsWith("https:")) { + safenessLevel=SafenessLevel.SECURE; + } else { + safenessLevel=SafenessLevel.NOT_SECURE; + } + } + showSubtitleWithUrl(url); + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + super.onPageStarted(view, url, favicon); + safenessLevel = SafenessLevel.NOT_ANALYZED_YET; + mProgressBar.setVisibility(View.VISIBLE); + mProgressBar.setProgress(0); + showSubtitleWithUrl(url); + } + + @Override + public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { + Toast.makeText(WebViewActivity.this, "Error loading page: " + error.getDescription(), Toast.LENGTH_LONG).show(); + if (ERROR_FAILED_SSL_HANDSHAKE == error.getErrorCode()) { + safenessLevel = SafenessLevel.BAD_SECURE; + } + } + + @Override + public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) { + Toast.makeText(WebViewActivity.this, "Network Error loading page: " + errorResponse.getReasonPhrase(), Toast.LENGTH_LONG).show(); + } + + @Override + public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { + super.onReceivedSslError(view, handler, error); + Toast.makeText(WebViewActivity.this, "SSL error loading page: " + error.toString(), Toast.LENGTH_LONG).show(); + safenessLevel = SafenessLevel.BAD_SECURE; + } + + private boolean isFst(WebResourceRequest request) { + return isFst(request.getUrl().toString()); + } + + private boolean isFst(String url) { + return url.endsWith(".fst"); + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + // managing avatar selections + if (isFst(request)) { + final String url = request.getUrl().toString(); + new Thread(new Runnable() { + public void run() { + nativeProcessURL(url); + } + }).start(); // Avoid deadlock in Qt dialog + WebViewActivity.this.finish(); + return true; + } + return super.shouldOverrideUrlLoading(view, request); + } + + @Override + public void onLoadResource(WebView view, String url) { + if (isFst(url)) { + // processed separately + } else { + super.onLoadResource(view, url); + } + } + } + + class HiFiWebChromeClient extends WebChromeClient { + + @Override + public void onProgressChanged(WebView view, int newProgress) { + super.onProgressChanged(view, newProgress); + mProgressBar.setProgress(newProgress); + } + + @Override + public void onReceivedTitle(WebView view, String title) { + super.onReceivedTitle(view, title); + mActionBar.setTitle(title); + } + + } +} diff --git a/android/app/src/main/res/drawable/ic_close_black_24dp.xml b/android/app/src/main/res/drawable/ic_close_black_24dp.xml new file mode 100644 index 0000000000..ede4b7108d --- /dev/null +++ b/android/app/src/main/res/drawable/ic_close_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/layout/activity_web_view.xml b/android/app/src/main/res/layout/activity_web_view.xml new file mode 100644 index 0000000000..1e30b58676 --- /dev/null +++ b/android/app/src/main/res/layout/activity_web_view.xml @@ -0,0 +1,34 @@ + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/menu/web_view_menu.xml b/android/app/src/main/res/menu/web_view_menu.xml new file mode 100644 index 0000000000..074e1608c5 --- /dev/null +++ b/android/app/src/main/res/menu/web_view_menu.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/values/dimens.xml b/android/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000000..a9ec657aa9 --- /dev/null +++ b/android/app/src/main/res/values/dimens.xml @@ -0,0 +1,12 @@ + + + + 16dp + 16dp + + + + 14dp + + 12dp + \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 5d6a4c1b99..b8080fae0f 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,3 +1,8 @@ - TestApp + Interface + Open in browser + Share link + Shared a link + Share link + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index 033324ac58..23fe67f029 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -11,5 +11,15 @@ - + + + + + diff --git a/cmake/macros/TargetOpenGL.cmake b/cmake/macros/TargetOpenGL.cmake index 6ad92259bb..b9de4e6253 100644 --- a/cmake/macros/TargetOpenGL.cmake +++ b/cmake/macros/TargetOpenGL.cmake @@ -11,7 +11,10 @@ macro(TARGET_OPENGL) find_library(OpenGL OpenGL) target_link_libraries(${TARGET_NAME} ${OpenGL}) elseif(ANDROID) - target_link_libraries(${TARGET_NAME} GLESv3 EGL) + find_library(EGL EGL) + find_library(OpenGLES3 GLESv3) + list(APPEND IGNORE_COPY_LIBS ${OpenGLES3} ${OpenGLES2} ${EGL}) + target_link_libraries(${TARGET_NAME} ${OpenGLES3} ${EGL}) else() find_package(OpenGL REQUIRED) if (${OPENGL_INCLUDE_DIR}) diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index e6b3392aad..cba8ce5479 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -58,6 +58,14 @@ else () list(REMOVE_ITEM INTERFACE_SRCS ${SPEECHRECOGNIZER_CPP}) endif () +if (ANDROID) + set(PLATFORM_QT_COMPONENTS AndroidExtras) +# set(PLATFORM_QT_LIBRARIES Qt5::AndroidExtras) +else () + set(PLATFORM_QT_COMPONENTS WebEngine WebEngineWidgets) + set(PLATFORM_QT_LIBRARIES Qt5::WebEngine Qt5::WebEngineWidgets) +endif () + find_package( Qt5 COMPONENTS Gui Multimedia Network OpenGL Qml Quick Script Svg @@ -93,21 +101,7 @@ endif() # setup the android parameters that will help us produce an APK if (ANDROID) - set(ANDROID_APK_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}/apk-build") - set(ANDROID_APK_OUTPUT_DIR "${CMAKE_CURRENT_BINARY_DIR}/apk") - - set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${ANDROID_APK_OUTPUT_DIR}/libs/${ANDROID_ABI}") - set(ANDROID_SDK_ROOT $ENV{ANDROID_HOME}) - set(ANDROID_APP_DISPLAY_NAME Interface) - set(ANDROID_API_LEVEL 19) - set(ANDROID_APK_PACKAGE io.highfidelity.interface) - set(ANDROID_ACTIVITY_NAME io.highfidelity.interface.InterfaceActivity) - set(ANDROID_APK_VERSION_NAME "0.1") - set(ANDROID_APK_VERSION_CODE 1) - set(ANDROID_APK_FULLSCREEN TRUE) - set(ANDROID_DEPLOY_QT_INSTALL "--install") - set(BUILD_SHARED_LIBS ON) endif () @@ -203,6 +197,13 @@ if (WIN32) set_property(TARGET ${TARGET_NAME} APPEND_STRING PROPERTY LINK_FLAGS_DEBUG "/OPT:NOREF /OPT:NOICF") endif() +if (NOT ANDROID) + set(NON_ANDROID_LIBRARIES gpu-gl) +else() + set(ANDROID_LIBRARIES gpu-gles) +endif () + + # link required hifi libraries link_hifi_libraries( shared octree ktx gpu gl procedural model render @@ -212,6 +213,8 @@ link_hifi_libraries( render-utils entities-renderer avatars-renderer ui auto-updater midi controllers plugins image trackers ui-plugins display-plugins input-plugins + ${ANDROID_LIBRARIES} + ${NON_ANDROID_LIBRARIES} ${PLATFORM_GL_BACKEND} ) @@ -262,15 +265,21 @@ endforeach() # include headers for interface and InterfaceConfig. include_directories("${PROJECT_SOURCE_DIR}/src") +if (ANDROID) + #set(ANDROID_PLATFORM_QT_LIBRARIES Qt5::WebView) +else() + set(NON_ANDROID_PLATFORM_QT_LIBRARIES Qt5::WebEngine) +endif () + target_link_libraries( ${TARGET_NAME} Qt5::Gui Qt5::Network Qt5::Multimedia Qt5::OpenGL Qt5::Qml Qt5::Quick Qt5::Script Qt5::Svg - Qt5::WebChannel Qt5::WebEngine - ${PLATFORM_QT_LIBRARIES} + Qt5::WebChannel ${NON_ANDROID_PLATFORM_QT_LIBRARIES} + ${ANDROID_PLATFORM_QT_LIBRARIES} ) -if (UNIX) +if (UNIX AND NOT ANDROID) if (CMAKE_SYSTEM_NAME MATCHES "Linux") # Linux target_link_libraries(${TARGET_NAME} pthread atomic) @@ -278,7 +287,7 @@ if (UNIX) # OSX target_link_libraries(${TARGET_NAME} pthread) endif () -endif(UNIX) +endif() # assume we are using a Qt build without bearer management add_definitions(-DQT_NO_BEARERMANAGEMENT) @@ -363,20 +372,6 @@ if (WIN32) package_libraries_for_deployment() endif() -if (ANDROID) - set(HIFI_URL_INTENT "\ - \n \ - \n \ - \n \ - \n \ - \n " - ) - - set(ANDROID_EXTRA_ACTIVITY_XML "${HIFI_URL_INTENT}") - - qt_create_apk() -endif () - add_dependency_external_projects(GifCreator) find_package(GifCreator REQUIRED) target_include_directories(${TARGET_NAME} PUBLIC ${GIFCREATOR_INCLUDE_DIRS}) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 8e890b6e77..12c8bcfc7d 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -210,7 +210,9 @@ #include "commerce/QmlCommerce.h" #include "webbrowser/WebBrowserSuggestionsEngine.h" - +#ifdef ANDROID +#include +#endif // On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU // FIXME seems to be broken. #if defined(Q_OS_WIN) @@ -232,6 +234,30 @@ extern "C" { } #endif +#ifdef ANDROID +extern "C" { + +JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeOnCreate(JNIEnv* env, jobject obj, jobject instance, jobject asset_mgr) { + qDebug() << "nativeOnCreate On thread " << QThread::currentThreadId(); +} + +JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeOnPause(JNIEnv* env, jobject obj) { + qDebug() << "nativeOnPause"; +} + +JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeOnResume(JNIEnv* env, jobject obj) { + qDebug() << "nativeOnResume"; +} + + + +JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeOnExitVr(JNIEnv* env, jobject obj) { + qDebug() << "nativeOnCreate On thread " << QThread::currentThreadId(); +} + + +} +#endif enum ApplicationEvent { // Execute a lambda function Lambda = QEvent::User + 1, diff --git a/interface/src/scripting/AssetMappingsScriptingInterface.cpp b/interface/src/scripting/AssetMappingsScriptingInterface.cpp index a1b2a9ccfc..5c27de84c9 100644 --- a/interface/src/scripting/AssetMappingsScriptingInterface.cpp +++ b/interface/src/scripting/AssetMappingsScriptingInterface.cpp @@ -37,7 +37,8 @@ AssetMappingsScriptingInterface::AssetMappingsScriptingInterface() { void AssetMappingsScriptingInterface::setMapping(QString path, QString hash, QJSValue callback) { auto assetClient = DependencyManager::get(); auto request = assetClient->createSetMappingRequest(path, hash); - +#ifndef ANDROID +// TODO: just to make android compile connect(request, &SetMappingRequest::finished, this, [this, callback](SetMappingRequest* request) mutable { if (callback.isCallable()) { QJSValueList args { request->getErrorString(), request->getPath() }; @@ -46,6 +47,7 @@ void AssetMappingsScriptingInterface::setMapping(QString path, QString hash, QJS request->deleteLater(); }); +#endif request->start(); } @@ -53,7 +55,8 @@ void AssetMappingsScriptingInterface::setMapping(QString path, QString hash, QJS void AssetMappingsScriptingInterface::getMapping(QString path, QJSValue callback) { auto assetClient = DependencyManager::get(); auto request = assetClient->createGetMappingRequest(path); - +#ifndef ANDROID +// TODO: just to make android compile connect(request, &GetMappingRequest::finished, this, [this, callback](GetMappingRequest* request) mutable { auto hash = request->getHash(); @@ -64,6 +67,7 @@ void AssetMappingsScriptingInterface::getMapping(QString path, QJSValue callback request->deleteLater(); }); +#endif request->start(); } @@ -140,7 +144,8 @@ void AssetMappingsScriptingInterface::uploadFile(QString path, QString mapping, void AssetMappingsScriptingInterface::deleteMappings(QStringList paths, QJSValue callback) { auto assetClient = DependencyManager::get(); auto request = assetClient->createDeleteMappingsRequest(paths); - +#ifndef ANDROID +// TODO: just to make android compile connect(request, &DeleteMappingsRequest::finished, this, [this, callback](DeleteMappingsRequest* request) mutable { if (callback.isCallable()) { QJSValueList args { request->getErrorString() }; @@ -149,6 +154,7 @@ void AssetMappingsScriptingInterface::deleteMappings(QStringList paths, QJSValue request->deleteLater(); }); +#endif request->start(); } @@ -156,7 +162,8 @@ void AssetMappingsScriptingInterface::deleteMappings(QStringList paths, QJSValue void AssetMappingsScriptingInterface::getAllMappings(QJSValue callback) { auto assetClient = DependencyManager::get(); auto request = assetClient->createGetAllMappingsRequest(); - +#ifndef ANDROID +// TODO: just to make android compile connect(request, &GetAllMappingsRequest::finished, this, [this, callback](GetAllMappingsRequest* request) mutable { auto mappings = request->getMappings(); auto map = callback.engine()->newObject(); @@ -172,6 +179,7 @@ void AssetMappingsScriptingInterface::getAllMappings(QJSValue callback) { request->deleteLater(); }); +#endif request->start(); } @@ -179,7 +187,8 @@ void AssetMappingsScriptingInterface::getAllMappings(QJSValue callback) { void AssetMappingsScriptingInterface::renameMapping(QString oldPath, QString newPath, QJSValue callback) { auto assetClient = DependencyManager::get(); auto request = assetClient->createRenameMappingRequest(oldPath, newPath); - +#ifndef ANDROID +// TODO: just to make android compile connect(request, &RenameMappingRequest::finished, this, [this, callback](RenameMappingRequest* request) mutable { if (callback.isCallable()) { QJSValueList args{ request->getErrorString() }; @@ -188,14 +197,15 @@ void AssetMappingsScriptingInterface::renameMapping(QString oldPath, QString new request->deleteLater(); }); - +#endif request->start(); } void AssetMappingsScriptingInterface::setBakingEnabled(QStringList paths, bool enabled, QJSValue callback) { auto assetClient = DependencyManager::get(); auto request = assetClient->createSetBakingEnabledRequest(paths, enabled); - +#ifndef ANDROID +// TODO: just to make android compile connect(request, &SetBakingEnabledRequest::finished, this, [this, callback](SetBakingEnabledRequest* request) mutable { if (callback.isCallable()) { QJSValueList args{ request->getErrorString() }; @@ -204,7 +214,7 @@ void AssetMappingsScriptingInterface::setBakingEnabled(QStringList paths, bool e request->deleteLater(); }); - +#endif request->start(); } diff --git a/interface/src/scripting/LimitlessConnection.h b/interface/src/scripting/LimitlessConnection.h index ee049aff8e..cdb64a8197 100644 --- a/interface/src/scripting/LimitlessConnection.h +++ b/interface/src/scripting/LimitlessConnection.h @@ -15,7 +15,9 @@ #include #include #include - +#ifdef ANDROID +#include +#endif class LimitlessConnection : public QObject { Q_OBJECT public: diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index f991420fe8..2e15f11c3e 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -42,8 +42,9 @@ using namespace std; static Stats* INSTANCE{ nullptr }; +#ifndef ANDROID QString getTextureMemoryPressureModeString(); - +#endif Stats* Stats::getInstance() { if (!INSTANCE) { Stats::registerType(); @@ -359,7 +360,9 @@ void Stats::updateStats(bool force) { STAT_UPDATE(gpuTextureResourceMemory, (int)BYTES_TO_MB(gpu::Context::getTextureResourceGPUMemSize())); STAT_UPDATE(gpuTextureResourcePopulatedMemory, (int)BYTES_TO_MB(gpu::Context::getTextureResourcePopulatedGPUMemSize())); STAT_UPDATE(gpuTextureExternalMemory, (int)BYTES_TO_MB(gpu::Context::getTextureExternalGPUMemSize())); +#ifndef ANDROID STAT_UPDATE(gpuTextureMemoryPressureState, getTextureMemoryPressureModeString()); +#endif STAT_UPDATE(gpuFreeMemory, (int)BYTES_TO_MB(gpu::Context::getFreeGPUMemSize())); STAT_UPDATE(rectifiedTextureCount, (int)RECTIFIED_TEXTURE_COUNT.load()); STAT_UPDATE(decimatedTextureCount, (int)DECIMATED_TEXTURE_COUNT.load()); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index c532e7659f..c41372ee55 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -117,7 +117,7 @@ Avatar::Avatar(QThread* thread) : } Avatar::~Avatar() { - auto treeRenderer = DependencyManager::get(); + auto treeRenderer = DependencyManager::get(); EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; if (entityTree) { entityTree->withWriteLock([&] { @@ -1287,7 +1287,7 @@ int Avatar::parseDataFromBuffer(const QByteArray& buffer) { const float MOVE_DISTANCE_THRESHOLD = 0.001f; _moving = glm::distance(oldPosition, getWorldPosition()) > MOVE_DISTANCE_THRESHOLD; if (_moving) { - addPhysicsFlags(Simulation::DIRTY_POSITION); + addPhysicsFlags(Simulation::DIRTY_POSITION); } if (_moving || _hasNewJointData) { locationChanged(); diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index e646ba27f5..90a4e75669 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -891,10 +891,14 @@ void OpenGLDisplayPlugin::copyTextureToQuickFramebuffer(NetworkTexturePointer ne GLuint fbo[2] {0, 0}; // need mipmaps for blitting texture - glGenerateTextureMipmap(sourceTexture); +#ifndef ANDROID + glGenerateTextureMipmap(sourceTexture); +#endif // create 2 fbos (one for initial texture, second for scaled one) - glCreateFramebuffers(2, fbo); +#ifndef ANDROID + glCreateFramebuffers(2, fbo); +#endif // setup source fbo glBindFramebuffer(GL_FRAMEBUFFER, fbo[0]); @@ -924,7 +928,9 @@ void OpenGLDisplayPlugin::copyTextureToQuickFramebuffer(NetworkTexturePointer ne } else { newY = (target->height() - newHeight) / 2; } - glBlitNamedFramebuffer(fbo[0], fbo[1], 0, 0, texWidth, texHeight, newX, newY, newX + newWidth, newY + newHeight, GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT, GL_NEAREST); +#ifndef ANDROID + glBlitNamedFramebuffer(fbo[0], fbo[1], 0, 0, texWidth, texHeight, newX, newY, newX + newWidth, newY + newHeight, GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT, GL_NEAREST); +#endif // don't delete the textures! glDeleteFramebuffers(2, fbo); diff --git a/libraries/entities-renderer/CMakeLists.txt b/libraries/entities-renderer/CMakeLists.txt index cf94ecc37c..0dd96ff7ab 100644 --- a/libraries/entities-renderer/CMakeLists.txt +++ b/libraries/entities-renderer/CMakeLists.txt @@ -15,4 +15,5 @@ include_hifi_library_headers(avatars) include_hifi_library_headers(controllers) target_bullet() -target_polyvox() \ No newline at end of file +target_polyvox() + diff --git a/libraries/script-engine/src/ArrayBufferClass.h b/libraries/script-engine/src/ArrayBufferClass.h index 69c2cc0799..d65693bece 100644 --- a/libraries/script-engine/src/ArrayBufferClass.h +++ b/libraries/script-engine/src/ArrayBufferClass.h @@ -19,6 +19,7 @@ #include #include #include +#include class ScriptEngine; diff --git a/libraries/script-engine/src/ConsoleScriptingInterface.cpp b/libraries/script-engine/src/ConsoleScriptingInterface.cpp index f3ceee63f7..1e0ca2e42f 100644 --- a/libraries/script-engine/src/ConsoleScriptingInterface.cpp +++ b/libraries/script-engine/src/ConsoleScriptingInterface.cpp @@ -17,6 +17,7 @@ #include "ConsoleScriptingInterface.h" #include "ScriptEngine.h" +#include #define INDENTATION 4 // 1 Tab - 4 spaces const QString LINE_SEPARATOR = "\n "; diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index 902f91f9b8..aa64610930 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -282,7 +282,7 @@ private: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f); -#if !defined(Q_OS_ANDROID) +#ifndef ANDROID glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -0.2f); #endif glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f); @@ -450,7 +450,7 @@ void initializeQmlEngine(QQmlEngine* engine, QQuickWindow* window) { if (!javaScriptToInject.isEmpty()) { rootContext->setContextProperty("eventBridgeJavaScriptToInject", QVariant(javaScriptToInject)); } -#if !defined(Q_OS_ANDROID) +#ifndef ANDROID rootContext->setContextProperty("FileTypeProfile", new FileTypeProfile(rootContext)); rootContext->setContextProperty("HFWebEngineProfile", new HFWebEngineProfile(rootContext)); #endif @@ -554,7 +554,9 @@ void OffscreenQmlSurface::render() { GLuint texture = offscreenTextures.getNextTexture(_size); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo); - glFramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0); +#ifndef ANDROID + glFramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0); +#endif glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); _renderControl->render(); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); diff --git a/libraries/ui/src/ui/types/FileTypeProfile.cpp b/libraries/ui/src/ui/types/FileTypeProfile.cpp index 90a2c6ba18..d31f09a981 100644 --- a/libraries/ui/src/ui/types/FileTypeProfile.cpp +++ b/libraries/ui/src/ui/types/FileTypeProfile.cpp @@ -16,13 +16,18 @@ #if !defined(Q_OS_ANDROID) static const QString QML_WEB_ENGINE_STORAGE_NAME = "qmlWebEngine"; -FileTypeProfile::FileTypeProfile(QObject* parent) : - QQuickWebEngineProfile(parent) +FileTypeProfile::FileTypeProfile(QObject* parent) +#ifndef ANDROID + : QQuickWebEngineProfile(parent) +#endif { static const QString WEB_ENGINE_USER_AGENT = "Chrome/48.0 (HighFidelityInterface)"; +#ifndef ANDROID setHttpUserAgent(WEB_ENGINE_USER_AGENT); auto requestInterceptor = new FileTypeRequestInterceptor(this); setRequestInterceptor(requestInterceptor); +#endif + } #endif \ No newline at end of file diff --git a/libraries/ui/src/ui/types/FileTypeRequestInterceptor.cpp b/libraries/ui/src/ui/types/FileTypeRequestInterceptor.cpp index 25866ad395..7178bc89ac 100644 --- a/libraries/ui/src/ui/types/FileTypeRequestInterceptor.cpp +++ b/libraries/ui/src/ui/types/FileTypeRequestInterceptor.cpp @@ -22,4 +22,4 @@ void FileTypeRequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo& info RequestFilters::interceptFileType(info); } -#endif \ No newline at end of file +#endif diff --git a/libraries/ui/src/ui/types/HFTabletWebEngineRequestInterceptor.h b/libraries/ui/src/ui/types/HFTabletWebEngineRequestInterceptor.h index e38549937e..5c5f30ce0b 100644 --- a/libraries/ui/src/ui/types/HFTabletWebEngineRequestInterceptor.h +++ b/libraries/ui/src/ui/types/HFTabletWebEngineRequestInterceptor.h @@ -11,14 +11,26 @@ #ifndef hifi_HFTabletWebEngineRequestInterceptor_h #define hifi_HFTabletWebEngineRequestInterceptor_h +#include +#ifndef ANDROID #include +#endif -class HFTabletWebEngineRequestInterceptor : public QWebEngineUrlRequestInterceptor { +class HFTabletWebEngineRequestInterceptor +#ifndef ANDROID + : public QWebEngineUrlRequestInterceptor +#endif +{ public: - HFTabletWebEngineRequestInterceptor(QObject* parent) : QWebEngineUrlRequestInterceptor(parent) {}; - - virtual void interceptRequest(QWebEngineUrlRequestInfo& info) override; + HFTabletWebEngineRequestInterceptor(QObject* parent) +#ifndef ANDROID + : QWebEngineUrlRequestInterceptor(parent) +#endif + {}; +#ifndef ANDROID + virtual void interceptRequest(QWebEngineUrlRequestInfo& info) override; +#endif }; #endif // hifi_HFWebEngineRequestInterceptor_h diff --git a/libraries/ui/src/ui/types/HFWebEngineProfile.cpp b/libraries/ui/src/ui/types/HFWebEngineProfile.cpp index 381bdb10bd..a443a7c160 100644 --- a/libraries/ui/src/ui/types/HFWebEngineProfile.cpp +++ b/libraries/ui/src/ui/types/HFWebEngineProfile.cpp @@ -17,14 +17,16 @@ static const QString QML_WEB_ENGINE_STORAGE_NAME = "qmlWebEngine"; -HFWebEngineProfile::HFWebEngineProfile(QObject* parent) : - QQuickWebEngineProfile(parent) +HFWebEngineProfile::HFWebEngineProfile(QObject* parent) +#ifndef ANDROID + : QQuickWebEngineProfile(parent) +#endif { - setStorageName(QML_WEB_ENGINE_STORAGE_NAME); - +#ifndef ANDROID setStorageName(QML_WEB_ENGINE_STORAGE_NAME); // we use the HFWebEngineRequestInterceptor to make sure that web requests are authenticated for the interface user - auto requestInterceptor = new HFWebEngineRequestInterceptor(this); + auto requestInterceptor = new HFWebEngineRequestInterceptor(this); setRequestInterceptor(requestInterceptor); +#endif } #endif \ No newline at end of file diff --git a/libraries/ui/src/ui/types/HFWebEngineRequestInterceptor.cpp b/libraries/ui/src/ui/types/HFWebEngineRequestInterceptor.cpp index 5a11c32efa..cecdddd2b6 100644 --- a/libraries/ui/src/ui/types/HFWebEngineRequestInterceptor.cpp +++ b/libraries/ui/src/ui/types/HFWebEngineRequestInterceptor.cpp @@ -22,4 +22,4 @@ void HFWebEngineRequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo& i RequestFilters::interceptHFWebEngineRequest(info); } -#endif \ No newline at end of file +#endif diff --git a/libraries/ui/src/ui/types/RequestFilters.cpp b/libraries/ui/src/ui/types/RequestFilters.cpp index 4cd51c6d98..0553d94df5 100644 --- a/libraries/ui/src/ui/types/RequestFilters.cpp +++ b/libraries/ui/src/ui/types/RequestFilters.cpp @@ -79,4 +79,4 @@ void RequestFilters::interceptFileType(QWebEngineUrlRequestInfo& info) { info.setHttpHeader(CONTENT_HEADER.toLocal8Bit(), TYPE_VALUE.toLocal8Bit()); } } -#endif \ No newline at end of file +#endif From dad526d22b3a888b69edd02e7015bb5708b86bad Mon Sep 17 00:00:00 2001 From: Gabriel Calero Date: Mon, 18 Dec 2017 15:35:34 -0300 Subject: [PATCH 02/17] Make Fade.slh compatible with Open GLES SL --- libraries/render-utils/src/Fade.slh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/render-utils/src/Fade.slh b/libraries/render-utils/src/Fade.slh index b85a824ca3..4bbca70b83 100644 --- a/libraries/render-utils/src/Fade.slh +++ b/libraries/render-utils/src/Fade.slh @@ -34,7 +34,7 @@ vec2 hash2D(vec3 position) { } float noise3D(vec3 position) { - float n = textureLod(fadeMaskMap, hash2D(position), 0).r; + float n = textureLod(fadeMaskMap, hash2D(position), 0.0).r; return pow(n, 1.0/2.2); // Remove sRGB. Need to fix this later directly in the texture } @@ -44,7 +44,7 @@ float evalFadeNoiseGradient(FadeObjectParams params, vec3 position) { vec3 noisePositionFloored = floor(noisePosition); vec3 noisePositionFraction = fract(noisePosition); - noisePositionFraction = noisePositionFraction*noisePositionFraction*(3 - 2*noisePositionFraction); + noisePositionFraction = noisePositionFraction*noisePositionFraction*(3.0 - 2.0*noisePositionFraction); float noiseLowXLowYLowZ = noise3D(noisePositionFloored); float noiseLowXHighYLowZ = noise3D(noisePositionFloored+vec3(0,1,0)); @@ -84,7 +84,7 @@ float evalFadeAlpha(FadeObjectParams params, vec3 position) { } void applyFadeClip(FadeObjectParams params, vec3 position) { - if (evalFadeAlpha(params, position) < 0) { + if (evalFadeAlpha(params, position) < 0.0) { discard; } } @@ -95,12 +95,12 @@ void applyFade(FadeObjectParams params, vec3 position, out vec3 emissive) { alpha = -alpha; } - if (alpha < 0) { + if (alpha < 0.0) { discard; } float edgeMask = alpha * fadeParameters[params.category]._edgeWidthInvWidth.y; - float edgeAlpha = 1.0-clamp(edgeMask, 0, 1); + float edgeAlpha = 1.0-clamp(edgeMask, 0.0, 1.0); edgeMask = step(edgeMask, 1.f); edgeAlpha *= edgeAlpha; // Square to have a nice ease out From ad7cb24ea3e5dce7dd2afb0ca3f510d4185de1e3 Mon Sep 17 00:00:00 2001 From: Gabriel Calero Date: Mon, 18 Dec 2017 15:38:48 -0300 Subject: [PATCH 03/17] Restore interface icon and name for android --- android/app/src/main/AndroidManifest.xml | 1 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3418 -> 5195 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 4208 -> 5023 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2206 -> 4333 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 2555 -> 4369 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 4842 -> 5931 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 6114 -> 5635 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 7718 -> 7424 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 10056 -> 6905 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 10486 -> 9114 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 14696 -> 8380 bytes 11 files changed, 1 insertion(+) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 8e3b89d605..4113f047fa 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ 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 V|G`l9k=ehub4}L-TYVKD{vUGiNFD$H diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png index 9a078e3e1a42d474c78470a73c7987cf7ac5d9a0..81a137957d489151137f4ec48490fed96ddf0373 100644 GIT binary patch literal 5023 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 O00000Dy!50Qvv`0D$NK z0Cg|`0P0`>06Lfe02gqax=}m;000JJOGiWi000000Qp0^e~}>MS-S8v^6viCUNk(br)=}e|C26>BG$I&hPFl z0n`T$T6T8sx%YnGfBCuR-fQ57+>q-c;^Dmy#{tlFn2UDq+?oI&A_0q{^MgM=w4=Lk z=F+#H+R}85Nzenk*Lo_T5=IGNOF6#e7|Ab=#vPg+1p80xx>*IZusDyUuUg;(%FCI%8vBr1IvaC$*~_j^vwQfCgoNV##{y$v9ZI51Ojx*&gRsY3YYnp zhxY#U)R{A93IN2u-`)Rgsu7QzIQq#Gzxi?h3joHirUw8(jmP7ydyZ^)t-htU>%uA7 z*DzmcPc+efq3H9#Heg%)1OR}Ui|uSEqvILgH8Elgf3M`+?Mqu`E)Qnk92uF6Wc_Lg(7pGrN^Smr&zrS3$&Epl4U#B2PD#|Gr}ORw zt1~09e^)YtnMfp}5TK14SEbfHx#-VYLiYGkMhEE)pFltwZdumTus$;ydolAtWfB$Z zKu1SMY|W!>zgKH`xoy>=WjQElsvzWjvc}U==y~w*#jhYj(X2#3*?ypBPkb)*x`2m* zcY?dJJh1yF>UhKVUf#456{4hEBkg*2YgglEf5j0WL%?#|Ov4#x3U}ZGM+Hgn)2akiH#X zXxRn;XvAWqWApI(;Qp$tzjomcG$h&p08Pmy$mwGko;dCE#o{}YzXGrtKt5=kun-d8 ze>u3hS4(ggn0<-blx%nXzA14l1Sz55+0$PYu_E~5n6*m+`^VGNv*U+fS&5LE6-Gd9 zT0;ALK&;P5Zou_^bn0J_Y(xNWKI<}%|4e`A(z}@ZXCM#;qxm*<9{??=Jtzn0YmcvO zThyI=CWIWu^P`w5WD!$pF_IfV|HZ?|f6Hl<7dN?&Cr=};q>z_WI5qkzE{vZH?WxAm zme=Z^JAd-yNncxlzG&-8uB!y%T=oRcWlt2X6sR3gBkVUheGJEke_<<@9R~nz?@IT* z4IK7bA}EBEM5B6NDPWu*FBpyp-n-RsDTab})|+Ir*b-#$AOc!4MPCXV(D^TPf8qe} z-jPDVhRB?`T5lu*D^_&WYH_M%aAd$v;9=kc%U+q#@m71KMx>W6ZAnxjK&_qisftKL zz=v76+RzD92T-@7p%$Ly)aR0TN{!{Rg<(UI5M;QNB`e`sS^Q~)E3kQfAY$`$20((? z-`3@1;Dc^J zK9vWG9aTVR))dSR>%&?)-PV_0t5*ZQ8V}@kougifD5{B07Od3v$8;$SwM9!PeL(kf|RAuAv;N_6#FElv!a!a_aN|W5AG3P2U7FQ(-{Cn(oQEA!7>Z(V7})h~gZCdy**8`g z_?a0w@t0%gFMBcgZY9RYXGt2`KNfbTzpQG)zke+wWNp~&g@?&p_t_CTQLXOO%1!E% zum5b|8y~!X=_~-gFgH3dFwL6zQUCw|07*qoM6N<$g8EDc A%K!iX diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png index efc028a636dd690a51db5a525cf781a5a7daba68..d032c300142532c9cd1d2679277d365c20f4e964 100644 GIT binary patch literal 4369 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 IM6N<$f~U;Z8~^|S diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index bfa42f0e7b91d006d22352c9ff2f134e504e3c1d..4a018d4ed9b066871679478f72e9a0bfee35c215 100644 GIT binary patch literal 5931 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 N002ovPDHLkV1nK$3jhEB literal 4842 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 diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png index 3af2608a4492ef9ae63a77ec3305aedda89594cb..3cccf1037b45e43400c9648cdc9cc129d8df2e86 100644 GIT binary patch literal 5635 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 literal 6114 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 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 324e72cdd7480cb983fa1bcc7ce686e51ef87fe7..e97062e0ee222ba85204e3e6b2758c622eddd6d1 100644 GIT binary patch literal 7424 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

TYSD#W4maIvLbRYGJNG}V|+SXfTh>av49 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 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index 9bec2e623103ac9713b00cad8502a057c1efda61..8d142881dae7d5d1812966ac692d219a176ef69d 100644 GIT binary patch literal 6905 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 literal 10056 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 Tu8OV$002TsQ?W|kJmh}>;TD+- literal 14696 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 From 468fc89c3ee8d58606b81398e2f41c64abf05cbd Mon Sep 17 00:00:00 2001 From: Gabriel Calero Date: Mon, 18 Dec 2017 16:10:19 -0300 Subject: [PATCH 04/17] Use forward pipeline for android --- interface/src/Application.cpp | 34 ++++++++++++++----- interface/src/SecondaryCamera.cpp | 8 +++-- interface/src/SecondaryCamera.h | 2 +- libraries/gl/src/gl/Config.cpp | 6 ++-- libraries/gl/src/gl/Config.h | 12 +++++-- libraries/gpu-gles/src/gpu/gl/GLBackend.cpp | 22 ++++++++++++ libraries/gpu-gles/src/gpu/gl/GLBackend.h | 16 +++++++++ .../gpu-gles/src/gpu/gl/GLBackendPipeline.cpp | 30 ++++++++++++++++ libraries/gpu-gles/src/gpu/gles/GLESBackend.h | 4 +++ .../src/gpu/gles/GLESBackendBuffer.cpp | 24 +++++++++++++ .../render-utils/src/RenderForwardTask.cpp | 6 ++-- libraries/shared/src/shared/FileUtils.cpp | 4 +++ 12 files changed, 147 insertions(+), 21 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 12c8bcfc7d..9127381bde 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -663,11 +663,11 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(ScriptEngine::CLIENT_SCRIPT); DependencyManager::set(); - DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(NodeType::Agent, listenPort); + DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -845,7 +845,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo #endif _logger = new FileLogger(this); +#ifndef Q_OS_ANDROID + // this prevents using logcat qInstallMessageHandler(messageHandler); +#endif QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "styles/Inconsolata.otf"); _window->setWindowTitle("High Fidelity Interface"); @@ -2201,6 +2204,7 @@ void Application::initializeGL() { _isGLInitialized = true; } + gl::initModuleGl(); _glWidget->makeCurrent(); _chromiumShareContext = new OffscreenGLCanvas(); _chromiumShareContext->setObjectName("ChromiumShareContext"); @@ -2223,9 +2227,15 @@ void Application::initializeGL() { // Set up the render engine render::CullFunctor cullFunctor = LODManager::shouldRender; static const QString RENDER_FORWARD = "HIFI_RENDER_FORWARD"; +#ifndef Q_OS_ANDROID bool isDeferred = !QProcessEnvironment::systemEnvironment().contains(RENDER_FORWARD); +#else + bool isDeferred = false; +#endif _renderEngine->addJob("UpdateScene"); - _renderEngine->addJob("SecondaryCameraJob", cullFunctor); +#ifndef Q_OS_ANDROID + _renderEngine->addJob("SecondaryCameraJob", cullFunctor, isDeferred); +#endif _renderEngine->addJob("RenderMainView", cullFunctor, isDeferred); _renderEngine->load(); _renderEngine->registerScene(_main3DScene); @@ -3907,12 +3917,18 @@ void Application::idle() { PROFILE_COUNTER_IF_CHANGED(app, "pendingProcessing", int, DependencyManager::get()->getStat("PendingProcessing").toInt()); auto renderConfig = _renderEngine->getConfiguration(); PROFILE_COUNTER_IF_CHANGED(render, "gpuTime", float, (float)_gpuContext->getFrameTimerGPUAverage()); + auto opaqueRangeTimer = renderConfig->getConfig("OpaqueRangeTimer"); + auto linearDepth = renderConfig->getConfig("LinearDepth"); + auto surfaceGeometry = renderConfig->getConfig("SurfaceGeometry"); + auto renderDeferred = renderConfig->getConfig("RenderDeferred"); + auto toneAndPostRangeTimer = renderConfig->getConfig("ToneAndPostRangeTimer"); + PROFILE_COUNTER(render_detail, "gpuTimes", { - { "OpaqueRangeTimer", renderConfig->getConfig("OpaqueRangeTimer")->property("gpuRunTime") }, - { "LinearDepth", renderConfig->getConfig("LinearDepth")->property("gpuRunTime") }, - { "SurfaceGeometry", renderConfig->getConfig("SurfaceGeometry")->property("gpuRunTime") }, - { "RenderDeferred", renderConfig->getConfig("RenderDeferred")->property("gpuRunTime") }, - { "ToneAndPostRangeTimer", renderConfig->getConfig("ToneAndPostRangeTimer")->property("gpuRunTime") } + { "OpaqueRangeTimer", opaqueRangeTimer ? opaqueRangeTimer->property("gpuRunTime") : 0 }, + { "LinearDepth", linearDepth ? linearDepth->property("gpuRunTime") : 0 }, + { "SurfaceGeometry", surfaceGeometry ? surfaceGeometry->property("gpuRunTime") : 0 }, + { "RenderDeferred", renderDeferred ? renderDeferred->property("gpuRunTime") : 0 }, + { "ToneAndPostRangeTimer", toneAndPostRangeTimer ? toneAndPostRangeTimer->property("gpuRunTime") : 0 } }); PROFILE_RANGE(app, __FUNCTION__); @@ -4277,9 +4293,9 @@ void Application::init() { // Make sure Login state is up to date DependencyManager::get()->toggleLoginDialog(); - +#ifndef Q_OS_ANDROID DependencyManager::get()->init(); - +#endif DependencyManager::get()->init(); _timerStart.start(); diff --git a/interface/src/SecondaryCamera.cpp b/interface/src/SecondaryCamera.cpp index 4cfa4d6156..5db34c9441 100644 --- a/interface/src/SecondaryCamera.cpp +++ b/interface/src/SecondaryCamera.cpp @@ -203,10 +203,14 @@ public: } }; -void SecondaryCameraRenderTask::build(JobModel& task, const render::Varying& inputs, render::Varying& outputs, render::CullFunctor cullFunctor) { +void SecondaryCameraRenderTask::build(JobModel& task, const render::Varying& inputs, render::Varying& outputs, render::CullFunctor cullFunctor, bool isDeferred) { const auto cachedArg = task.addJob("SecondaryCamera"); const auto items = task.addJob("FetchCullSort", cullFunctor); assert(items.canCast()); - task.addJob("RenderDeferredTask", items); + if (!isDeferred) { + task.addJob("Forward", items); + } else { + task.addJob("RenderDeferredTask", items); + } task.addJob("EndSecondaryCamera", cachedArg); } \ No newline at end of file diff --git a/interface/src/SecondaryCamera.h b/interface/src/SecondaryCamera.h index 38d75c391e..026b72d865 100644 --- a/interface/src/SecondaryCamera.h +++ b/interface/src/SecondaryCamera.h @@ -71,7 +71,7 @@ public: using JobModel = render::Task::Model; SecondaryCameraRenderTask() {} void configure(const Config& config) {} - void build(JobModel& task, const render::Varying& inputs, render::Varying& outputs, render::CullFunctor cullFunctor); + void build(JobModel& task, const render::Varying& inputs, render::Varying& outputs, render::CullFunctor cullFunctor, bool isDeferred = true); }; #endif diff --git a/libraries/gl/src/gl/Config.cpp b/libraries/gl/src/gl/Config.cpp index 1f29fe21b1..d862149a44 100644 --- a/libraries/gl/src/gl/Config.cpp +++ b/libraries/gl/src/gl/Config.cpp @@ -14,9 +14,9 @@ #include #if defined(Q_OS_ANDROID) -PFNGLQUERYCOUNTEREXTPROC glQueryCounterEXT = NULL; -PFNGLGETQUERYOBJECTUI64VEXTPROC glGetQueryObjectui64vEXT = NULL; -PFNGLFRAMEBUFFERTEXTUREEXTPROC glFramebufferTextureEXT = NULL; +PFNGLQUERYCOUNTEREXTPROC __glQueryCounterEXT = NULL; +PFNGLGETQUERYOBJECTUI64VEXTPROC __glGetQueryObjectui64vEXT = NULL; +PFNGLFRAMEBUFFERTEXTUREEXTPROC __glFramebufferTextureEXT = NULL; #endif void gl::initModuleGl() { diff --git a/libraries/gl/src/gl/Config.h b/libraries/gl/src/gl/Config.h index b1bafe1ba6..b38473c18a 100644 --- a/libraries/gl/src/gl/Config.h +++ b/libraries/gl/src/gl/Config.h @@ -50,9 +50,15 @@ extern "C" { typedef void (GL_APIENTRYP PFNGLQUERYCOUNTEREXTPROC) (GLuint id, GLenum target); typedef void (GL_APIENTRYP PFNGLGETQUERYOBJECTUI64VEXTPROC) (GLuint id, GLenum pname, GLuint64 *params); typedef void (GL_APIENTRYP PFNGLFRAMEBUFFERTEXTUREEXTPROC) (GLenum target, GLenum attachment, GLuint texture, GLint level); - extern PFNGLQUERYCOUNTEREXTPROC glQueryCounterEXT; - extern PFNGLGETQUERYOBJECTUI64VEXTPROC glGetQueryObjectui64vEXT; - extern PFNGLFRAMEBUFFERTEXTUREEXTPROC glFramebufferTextureEXT; + + + #define glQueryCounterEXT __glQueryCounterEXT + #define glGetQueryObjectui64vEXT __glGetQueryObjectui64vEXT + #define glFramebufferTextureEXT __glFramebufferTextureEXT + + extern PFNGLQUERYCOUNTEREXTPROC __glQueryCounterEXT; + extern PFNGLGETQUERYOBJECTUI64VEXTPROC __glGetQueryObjectui64vEXT; + extern PFNGLFRAMEBUFFERTEXTUREEXTPROC __glFramebufferTextureEXT; } #else // !defined(Q_OS_ANDROID) diff --git a/libraries/gpu-gles/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gles/src/gpu/gl/GLBackend.cpp index cb00d00b3e..c42d4008f9 100644 --- a/libraries/gpu-gles/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gles/src/gpu/gl/GLBackend.cpp @@ -94,6 +94,7 @@ GLBackend::CommandCall GLBackend::_commandCalls[Batch::NUM_COMMANDS] = (&::gpu::gl::GLBackend::do_setStateScissorRect), (&::gpu::gl::GLBackend::do_setUniformBuffer), + (&::gpu::gl::GLBackend::do_setResourceBuffer), (&::gpu::gl::GLBackend::do_setResourceTexture), (&::gpu::gl::GLBackend::do_setFramebuffer), @@ -107,6 +108,11 @@ GLBackend::CommandCall GLBackend::_commandCalls[Batch::NUM_COMMANDS] = (&::gpu::gl::GLBackend::do_resetStages), + (&::gpu::gl::GLBackend::do_disableContextViewCorrection), + (&::gpu::gl::GLBackend::do_restoreContextViewCorrection), + (&::gpu::gl::GLBackend::do_disableContextStereo), + (&::gpu::gl::GLBackend::do_restoreContextStereo), + (&::gpu::gl::GLBackend::do_runLambda), (&::gpu::gl::GLBackend::do_startNamedCall), @@ -333,6 +339,22 @@ void GLBackend::do_resetStages(const Batch& batch, size_t paramOffset) { resetStages(); } +void GLBackend::do_disableContextViewCorrection(const Batch& batch, size_t paramOffset) { + _transform._viewCorrectionEnabled = false; +} + +void GLBackend::do_restoreContextViewCorrection(const Batch& batch, size_t paramOffset) { + _transform._viewCorrectionEnabled = true; +} + +void GLBackend::do_disableContextStereo(const Batch& batch, size_t paramOffset) { + +} + +void GLBackend::do_restoreContextStereo(const Batch& batch, size_t paramOffset) { + +} + void GLBackend::do_runLambda(const Batch& batch, size_t paramOffset) { std::function f = batch._lambdas.get(batch._params[paramOffset]._uint); f(); diff --git a/libraries/gpu-gles/src/gpu/gl/GLBackend.h b/libraries/gpu-gles/src/gpu/gl/GLBackend.h index f8f307bc17..c6b2ff6cc0 100644 --- a/libraries/gpu-gles/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gles/src/gpu/gl/GLBackend.h @@ -92,6 +92,8 @@ namespace gpu { namespace gl { // this is the maximum per shader stage on the low end apple // TODO make it platform dependant at init time + static const int MAX_NUM_RESOURCE_BUFFERS = 16; + size_t getMaxNumResourceBuffers() const { return MAX_NUM_RESOURCE_BUFFERS; } static const int MAX_NUM_RESOURCE_TEXTURES = 16; size_t getMaxNumResourceTextures() const { return MAX_NUM_RESOURCE_TEXTURES; } @@ -121,6 +123,7 @@ namespace gpu { namespace gl { virtual void do_setUniformBuffer(const Batch& batch, size_t paramOffset) final; // Resource Stage + virtual void do_setResourceBuffer(const Batch& batch, size_t paramOffset) final; virtual void do_setResourceTexture(const Batch& batch, size_t paramOffset) final; // Pipeline Stage @@ -139,6 +142,12 @@ namespace gpu { namespace gl { // Reset stages virtual void do_resetStages(const Batch& batch, size_t paramOffset) final; + virtual void do_disableContextViewCorrection(const Batch& batch, size_t paramOffset) final; + virtual void do_restoreContextViewCorrection(const Batch& batch, size_t paramOffset) final; + + virtual void do_disableContextStereo(const Batch& batch, size_t paramOffset) final; + virtual void do_restoreContextStereo(const Batch& batch, size_t paramOffset) final; + virtual void do_runLambda(const Batch& batch, size_t paramOffset) final; virtual void do_startNamedCall(const Batch& batch, size_t paramOffset) final; @@ -322,6 +331,7 @@ namespace gpu { namespace gl { bool _skybox { false }; Transform _view; CameraCorrection _correction; + bool _viewCorrectionEnabled{ true }; Mat4 _projection; Vec4i _viewport { 0, 0, 1, 1 }; @@ -353,12 +363,18 @@ namespace gpu { namespace gl { void releaseUniformBuffer(uint32_t slot); void resetUniformStage(); + // update resource cache and do the gl bind/unbind call with the current gpu::Buffer cached at slot s + // This is using different gl object depending on the gl version + virtual bool bindResourceBuffer(uint32_t slot, BufferPointer& buffer) = 0; + virtual void releaseResourceBuffer(uint32_t slot) = 0; + // update resource cache and do the gl unbind call with the current gpu::Texture cached at slot s void releaseResourceTexture(uint32_t slot); void resetResourceStage(); struct ResourceStageState { + std::array _buffers; std::array _textures; //Textures _textures { { MAX_NUM_RESOURCE_TEXTURES } }; int findEmptyTextureSlot() const; diff --git a/libraries/gpu-gles/src/gpu/gl/GLBackendPipeline.cpp b/libraries/gpu-gles/src/gpu/gl/GLBackendPipeline.cpp index c35966d440..73a597e89a 100644 --- a/libraries/gpu-gles/src/gpu/gl/GLBackendPipeline.cpp +++ b/libraries/gpu-gles/src/gpu/gl/GLBackendPipeline.cpp @@ -197,6 +197,36 @@ void GLBackend::resetResourceStage() { } } +void GLBackend::do_setResourceBuffer(const Batch& batch, size_t paramOffset) { + GLuint slot = batch._params[paramOffset + 1]._uint; + if (slot >= (GLuint)MAX_NUM_RESOURCE_BUFFERS) { + qCDebug(gpugllogging) << "GLBackend::do_setResourceBuffer: Trying to set a resource Buffer at slot #" << slot << " which doesn't exist. MaxNumResourceBuffers = " << getMaxNumResourceBuffers(); + return; + } + + auto resourceBuffer = batch._buffers.get(batch._params[paramOffset + 0]._uint); + + if (!resourceBuffer) { + releaseResourceBuffer(slot); + return; + } + // check cache before thinking + if (_resource._buffers[slot] == resourceBuffer) { + return; + } + + // One more True Buffer bound + _stats._RSNumResourceBufferBounded++; + + // If successful bind then cache it + if (bindResourceBuffer(slot, resourceBuffer)) { + _resource._buffers[slot] = resourceBuffer; + } else { // else clear slot and cache + releaseResourceBuffer(slot); + return; + } +} + void GLBackend::do_setResourceTexture(const Batch& batch, size_t paramOffset) { GLuint slot = batch._params[paramOffset + 1]._uint; if (slot >= (GLuint) MAX_NUM_RESOURCE_TEXTURES) { diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackend.h b/libraries/gpu-gles/src/gpu/gles/GLESBackend.h index 69a417d952..ebecf29d44 100644 --- a/libraries/gpu-gles/src/gpu/gles/GLESBackend.h +++ b/libraries/gpu-gles/src/gpu/gles/GLESBackend.h @@ -87,6 +87,10 @@ protected: void updateTransform(const Batch& batch); void resetTransformStage(); + // Resource Stage + bool bindResourceBuffer(uint32_t slot, BufferPointer& buffer) override; + void releaseResourceBuffer(uint32_t slot) override; + // Output stage void do_blit(const Batch& batch, size_t paramOffset) override; }; diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackendBuffer.cpp b/libraries/gpu-gles/src/gpu/gles/GLESBackendBuffer.cpp index f6bdea45af..05bda34d7f 100644 --- a/libraries/gpu-gles/src/gpu/gles/GLESBackendBuffer.cpp +++ b/libraries/gpu-gles/src/gpu/gles/GLESBackendBuffer.cpp @@ -67,3 +67,27 @@ GLuint GLESBackend::getBufferID(const Buffer& buffer) { GLBuffer* GLESBackend::syncGPUObject(const Buffer& buffer) { return GLESBuffer::sync(*this, buffer); } + +bool GLESBackend::bindResourceBuffer(uint32_t slot, BufferPointer& buffer) { + GLBuffer* object = syncGPUObject((*buffer)); + if (object) { + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, slot, object->_id); + + (void)CHECK_GL_ERROR(); + + _resource._buffers[slot] = buffer; + + return true; + } + + return false; +} + +void GLESBackend::releaseResourceBuffer(uint32_t slot) { + auto& buf = _resource._buffers[slot]; + if (buf) { + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, slot, 0); + buf.reset(); + } +} + diff --git a/libraries/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp index c83251c605..09ff9510f5 100755 --- a/libraries/render-utils/src/RenderForwardTask.cpp +++ b/libraries/render-utils/src/RenderForwardTask.cpp @@ -21,7 +21,7 @@ #include "FramebufferCache.h" #include "TextureCache.h" - +#include #include #include "nop_frag.h" @@ -75,11 +75,11 @@ void PrepareFramebuffer::run(const RenderContextPointer& renderContext, auto colorFormat = gpu::Element::COLOR_SRGBA_32; auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - auto colorTexture = gpu::Texture::create2D(colorFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler); + auto colorTexture = gpu::Texture::createRenderBuffer(colorFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler); _framebuffer->setRenderBuffer(0, colorTexture); auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format - auto depthTexture = gpu::Texture::create2D(depthFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler); + auto depthTexture = gpu::Texture::createRenderBuffer(depthFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler); _framebuffer->setDepthStencilBuffer(depthTexture, depthFormat); } diff --git a/libraries/shared/src/shared/FileUtils.cpp b/libraries/shared/src/shared/FileUtils.cpp index dba0af7b16..d326bff1d4 100644 --- a/libraries/shared/src/shared/FileUtils.cpp +++ b/libraries/shared/src/shared/FileUtils.cpp @@ -84,7 +84,11 @@ void FileUtils::locateFile(QString filePath) { QString FileUtils::standardPath(QString subfolder) { // standard path // Mac: ~/Library/Application Support/Interface +#ifndef Q_OS_ANDROID QString path = QStandardPaths::writableLocation(QStandardPaths::DataLocation); +#else + QString path = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); +#endif if (!subfolder.startsWith("/")) { subfolder.prepend("/"); } From d31346c047413cc244056d114488c51564f0b593 Mon Sep 17 00:00:00 2001 From: Gabriel Calero Date: Mon, 18 Dec 2017 16:15:06 -0300 Subject: [PATCH 05/17] Use qml engine --- .../resources/qml/hifi/+android/Desktop.qml | 63 +++++++++++++++++++ libraries/ui/src/OffscreenQmlElement.h | 2 +- .../ui/src/ui/TabletScriptingInterface.cpp | 2 +- 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 interface/resources/qml/hifi/+android/Desktop.qml diff --git a/interface/resources/qml/hifi/+android/Desktop.qml b/interface/resources/qml/hifi/+android/Desktop.qml new file mode 100644 index 0000000000..99d792b664 --- /dev/null +++ b/interface/resources/qml/hifi/+android/Desktop.qml @@ -0,0 +1,63 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +import Qt.labs.settings 1.0 + +import "../desktop" as OriginalDesktop +import ".." +import "." +import "./toolbars" + +OriginalDesktop.Desktop { + id: desktop + + MouseArea { + id: hoverWatch + anchors.fill: parent + hoverEnabled: true + propagateComposedEvents: true + scrollGestureEnabled: false // we don't need/want these + onEntered: ApplicationCompositor.reticleOverDesktop = true + onExited: ApplicationCompositor.reticleOverDesktop = false + acceptedButtons: Qt.NoButton + + + } + + + Component { id: toolbarBuilder; Toolbar { } } + // This used to create sysToolbar dynamically with a call to getToolbar() within onCompleted. + // Beginning with QT 5.6, this stopped working, as anything added to toolbars too early got + // wiped during startup. + Toolbar { + id: sysToolbar; + objectName: "com.highfidelity.interface.toolbar.system"; + // Magic: sysToolbar.x and y come from settings, and are bound before the properties specified here are applied. + x: sysToolbar.x; + y: sysToolbar.y; + } + property var toolbars: (function (map) { // answer dictionary preloaded with sysToolbar + map[sysToolbar.objectName] = sysToolbar; + return map; })({}); + + Component.onCompleted: { + } + + // Accept a download through the webview + property bool webViewProfileSetup: false + property string currentUrl: "" + property string adaptedPath: "" + property string tempDir: "" + + // Create or fetch a toolbar with the given name + function getToolbar(name) { + var result = toolbars[name]; + if (!result) { + result = toolbars[name] = toolbarBuilder.createObject(desktop, {}); + result.objectName = name; + } + return result; + } +} + + diff --git a/libraries/ui/src/OffscreenQmlElement.h b/libraries/ui/src/OffscreenQmlElement.h index 4e07fcccd9..3597add964 100644 --- a/libraries/ui/src/OffscreenQmlElement.h +++ b/libraries/ui/src/OffscreenQmlElement.h @@ -40,7 +40,7 @@ public: \ private: #define HIFI_QML_DEF(x) \ - const QUrl x::QML = QUrl(#x ".qml"); \ + const QUrl x::QML = QUrl("qrc:///qml/" #x ".qml"); \ const QString x::NAME = #x; \ \ void x::registerType() { \ diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index 0e793fef21..6e86d75940 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -268,7 +268,7 @@ static const char* WEB_VIEW_SOURCE_URL = "hifi/tablet/TabletWebView.qml"; static const char* VRMENU_SOURCE_URL = "hifi/tablet/TabletMenu.qml"; class TabletRootWindow : public QmlWindowClass { - virtual QString qmlSource() const override { return "hifi/tablet/WindowRoot.qml"; } + virtual QString qmlSource() const override { return "qrc:///qml/hifi/tablet/WindowRoot.qml"; } }; TabletProxy::TabletProxy(QObject* parent, const QString& name) : QObject(parent), _name(name) { From 78f2a8c566425bf94a23b02c9206fb13ec0dbf84 Mon Sep 17 00:00:00 2001 From: Gabriel Calero Date: Tue, 19 Dec 2017 17:03:29 -0300 Subject: [PATCH 06/17] Copy scripts from android assets at startup --- interface/src/main.cpp | 5 +++++ libraries/shared/src/PathUtils.cpp | 29 +++++++++++++++++++++++++++++ libraries/shared/src/PathUtils.h | 1 + 3 files changed, 35 insertions(+) diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 5c07bebc23..ba555d6dad 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -105,6 +105,11 @@ int main(int argc, const char* argv[]) { if (allowMultipleInstances) { instanceMightBeRunning = false; } + QFileInfo fInfo("assets:/scripts/test.js"); + QDir dirInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); + QUrl androidPath = QUrl::fromLocalFile(dirInfo.canonicalPath() + "/scripts"); + PathUtils::copyDirDeep("assets:/scripts", androidPath.toLocalFile()); + // this needs to be done here in main, as the mechanism for setting the // scripts directory appears not to work. See the bug report // https://highfidelity.fogbugz.com/f/cases/5759/Issues-changing-scripts-directory-in-ScriptsEngine diff --git a/libraries/shared/src/PathUtils.cpp b/libraries/shared/src/PathUtils.cpp index b33b330fc0..b230917db9 100644 --- a/libraries/shared/src/PathUtils.cpp +++ b/libraries/shared/src/PathUtils.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -203,3 +204,31 @@ bool PathUtils::isDescendantOf(const QUrl& childURL, const QUrl& parentURL) { QString parent = stripFilename(parentURL); return child.startsWith(parent, PathUtils::getFSCaseSensitivity()); } + +void PathUtils::copyDirDeep(QString src, QString dst) { + QDir dir = QDir::root(); + dir.mkpath(dst); + QDirIterator it(src, QStringList() << "*", QDir::Files|QDir::AllDirs, QDirIterator::Subdirectories); + while (it.hasNext()) { + QString f = it.next(); + QFileInfo fInfo(f); + QString newDst = dst + (dst.endsWith("/")?"":"/") + fInfo.fileName(); + if (fInfo.isFile()) { + QFile dfile(f); + if (dfile.exists(f)) + { + if (dfile.copy(newDst)) { + QFile::setPermissions(newDst, QFile::ReadOwner); + } else { + QFile::setPermissions(newDst, QFile::ReadOwner); + // sometimes copy returns false but it worked anyway + //qWarning() << "Could not copy to " << newDst; + } + } + } else if (fInfo.isDir() ) { + copyDirDeep(f, newDst); + } + } +} + + diff --git a/libraries/shared/src/PathUtils.h b/libraries/shared/src/PathUtils.h index 2b4fe35d97..6991c91258 100644 --- a/libraries/shared/src/PathUtils.h +++ b/libraries/shared/src/PathUtils.h @@ -54,6 +54,7 @@ public: // note: this is FS-case-sensitive version of parentURL.isParentOf(childURL) static bool isDescendantOf(const QUrl& childURL, const QUrl& parentURL); static QUrl defaultScriptsLocation(const QString& newDefault = ""); + static void copyDirDeep(QString src, QString dst); }; From ef8ba1cad0cbe324b2cb398dce25e461e90d380b Mon Sep 17 00:00:00 2001 From: Gabriel Calero Date: Tue, 19 Dec 2017 17:47:07 -0300 Subject: [PATCH 07/17] Include config.h in ResourceImageItem.cpp. Minor fixes --- android/build.gradle | 4 +- interface/resources/qml/+android/Stats.qml | 394 +++++++++++++++++++++ interface/src/ui/ResourceImageItem.cpp | 4 +- 3 files changed, 399 insertions(+), 3 deletions(-) create mode 100644 interface/resources/qml/+android/Stats.qml diff --git a/android/build.gradle b/android/build.gradle index 903c3831bc..97eee378aa 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -438,8 +438,8 @@ task extractGvrBinaries(dependsOn: extractDependencies) { task qtBundle { doLast { parseQtDependencies(QT5_DEPS) - //def qmlImportFolder = new File("${appDir}/../../interface/resources/qml/") - def qmlImportFolder = new File("${projectDir}/app/src/main/cpp") + def qmlImportFolder = new File("${appDir}/../../interface/resources/qml/") + //def qmlImportFolder = new File("${projectDir}/app/src/main/cpp") scanQmlImports(qmlImportFolder) generateLibsXml() } diff --git a/interface/resources/qml/+android/Stats.qml b/interface/resources/qml/+android/Stats.qml new file mode 100644 index 0000000000..d961285a46 --- /dev/null +++ b/interface/resources/qml/+android/Stats.qml @@ -0,0 +1,394 @@ +import Hifi 1.0 as Hifi +import QtQuick 2.3 +import '.' + +Item { + id: stats + + anchors.leftMargin: 300 + objectName: "StatsItem" + property int modality: Qt.NonModal + implicitHeight: row.height + implicitWidth: row.width + + Component.onCompleted: { + stats.parentChanged.connect(fill); + fill(); + } + Component.onDestruction: { + stats.parentChanged.disconnect(fill); + } + + function fill() { + // This will cause a warning at shutdown, need to find another way to remove + // the warning other than filling the anchors to the parent + anchors.horizontalCenter = parent.horizontalCenter + } + + Hifi.Stats { + id: root + objectName: "Stats" + implicitHeight: row.height + implicitWidth: row.width + + anchors.horizontalCenter: parent.horizontalCenter + readonly property string bgColor: "#AA111111" + + Row { + id: row + spacing: 8 + Rectangle { + width: generalCol.width + 8; + height: generalCol.height + 8; + color: root.bgColor; + + MouseArea { + anchors.fill: parent + onClicked: { root.expanded = !root.expanded; } + hoverEnabled: true + } + + Column { + id: generalCol + spacing: 4; x: 4; y: 4; + StatText { + text: "Servers: " + root.serverCount + } + StatText { + text: "Avatars: " + root.avatarCount + } + StatText { + text: "Game Rate: " + root.gameLoopRate + } + StatText { + visible: root.expanded + text: root.gameUpdateStats + } + StatText { + text: "Render Rate: " + root.renderrate.toFixed(2); + } + StatText { + text: "Present Rate: " + root.presentrate.toFixed(2); + } + StatText { + visible: root.expanded + text: " Present New Rate: " + root.presentnewrate.toFixed(2); + } + StatText { + visible: root.expanded + text: " Present Drop Rate: " + root.presentdroprate.toFixed(2); + } + StatText { + text: "Stutter Rate: " + root.stutterrate.toFixed(3); + visible: root.stutterrate != -1; + } + StatText { + text: "Missed Frame Count: " + root.appdropped; + visible: root.appdropped > 0; + } + StatText { + text: "Long Render Count: " + root.longrenders; + visible: root.longrenders > 0; + } + StatText { + text: "Long Submit Count: " + root.longsubmits; + visible: root.longsubmits > 0; + } + StatText { + text: "Long Frame Count: " + root.longframes; + visible: root.longframes > 0; + } + StatText { + text: "Packets In/Out: " + root.packetInCount + "/" + root.packetOutCount + } + StatText { + text: "Mbps In/Out: " + root.mbpsIn.toFixed(2) + "/" + root.mbpsOut.toFixed(2) + } + StatText { + visible: root.expanded + text: "Asset Mbps In/Out: " + root.assetMbpsIn.toFixed(2) + "/" + root.assetMbpsOut.toFixed(2) + } + StatText { + visible: root.expanded + text: "Avatars Updated: " + root.updatedAvatarCount + } + StatText { + visible: root.expanded + text: "Avatars NOT Updated: " + root.notUpdatedAvatarCount + } + } + } + + Rectangle { + width: pingCol.width + 8 + height: pingCol.height + 8 + color: root.bgColor; + visible: root.audioPing != -2 + MouseArea { + anchors.fill: parent + onClicked: { root.expanded = !root.expanded; } + hoverEnabled: true + } + Column { + id: pingCol + spacing: 4; x: 4; y: 4; + StatText { + text: "Audio ping/loss: " + root.audioPing + "/" + root.audioPacketLoss + "%" + } + StatText { + text: "Avatar ping: " + root.avatarPing + } + StatText { + text: "Entities avg ping: " + root.entitiesPing + } + StatText { + text: "Asset ping: " + root.assetPing + } + StatText { + visible: root.expanded; + text: "Messages max ping: " + root.messagePing + } + } + } + + Rectangle { + width: geoCol.width + 8 + height: geoCol.height + 8 + color: root.bgColor; + MouseArea { + anchors.fill: parent + onClicked: { root.expanded = !root.expanded; } + hoverEnabled: true + } + Column { + id: geoCol + spacing: 4; x: 4; y: 4; + StatText { + text: "Position: " + root.position.x.toFixed(1) + ", " + + root.position.y.toFixed(1) + ", " + root.position.z.toFixed(1) + } + StatText { + text: "Speed: " + root.speed.toFixed(1) + } + StatText { + text: "Yaw: " + root.yaw.toFixed(1) + } + StatText { + visible: root.expanded; + text: "Avatar Mixer In: " + root.avatarMixerInKbps + " kbps, " + + root.avatarMixerInPps + "pps"; + } + StatText { + visible: root.expanded; + text: "Avatar Mixer Out: " + root.avatarMixerOutKbps + " kbps, " + + root.avatarMixerOutPps + "pps, " + + root.myAvatarSendRate.toFixed(2) + "hz"; + } + StatText { + visible: root.expanded; + text: "Audio Mixer In: " + root.audioMixerInKbps + " kbps, " + + root.audioMixerInPps + "pps"; + } + StatText { + visible: root.expanded; + text: "Audio In Audio: " + root.audioAudioInboundPPS + " pps, " + + "Silent: " + root.audioSilentInboundPPS + " pps"; + } + StatText { + visible: root.expanded; + text: "Audio Mixer Out: " + root.audioMixerOutKbps + " kbps, " + + root.audioMixerOutPps + "pps"; + } + StatText { + visible: root.expanded; + text: "Audio Out Mic: " + root.audioOutboundPPS + " pps, " + + "Silent: " + root.audioSilentOutboundPPS + " pps"; + } + StatText { + visible: root.expanded; + text: "Audio Codec: " + root.audioCodec + " Noise Gate: " + + root.audioNoiseGate; + } + StatText { + visible: root.expanded; + text: "Entity Servers In: " + root.entityPacketsInKbps + " kbps"; + } + StatText { + visible: root.expanded; + text: "Downloads: " + root.downloads + "/" + root.downloadLimit + + ", Pending: " + root.downloadsPending; + } + StatText { + visible: root.expanded; + text: "Processing: " + root.processing + + ", Pending: " + root.processingPending; + } + StatText { + visible: root.expanded && root.downloadUrls.length > 0; + text: "Download URLs:" + } + ListView { + width: geoCol.width + height: root.downloadUrls.length * 15 + + visible: root.expanded && root.downloadUrls.length > 0; + + model: root.downloadUrls + delegate: StatText { + visible: root.expanded; + text: modelData.length > 30 + ? modelData.substring(0, 5) + "..." + modelData.substring(modelData.length - 22) + : modelData + } + } + } + } + Rectangle { + width: octreeCol.width + 8 + height: octreeCol.height + 8 + color: root.bgColor; + MouseArea { + anchors.fill: parent + onClicked: { root.expanded = !root.expanded; } + hoverEnabled: true + } + Column { + id: octreeCol + spacing: 4; x: 4; y: 4; + StatText { + text: "Engine: " + root.engineFrameTime.toFixed(1) + " ms" + } + StatText { + text: "Batch: " + root.batchFrameTime.toFixed(1) + " ms" + } + StatText { + text: "GPU: " + root.gpuFrameTime.toFixed(1) + " ms" + } + StatText { + text: "Triangles: " + root.triangles + + " / Material Switches: " + root.materialSwitches + } + StatText { + text: "GPU Free Memory: " + root.gpuFreeMemory + " MB"; + } + StatText { + text: "GPU Textures: "; + } + StatText { + text: " Count: " + root.gpuTextures; + } + StatText { + text: " Pressure State: " + root.gpuTextureMemoryPressureState; + } + StatText { + text: " Resource Allocated / Populated / Pending: "; + } + StatText { + text: " " + root.gpuTextureResourceMemory + " / " + root.gpuTextureResourcePopulatedMemory + " / " + root.texturePendingTransfers + " MB"; + } + StatText { + text: " Resident Memory: " + root.gpuTextureResidentMemory + " MB"; + } + StatText { + text: " Framebuffer Memory: " + root.gpuTextureFramebufferMemory + " MB"; + } + StatText { + text: " External Memory: " + root.gpuTextureExternalMemory + " MB"; + } + StatText { + text: "GPU Buffers: " + } + StatText { + text: " Count: " + root.gpuBuffers; + } + StatText { + text: " Memory: " + root.gpuBufferMemory + " MB"; + } + StatText { + text: "GL Swapchain Memory: " + root.glContextSwapchainMemory + " MB"; + } + StatText { + text: "QML Texture Memory: " + root.qmlTextureMemory + " MB"; + } + StatText { + visible: root.expanded; + text: "Items rendered / considered: " + + root.itemRendered + " / " + root.itemConsidered; + } + StatText { + visible: root.expanded; + text: " out of view: " + root.itemOutOfView + + " too small: " + root.itemTooSmall; + } + StatText { + visible: root.expanded; + text: "Shadows rendered / considered: " + + root.shadowRendered + " / " + root.shadowConsidered; + } + StatText { + visible: root.expanded; + text: " out of view: " + root.shadowOutOfView + + " too small: " + root.shadowTooSmall; + } + StatText { + visible: !root.expanded + text: "Octree Elements Server: " + root.serverElements + + " Local: " + root.localElements; + } + StatText { + visible: root.expanded + text: "Octree Sending Mode: " + root.sendingMode; + } + StatText { + visible: root.expanded + text: "Octree Packets to Process: " + root.packetStats; + } + StatText { + visible: root.expanded + text: "Octree Elements - "; + } + StatText { + visible: root.expanded + text: "\tServer: " + root.serverElements + + " Internal: " + root.serverInternal + + " Leaves: " + root.serverLeaves; + } + StatText { + visible: root.expanded + text: "\tLocal: " + root.localElements + + " Internal: " + root.localInternal + + " Leaves: " + root.localLeaves; + } + StatText { + visible: root.expanded + text: "LOD: " + root.lodStatus; + } + } + } + } + + Rectangle { + y: 250 + visible: root.timingExpanded + width: perfText.width + 8 + height: perfText.height + 8 + color: root.bgColor; + StatText { + x: 4; y: 4 + id: perfText + font.family: root.monospaceFont + text: "------------------------------------------ Function " + + "--------------------------------------- --msecs- -calls--\n" + + root.timingStats; + } + } + + Connections { + target: root.parent + onWidthChanged: { + root.x = root.parent.width - root.width; + } + } + } + +} diff --git a/interface/src/ui/ResourceImageItem.cpp b/interface/src/ui/ResourceImageItem.cpp index 5b7c1896fe..7447e9f88b 100644 --- a/interface/src/ui/ResourceImageItem.cpp +++ b/interface/src/ui/ResourceImageItem.cpp @@ -16,7 +16,9 @@ #include #include - +#ifdef Q_OS_ANDROID +#include +#endif ResourceImageItem::ResourceImageItem() : QQuickFramebufferObject() { auto textureCache = DependencyManager::get(); connect(textureCache.data(), SIGNAL(spectatorCameraFramebufferReset()), this, SLOT(update())); From fc62b2d34a145498e1b43025d2365e71ec83247e Mon Sep 17 00:00:00 2001 From: Gabriel Calero Date: Thu, 21 Dec 2017 16:40:21 -0300 Subject: [PATCH 08/17] Fix ktx_cache path for android. Load the least js scripts for android --- interface/src/Application.cpp | 17 +++++++++++++ interface/src/Application.h | 3 +++ libraries/shared/src/PathUtils.cpp | 6 +++++ scripts/defaultScripts.js | 40 +++++++++++++++++++++++++++++- 4 files changed, 65 insertions(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9127381bde..e5ad9792f8 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5932,7 +5932,9 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, LocationScriptingInterface::locationSetter); +#ifndef ANDROID scriptEngine->registerFunction("OverlayWebWindow", QmlWebWindowClass::constructor); +#endif scriptEngine->registerFunction("OverlayWindow", QmlWindowClass::constructor); scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance()); @@ -5984,6 +5986,9 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe scriptEngine->registerGlobalObject("UserActivityLogger", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Users", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("App", this); + scriptEngine->registerFunction("App", "isAndroid", Application::isAndroid, 0); + scriptEngine->registerGlobalObject("LimitlessSpeechRecognition", DependencyManager::get().data()); scriptEngine->registerGlobalObject("GooglePoly", DependencyManager::get().data()); @@ -7567,6 +7572,18 @@ void Application::updateThreadPoolCount() const { QThreadPool::globalInstance()->setMaxThreadCount(threadPoolSize); } +QScriptValue Application::isAndroid(QScriptContext* context, QScriptEngine* engine) { + return QScriptValue(engine, isAndroid()); +} + +bool Application::isAndroid() { +#ifdef Q_OS_ANDROID + return true; +#else + return false; +#endif +} + void Application::updateSystemTabletMode() { qApp->setProperty(hifi::properties::HMD, isHMDMode()); if (isHMDMode()) { diff --git a/interface/src/Application.h b/interface/src/Application.h index ee16740f20..935321662f 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -282,6 +282,9 @@ public: bool getSaveAvatarOverrideUrl() { return _saveAvatarOverrideUrl; } void saveNextPhysicsStats(QString filename); + static Q_INVOKABLE QScriptValue isAndroid(QScriptContext* context, QScriptEngine* engine); + static bool isAndroid(); + signals: void svoImportRequested(const QString& url); diff --git a/libraries/shared/src/PathUtils.cpp b/libraries/shared/src/PathUtils.cpp index b230917db9..c6ff73492c 100644 --- a/libraries/shared/src/PathUtils.cpp +++ b/libraries/shared/src/PathUtils.cpp @@ -32,6 +32,8 @@ QString TEMP_DIR_FORMAT { "%1-%2-%3" }; const QString& PathUtils::resourcesPath() { #ifdef Q_OS_MAC static const QString staticResourcePath = QCoreApplication::applicationDirPath() + "/../Resources/"; +#elif defined (ANDROID) + static const QString staticResourcePath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/resources/"; #else static const QString staticResourcePath = QCoreApplication::applicationDirPath() + "/resources/"; #endif @@ -72,7 +74,11 @@ QString PathUtils::getAppLocalDataPath() { } // otherwise return standard path +#ifndef Q_OS_ANDROID return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/"; +#else + return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/"; +#endif } QString PathUtils::getAppDataFilePath(const QString& filename) { diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 89d4c75ae4..7ed20180b8 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -12,6 +12,16 @@ // var DEFAULT_SCRIPTS_COMBINED = [ +]; + +function pushAll(dest, orig) { + for (var k in orig) { + dest.push(orig[k]); + } +} + +if (!App.isAndroid()) { + pushAll(DEFAULT_SCRIPTS_COMBINED, [ "system/progress.js", "system/away.js", "system/audio.js", @@ -30,12 +40,40 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/dialTone.js", "system/firstPersonHMD.js", "system/tablet-ui/tabletUI.js" -]; + ]); +} else { + pushAll(DEFAULT_SCRIPTS_COMBINED, [ + "system/progress.js"/*, + "system/away.js", + "system/controllers/controllerDisplayManager.js", + "system/controllers/handControllerGrabAndroid.js", + "system/controllers/handControllerPointerAndroid.js", + "system/controllers/squeezeHands.js", + "system/controllers/grab.js", + "system/controllers/teleport.js", + "system/controllers/toggleAdvancedMovementForHandControllers.js", + "system/dialTone.js", + "system/firstPersonHMD.js", + "system/bubble.js", + "system/android.js", + "developer/debugging/debugAndroidMouse.js"*/ + ]); +} + var DEFAULT_SCRIPTS_SEPARATE = [ "system/controllers/controllerScripts.js" //"system/chat.js" ]; + +if (!App.isAndroid()) { + pushAll(DEFAULT_SCRIPTS_SEPARATE, [ + "system/controllers/controllerScripts.js" + ]); +} else { + pushAll(DEFAULT_SCRIPTS_SEPARATE, []); +} + // add a menu item for debugging var MENU_CATEGORY = "Developer"; var MENU_ITEM = "Debug defaultScripts.js"; From da28e0a8cb73a106a81dffe3ca0bbbab6055ab8b Mon Sep 17 00:00:00 2001 From: Cristian Luis Duarte Date: Fri, 22 Dec 2017 00:27:52 -0300 Subject: [PATCH 09/17] Implement generateAssetsFileList in build.gradle (same work as assets file generation by androiddeployqt) + Copy assets at startup --- android/build.gradle | 65 ++++++++++++++++++++++++++++++++++++++++-- interface/src/main.cpp | 12 ++++++-- 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 97eee378aa..c53eac1ed0 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -257,7 +257,7 @@ def parseQtDependencies = { List qtLibs -> def generateLibsXml = { def libDestinationDirectory = jniFolder def jarDestinationDirectory = new File(appDir, 'libs') - def assetDestinationDirectory = new File(appDir, 'src/main/assets/bundled'); + def assetDestinationDirectory = new File(appDir, 'src/main/assets/--Added-by-androiddeployqt--'); def libsXmlFile = new File(appDir, 'src/main/res/values/libs.xml') def libPrefix = 'lib' + File.separator def jarPrefix = 'jar' + File.separator @@ -293,7 +293,7 @@ def generateLibsXml = { } else if (relativePath.startsWith('jar')) { destinationFile = new File(jarDestinationDirectory, relativePath.substring(jarPrefix.size())) } else { - xmlParser.createNode(bundledAssetsNode, 'item', [:]).setValue("bundled/${relativePath}:${relativePath}".replace(File.separator, '/')) + xmlParser.createNode(bundledAssetsNode, 'item', [:]).setValue("--Added-by-androiddeployqt--/${relativePath}:${relativePath}".replace(File.separator, '/')) destinationFile = new File(assetDestinationDirectory, relativePath) } @@ -450,10 +450,69 @@ task setupDependencies(dependsOn: [setupScribe, copyDependencies, extractGvrBina task cleanDependencies(type: Delete) { delete HIFI_ANDROID_PRECOMPILED delete 'app/src/main/jniLibs/arm64-v8a' - delete 'app/src/main/assets/bundled' + delete 'app/src/main/assets/--Added-by-androiddeployqt--' delete 'app/src/main/res/values/libs.xml' } +task generateAssetsFileList() { + doLast { + def assetsPath = "${appDir}/src/main/assets/" + //def assetsPath = "/Users/cduarte/dev/workspace-hifi/hifiparallel/build_android_hifiqt59/interface/apk/assets/" + def addedByAndroidDeployQtName = "--Added-by-androiddeployqt--/" + //def addedByAndroidDeployQtName = "--Added-by-androiddeployqt--/" + + def addedByAndroidDeployQtPath = assetsPath + addedByAndroidDeployQtName + + def addedByAndroidDeployQt = new File(addedByAndroidDeployQtPath) + if (!addedByAndroidDeployQt.exists() && !addedByAndroidDeployQt.mkdirs()) { + throw new GradleScriptException("Failed to create directory " + addedByAndroidDeployQtPath, null); + } + def outputFilename = "/qt_cache_pregenerated_file_list" + //def outputFilename = "/qt_cache_pregenerated_file_list2" + def outputFile = new File(addedByAndroidDeployQtPath + outputFilename); + Map> directoryContents = new TreeMap<>(); + + def dir = new File(assetsPath) + dir.eachFileRecurse (FileType.ANY) { file -> + + def name = file.path.substring(assetsPath.length()) + int slashIndex = name.lastIndexOf('/') + def pathName = slashIndex >= 0 ? name.substring(0, slashIndex) : "/" + def fileName = slashIndex >= 0 ? name.substring(pathName.length() + 1) : name + if (!fileName.isEmpty() && file.isDirectory() && !fileName.endsWith("/")) { + fileName += "/" + } + + /*println ("full: [" + file.getAbsolutePath() + "]\n\t" + + "path: [" + pathName + "]\n\t" + + "name: [" + fileName + "]\n\t");*/ + + if (!directoryContents.containsKey(pathName)) { + directoryContents[pathName] = new ArrayList() + } + if (!fileName.isEmpty()) { + directoryContents[pathName].add(fileName); + } + } + DataOutputStream fos = new DataOutputStream(new FileOutputStream(outputFile)); + for (Map.Entry> e: directoryContents.entrySet()) { + def entryList = e.getValue() + //stream << it.key() << entryList.size(); + fos.writeInt(e.key.length()*2); // 2 bytes per char + fos.writeChars(e.key); + fos.writeInt(entryList.size()); + //println ("dir: " + e.key + " size: " + entryList.size()); + for (String entry: entryList) { + fos.writeInt(entry.length()*2); + fos.writeChars(entry); + //println("\tentry: " + entry); + //stream << entry; + } + } + fos.close(); + } +} + /* // FIXME derive the path from the gradle environment def toolchain = [ diff --git a/interface/src/main.cpp b/interface/src/main.cpp index ba555d6dad..3a21f5fadf 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -105,10 +105,16 @@ int main(int argc, const char* argv[]) { if (allowMultipleInstances) { instanceMightBeRunning = false; } - QFileInfo fInfo("assets:/scripts/test.js"); + + std::vector assetDirs = { + "/resources", + "/scripts", + }; QDir dirInfo(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); - QUrl androidPath = QUrl::fromLocalFile(dirInfo.canonicalPath() + "/scripts"); - PathUtils::copyDirDeep("assets:/scripts", androidPath.toLocalFile()); + for (std::vector::iterator it = assetDirs.begin() ; it != assetDirs.end(); ++it) { + QString dir = *it; + PathUtils::copyDirDeep("assets:" + dir, QUrl::fromLocalFile(dirInfo.canonicalPath() + dir).toLocalFile()); + } // this needs to be done here in main, as the mechanism for setting the // scripts directory appears not to work. See the bug report From 597114afea30c4b0f1426c50d0dd78f08826760e Mon Sep 17 00:00:00 2001 From: Gabriel Calero Date: Fri, 29 Dec 2017 17:33:35 -0300 Subject: [PATCH 10/17] Prepare forward pipeline for android --- android/app/CMakeLists.txt | 4 +-- android/app/src/main/AndroidManifest.xml | 1 + .../render-utils/src/RenderForwardTask.cpp | 36 ++++++++++++++----- .../render-utils/src/RenderPipelines.cpp | 30 +++++++++------- 4 files changed, 49 insertions(+), 22 deletions(-) diff --git a/android/app/CMakeLists.txt b/android/app/CMakeLists.txt index 4260882018..a0af43b30a 100644 --- a/android/app/CMakeLists.txt +++ b/android/app/CMakeLists.txt @@ -11,8 +11,8 @@ include_directories("${INTERFACE_DIR}/src") target_link_libraries(native-lib android log m interface) set(GVR_ROOT "${HIFI_ANDROID_PRECOMPILED}/gvr/gvr-android-sdk-1.101.0/") -target_include_directories(native-lib PRIVATE "${GVR_ROOT}/libraries/headers") -target_link_libraries(native-lib "${GVR_ROOT}/libraries/libgvr.so") +target_include_directories(native-lib PRIVATE "${GVR_ROOT}/libraries/headers" "libraries/ui/src") +target_link_libraries(native-lib "${GVR_ROOT}/libraries/libgvr.so" ui) # finished libraries # core -> qt diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4113f047fa..3c7f59703e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ + diff --git a/libraries/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp index 09ff9510f5..a0efc694cd 100755 --- a/libraries/render-utils/src/RenderForwardTask.cpp +++ b/libraries/render-utils/src/RenderForwardTask.cpp @@ -18,6 +18,9 @@ #include #include #include "StencilMaskPass.h" +#include "ZoneRenderer.h" +#include "FadeEffect.h" +#include "BackgroundStage.h" #include "FramebufferCache.h" #include "TextureCache.h" @@ -27,33 +30,50 @@ #include "nop_frag.h" using namespace render; -extern void initForwardPipelines(ShapePlumber& plumber); +extern void initForwardPipelines(ShapePlumber& plumber, const render::ShapePipeline::BatchSetter& batchSetter, const render::ShapePipeline::ItemSetter& itemSetter); void RenderForwardTask::build(JobModel& task, const render::Varying& input, render::Varying& output) { auto items = input.get(); + auto fadeEffect = DependencyManager::get(); // Prepare the ShapePipelines ShapePlumberPointer shapePlumber = std::make_shared(); - initForwardPipelines(*shapePlumber); + initForwardPipelines(*shapePlumber, fadeEffect->getBatchSetter(), fadeEffect->getItemUniformSetter()); // Extract opaques / transparents / lights / metas / overlays / background const auto& opaques = items.get0()[RenderFetchCullSortTask::OPAQUE_SHAPE]; -// const auto& transparents = items.get0()[RenderFetchCullSortTask::TRANSPARENT_SHAPE]; + const auto& transparents = items.get0()[RenderFetchCullSortTask::TRANSPARENT_SHAPE]; // const auto& lights = items.get0()[RenderFetchCullSortTask::LIGHT]; -// const auto& metas = items.get0()[RenderFetchCullSortTask::META]; + const auto& metas = items.get0()[RenderFetchCullSortTask::META]; // const auto& overlayOpaques = items.get0()[RenderFetchCullSortTask::OVERLAY_OPAQUE_SHAPE]; // const auto& overlayTransparents = items.get0()[RenderFetchCullSortTask::OVERLAY_TRANSPARENT_SHAPE]; - const auto& background = items.get0()[RenderFetchCullSortTask::BACKGROUND]; + //const auto& background = items.get0()[RenderFetchCullSortTask::BACKGROUND]; // const auto& spatialSelection = items[1]; const auto framebuffer = task.addJob("PrepareFramebuffer"); task.addJob("DrawOpaques", opaques, shapePlumber); task.addJob("Stencil"); - task.addJob("DrawBackground", background); - // Bounds do not draw on stencil buffer, so they must come last - task.addJob("DrawBounds", opaques); + const auto lightingModel = task.addJob("LightingModel"); + + // Filter zones from the general metas bucket + const auto zones = task.addJob("ZoneRenderer", metas); + +// task.addJob("DrawBackground", background); + // Similar to light stage, background stage has been filled by several potential render items and resolved for the frame in this job + task.addJob("DrawBackgroundDeferred", lightingModel); + + { // Debug the bounds of the rendered items, still look at the zbuffer + + task.addJob("DrawMetaBounds", metas); + task.addJob("DrawBounds", opaques); + + task.addJob("DrawZones", zones); + } + + task.addJob("DrawTransparents", transparents, shapePlumber); + // Blit! task.addJob("Blit", framebuffer); diff --git a/libraries/render-utils/src/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp index 7f644add72..8457ed021e 100644 --- a/libraries/render-utils/src/RenderPipelines.cpp +++ b/libraries/render-utils/src/RenderPipelines.cpp @@ -106,7 +106,7 @@ using namespace std::placeholders; void initOverlay3DPipelines(ShapePlumber& plumber, bool depthTest = false); void initDeferredPipelines(ShapePlumber& plumber, const render::ShapePipeline::BatchSetter& batchSetter, const render::ShapePipeline::ItemSetter& itemSetter); -void initForwardPipelines(ShapePlumber& plumber); +void initForwardPipelines(ShapePlumber& plumber, const render::ShapePipeline::BatchSetter& batchSetter, const render::ShapePipeline::ItemSetter& itemSetter); void initZPassPipelines(ShapePlumber& plumber, gpu::StatePointer state); void addPlumberPipeline(ShapePlumber& plumber, @@ -436,12 +436,13 @@ void initDeferredPipelines(render::ShapePlumber& plumber, const render::ShapePip skinModelShadowFadeVertex, modelShadowFadePixel, batchSetter, itemSetter); } -void initForwardPipelines(render::ShapePlumber& plumber) { +void initForwardPipelines(ShapePlumber& plumber, const render::ShapePipeline::BatchSetter& batchSetter, const render::ShapePipeline::ItemSetter& itemSetter) { // Vertex shaders auto modelVertex = gpu::Shader::createVertex(std::string(model_vert)); auto modelNormalMapVertex = gpu::Shader::createVertex(std::string(model_normal_map_vert)); auto skinModelVertex = gpu::Shader::createVertex(std::string(skin_model_vert)); auto skinModelNormalMapVertex = gpu::Shader::createVertex(std::string(skin_model_normal_map_vert)); + auto skinModelNormalMapFadeVertex = gpu::Shader::createVertex(std::string(skin_model_normal_map_fade_vert)); // Pixel shaders auto modelPixel = gpu::Shader::createPixel(std::string(forward_model_frag)); @@ -449,38 +450,43 @@ void initForwardPipelines(render::ShapePlumber& plumber) { auto modelNormalMapPixel = gpu::Shader::createPixel(std::string(forward_model_normal_map_frag)); auto modelSpecularMapPixel = gpu::Shader::createPixel(std::string(forward_model_specular_map_frag)); auto modelNormalSpecularMapPixel = gpu::Shader::createPixel(std::string(forward_model_normal_specular_map_frag)); + auto modelNormalMapFadePixel = gpu::Shader::createPixel(std::string(model_normal_map_fade_frag)); using Key = render::ShapeKey; - auto addPipeline = std::bind(&addPlumberPipeline, std::ref(plumber), _1, _2, _3, nullptr, nullptr); + auto addPipeline = std::bind(&addPlumberPipeline, std::ref(plumber), _1, _2, _3, _4, _5); // Opaques addPipeline( Key::Builder().withMaterial(), - modelVertex, modelPixel); + modelVertex, modelPixel, nullptr, nullptr); addPipeline( Key::Builder().withMaterial().withUnlit(), - modelVertex, modelUnlitPixel); + modelVertex, modelUnlitPixel, nullptr, nullptr); addPipeline( Key::Builder().withMaterial().withTangents(), - modelNormalMapVertex, modelNormalMapPixel); + modelNormalMapVertex, modelNormalMapPixel, nullptr, nullptr); addPipeline( Key::Builder().withMaterial().withSpecular(), - modelVertex, modelSpecularMapPixel); + modelVertex, modelSpecularMapPixel, nullptr, nullptr); addPipeline( Key::Builder().withMaterial().withTangents().withSpecular(), - modelNormalMapVertex, modelNormalSpecularMapPixel); + modelNormalMapVertex, modelNormalSpecularMapPixel, nullptr, nullptr); // Skinned addPipeline( Key::Builder().withMaterial().withSkinned(), - skinModelVertex, modelPixel); + skinModelVertex, modelPixel, nullptr, nullptr); addPipeline( Key::Builder().withMaterial().withSkinned().withTangents(), - skinModelNormalMapVertex, modelNormalMapPixel); + skinModelNormalMapVertex, modelNormalMapPixel, nullptr, nullptr); addPipeline( Key::Builder().withMaterial().withSkinned().withSpecular(), - skinModelVertex, modelSpecularMapPixel); + skinModelVertex, modelSpecularMapPixel, nullptr, nullptr); addPipeline( Key::Builder().withMaterial().withSkinned().withTangents().withSpecular(), - skinModelNormalMapVertex, modelNormalSpecularMapPixel); + skinModelNormalMapVertex, modelNormalSpecularMapPixel, nullptr, nullptr); + addPipeline( + Key::Builder().withMaterial().withSkinned().withTangents().withFade(), + skinModelNormalMapFadeVertex, modelNormalMapFadePixel, batchSetter, itemSetter, nullptr, nullptr); + } void addPlumberPipeline(ShapePlumber& plumber, From 40a7634a5394322057e5f6c77cebaa790276b006 Mon Sep 17 00:00:00 2001 From: Cristian Luis Duarte Date: Fri, 29 Dec 2017 17:38:42 -0300 Subject: [PATCH 11/17] Incorporate changes from gpu-gl into gpu-gles (changes in organization and architecture of classes). --- libraries/gpu-gles/CMakeLists.txt | 2 +- libraries/gpu-gles/src/gpu/gl/GLBackend.cpp | 47 +- libraries/gpu-gles/src/gpu/gl/GLBackend.h | 623 +++++++------- .../gpu-gles/src/gpu/gl/GLBackendShader.cpp | 530 ++++++++++++ .../gpu-gles/src/gpu/gl/GLBackendTexture.cpp | 55 +- .../src/gpu/gl/GLBackendTransform.cpp | 52 +- .../gpu-gles/src/gpu/gl/GLFramebuffer.cpp | 2 +- libraries/gpu-gles/src/gpu/gl/GLFramebuffer.h | 2 +- libraries/gpu-gles/src/gpu/gl/GLShader.cpp | 136 +--- libraries/gpu-gles/src/gpu/gl/GLShader.h | 7 + libraries/gpu-gles/src/gpu/gl/GLShared.cpp | 549 +------------ libraries/gpu-gles/src/gpu/gl/GLShared.h | 26 +- .../gpu-gles/src/gpu/gl/GLTexelFormat.cpp | 79 +- libraries/gpu-gles/src/gpu/gl/GLTexelFormat.h | 5 + libraries/gpu-gles/src/gpu/gl/GLTexture.cpp | 766 +++++++++++++----- libraries/gpu-gles/src/gpu/gl/GLTexture.h | 352 ++++---- .../gpu-gles/src/gpu/gl/GLTextureTransfer.cpp | 207 ----- .../gpu-gles/src/gpu/gl/GLTextureTransfer.h | 78 -- libraries/gpu-gles/src/gpu/gles/GLESBackend.h | 104 ++- .../src/gpu/gles/GLESBackendOutput.cpp | 16 +- .../src/gpu/gles/GLESBackendShader.cpp | 113 +++ .../src/gpu/gles/GLESBackendTexture.cpp | 668 ++++++++++++--- .../src/gpu/gles/GLESBackendTransform.cpp | 42 +- 23 files changed, 2581 insertions(+), 1880 deletions(-) create mode 100644 libraries/gpu-gles/src/gpu/gl/GLBackendShader.cpp delete mode 100644 libraries/gpu-gles/src/gpu/gl/GLTextureTransfer.cpp delete mode 100644 libraries/gpu-gles/src/gpu/gl/GLTextureTransfer.h create mode 100644 libraries/gpu-gles/src/gpu/gles/GLESBackendShader.cpp diff --git a/libraries/gpu-gles/CMakeLists.txt b/libraries/gpu-gles/CMakeLists.txt index 55ec53b184..e9875411e3 100644 --- a/libraries/gpu-gles/CMakeLists.txt +++ b/libraries/gpu-gles/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET_NAME gpu-gles) -setup_hifi_library(OpenGL) +setup_hifi_library(Concurrent) link_hifi_libraries(shared gl gpu) GroupSources("src") diff --git a/libraries/gpu-gles/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gles/src/gpu/gl/GLBackend.cpp index c42d4008f9..667b408801 100644 --- a/libraries/gpu-gles/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gles/src/gpu/gl/GLBackend.cpp @@ -22,17 +22,18 @@ #include "nvToolsExt.h" #endif +#include #include #include #include #include "GLTexture.h" #include "GLShader.h" + using namespace gpu; using namespace gpu::gl; static GLBackend* INSTANCE{ nullptr }; -static const char* GL_BACKEND_PROPERTY_NAME = "com.highfidelity.gl.backend"; BackendPointer GLBackend::createBackend() { // FIXME provide a mechanism to override the backend for testing @@ -45,18 +46,17 @@ BackendPointer GLBackend::createBackend() { result->initInput(); result->initTransform(); + result->initTextureManagementStage(); INSTANCE = result.get(); void* voidInstance = &(*result); - qApp->setProperty(GL_BACKEND_PROPERTY_NAME, QVariant::fromValue(voidInstance)); - - gl::GLTexture::initTextureTransferHelper(); + qApp->setProperty(hifi::properties::gl::BACKEND, QVariant::fromValue(voidInstance)); return result; } GLBackend& getBackend() { if (!INSTANCE) { - INSTANCE = static_cast(qApp->property(GL_BACKEND_PROPERTY_NAME).value()); + INSTANCE = static_cast(qApp->property(hifi::properties::gl::BACKEND).value()); } return *INSTANCE; } @@ -65,9 +65,6 @@ bool GLBackend::makeProgram(Shader& shader, const Shader::BindingSet& slotBindin return GLShader::makeProgram(getBackend(), shader, slotBindings); } -std::array commandNames = { - {QString("draw"),QString("drawIndexed"),QString("drawInstanced"),QString("drawIndexedInstanced"),QString("multiDrawIndirect"),QString("multiDrawIndexedIndirect"),QString("setInputFormat"),QString("setInputBuffer"),QString("setIndexBuffer"),QString("setIndirectBuffer"),QString("setModelTransform"),QString("setViewTransform"),QString("setProjectionTransform"),QString("setViewportTransform"),QString("setDepthRangeTransform"),QString("setPipeline"),QString("setStateBlendFactor"),QString("setStateScissorRect"),QString("setUniformBuffer"),QString("setResourceTexture"),QString("setFramebuffer"),QString("clearFramebuffer"),QString("blit"),QString("generateTextureMips"),QString("beginQuery"),QString("endQuery"),QString("getQuery"),QString("resetStages"),QString("runLambda"),QString("startNamedCall"),QString("stopNamedCall"),QString("glUniform1i"),QString("glUniform1f"),QString("glUniform2f"),QString("glUniform3f"),QString("glUniform4f"),QString("glUniform3fv"),QString("glUniform4fv"),QString("glUniform4iv"),QString("glUniformMatrix3fv"),QString("glUniformMatrix4fv"),QString("glColor4f"),QString("pushProfileRange"),QString("popProfileRange"),QString("NUM_COMMANDS")} -}; GLBackend::CommandCall GLBackend::_commandCalls[Batch::NUM_COMMANDS] = { @@ -161,6 +158,12 @@ void GLBackend::init() { qCDebug(gpugllogging, "Status: Using GLEW %s\n", glewGetString(GLEW_VERSION)); */ +#if THREADED_TEXTURE_BUFFERING + // This has to happen on the main thread in order to give the thread + // pool a reasonable parent object + GLVariableAllocationSupport::TransferJob::startBufferingThread(); +#endif + }); } @@ -171,8 +174,6 @@ GLBackend::GLBackend() { GLBackend::~GLBackend() { - resetStages(); - killInput(); killTransform(); } @@ -193,7 +194,7 @@ void GLBackend::renderPassTransfer(const Batch& batch) { } } - { // Sync all the buffers + { // Sync all the transform states ANDROID_PROFILE(render, "syncCPUTransform", 0xffaaaaff, 1) _transform._cameras.clear(); _transform._cameraOffsets.clear(); @@ -209,6 +210,14 @@ void GLBackend::renderPassTransfer(const Batch& batch) { _transform.preUpdate(_commandIndex, _stereo); break; + case Batch::COMMAND_disableContextStereo: + _stereo._contextDisable = true; + break; + + case Batch::COMMAND_restoreContextStereo: + _stereo._contextDisable = false; + break; + case Batch::COMMAND_setViewportTransform: case Batch::COMMAND_setViewTransform: case Batch::COMMAND_setProjectionTransform: { @@ -263,7 +272,8 @@ void GLBackend::renderPassDraw(const Batch& batch) { updateInput(); updateTransform(batch); updatePipeline(); - {ANDROID_PROFILE_COMMAND(render, (int)(*command), 0xff0000ff, 1) + { + ANDROID_PROFILE_COMMAND(render, (int)(*command), 0xff0000ff, 1) CommandCall call = _commandCalls[(*command)]; (this->*(call))(batch, *offset); } @@ -298,6 +308,11 @@ void GLBackend::render(const Batch& batch) { renderPassTransfer(batch); } +#ifdef GPU_STEREO_DRAWCALL_INSTANCED + if (_stereo.isStereo()) { + glEnable(GL_CLIP_DISTANCE0); + } +#endif { //PROFILE_RANGE(render_gpu_gl, _stereo._enable ? "Render Stereo" : "Render"); ANDROID_PROFILE(render, "RenderPassDraw", 0xff00ddff, 1) @@ -413,7 +428,7 @@ void GLBackend::do_glUniform1i(const Batch& batch, size_t paramOffset) { } updatePipeline(); - glUniform1f( + glUniform1i( GET_UNIFORM_LOCATION(batch._params[paramOffset + 1]._int), batch._params[paramOffset + 0]._int); (void)CHECK_GL_ERROR(); @@ -731,9 +746,9 @@ void GLBackend::recycle() const { } } -#ifndef THREADED_TEXTURE_TRANSFER - gl::GLTexture::_textureTransferHelper->process(); -#endif + GLVariableAllocationSupport::manageMemory(); + GLVariableAllocationSupport::_frameTexturesCreated = 0; + } void GLBackend::setCameraCorrection(const Mat4& correction) { diff --git a/libraries/gpu-gles/src/gpu/gl/GLBackend.h b/libraries/gpu-gles/src/gpu/gl/GLBackend.h index c6b2ff6cc0..f26bdc4818 100644 --- a/libraries/gpu-gles/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gles/src/gpu/gl/GLBackend.h @@ -53,257 +53,259 @@ #endif namespace gpu { namespace gl { - class GLBackend : public Backend, public std::enable_shared_from_this { - // Context Backend static interface required - friend class gpu::Context; - static void init(); - static BackendPointer createBackend(); +class GLBackend : public Backend, public std::enable_shared_from_this { + // Context Backend static interface required + friend class gpu::Context; + static void init(); + static BackendPointer createBackend(); - protected: - explicit GLBackend(bool syncCache); - GLBackend(); - public: - static bool makeProgram(Shader& shader, const Shader::BindingSet& slotBindings = Shader::BindingSet()); +protected: + explicit GLBackend(bool syncCache); + GLBackend(); +public: + static bool makeProgram(Shader& shader, const Shader::BindingSet& slotBindings = Shader::BindingSet()); - ~GLBackend(); + virtual ~GLBackend(); - void setCameraCorrection(const Mat4& correction); - void render(const Batch& batch) final override; + void setCameraCorrection(const Mat4& correction); + void render(const Batch& batch) final override; - // This call synchronize the Full Backend cache with the current GLState - // THis is only intended to be used when mixing raw gl calls with the gpu api usage in order to sync - // the gpu::Backend state with the true gl state which has probably been messed up by these ugly naked gl calls - // Let's try to avoid to do that as much as possible! - void syncCache() final override; + // This call synchronize the Full Backend cache with the current GLState + // THis is only intended to be used when mixing raw gl calls with the gpu api usage in order to sync + // the gpu::Backend state with the true gl state which has probably been messed up by these ugly naked gl calls + // Let's try to avoid to do that as much as possible! + void syncCache() final override; - // This is the ugly "download the pixels to sysmem for taking a snapshot" - // Just avoid using it, it's ugly and will break performances - virtual void downloadFramebuffer(const FramebufferPointer& srcFramebuffer, - const Vec4i& region, QImage& destImage) final override; + // This is the ugly "download the pixels to sysmem for taking a snapshot" + // Just avoid using it, it's ugly and will break performances + virtual void downloadFramebuffer(const FramebufferPointer& srcFramebuffer, + const Vec4i& region, QImage& destImage) final override; - // this is the maximum numeber of available input buffers - size_t getNumInputBuffers() const { return _input._invalidBuffers.size(); } + // this is the maximum numeber of available input buffers + size_t getNumInputBuffers() const { return _input._invalidBuffers.size(); } - // this is the maximum per shader stage on the low end apple - // TODO make it platform dependant at init time - static const int MAX_NUM_UNIFORM_BUFFERS = 12; - size_t getMaxNumUniformBuffers() const { return MAX_NUM_UNIFORM_BUFFERS; } + // this is the maximum per shader stage on the low end apple + // TODO make it platform dependant at init time + static const int MAX_NUM_UNIFORM_BUFFERS = 12; + size_t getMaxNumUniformBuffers() const { return MAX_NUM_UNIFORM_BUFFERS; } - // this is the maximum per shader stage on the low end apple - // TODO make it platform dependant at init time - static const int MAX_NUM_RESOURCE_BUFFERS = 16; - size_t getMaxNumResourceBuffers() const { return MAX_NUM_RESOURCE_BUFFERS; } - static const int MAX_NUM_RESOURCE_TEXTURES = 16; - size_t getMaxNumResourceTextures() const { return MAX_NUM_RESOURCE_TEXTURES; } + // this is the maximum per shader stage on the low end apple + // TODO make it platform dependant at init time + static const int MAX_NUM_RESOURCE_BUFFERS = 16; + size_t getMaxNumResourceBuffers() const { return MAX_NUM_RESOURCE_BUFFERS; } + static const int MAX_NUM_RESOURCE_TEXTURES = 16; + size_t getMaxNumResourceTextures() const { return MAX_NUM_RESOURCE_TEXTURES; } - // Draw Stage - virtual void do_draw(const Batch& batch, size_t paramOffset) = 0; - virtual void do_drawIndexed(const Batch& batch, size_t paramOffset) = 0; - virtual void do_drawInstanced(const Batch& batch, size_t paramOffset) = 0; - virtual void do_drawIndexedInstanced(const Batch& batch, size_t paramOffset) = 0; - virtual void do_multiDrawIndirect(const Batch& batch, size_t paramOffset) = 0; - virtual void do_multiDrawIndexedIndirect(const Batch& batch, size_t paramOffset) = 0; + // Draw Stage + virtual void do_draw(const Batch& batch, size_t paramOffset) = 0; + virtual void do_drawIndexed(const Batch& batch, size_t paramOffset) = 0; + virtual void do_drawInstanced(const Batch& batch, size_t paramOffset) = 0; + virtual void do_drawIndexedInstanced(const Batch& batch, size_t paramOffset) = 0; + virtual void do_multiDrawIndirect(const Batch& batch, size_t paramOffset) = 0; + virtual void do_multiDrawIndexedIndirect(const Batch& batch, size_t paramOffset) = 0; - // Input Stage - virtual void do_setInputFormat(const Batch& batch, size_t paramOffset) final; - virtual void do_setInputBuffer(const Batch& batch, size_t paramOffset) final; - virtual void do_setIndexBuffer(const Batch& batch, size_t paramOffset) final; - virtual void do_setIndirectBuffer(const Batch& batch, size_t paramOffset) final; - virtual void do_generateTextureMips(const Batch& batch, size_t paramOffset) final; + // Input Stage + virtual void do_setInputFormat(const Batch& batch, size_t paramOffset) final; + virtual void do_setInputBuffer(const Batch& batch, size_t paramOffset) final; + virtual void do_setIndexBuffer(const Batch& batch, size_t paramOffset) final; + virtual void do_setIndirectBuffer(const Batch& batch, size_t paramOffset) final; + virtual void do_generateTextureMips(const Batch& batch, size_t paramOffset) final; - // Transform Stage - virtual void do_setModelTransform(const Batch& batch, size_t paramOffset) final; - virtual void do_setViewTransform(const Batch& batch, size_t paramOffset) final; - virtual void do_setProjectionTransform(const Batch& batch, size_t paramOffset) final; - virtual void do_setViewportTransform(const Batch& batch, size_t paramOffset) final; - virtual void do_setDepthRangeTransform(const Batch& batch, size_t paramOffset) final; + // Transform Stage + virtual void do_setModelTransform(const Batch& batch, size_t paramOffset) final; + virtual void do_setViewTransform(const Batch& batch, size_t paramOffset) final; + virtual void do_setProjectionTransform(const Batch& batch, size_t paramOffset) final; + virtual void do_setViewportTransform(const Batch& batch, size_t paramOffset) final; + virtual void do_setDepthRangeTransform(const Batch& batch, size_t paramOffset) final; - // Uniform Stage - virtual void do_setUniformBuffer(const Batch& batch, size_t paramOffset) final; + // Uniform Stage + virtual void do_setUniformBuffer(const Batch& batch, size_t paramOffset) final; - // Resource Stage - virtual void do_setResourceBuffer(const Batch& batch, size_t paramOffset) final; - virtual void do_setResourceTexture(const Batch& batch, size_t paramOffset) final; + // Resource Stage + virtual void do_setResourceBuffer(const Batch& batch, size_t paramOffset) final; + virtual void do_setResourceTexture(const Batch& batch, size_t paramOffset) final; - // Pipeline Stage - virtual void do_setPipeline(const Batch& batch, size_t paramOffset) final; + // Pipeline Stage + virtual void do_setPipeline(const Batch& batch, size_t paramOffset) final; - // Output stage - virtual void do_setFramebuffer(const Batch& batch, size_t paramOffset) final; - virtual void do_clearFramebuffer(const Batch& batch, size_t paramOffset) final; - virtual void do_blit(const Batch& batch, size_t paramOffset) = 0; + // Output stage + virtual void do_setFramebuffer(const Batch& batch, size_t paramOffset) final; + virtual void do_clearFramebuffer(const Batch& batch, size_t paramOffset) final; + virtual void do_blit(const Batch& batch, size_t paramOffset) = 0; - // Query section - virtual void do_beginQuery(const Batch& batch, size_t paramOffset) final; - virtual void do_endQuery(const Batch& batch, size_t paramOffset) final; - virtual void do_getQuery(const Batch& batch, size_t paramOffset) final; + // Query section + virtual void do_beginQuery(const Batch& batch, size_t paramOffset) final; + virtual void do_endQuery(const Batch& batch, size_t paramOffset) final; + virtual void do_getQuery(const Batch& batch, size_t paramOffset) final; - // Reset stages - virtual void do_resetStages(const Batch& batch, size_t paramOffset) final; + // Reset stages + virtual void do_resetStages(const Batch& batch, size_t paramOffset) final; - virtual void do_disableContextViewCorrection(const Batch& batch, size_t paramOffset) final; - virtual void do_restoreContextViewCorrection(const Batch& batch, size_t paramOffset) final; + + virtual void do_disableContextViewCorrection(const Batch& batch, size_t paramOffset) final; + virtual void do_restoreContextViewCorrection(const Batch& batch, size_t paramOffset) final; - virtual void do_disableContextStereo(const Batch& batch, size_t paramOffset) final; - virtual void do_restoreContextStereo(const Batch& batch, size_t paramOffset) final; + virtual void do_disableContextStereo(const Batch& batch, size_t paramOffset) final; + virtual void do_restoreContextStereo(const Batch& batch, size_t paramOffset) final; - virtual void do_runLambda(const Batch& batch, size_t paramOffset) final; + virtual void do_runLambda(const Batch& batch, size_t paramOffset) final; - virtual void do_startNamedCall(const Batch& batch, size_t paramOffset) final; - virtual void do_stopNamedCall(const Batch& batch, size_t paramOffset) final; + virtual void do_startNamedCall(const Batch& batch, size_t paramOffset) final; + virtual void do_stopNamedCall(const Batch& batch, size_t paramOffset) final; - static const int MAX_NUM_ATTRIBUTES = Stream::NUM_INPUT_SLOTS; - // The drawcall Info attribute channel is reserved and is the upper bound for the number of availables Input buffers - static const int MAX_NUM_INPUT_BUFFERS = Stream::DRAW_CALL_INFO; + static const int MAX_NUM_ATTRIBUTES = Stream::NUM_INPUT_SLOTS; + // The drawcall Info attribute channel is reserved and is the upper bound for the number of availables Input buffers + static const int MAX_NUM_INPUT_BUFFERS = Stream::DRAW_CALL_INFO; - virtual void do_pushProfileRange(const Batch& batch, size_t paramOffset) final; - virtual void do_popProfileRange(const Batch& batch, size_t paramOffset) final; + virtual void do_pushProfileRange(const Batch& batch, size_t paramOffset) final; + virtual void do_popProfileRange(const Batch& batch, size_t paramOffset) final; - // TODO: As long as we have gl calls explicitely issued from interface - // code, we need to be able to record and batch these calls. THe long - // term strategy is to get rid of any GL calls in favor of the HIFI GPU API - virtual void do_glUniform1i(const Batch& batch, size_t paramOffset) final; - virtual void do_glUniform1f(const Batch& batch, size_t paramOffset) final; - virtual void do_glUniform2f(const Batch& batch, size_t paramOffset) final; - virtual void do_glUniform3f(const Batch& batch, size_t paramOffset) final; - virtual void do_glUniform4f(const Batch& batch, size_t paramOffset) final; - virtual void do_glUniform3fv(const Batch& batch, size_t paramOffset) final; - virtual void do_glUniform4fv(const Batch& batch, size_t paramOffset) final; - virtual void do_glUniform4iv(const Batch& batch, size_t paramOffset) final; - virtual void do_glUniformMatrix3fv(const Batch& batch, size_t paramOffset) final; - virtual void do_glUniformMatrix4fv(const Batch& batch, size_t paramOffset) final; + // TODO: As long as we have gl calls explicitely issued from interface + // code, we need to be able to record and batch these calls. THe long + // term strategy is to get rid of any GL calls in favor of the HIFI GPU API + virtual void do_glUniform1i(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniform1f(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniform2f(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniform3f(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniform4f(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniform3fv(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniform4fv(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniform4iv(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniformMatrix3fv(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniformMatrix4fv(const Batch& batch, size_t paramOffset) final; - virtual void do_glColor4f(const Batch& batch, size_t paramOffset) final; + virtual void do_glColor4f(const Batch& batch, size_t paramOffset) final; - // The State setters called by the GLState::Commands when a new state is assigned - virtual void do_setStateFillMode(int32 mode) final; - virtual void do_setStateCullMode(int32 mode) final; - virtual void do_setStateFrontFaceClockwise(bool isClockwise) final; - virtual void do_setStateDepthClampEnable(bool enable) final; - virtual void do_setStateScissorEnable(bool enable) final; - virtual void do_setStateMultisampleEnable(bool enable) final; - virtual void do_setStateAntialiasedLineEnable(bool enable) final; - virtual void do_setStateDepthBias(Vec2 bias) final; - virtual void do_setStateDepthTest(State::DepthTest test) final; - virtual void do_setStateStencil(State::StencilActivation activation, State::StencilTest frontTest, State::StencilTest backTest) final; - virtual void do_setStateAlphaToCoverageEnable(bool enable) final; - virtual void do_setStateSampleMask(uint32 mask) final; - virtual void do_setStateBlend(State::BlendFunction blendFunction) final; - virtual void do_setStateColorWriteMask(uint32 mask) final; - virtual void do_setStateBlendFactor(const Batch& batch, size_t paramOffset) final; - virtual void do_setStateScissorRect(const Batch& batch, size_t paramOffset) final; + // The State setters called by the GLState::Commands when a new state is assigned + virtual void do_setStateFillMode(int32 mode) final; + virtual void do_setStateCullMode(int32 mode) final; + virtual void do_setStateFrontFaceClockwise(bool isClockwise) final; + virtual void do_setStateDepthClampEnable(bool enable) final; + virtual void do_setStateScissorEnable(bool enable) final; + virtual void do_setStateMultisampleEnable(bool enable) final; + virtual void do_setStateAntialiasedLineEnable(bool enable) final; + virtual void do_setStateDepthBias(Vec2 bias) final; + virtual void do_setStateDepthTest(State::DepthTest test) final; + virtual void do_setStateStencil(State::StencilActivation activation, State::StencilTest frontTest, State::StencilTest backTest) final; + virtual void do_setStateAlphaToCoverageEnable(bool enable) final; + virtual void do_setStateSampleMask(uint32 mask) final; + virtual void do_setStateBlend(State::BlendFunction blendFunction) final; + virtual void do_setStateColorWriteMask(uint32 mask) final; + virtual void do_setStateBlendFactor(const Batch& batch, size_t paramOffset) final; + virtual void do_setStateScissorRect(const Batch& batch, size_t paramOffset) final; - virtual GLuint getFramebufferID(const FramebufferPointer& framebuffer) = 0; - virtual GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) = 0; - virtual GLuint getBufferID(const Buffer& buffer) = 0; - virtual GLuint getQueryID(const QueryPointer& query) = 0; - virtual bool isTextureReady(const TexturePointer& texture); + virtual GLuint getFramebufferID(const FramebufferPointer& framebuffer) = 0; + virtual GLuint getTextureID(const TexturePointer& texture) final; + virtual GLuint getBufferID(const Buffer& buffer) = 0; + virtual GLuint getQueryID(const QueryPointer& query) = 0; - virtual void releaseBuffer(GLuint id, Size size) const; - virtual void releaseExternalTexture(GLuint id, const Texture::ExternalRecycler& recycler) const; - virtual void releaseTexture(GLuint id, Size size) const; - virtual void releaseFramebuffer(GLuint id) const; - virtual void releaseShader(GLuint id) const; - virtual void releaseProgram(GLuint id) const; - virtual void releaseQuery(GLuint id) const; - virtual void queueLambda(const std::function lambda) const; + virtual GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) = 0; + virtual GLBuffer* syncGPUObject(const Buffer& buffer) = 0; + virtual GLTexture* syncGPUObject(const TexturePointer& texture); + virtual GLQuery* syncGPUObject(const Query& query) = 0; + //virtual bool isTextureReady(const TexturePointer& texture); - bool isTextureManagementSparseEnabled() const override { return (_textureManagement._sparseCapable && Texture::getEnableSparseTextures()); } + virtual void releaseBuffer(GLuint id, Size size) const; + virtual void releaseExternalTexture(GLuint id, const Texture::ExternalRecycler& recycler) const; + virtual void releaseTexture(GLuint id, Size size) const; + virtual void releaseFramebuffer(GLuint id) const; + virtual void releaseShader(GLuint id) const; + virtual void releaseProgram(GLuint id) const; + virtual void releaseQuery(GLuint id) const; + virtual void queueLambda(const std::function lambda) const; - protected: + bool isTextureManagementSparseEnabled() const override { return (_textureManagement._sparseCapable && Texture::getEnableSparseTextures()); } - void recycle() const override; - virtual GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) = 0; - virtual GLBuffer* syncGPUObject(const Buffer& buffer) = 0; - virtual GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) = 0; - virtual GLQuery* syncGPUObject(const Query& query) = 0; +protected: - static const size_t INVALID_OFFSET = (size_t)-1; - bool _inRenderTransferPass { false }; - int32_t _uboAlignment { 0 }; - int _currentDraw { -1 }; + void recycle() const override; - std::list profileRanges; - mutable Mutex _trashMutex; - mutable std::list> _buffersTrash; - mutable std::list> _texturesTrash; - mutable std::list> _externalTexturesTrash; - mutable std::list _framebuffersTrash; - mutable std::list _shadersTrash; - mutable std::list _programsTrash; - mutable std::list _queriesTrash; - mutable std::list> _lambdaQueue; + static const size_t INVALID_OFFSET = (size_t)-1; + bool _inRenderTransferPass { false }; + int32_t _uboAlignment { 0 }; + int _currentDraw { -1 }; - void renderPassTransfer(const Batch& batch); - void renderPassDraw(const Batch& batch); - void setupStereoSide(int side); + std::list profileRanges; + mutable Mutex _trashMutex; + mutable std::list> _buffersTrash; + mutable std::list> _texturesTrash; + mutable std::list> _externalTexturesTrash; + mutable std::list _framebuffersTrash; + mutable std::list _shadersTrash; + mutable std::list _programsTrash; + mutable std::list _queriesTrash; + mutable std::list> _lambdaQueue; - virtual void initInput() final; - virtual void killInput() final; - virtual void syncInputStateCache() final; - virtual void resetInputStage(); - virtual void updateInput(); + void renderPassTransfer(const Batch& batch); + void renderPassDraw(const Batch& batch); + void setupStereoSide(int side); - struct InputStageState { - bool _invalidFormat { true }; - Stream::FormatPointer _format; - std::string _formatKey; + virtual void initInput() final; + virtual void killInput() final; + virtual void syncInputStateCache() final; + virtual void resetInputStage(); + virtual void updateInput() = 0; - typedef std::bitset ActivationCache; - ActivationCache _attributeActivation { 0 }; + struct InputStageState { + bool _invalidFormat { true }; + Stream::FormatPointer _format; + std::string _formatKey; - typedef std::bitset BuffersState; + typedef std::bitset ActivationCache; + ActivationCache _attributeActivation { 0 }; - BuffersState _invalidBuffers{ 0 }; - BuffersState _attribBindingBuffers{ 0 }; + typedef std::bitset BuffersState; - Buffers _buffers; - Offsets _bufferOffsets; - Offsets _bufferStrides; - std::vector _bufferVBOs; + BuffersState _invalidBuffers{ 0 }; + BuffersState _attribBindingBuffers{ 0 }; - glm::vec4 _colorAttribute{ 0.0f }; + Buffers _buffers; + Offsets _bufferOffsets; + Offsets _bufferStrides; + std::vector _bufferVBOs; - BufferPointer _indexBuffer; - Offset _indexBufferOffset { 0 }; - Type _indexBufferType { UINT32 }; + glm::vec4 _colorAttribute{ 0.0f }; - BufferPointer _indirectBuffer; - Offset _indirectBufferOffset{ 0 }; - Offset _indirectBufferStride{ 0 }; + BufferPointer _indexBuffer; + Offset _indexBufferOffset { 0 }; + Type _indexBufferType { UINT32 }; + + BufferPointer _indirectBuffer; + Offset _indirectBufferOffset{ 0 }; + Offset _indirectBufferStride{ 0 }; - GLuint _defaultVAO { 0 }; + GLuint _defaultVAO { 0 }; - InputStageState() : - _invalidFormat(true), - _format(0), - _formatKey(), - _attributeActivation(0), - _buffers(_invalidBuffers.size(), BufferPointer(0)), - _bufferOffsets(_invalidBuffers.size(), 0), - _bufferStrides(_invalidBuffers.size(), 0), - _bufferVBOs(_invalidBuffers.size(), 0) {} - } _input; + InputStageState() : + _invalidFormat(true), + _format(0), + _formatKey(), + _attributeActivation(0), + _buffers(_invalidBuffers.size(), BufferPointer(0)), + _bufferOffsets(_invalidBuffers.size(), 0), + _bufferStrides(_invalidBuffers.size(), 0), + _bufferVBOs(_invalidBuffers.size(), 0) {} + } _input; - virtual void initTransform() = 0; - void killTransform(); - // Synchronize the state cache of this Backend with the actual real state of the GL Context - void syncTransformStateCache(); - void updateTransform(const Batch& batch); - void resetTransformStage(); + virtual void initTransform() = 0; + void killTransform(); + // Synchronize the state cache of this Backend with the actual real state of the GL Context + void syncTransformStateCache(); + virtual void updateTransform(const Batch& batch) = 0; + virtual void resetTransformStage(); - // Allows for correction of the camera pose to account for changes - // between the time when a was recorded and the time(s) when it is - // executed - struct CameraCorrection { - Mat4 correction; - Mat4 correctionInverse; - }; + // Allows for correction of the camera pose to account for changes + // between the time when a was recorded and the time(s) when it is + // executed + struct CameraCorrection { + Mat4 correction; + Mat4 correctionInverse; + }; - struct TransformStageState { + struct TransformStageState { #ifdef GPU_STEREO_CAMERA_BUFFER - struct Cameras { + struct Cameras { TransformCamera _cams[2]; Cameras() {}; @@ -313,131 +315,158 @@ namespace gpu { namespace gl { using CameraBufferElement = Cameras; #else - using CameraBufferElement = TransformCamera; + using CameraBufferElement = TransformCamera; #endif - using TransformCameras = std::vector; + using TransformCameras = std::vector; - TransformCamera _camera; - TransformCameras _cameras; + TransformCamera _camera; + TransformCameras _cameras; - mutable std::map _drawCallInfoOffsets; + mutable std::map _drawCallInfoOffsets; - GLuint _objectBuffer { 0 }; - GLuint _cameraBuffer { 0 }; - GLuint _drawCallInfoBuffer { 0 }; - GLuint _objectBufferTexture { 0 }; - size_t _cameraUboSize { 0 }; - bool _viewIsCamera{ false }; - bool _skybox { false }; - Transform _view; - CameraCorrection _correction; - bool _viewCorrectionEnabled{ true }; + GLuint _objectBuffer { 0 }; + GLuint _cameraBuffer { 0 }; + GLuint _drawCallInfoBuffer { 0 }; + GLuint _objectBufferTexture { 0 }; + size_t _cameraUboSize { 0 }; + bool _viewIsCamera{ false }; + bool _skybox { false }; + Transform _view; + CameraCorrection _correction; + bool _viewCorrectionEnabled{ true }; - Mat4 _projection; - Vec4i _viewport { 0, 0, 1, 1 }; - Vec2 _depthRange { 0.0f, 1.0f }; - bool _invalidView { false }; - bool _invalidProj { false }; - bool _invalidViewport { false }; - bool _enabledDrawcallInfoBuffer{ false }; + Mat4 _projection; + Vec4i _viewport { 0, 0, 1, 1 }; + Vec2 _depthRange { 0.0f, 1.0f }; + bool _invalidView { false }; + bool _invalidProj { false }; + bool _invalidViewport { false }; - using Pair = std::pair; - using List = std::list; - List _cameraOffsets; - mutable List::const_iterator _camerasItr; - mutable size_t _currentCameraOffset{ INVALID_OFFSET }; + bool _enabledDrawcallInfoBuffer{ false }; - void preUpdate(size_t commandIndex, const StereoState& stereo); - void update(size_t commandIndex, const StereoState& stereo) const; - void bindCurrentCamera(int stereoSide) const; - } _transform; + using Pair = std::pair; + using List = std::list; + List _cameraOffsets; + mutable List::const_iterator _camerasItr; + mutable size_t _currentCameraOffset{ INVALID_OFFSET }; - virtual void transferTransformState(const Batch& batch) const = 0; + void preUpdate(size_t commandIndex, const StereoState& stereo); + void update(size_t commandIndex, const StereoState& stereo) const; + void bindCurrentCamera(int stereoSide) const; + } _transform; - struct UniformStageState { - std::array _buffers; - //Buffers _buffers { }; - } _uniform; + virtual void transferTransformState(const Batch& batch) const = 0; - void releaseUniformBuffer(uint32_t slot); - void resetUniformStage(); + struct UniformStageState { + std::array _buffers; + //Buffers _buffers { }; + } _uniform; - // update resource cache and do the gl bind/unbind call with the current gpu::Buffer cached at slot s - // This is using different gl object depending on the gl version - virtual bool bindResourceBuffer(uint32_t slot, BufferPointer& buffer) = 0; - virtual void releaseResourceBuffer(uint32_t slot) = 0; + void releaseUniformBuffer(uint32_t slot); + void resetUniformStage(); - // update resource cache and do the gl unbind call with the current gpu::Texture cached at slot s - void releaseResourceTexture(uint32_t slot); + // update resource cache and do the gl bind/unbind call with the current gpu::Buffer cached at slot s + // This is using different gl object depending on the gl version + virtual bool bindResourceBuffer(uint32_t slot, BufferPointer& buffer) = 0; + virtual void releaseResourceBuffer(uint32_t slot) = 0; - void resetResourceStage(); + // update resource cache and do the gl unbind call with the current gpu::Texture cached at slot s + void releaseResourceTexture(uint32_t slot); - struct ResourceStageState { - std::array _buffers; - std::array _textures; - //Textures _textures { { MAX_NUM_RESOURCE_TEXTURES } }; - int findEmptyTextureSlot() const; - } _resource; + void resetResourceStage(); - size_t _commandIndex{ 0 }; + struct ResourceStageState { + std::array _buffers; + std::array _textures; + //Textures _textures { { MAX_NUM_RESOURCE_TEXTURES } }; + int findEmptyTextureSlot() const; + } _resource; - // Standard update pipeline check that the current Program and current State or good to go for a - void updatePipeline(); - // Force to reset all the state fields indicated by the 'toBeReset" signature - void resetPipelineState(State::Signature toBeReset); - // Synchronize the state cache of this Backend with the actual real state of the GL Context - void syncPipelineStateCache(); - void resetPipelineStage(); + size_t _commandIndex{ 0 }; - struct PipelineStageState { - PipelinePointer _pipeline; + // Standard update pipeline check that the current Program and current State or good to go for a + void updatePipeline(); + // Force to reset all the state fields indicated by the 'toBeReset" signature + void resetPipelineState(State::Signature toBeReset); + // Synchronize the state cache of this Backend with the actual real state of the GL Context + void syncPipelineStateCache(); + void resetPipelineStage(); - GLuint _program { 0 }; - GLint _cameraCorrectionLocation { -1 }; - GLShader* _programShader { nullptr }; - bool _invalidProgram { false }; + struct PipelineStageState { + PipelinePointer _pipeline; - BufferView _cameraCorrectionBuffer { gpu::BufferView(std::make_shared(sizeof(CameraCorrection), nullptr )) }; + GLuint _program { 0 }; + GLint _cameraCorrectionLocation { -1 }; + GLShader* _programShader { nullptr }; + bool _invalidProgram { false }; - State::Data _stateCache{ State::DEFAULT }; - State::Signature _stateSignatureCache { 0 }; + BufferView _cameraCorrectionBuffer { gpu::BufferView(std::make_shared(sizeof(CameraCorrection), nullptr )) }; + BufferView _cameraCorrectionBufferIdentity { gpu::BufferView(std::make_shared(sizeof(CameraCorrection), nullptr )) }; - GLState* _state { nullptr }; - bool _invalidState { false }; + State::Data _stateCache{ State::DEFAULT }; + State::Signature _stateSignatureCache { 0 }; - PipelineStageState() { - _cameraCorrectionBuffer.edit() = CameraCorrection(); - } - } _pipeline; + GLState* _state { nullptr }; + bool _invalidState { false }; - // Synchronize the state cache of this Backend with the actual real state of the GL Context - void syncOutputStateCache(); - void resetOutputStage(); + PipelineStageState() { + _cameraCorrectionBuffer.edit() = CameraCorrection(); + _cameraCorrectionBufferIdentity.edit() = CameraCorrection(); + _cameraCorrectionBufferIdentity._buffer->flush(); + } + } _pipeline; - struct OutputStageState { - FramebufferPointer _framebuffer { nullptr }; - GLuint _drawFBO { 0 }; - } _output; + // Backend dependant compilation of the shader + virtual GLShader* compileBackendProgram(const Shader& program); + virtual GLShader* compileBackendShader(const Shader& shader); + virtual std::string getBackendShaderHeader() const; + virtual void makeProgramBindings(ShaderObject& shaderObject); + class ElementResource { + public: + gpu::Element _element; + uint16 _resource; + ElementResource(Element&& elem, uint16 resource) : _element(elem), _resource(resource) {} + }; + ElementResource getFormatFromGLUniform(GLenum gltype); + static const GLint UNUSED_SLOT {-1}; + static bool isUnusedSlot(GLint binding) { return (binding == UNUSED_SLOT); } + virtual int makeUniformSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, + Shader::SlotSet& uniforms, Shader::SlotSet& textures, Shader::SlotSet& samplers); + virtual int makeUniformBlockSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& buffers); + virtual int makeResourceBufferSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& resourceBuffers) = 0; + virtual int makeInputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& inputs); + virtual int makeOutputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& outputs); - void resetQueryStage(); - struct QueryStageState { - uint32_t _rangeQueryDepth { 0 }; - } _queryStage; - void resetStages(); + // Synchronize the state cache of this Backend with the actual real state of the GL Context + void syncOutputStateCache(); + void resetOutputStage(); + + struct OutputStageState { + FramebufferPointer _framebuffer { nullptr }; + GLuint _drawFBO { 0 }; + } _output; - struct TextureManagementStageState { - bool _sparseCapable { false }; - } _textureManagement; - virtual void initTextureManagementStage() {} + void resetQueryStage(); + struct QueryStageState { + uint32_t _rangeQueryDepth { 0 }; + } _queryStage; - typedef void (GLBackend::*CommandCall)(const Batch&, size_t); - static CommandCall _commandCalls[Batch::NUM_COMMANDS]; - friend class GLState; - friend class GLTexture; - }; + void resetStages(); - } } + struct TextureManagementStageState { + bool _sparseCapable { false }; + } _textureManagement; + virtual void initTextureManagementStage() {} + + typedef void (GLBackend::*CommandCall)(const Batch&, size_t); + static CommandCall _commandCalls[Batch::NUM_COMMANDS]; + friend class GLState; + friend class GLTexture; + friend class GLShader; +}; + +} } #endif diff --git a/libraries/gpu-gles/src/gpu/gl/GLBackendShader.cpp b/libraries/gpu-gles/src/gpu/gl/GLBackendShader.cpp new file mode 100644 index 0000000000..9e3768cb86 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gl/GLBackendShader.cpp @@ -0,0 +1,530 @@ +// +// Created by Gabriel Calero & Cristian Duarte on 2017/12/28 +// Copyright 2013-2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "GLBackend.h" +#include "GLShader.h" +#include + +using namespace gpu; +using namespace gpu::gl; + +// GLSL version +std::string GLBackend::getBackendShaderHeader() const { + return std::string("#version 310 es"); +} + +// Shader domain +static const size_t NUM_SHADER_DOMAINS = 2; + +// GL Shader type enums +// Must match the order of type specified in gpu::Shader::Type +static const std::array SHADER_DOMAINS { { + GL_VERTEX_SHADER, + GL_FRAGMENT_SHADER, +// GL_GEOMETRY_SHADER, +} }; + +// Domain specific defines +// Must match the order of type specified in gpu::Shader::Type +static const std::array DOMAIN_DEFINES { { + "#define GPU_VERTEX_SHADER", + "#define GPU_PIXEL_SHADER", +// "#define GPU_GEOMETRY_SHADER", +} }; + +// Stereo specific defines +static const std::string stereoVersion { +#ifdef GPU_STEREO_DRAWCALL_INSTANCED + "#define GPU_TRANSFORM_IS_STEREO\n#define GPU_TRANSFORM_STEREO_CAMERA\n#define GPU_TRANSFORM_STEREO_CAMERA_INSTANCED\n#define GPU_TRANSFORM_STEREO_SPLIT_SCREEN" +#endif +#ifdef GPU_STEREO_DRAWCALL_DOUBLED +#ifdef GPU_STEREO_CAMERA_BUFFER + "#define GPU_TRANSFORM_IS_STEREO\n#define GPU_TRANSFORM_STEREO_CAMERA\n#define GPU_TRANSFORM_STEREO_CAMERA_ATTRIBUTED" +#else + "#define GPU_TRANSFORM_IS_STEREO" +#endif +#endif +}; + +// Versions specific of the shader +static const std::array VERSION_DEFINES { { + "", + stereoVersion +} }; + +GLShader* GLBackend::compileBackendShader(const Shader& shader) { + // Any GLSLprogram ? normally yes... + const std::string& shaderSource = shader.getSource().getCode(); + GLenum shaderDomain = SHADER_DOMAINS[shader.getType()]; + GLShader::ShaderObjects shaderObjects; + + for (int version = 0; version < GLShader::NumVersions; version++) { + auto& shaderObject = shaderObjects[version]; + + std::string shaderDefines = getBackendShaderHeader() + "\n" + DOMAIN_DEFINES[shader.getType()] + "\n" + VERSION_DEFINES[version] + + "\n#extension GL_EXT_texture_buffer : enable" + + "\nprecision lowp float; // check precision 2" + + "\nprecision lowp samplerBuffer;" + + "\nprecision lowp sampler2DShadow;"; + std::string error; + +#ifdef SEPARATE_PROGRAM + bool result = ::gl::compileShader(shaderDomain, shaderSource, shaderDefines, shaderObject.glshader, shaderObject.glprogram, error); +#else + bool result = ::gl::compileShader(shaderDomain, shaderSource, shaderDefines, shaderObject.glshader, error); +#endif + if (!result) { + qCWarning(gpugllogging) << "GLBackend::compileBackendProgram - Shader didn't compile:\n" << error.c_str(); + return nullptr; + } + } + + // So far so good, the shader is created successfully + GLShader* object = new GLShader(this->shared_from_this()); + object->_shaderObjects = shaderObjects; + + return object; +} + +GLShader* GLBackend::compileBackendProgram(const Shader& program) { + if (!program.isProgram()) { + return nullptr; + } + + GLShader::ShaderObjects programObjects; + + for (int version = 0; version < GLShader::NumVersions; version++) { + auto& programObject = programObjects[version]; + + // Let's go through every shaders and make sure they are ready to go + std::vector< GLuint > shaderGLObjects; + for (auto subShader : program.getShaders()) { + auto object = GLShader::sync((*this), *subShader); + if (object) { + shaderGLObjects.push_back(object->_shaderObjects[version].glshader); + } else { + qCWarning(gpugllogging) << "GLBackend::compileBackendProgram - One of the shaders of the program is not compiled?"; + return nullptr; + } + } + + std::string error; + GLuint glprogram = ::gl::compileProgram(shaderGLObjects, error); + if (glprogram == 0) { + qCWarning(gpugllogging) << "GLBackend::compileBackendProgram - Program didn't link:\n" << error.c_str(); + return nullptr; + } + + programObject.glprogram = glprogram; + + makeProgramBindings(programObject); + } + + // So far so good, the program versions have all been created successfully + GLShader* object = new GLShader(this->shared_from_this()); + object->_shaderObjects = programObjects; + + return object; +} + +GLBackend::ElementResource GLBackend::getFormatFromGLUniform(GLenum gltype) { + switch (gltype) { + case GL_FLOAT: return ElementResource(Element(SCALAR, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_FLOAT_VEC2: return ElementResource(Element(VEC2, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_FLOAT_VEC3: return ElementResource(Element(VEC3, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_FLOAT_VEC4: return ElementResource(Element(VEC4, gpu::FLOAT, UNIFORM), Resource::BUFFER); + /* + case GL_DOUBLE: return ElementResource(Element(SCALAR, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_DOUBLE_VEC2: return ElementResource(Element(VEC2, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_DOUBLE_VEC3: return ElementResource(Element(VEC3, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_DOUBLE_VEC4: return ElementResource(Element(VEC4, gpu::FLOAT, UNIFORM), Resource::BUFFER); + */ + case GL_INT: return ElementResource(Element(SCALAR, gpu::INT32, UNIFORM), Resource::BUFFER); + case GL_INT_VEC2: return ElementResource(Element(VEC2, gpu::INT32, UNIFORM), Resource::BUFFER); + case GL_INT_VEC3: return ElementResource(Element(VEC3, gpu::INT32, UNIFORM), Resource::BUFFER); + case GL_INT_VEC4: return ElementResource(Element(VEC4, gpu::INT32, UNIFORM), Resource::BUFFER); + + case GL_UNSIGNED_INT: return ElementResource(Element(SCALAR, gpu::UINT32, UNIFORM), Resource::BUFFER); +#if defined(Q_OS_WIN) + case GL_UNSIGNED_INT_VEC2: return ElementResource(Element(VEC2, gpu::UINT32, UNIFORM), Resource::BUFFER); + case GL_UNSIGNED_INT_VEC3: return ElementResource(Element(VEC3, gpu::UINT32, UNIFORM), Resource::BUFFER); + case GL_UNSIGNED_INT_VEC4: return ElementResource(Element(VEC4, gpu::UINT32, UNIFORM), Resource::BUFFER); +#endif + + case GL_BOOL: return ElementResource(Element(SCALAR, gpu::BOOL, UNIFORM), Resource::BUFFER); + case GL_BOOL_VEC2: return ElementResource(Element(VEC2, gpu::BOOL, UNIFORM), Resource::BUFFER); + case GL_BOOL_VEC3: return ElementResource(Element(VEC3, gpu::BOOL, UNIFORM), Resource::BUFFER); + case GL_BOOL_VEC4: return ElementResource(Element(VEC4, gpu::BOOL, UNIFORM), Resource::BUFFER); + + + case GL_FLOAT_MAT2: return ElementResource(Element(gpu::MAT2, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_FLOAT_MAT3: return ElementResource(Element(MAT3, gpu::FLOAT, UNIFORM), Resource::BUFFER); + case GL_FLOAT_MAT4: return ElementResource(Element(MAT4, gpu::FLOAT, UNIFORM), Resource::BUFFER); + + /* {GL_FLOAT_MAT2x3 mat2x3}, + {GL_FLOAT_MAT2x4 mat2x4}, + {GL_FLOAT_MAT3x2 mat3x2}, + {GL_FLOAT_MAT3x4 mat3x4}, + {GL_FLOAT_MAT4x2 mat4x2}, + {GL_FLOAT_MAT4x3 mat4x3}, + {GL_DOUBLE_MAT2 dmat2}, + {GL_DOUBLE_MAT3 dmat3}, + {GL_DOUBLE_MAT4 dmat4}, + {GL_DOUBLE_MAT2x3 dmat2x3}, + {GL_DOUBLE_MAT2x4 dmat2x4}, + {GL_DOUBLE_MAT3x2 dmat3x2}, + {GL_DOUBLE_MAT3x4 dmat3x4}, + {GL_DOUBLE_MAT4x2 dmat4x2}, + {GL_DOUBLE_MAT4x3 dmat4x3}, + */ + + //case GL_SAMPLER_1D: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_1D); + case GL_SAMPLER_2D: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_2D); + + case GL_SAMPLER_3D: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_3D); + case GL_SAMPLER_CUBE: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_CUBE); + +#if defined(Q_OS_WIN) + case GL_SAMPLER_2D_MULTISAMPLE: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D); + case GL_SAMPLER_1D_ARRAY: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_1D_ARRAY); + case GL_SAMPLER_2D_ARRAY: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_2D_ARRAY); + case GL_SAMPLER_2D_MULTISAMPLE_ARRAY: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D_ARRAY); +#endif + + case GL_SAMPLER_2D_SHADOW: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_SHADOW), Resource::TEXTURE_2D); +#if defined(Q_OS_WIN) + case GL_SAMPLER_CUBE_SHADOW: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_SHADOW), Resource::TEXTURE_CUBE); + + case GL_SAMPLER_2D_ARRAY_SHADOW: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_SHADOW), Resource::TEXTURE_2D_ARRAY); +#endif + + // {GL_SAMPLER_1D_SHADOW sampler1DShadow}, + // {GL_SAMPLER_1D_ARRAY_SHADOW sampler1DArrayShadow}, + + case GL_SAMPLER_BUFFER: return ElementResource(Element(SCALAR, gpu::FLOAT, RESOURCE_BUFFER), Resource::BUFFER); + + // {GL_SAMPLER_2D_RECT sampler2DRect}, + // {GL_SAMPLER_2D_RECT_SHADOW sampler2DRectShadow}, + +#if defined(Q_OS_WIN) + case GL_INT_SAMPLER_1D: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_1D); + case GL_INT_SAMPLER_2D: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_2D); + case GL_INT_SAMPLER_2D_MULTISAMPLE: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D); + case GL_INT_SAMPLER_3D: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_3D); + case GL_INT_SAMPLER_CUBE: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_CUBE); + + case GL_INT_SAMPLER_1D_ARRAY: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_1D_ARRAY); + case GL_INT_SAMPLER_2D_ARRAY: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_2D_ARRAY); + case GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D_ARRAY); + + // {GL_INT_SAMPLER_BUFFER isamplerBuffer}, + // {GL_INT_SAMPLER_2D_RECT isampler2DRect}, + + case GL_UNSIGNED_INT_SAMPLER_1D: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_1D); + case GL_UNSIGNED_INT_SAMPLER_2D: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_2D); + case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D); + case GL_UNSIGNED_INT_SAMPLER_3D: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_3D); + case GL_UNSIGNED_INT_SAMPLER_CUBE: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_CUBE); + + case GL_UNSIGNED_INT_SAMPLER_1D_ARRAY: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_1D_ARRAY); + case GL_UNSIGNED_INT_SAMPLER_2D_ARRAY: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_2D_ARRAY); + case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D_ARRAY); +#endif + // {GL_UNSIGNED_INT_SAMPLER_BUFFER usamplerBuffer}, + // {GL_UNSIGNED_INT_SAMPLER_2D_RECT usampler2DRect}, + /* + {GL_IMAGE_1D image1D}, + {GL_IMAGE_2D image2D}, + {GL_IMAGE_3D image3D}, + {GL_IMAGE_2D_RECT image2DRect}, + {GL_IMAGE_CUBE imageCube}, + {GL_IMAGE_BUFFER imageBuffer}, + {GL_IMAGE_1D_ARRAY image1DArray}, + {GL_IMAGE_2D_ARRAY image2DArray}, + {GL_IMAGE_2D_MULTISAMPLE image2DMS}, + {GL_IMAGE_2D_MULTISAMPLE_ARRAY image2DMSArray}, + {GL_INT_IMAGE_1D iimage1D}, + {GL_INT_IMAGE_2D iimage2D}, + {GL_INT_IMAGE_3D iimage3D}, + {GL_INT_IMAGE_2D_RECT iimage2DRect}, + {GL_INT_IMAGE_CUBE iimageCube}, + {GL_INT_IMAGE_BUFFER iimageBuffer}, + {GL_INT_IMAGE_1D_ARRAY iimage1DArray}, + {GL_INT_IMAGE_2D_ARRAY iimage2DArray}, + {GL_INT_IMAGE_2D_MULTISAMPLE iimage2DMS}, + {GL_INT_IMAGE_2D_MULTISAMPLE_ARRAY iimage2DMSArray}, + {GL_UNSIGNED_INT_IMAGE_1D uimage1D}, + {GL_UNSIGNED_INT_IMAGE_2D uimage2D}, + {GL_UNSIGNED_INT_IMAGE_3D uimage3D}, + {GL_UNSIGNED_INT_IMAGE_2D_RECT uimage2DRect}, + {GL_UNSIGNED_INT_IMAGE_CUBE uimageCube},+ [0] {_name="fInnerRadius" _location=0 _element={_semantic=15 '\xf' _dimension=0 '\0' _type=0 '\0' } } gpu::Shader::Slot + + {GL_UNSIGNED_INT_IMAGE_BUFFER uimageBuffer}, + {GL_UNSIGNED_INT_IMAGE_1D_ARRAY uimage1DArray}, + {GL_UNSIGNED_INT_IMAGE_2D_ARRAY uimage2DArray}, + {GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE uimage2DMS}, + {GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY uimage2DMSArray}, + {GL_UNSIGNED_INT_ATOMIC_COUNTER atomic_uint} + */ + default: + return ElementResource(Element(), Resource::BUFFER); + } + +}; + +int GLBackend::makeUniformSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, + Shader::SlotSet& uniforms, Shader::SlotSet& textures, Shader::SlotSet& samplers) { + GLint uniformsCount = 0; + + glGetProgramiv(glprogram, GL_ACTIVE_UNIFORMS, &uniformsCount); + + for (int i = 0; i < uniformsCount; i++) { + const GLint NAME_LENGTH = 256; + GLchar name[NAME_LENGTH]; + GLint length = 0; + GLint size = 0; + GLenum type = 0; + glGetActiveUniform(glprogram, i, NAME_LENGTH, &length, &size, &type, name); + GLint location = glGetUniformLocation(glprogram, name); + const GLint INVALID_UNIFORM_LOCATION = -1; + + // Try to make sense of the gltype + auto elementResource = getFormatFromGLUniform(type); + + // The uniform as a standard var type + if (location != INVALID_UNIFORM_LOCATION) { + // Let's make sure the name doesn't contains an array element + std::string sname(name); + auto foundBracket = sname.find_first_of('['); + if (foundBracket != std::string::npos) { + // std::string arrayname = sname.substr(0, foundBracket); + + if (sname[foundBracket + 1] == '0') { + sname = sname.substr(0, foundBracket); + } else { + // skip this uniform since it's not the first element of an array + continue; + } + } + + if (elementResource._resource == Resource::BUFFER) { + uniforms.insert(Shader::Slot(sname, location, elementResource._element, elementResource._resource)); + } else { + // For texture/Sampler, the location is the actual binding value + GLint binding = -1; + glGetUniformiv(glprogram, location, &binding); + + auto requestedBinding = slotBindings.find(std::string(sname)); + if (requestedBinding != slotBindings.end()) { + if (binding != (*requestedBinding)._location) { + binding = (*requestedBinding)._location; + for (auto i = 0; i < size; i++) { + // If we are working with an array of textures, reserve for each elemet + glProgramUniform1i(glprogram, location+i, binding+i); + } + } + } + + textures.insert(Shader::Slot(name, binding, elementResource._element, elementResource._resource)); + samplers.insert(Shader::Slot(name, binding, elementResource._element, elementResource._resource)); + } + } + } + + return uniformsCount; +} + +int GLBackend::makeUniformBlockSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& buffers) { + GLint buffersCount = 0; + + glGetProgramiv(glprogram, GL_ACTIVE_UNIFORM_BLOCKS, &buffersCount); + + // fast exit + if (buffersCount == 0) { + return 0; + } + + GLint maxNumUniformBufferSlots = 0; + glGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS, &maxNumUniformBufferSlots); + std::vector uniformBufferSlotMap(maxNumUniformBufferSlots, -1); + + struct UniformBlockInfo { + using Vector = std::vector; + const GLuint index{ 0 }; + const std::string name; + GLint binding{ -1 }; + GLint size{ 0 }; + + static std::string getName(GLuint glprogram, GLuint i) { + static const GLint NAME_LENGTH = 256; + GLint length = 0; + GLchar nameBuffer[NAME_LENGTH]; + glGetActiveUniformBlockiv(glprogram, i, GL_UNIFORM_BLOCK_NAME_LENGTH, &length); + glGetActiveUniformBlockName(glprogram, i, NAME_LENGTH, &length, nameBuffer); + return std::string(nameBuffer); + } + + UniformBlockInfo(GLuint glprogram, GLuint i) : index(i), name(getName(glprogram, i)) { + glGetActiveUniformBlockiv(glprogram, index, GL_UNIFORM_BLOCK_BINDING, &binding); + glGetActiveUniformBlockiv(glprogram, index, GL_UNIFORM_BLOCK_DATA_SIZE, &size); + } + }; + + UniformBlockInfo::Vector uniformBlocks; + uniformBlocks.reserve(buffersCount); + for (int i = 0; i < buffersCount; i++) { + uniformBlocks.push_back(UniformBlockInfo(glprogram, i)); + } + + for (auto& info : uniformBlocks) { + auto requestedBinding = slotBindings.find(info.name); + if (requestedBinding != slotBindings.end()) { + info.binding = (*requestedBinding)._location; + glUniformBlockBinding(glprogram, info.index, info.binding); + uniformBufferSlotMap[info.binding] = info.index; + } + } + + for (auto& info : uniformBlocks) { + if (slotBindings.count(info.name)) { + continue; + } + + // If the binding is 0, or the binding maps to an already used binding + if (info.binding == 0 || !isUnusedSlot(uniformBufferSlotMap[info.binding])) { + // If no binding was assigned then just do it finding a free slot + auto slotIt = std::find_if(uniformBufferSlotMap.begin(), uniformBufferSlotMap.end(), GLBackend::isUnusedSlot); + if (slotIt != uniformBufferSlotMap.end()) { + info.binding = slotIt - uniformBufferSlotMap.begin(); + glUniformBlockBinding(glprogram, info.index, info.binding); + } else { + // This should neve happen, an active ubo cannot find an available slot among the max available?! + info.binding = -1; + } + } + + uniformBufferSlotMap[info.binding] = info.index; + } + + for (auto& info : uniformBlocks) { + static const Element element(SCALAR, gpu::UINT32, gpu::UNIFORM_BUFFER); + buffers.insert(Shader::Slot(info.name, info.binding, element, Resource::BUFFER, info.size)); + } + return buffersCount; +} + +int GLBackend::makeInputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& inputs) { + GLint inputsCount = 0; + + glGetProgramiv(glprogram, GL_ACTIVE_ATTRIBUTES, &inputsCount); + + for (int i = 0; i < inputsCount; i++) { + const GLint NAME_LENGTH = 256; + GLchar name[NAME_LENGTH]; + GLint length = 0; + GLint size = 0; + GLenum type = 0; + glGetActiveAttrib(glprogram, i, NAME_LENGTH, &length, &size, &type, name); + + GLint binding = glGetAttribLocation(glprogram, name); + + auto elementResource = getFormatFromGLUniform(type); + inputs.insert(Shader::Slot(name, binding, elementResource._element, -1)); + } + + return inputsCount; +} + +int GLBackend::makeOutputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& outputs) { + /* GLint outputsCount = 0; + + glGetProgramiv(glprogram, GL_ACTIVE_, &outputsCount); + + for (int i = 0; i < inputsCount; i++) { + const GLint NAME_LENGTH = 256; + GLchar name[NAME_LENGTH]; + GLint length = 0; + GLint size = 0; + GLenum type = 0; + glGetActiveAttrib(glprogram, i, NAME_LENGTH, &length, &size, &type, name); + + auto element = getFormatFromGLUniform(type); + outputs.insert(Shader::Slot(name, i, element)); + } + */ + return 0; //inputsCount; +} + +void GLBackend::makeProgramBindings(ShaderObject& shaderObject) { + if (!shaderObject.glprogram) { + return; + } + GLuint glprogram = shaderObject.glprogram; + GLint loc = -1; + + //Check for gpu specific attribute slotBindings + loc = glGetAttribLocation(glprogram, "inPosition"); + if (loc >= 0 && loc != gpu::Stream::POSITION) { + glBindAttribLocation(glprogram, gpu::Stream::POSITION, "inPosition"); + } + + loc = glGetAttribLocation(glprogram, "inNormal"); + if (loc >= 0 && loc != gpu::Stream::NORMAL) { + glBindAttribLocation(glprogram, gpu::Stream::NORMAL, "inNormal"); + } + + loc = glGetAttribLocation(glprogram, "inColor"); + if (loc >= 0 && loc != gpu::Stream::COLOR) { + glBindAttribLocation(glprogram, gpu::Stream::COLOR, "inColor"); + } + + loc = glGetAttribLocation(glprogram, "inTexCoord0"); + if (loc >= 0 && loc != gpu::Stream::TEXCOORD) { + glBindAttribLocation(glprogram, gpu::Stream::TEXCOORD, "inTexCoord0"); + } + + loc = glGetAttribLocation(glprogram, "inTangent"); + if (loc >= 0 && loc != gpu::Stream::TANGENT) { + glBindAttribLocation(glprogram, gpu::Stream::TANGENT, "inTangent"); + } + + char attribName[] = "inTexCoordn"; + for (auto i = 0; i < 4; i++) { + auto streamId = gpu::Stream::TEXCOORD1 + i; + + attribName[strlen(attribName) - 1] = '1' + i; + loc = glGetAttribLocation(glprogram, attribName); + if (loc >= 0 && loc != streamId) { + glBindAttribLocation(glprogram, streamId, attribName); + } + } + + loc = glGetAttribLocation(glprogram, "inSkinClusterIndex"); + if (loc >= 0 && loc != gpu::Stream::SKIN_CLUSTER_INDEX) { + glBindAttribLocation(glprogram, gpu::Stream::SKIN_CLUSTER_INDEX, "inSkinClusterIndex"); + } + + loc = glGetAttribLocation(glprogram, "inSkinClusterWeight"); + if (loc >= 0 && loc != gpu::Stream::SKIN_CLUSTER_WEIGHT) { + glBindAttribLocation(glprogram, gpu::Stream::SKIN_CLUSTER_WEIGHT, "inSkinClusterWeight"); + } + + loc = glGetAttribLocation(glprogram, "_drawCallInfo"); + if (loc >= 0 && loc != gpu::Stream::DRAW_CALL_INFO) { + glBindAttribLocation(glprogram, gpu::Stream::DRAW_CALL_INFO, "_drawCallInfo"); + } + + // Link again to take into account the assigned attrib location + glLinkProgram(glprogram); + + GLint linked = 0; + glGetProgramiv(glprogram, GL_LINK_STATUS, &linked); + if (!linked) { + qCWarning(gpugllogging) << "GLShader::makeBindings - failed to link after assigning slotBindings?"; + } +} + diff --git a/libraries/gpu-gles/src/gpu/gl/GLBackendTexture.cpp b/libraries/gpu-gles/src/gpu/gl/GLBackendTexture.cpp index 4be7682a4f..13c5bd98af 100644 --- a/libraries/gpu-gles/src/gpu/gl/GLBackendTexture.cpp +++ b/libraries/gpu-gles/src/gpu/gl/GLBackendTexture.cpp @@ -15,13 +15,56 @@ using namespace gpu; using namespace gpu::gl; -bool GLBackend::isTextureReady(const TexturePointer& texture) { - // DO not transfer the texture, this call is expected for rendering texture - GLTexture* object = syncGPUObject(texture, true); - qDebug() << "GLBackendTexture isTextureReady syncGPUObject"; - return object && object->isReady(); + +GLuint GLBackend::getTextureID(const TexturePointer& texture) { + GLTexture* object = syncGPUObject(texture); + + if (!object) { + return 0; + } + + return object->_id; } +GLTexture* GLBackend::syncGPUObject(const TexturePointer& texturePointer) { + const Texture& texture = *texturePointer; + // Special case external textures + if (TextureUsageType::EXTERNAL == texture.getUsageType()) { + Texture::ExternalUpdates updates = texture.getUpdates(); + if (!updates.empty()) { + Texture::ExternalRecycler recycler = texture.getExternalRecycler(); + Q_ASSERT(recycler); + // Discard any superfluous updates + while (updates.size() > 1) { + const auto& update = updates.front(); + // Superfluous updates will never have been read, but we want to ensure the previous + // writes to them are complete before they're written again, so return them with the + // same fences they arrived with. This can happen on any thread because no GL context + // work is involved + recycler(update.first, update.second); + updates.pop_front(); + } + + // The last texture remaining is the one we'll use to create the GLTexture + const auto& update = updates.front(); + // Check for a fence, and if it exists, inject a wait into the command stream, then destroy the fence + if (update.second) { + GLsync fence = static_cast(update.second); + glWaitSync(fence, 0, GL_TIMEOUT_IGNORED); + glDeleteSync(fence); + } + + // Create the new texture object (replaces any previous texture object) + new GLExternalTexture(shared_from_this(), texture, update.first); + } + + // Return the texture object (if any) associated with the texture, without extensive logic + // (external textures are + return Backend::getGPUObject(texture); + } + + return nullptr; +} void GLBackend::do_generateTextureMips(const Batch& batch, size_t paramOffset) { TexturePointer resourceTexture = batch._textures.get(batch._params[paramOffset + 0]._uint); @@ -30,7 +73,7 @@ void GLBackend::do_generateTextureMips(const Batch& batch, size_t paramOffset) { } // DO not transfer the texture, this call is expected for rendering texture - GLTexture* object = syncGPUObject(resourceTexture, false); + GLTexture* object = syncGPUObject(resourceTexture); qDebug() << "GLBackendTexture do_generateTextureMips syncGPUObject"; if (!object) { return; diff --git a/libraries/gpu-gles/src/gpu/gl/GLBackendTransform.cpp b/libraries/gpu-gles/src/gpu/gl/GLBackendTransform.cpp index 3068e24dac..23926dc11d 100644 --- a/libraries/gpu-gles/src/gpu/gl/GLBackendTransform.cpp +++ b/libraries/gpu-gles/src/gpu/gl/GLBackendTransform.cpp @@ -38,7 +38,7 @@ void GLBackend::do_setViewportTransform(const Batch& batch, size_t paramOffset) glViewport(vp.x, vp.y, vp.z, vp.w); // Where we assign the GL viewport - if (_stereo._enable) { + if (_stereo.isStereo()) { vp.z /= 2; if (_stereo._pass) { vp.x += vp.z; @@ -103,7 +103,7 @@ void GLBackend::TransformStageState::preUpdate(size_t commandIndex, const Stereo if (_invalidView) { // Apply the correction - if (_viewIsCamera && _correction.correction != glm::mat4()) { + if (_viewIsCamera && (_viewCorrectionEnabled && _correction.correction != glm::mat4())) { // FIXME should I switch to using the camera correction buffer in Transform.slf and leave this out? Transform result; _view.mult(result, _view, _correction.correction); @@ -120,7 +120,7 @@ void GLBackend::TransformStageState::preUpdate(size_t commandIndex, const Stereo size_t offset = _cameraUboSize * _cameras.size(); _cameraOffsets.push_back(TransformStageState::Pair(commandIndex, offset)); - if (stereo._enable) { + if (stereo.isStereo()) { #ifdef GPU_STEREO_CAMERA_BUFFER _cameras.push_back(CameraBufferElement(_camera.getEyeCamera(0, stereo, _view), _camera.getEyeCamera(1, stereo, _view))); #else @@ -152,7 +152,7 @@ void GLBackend::TransformStageState::update(size_t commandIndex, const StereoSta #ifdef GPU_STEREO_CAMERA_BUFFER bindCurrentCamera(0); #else - if (!stereo._enable) { + if (!stereo.isStereo()) { bindCurrentCamera(0); } #endif @@ -162,51 +162,11 @@ void GLBackend::TransformStageState::update(size_t commandIndex, const StereoSta void GLBackend::TransformStageState::bindCurrentCamera(int eye) const { if (_currentCameraOffset != INVALID_OFFSET) { - //qDebug() << "GLBackend::TransformStageState::bindCurrentCamera"; glBindBufferRange(GL_UNIFORM_BUFFER, TRANSFORM_CAMERA_SLOT, _cameraBuffer, _currentCameraOffset + eye * _cameraUboSize, sizeof(CameraBufferElement)); } } -void GLBackend::updateTransform(const Batch& batch) { - _transform.update(_commandIndex, _stereo); - - auto& drawCallInfoBuffer = batch.getDrawCallInfoBuffer(); - if (batch._currentNamedCall.empty()) { - (void)CHECK_GL_ERROR(); - auto& drawCallInfo = drawCallInfoBuffer[_currentDraw]; - glDisableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); // Make sure attrib array is disabled - (void)CHECK_GL_ERROR(); - GLint current_vao, current_vbo, maxVertexAtribs; - glGetIntegerv(GL_VERTEX_ARRAY_BINDING, ¤t_vao); - glGetIntegerv(GL_ARRAY_BUFFER_BINDING, ¤t_vbo); - glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &maxVertexAtribs); - glVertexAttribI4i(gpu::Stream::DRAW_CALL_INFO, drawCallInfo.index, drawCallInfo.unused, 0, 0); - - //int values[] = {drawCallInfo.index, drawCallInfo.unused}; - //glVertexAttribIPointer(gpu::Stream::DRAW_CALL_INFO, 2, GL_INT, 0, (const GLvoid *) values); - - /* - //glDisableVertexAttribArray currentvao 1 current vbo 0 - GL_INVALID_OPERATION is generated - a non-zero vertex array object is bound, - zero is bound to the GL_ARRAY_BUFFER buffer object binding point and - the pointer argument is not NULL. TRUE - */ - //qDebug() << "GLBackend::updateTransform glVertexAttribIPointer done"; - (void)CHECK_GL_ERROR(); - - } else { - //qDebug() << "GLBackend::updateTransform else"; - glEnableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); // Make sure attrib array is enabled - glBindBuffer(GL_ARRAY_BUFFER, _transform._drawCallInfoBuffer); - glVertexAttribIPointer(gpu::Stream::DRAW_CALL_INFO, 2, GL_UNSIGNED_SHORT, 0, - _transform._drawCallInfoOffsets[batch._currentNamedCall]); - glVertexAttribDivisor(gpu::Stream::DRAW_CALL_INFO, 1); - } - - (void)CHECK_GL_ERROR(); -} - void GLBackend::resetTransformStage() { - + glDisableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); + _transform._enabledDrawcallInfoBuffer = false; } diff --git a/libraries/gpu-gles/src/gpu/gl/GLFramebuffer.cpp b/libraries/gpu-gles/src/gpu/gl/GLFramebuffer.cpp index 150bb2be70..b04dd1da84 100644 --- a/libraries/gpu-gles/src/gpu/gl/GLFramebuffer.cpp +++ b/libraries/gpu-gles/src/gpu/gl/GLFramebuffer.cpp @@ -21,7 +21,7 @@ GLFramebuffer::~GLFramebuffer() { } } -bool GLFramebuffer::checkStatus(GLenum target) const { +bool GLFramebuffer::checkStatus() const { bool result = false; switch (_status) { case GL_FRAMEBUFFER_COMPLETE: diff --git a/libraries/gpu-gles/src/gpu/gl/GLFramebuffer.h b/libraries/gpu-gles/src/gpu/gl/GLFramebuffer.h index a2fd0999f3..5a388e1965 100644 --- a/libraries/gpu-gles/src/gpu/gl/GLFramebuffer.h +++ b/libraries/gpu-gles/src/gpu/gl/GLFramebuffer.h @@ -64,7 +64,7 @@ public: protected: GLenum _status { GL_FRAMEBUFFER_COMPLETE }; virtual void update() = 0; - bool checkStatus(GLenum target) const; + bool checkStatus() const; GLFramebuffer(const std::weak_ptr& backend, const Framebuffer& framebuffer, GLuint id) : GLObject(backend, framebuffer, id) {} ~GLFramebuffer(); diff --git a/libraries/gpu-gles/src/gpu/gl/GLShader.cpp b/libraries/gpu-gles/src/gpu/gl/GLShader.cpp index b728010470..7ed9121978 100644 --- a/libraries/gpu-gles/src/gpu/gl/GLShader.cpp +++ b/libraries/gpu-gles/src/gpu/gl/GLShader.cpp @@ -30,126 +30,6 @@ GLShader::~GLShader() { } } -// GLSL version -static const std::string glslVersion { - "#version 310 es" -}; - -// Shader domain -static const size_t NUM_SHADER_DOMAINS = 3; - -// GL Shader type enums -// Must match the order of type specified in gpu::Shader::Type -static const std::array SHADER_DOMAINS { { - GL_VERTEX_SHADER, - GL_FRAGMENT_SHADER, - //GL_GEOMETRY_SHADER, -} }; - -// Domain specific defines -// Must match the order of type specified in gpu::Shader::Type -static const std::array DOMAIN_DEFINES { { - "#define GPU_VERTEX_SHADER", - "#define GPU_PIXEL_SHADER", - "#define GPU_GEOMETRY_SHADER", -} }; - -// Stereo specific defines -static const std::string stereoVersion { -#ifdef GPU_STEREO_DRAWCALL_INSTANCED - "#define GPU_TRANSFORM_IS_STEREO\n#define GPU_TRANSFORM_STEREO_CAMERA\n#define GPU_TRANSFORM_STEREO_CAMERA_INSTANCED\n#define GPU_TRANSFORM_STEREO_SPLIT_SCREEN" -#endif -#ifdef GPU_STEREO_DRAWCALL_DOUBLED -#ifdef GPU_STEREO_CAMERA_BUFFER - "#define GPU_TRANSFORM_IS_STEREO\n#define GPU_TRANSFORM_STEREO_CAMERA\n#define GPU_TRANSFORM_STEREO_CAMERA_ATTRIBUTED" -#else - "#define GPU_TRANSFORM_IS_STEREO" -#endif -#endif -}; - -// Versions specific of the shader -static const std::array VERSION_DEFINES { { - "", - stereoVersion -} }; - -GLShader* compileBackendShader(GLBackend& backend, const Shader& shader) { - // Any GLSLprogram ? normally yes... - const std::string& shaderSource = shader.getSource().getCode(); - GLenum shaderDomain = SHADER_DOMAINS[shader.getType()]; - GLShader::ShaderObjects shaderObjects; - - for (int version = 0; version < GLShader::NumVersions; version++) { - auto& shaderObject = shaderObjects[version]; - std::string shaderDefines = glslVersion + "\n" + DOMAIN_DEFINES[shader.getType()] + "\n" + VERSION_DEFINES[version] - + "\n" + "#extension GL_EXT_texture_buffer : enable" - + "\nprecision lowp float; // check precision 2" - + "\nprecision lowp samplerBuffer;" - + "\nprecision lowp sampler2DShadow;"; - // TODO Delete bool result = compileShader(shaderDomain, shaderSource, shaderDefines, shaderObject.glshader, shaderObject.glprogram); - std::string error; - - -#ifdef SEPARATE_PROGRAM - bool result = ::gl::compileShader(shaderDomain, shaderSource.c_str(), shaderDefines.c_str(), shaderObject.glshader, shaderObject.glprogram, error); -#else - bool result = ::gl::compileShader(shaderDomain, shaderSource, shaderDefines, shaderObject.glshader, error); -#endif - if (!result) { - qCWarning(gpugllogging) << "GLBackend::compileBackendProgram - Shader didn't compile:\n" << error.c_str(); - return nullptr; - } - } - - // So far so good, the shader is created successfully - GLShader* object = new GLShader(backend.shared_from_this()); - object->_shaderObjects = shaderObjects; - - return object; -} - -GLShader* compileBackendProgram(GLBackend& backend, const Shader& program) { - if (!program.isProgram()) { - return nullptr; - } - - GLShader::ShaderObjects programObjects; - - for (int version = 0; version < GLShader::NumVersions; version++) { - auto& programObject = programObjects[version]; - - // Let's go through every shaders and make sure they are ready to go - std::vector< GLuint > shaderGLObjects; - for (auto subShader : program.getShaders()) { - auto object = GLShader::sync(backend, *subShader); - if (object) { - shaderGLObjects.push_back(object->_shaderObjects[version].glshader); - } else { - qCDebug(gpugllogging) << "GLShader::compileBackendProgram - One of the shaders of the program is not compiled?"; - return nullptr; - } - } - - std::string error; - GLuint glprogram = ::gl::compileProgram(shaderGLObjects, error); - if (glprogram == 0) { - qCWarning(gpugllogging) << error.c_str(); - return nullptr; - } - - programObject.glprogram = glprogram; - - makeProgramBindings(programObject); - } - - // So far so good, the program versions have all been created successfully - GLShader* object = new GLShader(backend.shared_from_this()); - object->_shaderObjects = programObjects; - - return object; -} - GLShader* GLShader::sync(GLBackend& backend, const Shader& shader) { GLShader* object = Backend::getGPUObject(shader); @@ -159,13 +39,13 @@ GLShader* GLShader::sync(GLBackend& backend, const Shader& shader) { } // need to have a gpu object? if (shader.isProgram()) { - GLShader* tempObject = compileBackendProgram(backend, shader); + GLShader* tempObject = backend.compileBackendProgram(shader); if (tempObject) { object = tempObject; Backend::setGPUObject(shader, object); } } else if (shader.isDomain()) { - GLShader* tempObject = compileBackendShader(backend, shader); + GLShader* tempObject = backend.compileBackendShader(shader); if (tempObject) { object = tempObject; Backend::setGPUObject(shader, object); @@ -189,21 +69,21 @@ bool GLShader::makeProgram(GLBackend& backend, Shader& shader, const Shader::Bin auto& shaderObject = object->_shaderObjects[version]; if (shaderObject.glprogram) { Shader::SlotSet buffers; - makeUniformBlockSlots(shaderObject.glprogram, slotBindings, buffers); + backend.makeUniformBlockSlots(shaderObject.glprogram, slotBindings, buffers); Shader::SlotSet uniforms; Shader::SlotSet textures; Shader::SlotSet samplers; - makeUniformSlots(shaderObject.glprogram, slotBindings, uniforms, textures, samplers); + backend.makeUniformSlots(shaderObject.glprogram, slotBindings, uniforms, textures, samplers); Shader::SlotSet resourceBuffers; - makeResourceBufferSlots(shaderObject.glprogram, slotBindings, resourceBuffers); + backend.makeResourceBufferSlots(shaderObject.glprogram, slotBindings, resourceBuffers); Shader::SlotSet inputs; - makeInputSlots(shaderObject.glprogram, slotBindings, inputs); + backend.makeInputSlots(shaderObject.glprogram, slotBindings, inputs); Shader::SlotSet outputs; - makeOutputSlots(shaderObject.glprogram, slotBindings, outputs); + backend.makeOutputSlots(shaderObject.glprogram, slotBindings, outputs); // Define the public slots only from the default version if (version == 0) { @@ -222,3 +102,5 @@ bool GLShader::makeProgram(GLBackend& backend, Shader& shader, const Shader::Bin return true; } + + diff --git a/libraries/gpu-gles/src/gpu/gl/GLShader.h b/libraries/gpu-gles/src/gpu/gl/GLShader.h index e03b487a60..dcf2dc330d 100644 --- a/libraries/gpu-gles/src/gpu/gl/GLShader.h +++ b/libraries/gpu-gles/src/gpu/gl/GLShader.h @@ -12,6 +12,13 @@ namespace gpu { namespace gl { +struct ShaderObject { + GLuint glshader { 0 }; + GLuint glprogram { 0 }; + GLint transformCameraSlot { -1 }; + GLint transformObjectSlot { -1 }; +}; + class GLShader : public GPUObject { public: static GLShader* sync(GLBackend& backend, const Shader& shader); diff --git a/libraries/gpu-gles/src/gpu/gl/GLShared.cpp b/libraries/gpu-gles/src/gpu/gl/GLShared.cpp index 5d340889a6..0b3d84a09c 100644 --- a/libraries/gpu-gles/src/gpu/gl/GLShared.cpp +++ b/libraries/gpu-gles/src/gpu/gl/GLShared.cpp @@ -27,22 +27,22 @@ bool checkGLError(const char* name) { } else { switch (error) { case GL_INVALID_ENUM: - qCDebug(gpugllogging) << "GLBackend::" << name << ": An unacceptable value is specified for an enumerated argument.The offending command is ignored and has no other side effect than to set the error flag."; + qCWarning(gpugllogging) << "GLBackend::" << name << ": An unacceptable value is specified for an enumerated argument.The offending command is ignored and has no other side effect than to set the error flag."; break; case GL_INVALID_VALUE: - qCDebug(gpugllogging) << "GLBackend" << name << ": A numeric argument is out of range.The offending command is ignored and has no other side effect than to set the error flag"; + qCWarning(gpugllogging) << "GLBackend" << name << ": A numeric argument is out of range.The offending command is ignored and has no other side effect than to set the error flag"; break; case GL_INVALID_OPERATION: - qCDebug(gpugllogging) << "GLBackend" << name << ": The specified operation is not allowed in the current state.The offending command is ignored and has no other side effect than to set the error flag.."; + qCWarning(gpugllogging) << "GLBackend" << name << ": The specified operation is not allowed in the current state.The offending command is ignored and has no other side effect than to set the error flag.."; break; case GL_INVALID_FRAMEBUFFER_OPERATION: - qCDebug(gpugllogging) << "GLBackend" << name << ": The framebuffer object is not complete.The offending command is ignored and has no other side effect than to set the error flag."; + qCWarning(gpugllogging) << "GLBackend" << name << ": The framebuffer object is not complete.The offending command is ignored and has no other side effect than to set the error flag."; break; case GL_OUT_OF_MEMORY: - qCDebug(gpugllogging) << "GLBackend" << name << ": There is not enough memory left to execute the command.The state of the GL is undefined, except for the state of the error flags, after this error is recorded."; + qCWarning(gpugllogging) << "GLBackend" << name << ": There is not enough memory left to execute the command.The state of the GL is undefined, except for the state of the error flags, after this error is recorded."; break; default: - qCDebug(gpugllogging) << "GLBackend" << name << ": Unknown error: " << error; + qCWarning(gpugllogging) << "GLBackend" << name << ": Unknown error: " << error; break; } return true; @@ -53,7 +53,7 @@ bool checkGLErrorDebug(const char* name) { #ifdef DEBUG return checkGLError(name); #else - Q_UNUSED(name); + Q_UNUSED(name); return false; #endif } @@ -86,43 +86,6 @@ gpu::Size getFreeDedicatedMemory() { return result; } -gpu::Size getDedicatedMemory() { - static Size dedicatedMemory { 0 }; - static std::once_flag once; - std::call_once(once, [&] { - if (!dedicatedMemory) { - GLint atiGpuMemory[4]; - // not really total memory, but close enough if called early enough in the application lifecycle - //glGetIntegerv(GL_TEXTURE_FREE_MEMORY_ATI, atiGpuMemory); - qDebug() << "TODO: GLShared.cpp.cpp:initInput GL_TEXTURE_FREE_MEMORY_ATI"; - if (GL_NO_ERROR == glGetError()) { - dedicatedMemory = KB_TO_BYTES(atiGpuMemory[0]); - } - } - - if (!dedicatedMemory) { - GLint nvGpuMemory { 0 }; - qDebug() << "TODO: GLShared.cpp.cpp:initInput GL_GPU_MEMORY_INFO_DEDICATED_VIDMEM_NVX"; - //glGetIntegerv(GL_GPU_MEMORY_INFO_DEDICATED_VIDMEM_NVX, &nvGpuMemory); - if (GL_NO_ERROR == glGetError()) { - dedicatedMemory = KB_TO_BYTES(nvGpuMemory); - } - } - - if (!dedicatedMemory) { - auto gpuIdent = GPUIdent::getInstance(); - if (gpuIdent && gpuIdent->isValid()) { - dedicatedMemory = MB_TO_BYTES(gpuIdent->getMemory()); - } - } - }); - - return dedicatedMemory; -} - - - - ComparisonFunction comparisonFuncFromGL(GLenum func) { if (func == GL_NEVER) { return NEVER; @@ -354,504 +317,6 @@ void getCurrentGLState(State::Data& state) { } -class ElementResource { -public: - gpu::Element _element; - uint16 _resource; - - ElementResource(Element&& elem, uint16 resource) : _element(elem), _resource(resource) {} -}; - -ElementResource getFormatFromGLUniform(GLenum gltype) { - switch (gltype) { - case GL_FLOAT: return ElementResource(Element(SCALAR, gpu::FLOAT, UNIFORM), Resource::BUFFER); - case GL_FLOAT_VEC2: return ElementResource(Element(VEC2, gpu::FLOAT, UNIFORM), Resource::BUFFER); - case GL_FLOAT_VEC3: return ElementResource(Element(VEC3, gpu::FLOAT, UNIFORM), Resource::BUFFER); - case GL_FLOAT_VEC4: return ElementResource(Element(VEC4, gpu::FLOAT, UNIFORM), Resource::BUFFER); - /* - case GL_DOUBLE: return ElementResource(Element(SCALAR, gpu::FLOAT, UNIFORM), Resource::BUFFER); - case GL_DOUBLE_VEC2: return ElementResource(Element(VEC2, gpu::FLOAT, UNIFORM), Resource::BUFFER); - case GL_DOUBLE_VEC3: return ElementResource(Element(VEC3, gpu::FLOAT, UNIFORM), Resource::BUFFER); - case GL_DOUBLE_VEC4: return ElementResource(Element(VEC4, gpu::FLOAT, UNIFORM), Resource::BUFFER); - */ - case GL_INT: return ElementResource(Element(SCALAR, gpu::INT32, UNIFORM), Resource::BUFFER); - case GL_INT_VEC2: return ElementResource(Element(VEC2, gpu::INT32, UNIFORM), Resource::BUFFER); - case GL_INT_VEC3: return ElementResource(Element(VEC3, gpu::INT32, UNIFORM), Resource::BUFFER); - case GL_INT_VEC4: return ElementResource(Element(VEC4, gpu::INT32, UNIFORM), Resource::BUFFER); - - case GL_UNSIGNED_INT: return ElementResource(Element(SCALAR, gpu::UINT32, UNIFORM), Resource::BUFFER); -#if defined(Q_OS_WIN) - case GL_UNSIGNED_INT_VEC2: return ElementResource(Element(VEC2, gpu::UINT32, UNIFORM), Resource::BUFFER); - case GL_UNSIGNED_INT_VEC3: return ElementResource(Element(VEC3, gpu::UINT32, UNIFORM), Resource::BUFFER); - case GL_UNSIGNED_INT_VEC4: return ElementResource(Element(VEC4, gpu::UINT32, UNIFORM), Resource::BUFFER); -#endif - - case GL_BOOL: return ElementResource(Element(SCALAR, gpu::BOOL, UNIFORM), Resource::BUFFER); - case GL_BOOL_VEC2: return ElementResource(Element(VEC2, gpu::BOOL, UNIFORM), Resource::BUFFER); - case GL_BOOL_VEC3: return ElementResource(Element(VEC3, gpu::BOOL, UNIFORM), Resource::BUFFER); - case GL_BOOL_VEC4: return ElementResource(Element(VEC4, gpu::BOOL, UNIFORM), Resource::BUFFER); - - - case GL_FLOAT_MAT2: return ElementResource(Element(gpu::MAT2, gpu::FLOAT, UNIFORM), Resource::BUFFER); - case GL_FLOAT_MAT3: return ElementResource(Element(MAT3, gpu::FLOAT, UNIFORM), Resource::BUFFER); - case GL_FLOAT_MAT4: return ElementResource(Element(MAT4, gpu::FLOAT, UNIFORM), Resource::BUFFER); - - /* {GL_FLOAT_MAT2x3 mat2x3}, - {GL_FLOAT_MAT2x4 mat2x4}, - {GL_FLOAT_MAT3x2 mat3x2}, - {GL_FLOAT_MAT3x4 mat3x4}, - {GL_FLOAT_MAT4x2 mat4x2}, - {GL_FLOAT_MAT4x3 mat4x3}, - {GL_DOUBLE_MAT2 dmat2}, - {GL_DOUBLE_MAT3 dmat3}, - {GL_DOUBLE_MAT4 dmat4}, - {GL_DOUBLE_MAT2x3 dmat2x3}, - {GL_DOUBLE_MAT2x4 dmat2x4}, - {GL_DOUBLE_MAT3x2 dmat3x2}, - {GL_DOUBLE_MAT3x4 dmat3x4}, - {GL_DOUBLE_MAT4x2 dmat4x2}, - {GL_DOUBLE_MAT4x3 dmat4x3}, - */ - - //qDebug() << "TODO: GLShared.cpp.cpp:ElementResource GL_SAMPLER_1D"; - //case GL_SAMPLER_1D: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_1D); - case GL_SAMPLER_2D: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_2D); - - case GL_SAMPLER_3D: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_3D); - case GL_SAMPLER_CUBE: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_CUBE); - -#if defined(Q_OS_WIN) - case GL_SAMPLER_2D_MULTISAMPLE: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D); - case GL_SAMPLER_1D_ARRAY: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_1D_ARRAY); - case GL_SAMPLER_2D_ARRAY: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER), Resource::TEXTURE_2D_ARRAY); - case GL_SAMPLER_2D_MULTISAMPLE_ARRAY: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D_ARRAY); -#endif - - case GL_SAMPLER_2D_SHADOW: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_SHADOW), Resource::TEXTURE_2D); -#if defined(Q_OS_WIN) - case GL_SAMPLER_CUBE_SHADOW: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_SHADOW), Resource::TEXTURE_CUBE); - - case GL_SAMPLER_2D_ARRAY_SHADOW: return ElementResource(Element(SCALAR, gpu::FLOAT, SAMPLER_SHADOW), Resource::TEXTURE_2D_ARRAY); -#endif - - // {GL_SAMPLER_1D_SHADOW sampler1DShadow}, - // {GL_SAMPLER_1D_ARRAY_SHADOW sampler1DArrayShadow}, - - // {GL_SAMPLER_BUFFER samplerBuffer}, - // {GL_SAMPLER_2D_RECT sampler2DRect}, - // {GL_SAMPLER_2D_RECT_SHADOW sampler2DRectShadow}, - -#if defined(Q_OS_WIN) - case GL_INT_SAMPLER_1D: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_1D); - case GL_INT_SAMPLER_2D: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_2D); - case GL_INT_SAMPLER_2D_MULTISAMPLE: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D); - case GL_INT_SAMPLER_3D: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_3D); - case GL_INT_SAMPLER_CUBE: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_CUBE); - - case GL_INT_SAMPLER_1D_ARRAY: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_1D_ARRAY); - case GL_INT_SAMPLER_2D_ARRAY: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER), Resource::TEXTURE_2D_ARRAY); - case GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY: return ElementResource(Element(SCALAR, gpu::INT32, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D_ARRAY); - - // {GL_INT_SAMPLER_BUFFER isamplerBuffer}, - // {GL_INT_SAMPLER_2D_RECT isampler2DRect}, - - case GL_UNSIGNED_INT_SAMPLER_1D: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_1D); - case GL_UNSIGNED_INT_SAMPLER_2D: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_2D); - case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D); - case GL_UNSIGNED_INT_SAMPLER_3D: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_3D); - case GL_UNSIGNED_INT_SAMPLER_CUBE: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_CUBE); - - case GL_UNSIGNED_INT_SAMPLER_1D_ARRAY: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_1D_ARRAY); - case GL_UNSIGNED_INT_SAMPLER_2D_ARRAY: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER), Resource::TEXTURE_2D_ARRAY); - case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY: return ElementResource(Element(SCALAR, gpu::UINT32, SAMPLER_MULTISAMPLE), Resource::TEXTURE_2D_ARRAY); -#endif - // {GL_UNSIGNED_INT_SAMPLER_BUFFER usamplerBuffer}, - // {GL_UNSIGNED_INT_SAMPLER_2D_RECT usampler2DRect}, - /* - {GL_IMAGE_1D image1D}, - {GL_IMAGE_2D image2D}, - {GL_IMAGE_3D image3D}, - {GL_IMAGE_2D_RECT image2DRect}, - {GL_IMAGE_CUBE imageCube}, - {GL_IMAGE_BUFFER imageBuffer}, - {GL_IMAGE_1D_ARRAY image1DArray}, - {GL_IMAGE_2D_ARRAY image2DArray}, - {GL_IMAGE_2D_MULTISAMPLE image2DMS}, - {GL_IMAGE_2D_MULTISAMPLE_ARRAY image2DMSArray}, - {GL_INT_IMAGE_1D iimage1D}, - {GL_INT_IMAGE_2D iimage2D}, - {GL_INT_IMAGE_3D iimage3D}, - {GL_INT_IMAGE_2D_RECT iimage2DRect}, - {GL_INT_IMAGE_CUBE iimageCube}, - {GL_INT_IMAGE_BUFFER iimageBuffer}, - {GL_INT_IMAGE_1D_ARRAY iimage1DArray}, - {GL_INT_IMAGE_2D_ARRAY iimage2DArray}, - {GL_INT_IMAGE_2D_MULTISAMPLE iimage2DMS}, - {GL_INT_IMAGE_2D_MULTISAMPLE_ARRAY iimage2DMSArray}, - {GL_UNSIGNED_INT_IMAGE_1D uimage1D}, - {GL_UNSIGNED_INT_IMAGE_2D uimage2D}, - {GL_UNSIGNED_INT_IMAGE_3D uimage3D}, - {GL_UNSIGNED_INT_IMAGE_2D_RECT uimage2DRect}, - {GL_UNSIGNED_INT_IMAGE_CUBE uimageCube},+ [0] {_name="fInnerRadius" _location=0 _element={_semantic=15 '\xf' _dimension=0 '\0' _type=0 '\0' } } gpu::Shader::Slot - - {GL_UNSIGNED_INT_IMAGE_BUFFER uimageBuffer}, - {GL_UNSIGNED_INT_IMAGE_1D_ARRAY uimage1DArray}, - {GL_UNSIGNED_INT_IMAGE_2D_ARRAY uimage2DArray}, - {GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE uimage2DMS}, - {GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY uimage2DMSArray}, - {GL_UNSIGNED_INT_ATOMIC_COUNTER atomic_uint} - */ - default: - return ElementResource(Element(), Resource::BUFFER); - } - -}; - -int makeUniformSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, - Shader::SlotSet& uniforms, Shader::SlotSet& textures, Shader::SlotSet& samplers) { - GLint uniformsCount = 0; - - glGetProgramiv(glprogram, GL_ACTIVE_UNIFORMS, &uniformsCount); - - for (int i = 0; i < uniformsCount; i++) { - const GLint NAME_LENGTH = 256; - GLchar name[NAME_LENGTH]; - GLint length = 0; - GLint size = 0; - GLenum type = 0; - glGetActiveUniform(glprogram, i, NAME_LENGTH, &length, &size, &type, name); - GLint location = glGetUniformLocation(glprogram, name); - const GLint INVALID_UNIFORM_LOCATION = -1; - - // Try to make sense of the gltype - auto elementResource = getFormatFromGLUniform(type); - - // The uniform as a standard var type - if (location != INVALID_UNIFORM_LOCATION) { - // Let's make sure the name doesn't contains an array element - std::string sname(name); - auto foundBracket = sname.find_first_of('['); - if (foundBracket != std::string::npos) { - // std::string arrayname = sname.substr(0, foundBracket); - - if (sname[foundBracket + 1] == '0') { - sname = sname.substr(0, foundBracket); - } else { - // skip this uniform since it's not the first element of an array - continue; - } - } - - if (elementResource._resource == Resource::BUFFER) { - uniforms.insert(Shader::Slot(sname, location, elementResource._element, elementResource._resource)); - } else { - // For texture/Sampler, the location is the actual binding value - GLint binding = -1; - glGetUniformiv(glprogram, location, &binding); - - auto requestedBinding = slotBindings.find(std::string(sname)); - if (requestedBinding != slotBindings.end()) { - if (binding != (*requestedBinding)._location) { - binding = (*requestedBinding)._location; - glProgramUniform1i(glprogram, location, binding); - } - } - - textures.insert(Shader::Slot(name, binding, elementResource._element, elementResource._resource)); - samplers.insert(Shader::Slot(name, binding, elementResource._element, elementResource._resource)); - } - } - } - - return uniformsCount; -} - -const GLint UNUSED_SLOT = -1; -bool isUnusedSlot(GLint binding) { - return (binding == UNUSED_SLOT); -} - -int makeUniformBlockSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& buffers) { - GLint buffersCount = 0; - - glGetProgramiv(glprogram, GL_ACTIVE_UNIFORM_BLOCKS, &buffersCount); - - // fast exit - if (buffersCount == 0) { - return 0; - } - - GLint maxNumUniformBufferSlots = 0; - glGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS, &maxNumUniformBufferSlots); - std::vector uniformBufferSlotMap(maxNumUniformBufferSlots, -1); - - struct UniformBlockInfo { - using Vector = std::vector; - const GLuint index{ 0 }; - const std::string name; - GLint binding{ -1 }; - GLint size{ 0 }; - - static std::string getName(GLuint glprogram, GLuint i) { - static const GLint NAME_LENGTH = 256; - GLint length = 0; - GLchar nameBuffer[NAME_LENGTH]; - glGetActiveUniformBlockiv(glprogram, i, GL_UNIFORM_BLOCK_NAME_LENGTH, &length); - glGetActiveUniformBlockName(glprogram, i, NAME_LENGTH, &length, nameBuffer); - return std::string(nameBuffer); - } - - UniformBlockInfo(GLuint glprogram, GLuint i) : index(i), name(getName(glprogram, i)) { - glGetActiveUniformBlockiv(glprogram, index, GL_UNIFORM_BLOCK_BINDING, &binding); - glGetActiveUniformBlockiv(glprogram, index, GL_UNIFORM_BLOCK_DATA_SIZE, &size); - } - }; - - UniformBlockInfo::Vector uniformBlocks; - uniformBlocks.reserve(buffersCount); - for (int i = 0; i < buffersCount; i++) { - uniformBlocks.push_back(UniformBlockInfo(glprogram, i)); - } - - for (auto& info : uniformBlocks) { - auto requestedBinding = slotBindings.find(info.name); - if (requestedBinding != slotBindings.end()) { - info.binding = (*requestedBinding)._location; - glUniformBlockBinding(glprogram, info.index, info.binding); - uniformBufferSlotMap[info.binding] = info.index; - } - } - - for (auto& info : uniformBlocks) { - if (slotBindings.count(info.name)) { - continue; - } - - // If the binding is 0, or the binding maps to an already used binding - if (info.binding == 0 || uniformBufferSlotMap[info.binding] != UNUSED_SLOT) { - // If no binding was assigned then just do it finding a free slot - auto slotIt = std::find_if(uniformBufferSlotMap.begin(), uniformBufferSlotMap.end(), isUnusedSlot); - if (slotIt != uniformBufferSlotMap.end()) { - info.binding = slotIt - uniformBufferSlotMap.begin(); - glUniformBlockBinding(glprogram, info.index, info.binding); - } else { - // This should neve happen, an active ubo cannot find an available slot among the max available?! - info.binding = -1; - } - } - - uniformBufferSlotMap[info.binding] = info.index; - } - - for (auto& info : uniformBlocks) { - static const Element element(SCALAR, gpu::UINT32, gpu::UNIFORM_BUFFER); - buffers.insert(Shader::Slot(info.name, info.binding, element, Resource::BUFFER, info.size)); - } - return buffersCount; -} -//CLIMAX_MERGE_START -//This has been copied over from gl45backendshader.cpp -int makeResourceBufferSlots(GLuint glprogram, const Shader::BindingSet& slotBindings,Shader::SlotSet& resourceBuffers) { - GLint buffersCount = 0; - glGetProgramInterfaceiv(glprogram, GL_SHADER_STORAGE_BLOCK, GL_ACTIVE_RESOURCES, &buffersCount); - - // fast exit - if (buffersCount == 0) { - return 0; - } - - GLint maxNumResourceBufferSlots = 0; - glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, &maxNumResourceBufferSlots); - std::vector resourceBufferSlotMap(maxNumResourceBufferSlots, -1); - - struct ResourceBlockInfo { - using Vector = std::vector; - const GLuint index{ 0 }; - const std::string name; - GLint binding{ -1 }; - GLint size{ 0 }; - - static std::string getName(GLuint glprogram, GLuint i) { - static const GLint NAME_LENGTH = 256; - GLint length = 0; - GLchar nameBuffer[NAME_LENGTH]; - glGetProgramResourceName(glprogram, GL_SHADER_STORAGE_BLOCK, i, NAME_LENGTH, &length, nameBuffer); - return std::string(nameBuffer); - } - - ResourceBlockInfo(GLuint glprogram, GLuint i) : index(i), name(getName(glprogram, i)) { - GLenum props[2] = { GL_BUFFER_BINDING, GL_BUFFER_DATA_SIZE}; - glGetProgramResourceiv(glprogram, GL_SHADER_STORAGE_BLOCK, i, 2, props, 2, nullptr, &binding); - } - }; - - ResourceBlockInfo::Vector resourceBlocks; - resourceBlocks.reserve(buffersCount); - for (int i = 0; i < buffersCount; i++) { - resourceBlocks.push_back(ResourceBlockInfo(glprogram, i)); - } - - for (auto& info : resourceBlocks) { - auto requestedBinding = slotBindings.find(info.name); - if (requestedBinding != slotBindings.end()) { - info.binding = (*requestedBinding)._location; - glUniformBlockBinding(glprogram, info.index, info.binding); - resourceBufferSlotMap[info.binding] = info.index; - } - } - - for (auto& info : resourceBlocks) { - if (slotBindings.count(info.name)) { - continue; - } - - // If the binding is -1, or the binding maps to an already used binding - if (info.binding == -1 || !isUnusedSlot(resourceBufferSlotMap[info.binding])) { - // If no binding was assigned then just do it finding a free slot - auto slotIt = std::find_if(resourceBufferSlotMap.begin(), resourceBufferSlotMap.end(), isUnusedSlot); - if (slotIt != resourceBufferSlotMap.end()) { - info.binding = slotIt - resourceBufferSlotMap.begin(); - glUniformBlockBinding(glprogram, info.index, info.binding); - } else { - // This should never happen, an active ssbo cannot find an available slot among the max available?! - info.binding = -1; - } - } - - resourceBufferSlotMap[info.binding] = info.index; - } - - for (auto& info : resourceBlocks) { - static const Element element(SCALAR, gpu::UINT32, gpu::RESOURCE_BUFFER); - resourceBuffers.insert(Shader::Slot(info.name, info.binding, element, Resource::BUFFER, info.size)); - } - return buffersCount; -} -//CLIMAX_MERGE_END - -int makeInputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& inputs) { - GLint inputsCount = 0; - - glGetProgramiv(glprogram, GL_ACTIVE_ATTRIBUTES, &inputsCount); - - for (int i = 0; i < inputsCount; i++) { - const GLint NAME_LENGTH = 256; - GLchar name[NAME_LENGTH]; - GLint length = 0; - GLint size = 0; - GLenum type = 0; - glGetActiveAttrib(glprogram, i, NAME_LENGTH, &length, &size, &type, name); - - GLint binding = glGetAttribLocation(glprogram, name); - - auto elementResource = getFormatFromGLUniform(type); - inputs.insert(Shader::Slot(name, binding, elementResource._element, -1)); - } - - return inputsCount; -} - -int makeOutputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& outputs) { - /* GLint outputsCount = 0; - - glGetProgramiv(glprogram, GL_ACTIVE_, &outputsCount); - - for (int i = 0; i < inputsCount; i++) { - const GLint NAME_LENGTH = 256; - GLchar name[NAME_LENGTH]; - GLint length = 0; - GLint size = 0; - GLenum type = 0; - glGetActiveAttrib(glprogram, i, NAME_LENGTH, &length, &size, &type, name); - - auto element = getFormatFromGLUniform(type); - outputs.insert(Shader::Slot(name, i, element)); - } - */ - return 0; //inputsCount; -} - -void makeProgramBindings(ShaderObject& shaderObject) { - if (!shaderObject.glprogram) { - return; - } - GLuint glprogram = shaderObject.glprogram; - GLint loc = -1; - - //Check for gpu specific attribute slotBindings - loc = glGetAttribLocation(glprogram, "inPosition"); - if (loc >= 0 && loc != gpu::Stream::POSITION) { - glBindAttribLocation(glprogram, gpu::Stream::POSITION, "inPosition"); - } - - loc = glGetAttribLocation(glprogram, "inNormal"); - if (loc >= 0 && loc != gpu::Stream::NORMAL) { - glBindAttribLocation(glprogram, gpu::Stream::NORMAL, "inNormal"); - } - - loc = glGetAttribLocation(glprogram, "inColor"); - if (loc >= 0 && loc != gpu::Stream::COLOR) { - glBindAttribLocation(glprogram, gpu::Stream::COLOR, "inColor"); - } - - loc = glGetAttribLocation(glprogram, "inTexCoord0"); - if (loc >= 0 && loc != gpu::Stream::TEXCOORD) { - glBindAttribLocation(glprogram, gpu::Stream::TEXCOORD, "inTexCoord0"); - } - - loc = glGetAttribLocation(glprogram, "inTangent"); - if (loc >= 0 && loc != gpu::Stream::TANGENT) { - glBindAttribLocation(glprogram, gpu::Stream::TANGENT, "inTangent"); - } - - loc = glGetAttribLocation(glprogram, "inTexCoord1"); - if (loc >= 0 && loc != gpu::Stream::TEXCOORD1) { - glBindAttribLocation(glprogram, gpu::Stream::TEXCOORD1, "inTexCoord1"); - } - - loc = glGetAttribLocation(glprogram, "inSkinClusterIndex"); - if (loc >= 0 && loc != gpu::Stream::SKIN_CLUSTER_INDEX) { - glBindAttribLocation(glprogram, gpu::Stream::SKIN_CLUSTER_INDEX, "inSkinClusterIndex"); - } - - loc = glGetAttribLocation(glprogram, "inSkinClusterWeight"); - if (loc >= 0 && loc != gpu::Stream::SKIN_CLUSTER_WEIGHT) { - glBindAttribLocation(glprogram, gpu::Stream::SKIN_CLUSTER_WEIGHT, "inSkinClusterWeight"); - } - - loc = glGetAttribLocation(glprogram, "_drawCallInfo"); - if (loc >= 0 && loc != gpu::Stream::DRAW_CALL_INFO) { - glBindAttribLocation(glprogram, gpu::Stream::DRAW_CALL_INFO, "_drawCallInfo"); - } - - // Link again to take into account the assigned attrib location - glLinkProgram(glprogram); - - GLint linked = 0; - glGetProgramiv(glprogram, GL_LINK_STATUS, &linked); - if (!linked) { - qCWarning(gpugllogging) << "GLShader::makeBindings - failed to link after assigning slotBindings?"; - } - - // now assign the ubo binding, then DON't relink! - - //Check for gpu specific uniform slotBindings - loc = glGetProgramResourceIndex(glprogram, GL_SHADER_STORAGE_BLOCK, "transformObjectBuffer"); - if (loc >= 0) { - // FIXME GLES - // glShaderStorageBlockBinding(glprogram, loc, TRANSFORM_OBJECT_SLOT); - shaderObject.transformObjectSlot = TRANSFORM_OBJECT_SLOT; - } - - loc = glGetUniformBlockIndex(glprogram, "transformCameraBuffer"); - if (loc >= 0) { - glUniformBlockBinding(glprogram, loc, TRANSFORM_CAMERA_SLOT); - shaderObject.transformCameraSlot = TRANSFORM_CAMERA_SLOT; - } - - (void)CHECK_GL_ERROR(); -} - void serverWait() { auto fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); assert(fence); diff --git a/libraries/gpu-gles/src/gpu/gl/GLShared.h b/libraries/gpu-gles/src/gpu/gl/GLShared.h index 54209b106d..5b25e55c82 100644 --- a/libraries/gpu-gles/src/gpu/gl/GLShared.h +++ b/libraries/gpu-gles/src/gpu/gl/GLShared.h @@ -17,9 +17,9 @@ Q_DECLARE_LOGGING_CATEGORY(gpugllogging) Q_DECLARE_LOGGING_CATEGORY(trace_render_gpu_gl) -namespace gpu { namespace gl { +#define BUFFER_OFFSET(bytes) ((GLubyte*) nullptr + (bytes)) - static const GLint TRANSFORM_OBJECT_SLOT { 14 }; // SSBO binding slot +namespace gpu { namespace gl { // Create a fence and inject a GPU wait on the fence void serverWait(); @@ -27,7 +27,6 @@ void serverWait(); // Create a fence and synchronously wait on the fence void clientWait(); -gpu::Size getDedicatedMemory(); gpu::Size getFreeDedicatedMemory(); ComparisonFunction comparisonFuncFromGL(GLenum func); State::StencilOp stencilOpFromGL(GLenum stencilOp); @@ -35,25 +34,6 @@ State::BlendOp blendOpFromGL(GLenum blendOp); State::BlendArg blendArgFromGL(GLenum blendArg); void getCurrentGLState(State::Data& state); -struct ShaderObject { - GLuint glshader { 0 }; - GLuint glprogram { 0 }; - GLint transformCameraSlot { -1 }; - GLint transformObjectSlot { -1 }; -}; - -int makeUniformSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, - Shader::SlotSet& uniforms, Shader::SlotSet& textures, Shader::SlotSet& samplers); -int makeUniformBlockSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& buffers); -int makeInputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& inputs); -int makeOutputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& outputs); -//CLIMAX_MERGE_START -//makeResourceBufferSlots has been added to glbacked as a virtual function and is being used in gl42 and gl45 overrides. -//Since these files dont exist in the andoid version create a stub here. -int makeResourceBufferSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& resourceBuffers); -//CLIMAX_MERGE_END -void makeProgramBindings(ShaderObject& shaderObject); - enum GLSyncState { // The object is currently undergoing no processing, although it's content // may be out of date, or it's storage may be invalid relative to the @@ -128,6 +108,7 @@ static const GLenum ELEMENT_TYPE_TO_GL[gpu::NUM_TYPES] = { GL_SHORT, GL_UNSIGNED_SHORT, GL_BYTE, + GL_UNSIGNED_BYTE, GL_UNSIGNED_BYTE }; @@ -156,6 +137,7 @@ class GLQuery; class GLState; class GLShader; class GLTexture; +struct ShaderObject; } } // namespace gpu::gl diff --git a/libraries/gpu-gles/src/gpu/gl/GLTexelFormat.cpp b/libraries/gpu-gles/src/gpu/gl/GLTexelFormat.cpp index 9808d389f1..a390079322 100644 --- a/libraries/gpu-gles/src/gpu/gl/GLTexelFormat.cpp +++ b/libraries/gpu-gles/src/gpu/gl/GLTexelFormat.cpp @@ -11,6 +11,9 @@ using namespace gpu; using namespace gpu::gl; +bool GLTexelFormat::isCompressed() const { + return false; +} GLenum GLTexelFormat::evalGLTexelFormatInternal(const gpu::Element& dstFormat) { // qDebug() << "GLTexelFormat::evalGLTexelFormatInternal " << dstFormat.getDimension() << ", " << dstFormat.getSemantic() << ", " << dstFormat.getType(); @@ -18,6 +21,7 @@ GLenum GLTexelFormat::evalGLTexelFormatInternal(const gpu::Element& dstFormat) { switch (dstFormat.getDimension()) { case gpu::SCALAR: { switch (dstFormat.getSemantic()) { + case gpu::RED: case gpu::RGB: case gpu::RGBA: case gpu::SRGB: @@ -74,7 +78,10 @@ GLenum GLTexelFormat::evalGLTexelFormatInternal(const gpu::Element& dstFormat) { // the type should be float result = GL_R11F_G11F_B10F; break; - + case gpu::RGB9E5: + // the type should be float + result = GL_RGB9_E5; + break; case gpu::DEPTH: result = GL_DEPTH_COMPONENT16; switch (dstFormat.getType()) { @@ -114,6 +121,7 @@ GLenum GLTexelFormat::evalGLTexelFormatInternal(const gpu::Element& dstFormat) { switch (dstFormat.getSemantic()) { case gpu::RGB: case gpu::RGBA: + case gpu::XY: result = GL_RG8; break; default: @@ -178,12 +186,12 @@ GLenum GLTexelFormat::evalGLTexelFormatInternal(const gpu::Element& dstFormat) { case gpu::NINT8: result = GL_RGBA8_SNORM; break; - case gpu::COMPRESSED: case gpu::NUINT2: case gpu::NINT16: case gpu::NUINT16: case gpu::NUINT32: case gpu::NINT32: + case gpu::COMPRESSED: case gpu::NUM_TYPES: // quiet compiler Q_UNREACHABLE(); } @@ -201,7 +209,37 @@ GLenum GLTexelFormat::evalGLTexelFormatInternal(const gpu::Element& dstFormat) { } break; } + // TODO: implement compression formats supported by android (ASTC, ETC2) + /* + case gpu::TILE4x4: { + switch (dstFormat.getSemantic()) { + case gpu::COMPRESSED_BC4_RED: + result = GL_COMPRESSED_RED_RGTC1; + break; + case gpu::COMPRESSED_BC1_SRGB: + result = GL_COMPRESSED_SRGB_S3TC_DXT1_EXT; + break; + case gpu::COMPRESSED_BC1_SRGBA: + result = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT; + break; + case gpu::COMPRESSED_BC3_SRGBA: + result = GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT; + break; + case gpu::COMPRESSED_BC5_XY: + result = GL_COMPRESSED_RG_RGTC2; + break; + case gpu::COMPRESSED_BC6_RGB: + result = GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT; + break; + case gpu::COMPRESSED_BC7_SRGBA: + result = GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM; + break; + default: + qCWarning(gpugllogging) << "Unknown combination of texel format"; + } + break; + */ default: qCDebug(gpugllogging) << "Unknown combination of texel format"; } @@ -212,8 +250,6 @@ GLenum GLTexelFormat::evalGLTexelFormatInternal(const gpu::Element& dstFormat) { } GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const Element& srcFormat) { -// qDebug() << "GLTexelFormat::evalGLTexelFormat dst.getDimension=" << dstFormat.getDimension() << " dst.getType=" << dstFormat.getType() << " dst.getSemantic=" << dstFormat.getSemantic(); -// qDebug() << "GLTexelFormat::evalGLTexelFormat src.getDimension=" << srcFormat.getDimension() << " src.getType=" << srcFormat.getType() << " src.getSemantic=" << srcFormat.getSemantic(); if (dstFormat != srcFormat) { GLTexelFormat texel = { GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE }; @@ -237,6 +273,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E //CLIMAX_MERGE_END case gpu::DEPTH: + texel.format = GL_DEPTH_COMPONENT; texel.internalFormat = GL_DEPTH_COMPONENT32_OES; break; case gpu::DEPTH_STENCIL: @@ -245,7 +282,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E texel.internalFormat = GL_DEPTH24_STENCIL8; break; default: - qCDebug(gpugllogging) << "Unknown combination of texel format"; + qCWarning(gpugllogging) << "Unknown combination of texel format"; } break; } @@ -257,10 +294,11 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E switch (dstFormat.getSemantic()) { case gpu::RGB: case gpu::RGBA: + case gpu::XY: texel.internalFormat = GL_RG8; break; default: - qCDebug(gpugllogging) << "Unknown combination of texel format"; + qCWarning(gpugllogging) << "Unknown combination of texel format"; } break; @@ -288,7 +326,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E // break; //CLIMAX_MERGE_END default: - qCDebug(gpugllogging) << "Unknown combination of texel format"; + qCWarning(gpugllogging) << "Unknown combination of texel format"; } break; @@ -356,13 +394,13 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E */ default: - qCDebug(gpugllogging) << "Unknown combination of texel format"; + qCWarning(gpugllogging) << "Unknown combination of texel format"; } break; } default: - qCDebug(gpugllogging) << "Unknown combination of texel format"; + qCWarning(gpugllogging) << "Unknown combination of texel format"; } return texel; } else { @@ -374,12 +412,8 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E texel.type = ELEMENT_TYPE_TO_GL[dstFormat.getType()]; switch (dstFormat.getSemantic()) { - //CLIMAX_MERGE_START - // case gpu::COMPRESSED_R: { - // qDebug() << "TODO: GLTexelFormat.cpp:evalGLTexelFormat GL_COMPRESSED_RED_RGTC1"; - // //texel.internalFormat = GL_COMPRESSED_RED_RGTC1; - // break; - // } + + case gpu::RED: case gpu::RGB: case gpu::RGBA: case gpu::SRGB: @@ -465,6 +499,12 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E texel.internalFormat = GL_R11F_G11F_B10F; break; + case gpu::RGB9E5: + texel.format = GL_RGB; + texel.type = GL_UNSIGNED_INT_5_9_9_9_REV; + texel.internalFormat = GL_RGB9_E5; + break; + case gpu::DEPTH: texel.format = GL_DEPTH_COMPONENT; // It's depth component to load it texel.internalFormat = GL_DEPTH_COMPONENT32_OES; @@ -508,7 +548,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E texel.internalFormat = GL_DEPTH24_STENCIL8; break; default: - qCDebug(gpugllogging) << "Unknown combination of texel format"; + qCWarning(gpugllogging) << "Unknown combination of texel format"; } break; @@ -521,10 +561,11 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E switch (dstFormat.getSemantic()) { case gpu::RGB: case gpu::RGBA: + case gpu::XY: texel.internalFormat = GL_RG8; break; default: - qCDebug(gpugllogging) << "Unknown combination of texel format"; + qCWarning(gpugllogging) << "Unknown combination of texel format"; } break; @@ -555,7 +596,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E // qDebug() << "TODO: GLTexelFormat.cpp:evalGLTexelFormat GL_COMPRESSED_SRGB"; // break; default: - qCDebug(gpugllogging) << "Unknown combination of texel format"; + qCWarning(gpugllogging) << "Unknown combination of texel format"; } break; } @@ -645,7 +686,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E // //texel.internalFormat = GL_COMPRESSED_SRGB_ALPHA; // break; default: - qCDebug(gpugllogging) << "Unknown combination of texel format"; + qCWarning(gpugllogging) << "Unknown combination of texel format"; } break; } diff --git a/libraries/gpu-gles/src/gpu/gl/GLTexelFormat.h b/libraries/gpu-gles/src/gpu/gl/GLTexelFormat.h index 94ded3dc23..8f37f6b604 100644 --- a/libraries/gpu-gles/src/gpu/gl/GLTexelFormat.h +++ b/libraries/gpu-gles/src/gpu/gl/GLTexelFormat.h @@ -18,6 +18,11 @@ public: GLenum format; GLenum type; + GLTexelFormat(GLenum glinternalFormat, GLenum glformat, GLenum gltype) : internalFormat(glinternalFormat), format(glformat), type(gltype) {} + GLTexelFormat(GLenum glinternalFormat) : internalFormat(glinternalFormat) {} + + bool isCompressed() const; + static GLTexelFormat evalGLTexelFormat(const Element& dstFormat) { return evalGLTexelFormat(dstFormat, dstFormat); } diff --git a/libraries/gpu-gles/src/gpu/gl/GLTexture.cpp b/libraries/gpu-gles/src/gpu/gl/GLTexture.cpp index 5f5e3a9be1..780dbe17a1 100644 --- a/libraries/gpu-gles/src/gpu/gl/GLTexture.cpp +++ b/libraries/gpu-gles/src/gpu/gl/GLTexture.cpp @@ -8,27 +8,22 @@ #include "GLTexture.h" +#include #include -#include "GLTextureTransfer.h" #include "GLBackend.h" using namespace gpu; using namespace gpu::gl; -std::shared_ptr GLTexture::_textureTransferHelper; -// FIXME placeholder for texture memory over-use -#define DEFAULT_MAX_MEMORY_MB 256 -#define MIN_FREE_GPU_MEMORY_PERCENTAGE 0.25f -#define OVER_MEMORY_PRESSURE 2.0f - -const GLenum GLTexture::CUBE_FACE_LAYOUT[6] = { +const GLenum GLTexture::CUBE_FACE_LAYOUT[GLTexture::TEXTURE_CUBE_NUM_FACES] = { GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, GL_TEXTURE_CUBE_MAP_POSITIVE_Z, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z }; + const GLenum GLTexture::WRAP_MODES[Sampler::NUM_WRAP_MODES] = { GL_REPEAT, // WRAP_REPEAT, GL_MIRRORED_REPEAT, // WRAP_MIRROR, @@ -73,6 +68,17 @@ GLenum GLTexture::getGLTextureType(const Texture& texture) { return GL_TEXTURE_2D; } +uint8_t GLTexture::getFaceCount(GLenum target) { + switch (target) { + case GL_TEXTURE_2D: + return TEXTURE_2D_NUM_FACES; + case GL_TEXTURE_CUBE_MAP: + return TEXTURE_CUBE_NUM_FACES; + default: + Q_UNREACHABLE(); + break; + } +} const std::vector& GLTexture::getFaceTargets(GLenum target) { static std::vector cubeFaceTargets { @@ -96,228 +102,602 @@ const std::vector& GLTexture::getFaceTargets(GLenum target) { return faceTargets; } -// Default texture memory = GPU total memory - 2GB -#define GPU_MEMORY_RESERVE_BYTES MB_TO_BYTES(2048) -// Minimum texture memory = 1GB -#define TEXTURE_MEMORY_MIN_BYTES MB_TO_BYTES(1024) - - -float GLTexture::getMemoryPressure() { - // Check for an explicit memory limit - auto availableTextureMemory = Texture::getAllowedGPUMemoryUsage(); - - - // If no memory limit has been set, use a percentage of the total dedicated memory - if (!availableTextureMemory) { -#if 0 - auto totalMemory = getDedicatedMemory(); - if ((GPU_MEMORY_RESERVE_BYTES + TEXTURE_MEMORY_MIN_BYTES) > totalMemory) { - availableTextureMemory = TEXTURE_MEMORY_MIN_BYTES; - } else { - availableTextureMemory = totalMemory - GPU_MEMORY_RESERVE_BYTES; - } -#else - // Hardcode texture limit for sparse textures at 1 GB for now - availableTextureMemory = TEXTURE_MEMORY_MIN_BYTES; -#endif - } - - // Return the consumed texture memory divided by the available texture memory. - //CLIMAX_MERGE_START - //auto consumedGpuMemory = Context::getTextureGPUMemoryUsage() - Context::getTextureGPUFramebufferMemoryUsage(); - //float memoryPressure = (float)consumedGpuMemory / (float)availableTextureMemory; - //static Context::Size lastConsumedGpuMemory = 0; - //if (memoryPressure > 1.0f && lastConsumedGpuMemory != consumedGpuMemory) { - // lastConsumedGpuMemory = consumedGpuMemory; - // qCDebug(gpugllogging) << "Exceeded max allowed texture memory: " << consumedGpuMemory << " / " << availableTextureMemory; - //} - //return memoryPressure; - return 0; - -} - - -// Create the texture and allocate storage -GLTexture::GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id, bool transferrable) : - GLObject(backend, texture, id), - _external(false), - _source(texture.source()), - _storageStamp(texture.getStamp()), - _target(getGLTextureType(texture)), - _internalFormat(gl::GLTexelFormat::evalGLTexelFormatInternal(texture.getTexelFormat())), - _maxMip(texture.getMaxMip()), - _minMip(texture.getMinMip()), - _virtualSize(texture.evalTotalSize()), - _transferrable(transferrable) -{ - //qDebug() << "GLTexture::GLTexture building GLTexture with _internalFormat" << _internalFormat; - auto strongBackend = _backend.lock(); - strongBackend->recycle(); - //CLIMAX_MERGE_START - //Backend::incrementTextureGPUCount(); - //Backend::updateTextureGPUVirtualMemoryUsage(0, _virtualSize); - //CLIMAX_MERGE_END - Backend::setGPUObject(texture, this); -} - GLTexture::GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id) : - GLObject(backend, texture, id), - _external(true), - _source(texture.source()), - _storageStamp(0), - _target(getGLTextureType(texture)), - _internalFormat(GL_RGBA8), - // FIXME force mips to 0? - _maxMip(texture.getMaxMip()), - _minMip(texture.getMinMip()), - _virtualSize(0), - _transferrable(false) + GLObject(backend, texture, id), + _source(texture.source()), + _target(getGLTextureType(texture)), + _texelFormat(GLTexelFormat::evalGLTexelFormatInternal(texture.getTexelFormat())) { Backend::setGPUObject(texture, this); - - // FIXME Is this necessary? - //withPreservedTexture([this] { - // syncSampler(); - // if (_gpuObject.isAutogenerateMips()) { - // generateMips(); - // } - //}); } GLTexture::~GLTexture() { auto backend = _backend.lock(); - if (backend) { - if (_external) { - auto recycler = _gpuObject.getExternalRecycler(); - if (recycler) { - backend->releaseExternalTexture(_id, recycler); - } else { - qWarning() << "No recycler available for texture " << _id << " possible leak"; - } - } else if (_id) { - // WARNING! Sparse textures do not use this code path. See GL45BackendTexture for - // the GL45Texture destructor for doing any required work tracking GPU stats - backend->releaseTexture(_id, _size); - } - - ////CLIMAX_MERGE_START - //if (!_external && !_transferrable) { - // Backend::updateTextureGPUFramebufferMemoryUsage(_size, 0); - //} + if (backend && _id) { + backend->releaseTexture(_id, 0); } - //Backend::updateTextureGPUVirtualMemoryUsage(_virtualSize, 0); - //CLIMAX_MERGE_END } -void GLTexture::createTexture() { - withPreservedTexture([&] { - allocateStorage(); - (void)CHECK_GL_ERROR(); - syncSampler(); - (void)CHECK_GL_ERROR(); +Size GLTexture::copyMipFaceFromTexture(uint16_t sourceMip, uint16_t targetMip, uint8_t face) const { + if (!_gpuObject.isStoredMipFaceAvailable(sourceMip)) { + return 0; + } + auto dim = _gpuObject.evalMipDimensions(sourceMip); + auto mipData = _gpuObject.accessStoredMipFace(sourceMip, face); + auto mipSize = _gpuObject.getStoredMipFaceSize(sourceMip, face); + if (mipData) { + GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), _gpuObject.getStoredMipFormat()); + return copyMipFaceLinesFromTexture(targetMip, face, dim, 0, texelFormat.internalFormat, texelFormat.format, texelFormat.type, mipSize, mipData->readData()); + } else { + qCDebug(gpugllogging) << "Missing mipData level=" << sourceMip << " face=" << (int)face << " for texture " << _gpuObject.source().c_str(); + } + return 0; +} + + +GLExternalTexture::GLExternalTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id) + : Parent(backend, texture, id) { + Backend::textureExternalCount.increment(); +} + +GLExternalTexture::~GLExternalTexture() { + auto backend = _backend.lock(); + if (backend) { + auto recycler = _gpuObject.getExternalRecycler(); + if (recycler) { + backend->releaseExternalTexture(_id, recycler); + } else { + qCWarning(gpugllogging) << "No recycler available for texture " << _id << " possible leak"; + } + const_cast(_id) = 0; + } + Backend::textureExternalCount.decrement(); +} + + +// Variable sized textures +using MemoryPressureState = GLVariableAllocationSupport::MemoryPressureState; +using WorkQueue = GLVariableAllocationSupport::WorkQueue; +using TransferJobPointer = GLVariableAllocationSupport::TransferJobPointer; + +std::list GLVariableAllocationSupport::_memoryManagedTextures; +MemoryPressureState GLVariableAllocationSupport::_memoryPressureState { MemoryPressureState::Idle }; +std::atomic GLVariableAllocationSupport::_memoryPressureStateStale { false }; +const uvec3 GLVariableAllocationSupport::INITIAL_MIP_TRANSFER_DIMENSIONS { 64, 64, 1 }; +WorkQueue GLVariableAllocationSupport::_transferQueue; +WorkQueue GLVariableAllocationSupport::_promoteQueue; +WorkQueue GLVariableAllocationSupport::_demoteQueue; +size_t GLVariableAllocationSupport::_frameTexturesCreated { 0 }; + +#define OVERSUBSCRIBED_PRESSURE_VALUE 0.95f +#define UNDERSUBSCRIBED_PRESSURE_VALUE 0.85f +#define DEFAULT_ALLOWED_TEXTURE_MEMORY_MB ((size_t)1024) + +static const size_t DEFAULT_ALLOWED_TEXTURE_MEMORY = MB_TO_BYTES(DEFAULT_ALLOWED_TEXTURE_MEMORY_MB); + +using TransferJob = GLVariableAllocationSupport::TransferJob; + +const uvec3 GLVariableAllocationSupport::MAX_TRANSFER_DIMENSIONS { 1024, 1024, 1 }; +const size_t GLVariableAllocationSupport::MAX_TRANSFER_SIZE = GLVariableAllocationSupport::MAX_TRANSFER_DIMENSIONS.x * GLVariableAllocationSupport::MAX_TRANSFER_DIMENSIONS.y * 4; + +#if THREADED_TEXTURE_BUFFERING + +TexturePointer GLVariableAllocationSupport::_currentTransferTexture; +TransferJobPointer GLVariableAllocationSupport::_currentTransferJob; +QThreadPool* TransferJob::_bufferThreadPool { nullptr }; + +void TransferJob::startBufferingThread() { + static std::once_flag once; + std::call_once(once, [&] { + _bufferThreadPool = new QThreadPool(qApp); + _bufferThreadPool->setMaxThreadCount(1); }); } -void GLTexture::withPreservedTexture(std::function f) const { - GLint boundTex = -1; - switch (_target) { - case GL_TEXTURE_2D: - glGetIntegerv(GL_TEXTURE_BINDING_2D, &boundTex); - break; +#endif - case GL_TEXTURE_CUBE_MAP: - glGetIntegerv(GL_TEXTURE_BINDING_CUBE_MAP, &boundTex); - break; +TransferJob::TransferJob(const GLTexture& parent, uint16_t sourceMip, uint16_t targetMip, uint8_t face, uint32_t lines, uint32_t lineOffset) + : _parent(parent) { - default: - qFatal("Unsupported texture type"); + auto transferDimensions = _parent._gpuObject.evalMipDimensions(sourceMip); + GLenum format; + GLenum internalFormat; + GLenum type; + GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_parent._gpuObject.getTexelFormat(), _parent._gpuObject.getStoredMipFormat()); + format = texelFormat.format; + internalFormat = texelFormat.internalFormat; + type = texelFormat.type; + _transferSize = _parent._gpuObject.getStoredMipFaceSize(sourceMip, face); + + // If we're copying a subsection of the mip, do additional calculations to find the size and offset of the segment + if (0 != lines) { + transferDimensions.y = lines; + auto dimensions = _parent._gpuObject.evalMipDimensions(sourceMip); + auto bytesPerLine = (uint32_t)_transferSize / dimensions.y; + _transferOffset = bytesPerLine * lineOffset; + _transferSize = bytesPerLine * lines; } - (void)CHECK_GL_ERROR(); - glBindTexture(_target, _texture); - f(); - glBindTexture(_target, boundTex); - (void)CHECK_GL_ERROR(); + Backend::texturePendingGPUTransferMemSize.update(0, _transferSize); + + if (_transferSize > GLVariableAllocationSupport::MAX_TRANSFER_SIZE) { + qCWarning(gpugllogging) << "Transfer size of " << _transferSize << " exceeds theoretical maximum transfer size"; + } + + // Buffering can invoke disk IO, so it should be off of the main and render threads + _bufferingLambda = [=] { + auto mipStorage = _parent._gpuObject.accessStoredMipFace(sourceMip, face); + if (mipStorage) { + _mipData = mipStorage->createView(_transferSize, _transferOffset); + } else { + qCWarning(gpugllogging) << "Buffering failed because mip could not be retrieved from texture " << _parent._source.c_str() ; + } + }; + + _transferLambda = [=] { + if (_mipData) { + _parent.copyMipFaceLinesFromTexture(targetMip, face, transferDimensions, lineOffset, internalFormat, format, type, _mipData->size(), _mipData->readData()); + _mipData.reset(); + } else { + qCWarning(gpugllogging) << "Transfer failed because mip could not be retrieved from texture " << _parent._source.c_str(); + } + }; } -void GLTexture::setSize(GLuint size) const { - ////CLIMAX_MERGE_START - //if (!_external && !_transferrable) { - // Backend::updateTextureGPUFramebufferMemoryUsage(_size, 0); - //} - //Backend::updateTextureGPUMemoryUsage(_size, size); - const_cast(_size) = size; +TransferJob::TransferJob(const GLTexture& parent, std::function transferLambda) + : _parent(parent), _bufferingRequired(false), _transferLambda(transferLambda) { } -bool GLTexture::isInvalid() const { - return _storageStamp < _gpuObject.getStamp(); +TransferJob::~TransferJob() { + Backend::texturePendingGPUTransferMemSize.update(_transferSize, 0); } -bool GLTexture::isOutdated() const { - return GLSyncState::Idle == _syncState && _contentStamp < _gpuObject.getDataStamp(); -} - -bool GLTexture::isReady() const { - // If we have an invalid texture, we're never ready - if (isInvalid()) { +bool TransferJob::tryTransfer() { +#if THREADED_TEXTURE_BUFFERING + // Are we ready to transfer + if (!bufferingCompleted()) { + startBuffering(); return false; } - - auto syncState = _syncState.load(); - if (isOutdated() || Idle != syncState) { - return false; +#else + if (_bufferingRequired) { + _bufferingLambda(); } - +#endif + _transferLambda(); return true; } +#if THREADED_TEXTURE_BUFFERING +bool TransferJob::bufferingRequired() const { + if (!_bufferingRequired) { + return false; + } -// Do any post-transfer operations that might be required on the main context / rendering thread -void GLTexture::postTransfer() { - //CLIMAX_MERGE_START - - // setSyncState(GLSyncState::Idle); - // ++_transferCount; + // The default state of a QFuture is with status Canceled | Started | Finished, + // so we have to check isCancelled before we check the actual state + if (_bufferingStatus.isCanceled()) { + return true; + } - // // At this point the mip pixels have been loaded, we can notify the gpu texture to abandon it's memory - // switch (_gpuObject.getType()) { - // case Texture::TEX_2D: - // for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { - // if (_gpuObject.isStoredMipFaceAvailable(i)) { - // _gpuObject.notifyMipFaceGPULoaded(i); - // } - // } - // break; - - // case Texture::TEX_CUBE: - // // transfer pixels from each faces - // for (uint8_t f = 0; f < CUBE_NUM_FACES; f++) { - // for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { - // if (_gpuObject.isStoredMipFaceAvailable(i, f)) { - // _gpuObject.notifyMipFaceGPULoaded(i, f); - // } - // } - // } - // break; - - // default: - // qCWarning(gpugllogging) << __FUNCTION__ << " case for Texture Type " << _gpuObject.getType() << " not supported"; - // break; - // } - //CLIMAX_MERGE_END + return !_bufferingStatus.isStarted(); } -void GLTexture::initTextureTransferHelper() { - _textureTransferHelper = std::make_shared(); +bool TransferJob::bufferingCompleted() const { + if (!_bufferingRequired) { + return true; + } + + // The default state of a QFuture is with status Canceled | Started | Finished, + // so we have to check isCancelled before we check the actual state + if (_bufferingStatus.isCanceled()) { + return false; + } + + return _bufferingStatus.isFinished(); } -void GLTexture::startTransfer() { - createTexture(); +void TransferJob::startBuffering() { + if (bufferingRequired()) { + assert(_bufferingStatus.isCanceled()); + _bufferingStatus = QtConcurrent::run(_bufferThreadPool, [=] { + _bufferingLambda(); + }); + assert(!_bufferingStatus.isCanceled()); + assert(_bufferingStatus.isStarted()); + } +} +#endif + +GLVariableAllocationSupport::GLVariableAllocationSupport() { + _memoryPressureStateStale = true; } -void GLTexture::finishTransfer() { - if (_gpuObject.isAutogenerateMips()) { - generateMips(); +GLVariableAllocationSupport::~GLVariableAllocationSupport() { + _memoryPressureStateStale = true; +} + +void GLVariableAllocationSupport::addMemoryManagedTexture(const TexturePointer& texturePointer) { + _memoryManagedTextures.push_back(texturePointer); + if (MemoryPressureState::Idle != _memoryPressureState) { + addToWorkQueue(texturePointer); } } +void GLVariableAllocationSupport::addToWorkQueue(const TexturePointer& texturePointer) { + GLTexture* gltexture = Backend::getGPUObject(*texturePointer); + GLVariableAllocationSupport* vargltexture = dynamic_cast(gltexture); + switch (_memoryPressureState) { + case MemoryPressureState::Oversubscribed: + if (vargltexture->canDemote()) { + // Demote largest first + _demoteQueue.push({ texturePointer, (float)gltexture->size() }); + } + break; + + case MemoryPressureState::Undersubscribed: + if (vargltexture->canPromote()) { + // Promote smallest first + _promoteQueue.push({ texturePointer, 1.0f / (float)gltexture->size() }); + } + break; + + case MemoryPressureState::Transfer: + if (vargltexture->hasPendingTransfers()) { + // Transfer priority given to smaller mips first + _transferQueue.push({ texturePointer, 1.0f / (float)gltexture->_gpuObject.evalMipSize(vargltexture->_populatedMip) }); + } + break; + + case MemoryPressureState::Idle: + Q_UNREACHABLE(); + break; + } +} + +WorkQueue& GLVariableAllocationSupport::getActiveWorkQueue() { + static WorkQueue empty; + switch (_memoryPressureState) { + case MemoryPressureState::Oversubscribed: + return _demoteQueue; + + case MemoryPressureState::Undersubscribed: + return _promoteQueue; + + case MemoryPressureState::Transfer: + return _transferQueue; + + case MemoryPressureState::Idle: + Q_UNREACHABLE(); + break; + } + return empty; +} + +// FIXME hack for stats display +QString getTextureMemoryPressureModeString() { + switch (GLVariableAllocationSupport::_memoryPressureState) { + case MemoryPressureState::Oversubscribed: + return "Oversubscribed"; + + case MemoryPressureState::Undersubscribed: + return "Undersubscribed"; + + case MemoryPressureState::Transfer: + return "Transfer"; + + case MemoryPressureState::Idle: + return "Idle"; + } + Q_UNREACHABLE(); + return "Unknown"; +} + +void GLVariableAllocationSupport::updateMemoryPressure() { + static size_t lastAllowedMemoryAllocation = gpu::Texture::getAllowedGPUMemoryUsage(); + + size_t allowedMemoryAllocation = gpu::Texture::getAllowedGPUMemoryUsage(); + if (0 == allowedMemoryAllocation) { + allowedMemoryAllocation = DEFAULT_ALLOWED_TEXTURE_MEMORY; + } + + // If the user explicitly changed the allowed memory usage, we need to mark ourselves stale + // so that we react + if (allowedMemoryAllocation != lastAllowedMemoryAllocation) { + _memoryPressureStateStale = true; + lastAllowedMemoryAllocation = allowedMemoryAllocation; + } + + if (!_memoryPressureStateStale.exchange(false)) { + return; + } + + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + + // Clear any defunct textures (weak pointers that no longer have a valid texture) + _memoryManagedTextures.remove_if([&](const TextureWeakPointer& weakPointer) { + return weakPointer.expired(); + }); + + // Convert weak pointers to strong. This new list may still contain nulls if a texture was + // deleted on another thread between the previous line and this one + std::vector strongTextures; { + strongTextures.reserve(_memoryManagedTextures.size()); + std::transform( + _memoryManagedTextures.begin(), _memoryManagedTextures.end(), + std::back_inserter(strongTextures), + [](const TextureWeakPointer& p) { return p.lock(); }); + } + + size_t totalVariableMemoryAllocation = 0; + size_t idealMemoryAllocation = 0; + bool canDemote = false; + bool canPromote = false; + bool hasTransfers = false; + for (const auto& texture : strongTextures) { + // Race conditions can still leave nulls in the list, so we need to check + if (!texture) { + continue; + } + GLTexture* gltexture = Backend::getGPUObject(*texture); + GLVariableAllocationSupport* vartexture = dynamic_cast(gltexture); + // Track how much the texture thinks it should be using + idealMemoryAllocation += texture->evalTotalSize(); + // Track how much we're actually using + totalVariableMemoryAllocation += gltexture->size(); + canDemote |= vartexture->canDemote(); + canPromote |= vartexture->canPromote(); + hasTransfers |= vartexture->hasPendingTransfers(); + } + + size_t unallocated = idealMemoryAllocation - totalVariableMemoryAllocation; + float pressure = (float)totalVariableMemoryAllocation / (float)allowedMemoryAllocation; + + auto newState = MemoryPressureState::Idle; + if (pressure < UNDERSUBSCRIBED_PRESSURE_VALUE && (unallocated != 0 && canPromote)) { + newState = MemoryPressureState::Undersubscribed; + } else if (pressure > OVERSUBSCRIBED_PRESSURE_VALUE && canDemote) { + newState = MemoryPressureState::Oversubscribed; + } else if (hasTransfers) { + newState = MemoryPressureState::Transfer; + } + + if (newState != _memoryPressureState) { + _memoryPressureState = newState; + // Clear the existing queue + _transferQueue = WorkQueue(); + _promoteQueue = WorkQueue(); + _demoteQueue = WorkQueue(); + + // Populate the existing textures into the queue + if (_memoryPressureState != MemoryPressureState::Idle) { + for (const auto& texture : strongTextures) { + // Race conditions can still leave nulls in the list, so we need to check + if (!texture) { + continue; + } + addToWorkQueue(texture); + } + } + } +} + +TexturePointer GLVariableAllocationSupport::getNextWorkQueueItem(WorkQueue& workQueue) { + while (!workQueue.empty()) { + auto workTarget = workQueue.top(); + + auto texture = workTarget.first.lock(); + if (!texture) { + workQueue.pop(); + continue; + } + + // Check whether the resulting texture can actually have work performed + GLTexture* gltexture = Backend::getGPUObject(*texture); + GLVariableAllocationSupport* vartexture = dynamic_cast(gltexture); + switch (_memoryPressureState) { + case MemoryPressureState::Oversubscribed: + if (vartexture->canDemote()) { + return texture; + } + break; + + case MemoryPressureState::Undersubscribed: + if (vartexture->canPromote()) { + return texture; + } + break; + + case MemoryPressureState::Transfer: + if (vartexture->hasPendingTransfers()) { + return texture; + } + break; + + case MemoryPressureState::Idle: + Q_UNREACHABLE(); + break; + } + + // If we got here, then the texture has no work to do in the current state, + // so pop it off the queue and continue + workQueue.pop(); + } + + return TexturePointer(); +} + +void GLVariableAllocationSupport::processWorkQueue(WorkQueue& workQueue) { + if (workQueue.empty()) { + return; + } + + // Get the front of the work queue to perform work + auto texture = getNextWorkQueueItem(workQueue); + if (!texture) { + return; + } + + // Grab the first item off the demote queue + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + + GLTexture* gltexture = Backend::getGPUObject(*texture); + GLVariableAllocationSupport* vartexture = dynamic_cast(gltexture); + switch (_memoryPressureState) { + case MemoryPressureState::Oversubscribed: + vartexture->demote(); + workQueue.pop(); + addToWorkQueue(texture); + _memoryPressureStateStale = true; + break; + + case MemoryPressureState::Undersubscribed: + vartexture->promote(); + workQueue.pop(); + addToWorkQueue(texture); + _memoryPressureStateStale = true; + break; + + case MemoryPressureState::Transfer: + if (vartexture->executeNextTransfer(texture)) { + workQueue.pop(); + addToWorkQueue(texture); + +#if THREADED_TEXTURE_BUFFERING + // Eagerly start the next buffering job if possible + texture = getNextWorkQueueItem(workQueue); + if (texture) { + gltexture = Backend::getGPUObject(*texture); + vartexture = dynamic_cast(gltexture); + vartexture->executeNextBuffer(texture); + } +#endif + } + break; + + case MemoryPressureState::Idle: + Q_UNREACHABLE(); + break; + } +} + +void GLVariableAllocationSupport::processWorkQueues() { + if (MemoryPressureState::Idle == _memoryPressureState) { + return; + } + + auto& workQueue = getActiveWorkQueue(); + // Do work on the front of the queue + processWorkQueue(workQueue); + + if (workQueue.empty()) { + _memoryPressureState = MemoryPressureState::Idle; + _memoryPressureStateStale = true; + } +} + +void GLVariableAllocationSupport::manageMemory() { + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + updateMemoryPressure(); + processWorkQueues(); +} + +bool GLVariableAllocationSupport::executeNextTransfer(const TexturePointer& currentTexture) { +#if THREADED_TEXTURE_BUFFERING + // If a transfer job is active on the buffering thread, but has not completed it's buffering lambda, + // then we need to exit early, since we don't want to have the transfer job leave scope while it's + // being used in another thread -- See https://highfidelity.fogbugz.com/f/cases/4626 + if (_currentTransferJob && !_currentTransferJob->bufferingCompleted()) { + return false; + } +#endif + + if (_populatedMip <= _allocatedMip) { +#if THREADED_TEXTURE_BUFFERING + _currentTransferJob.reset(); + _currentTransferTexture.reset(); +#endif + return true; + } + + // If the transfer queue is empty, rebuild it + if (_pendingTransfers.empty()) { + populateTransferQueue(); + } + + bool result = false; + if (!_pendingTransfers.empty()) { +#if THREADED_TEXTURE_BUFFERING + // If there is a current transfer, but it's not the top of the pending transfer queue, then it's an orphan, so we want to abandon it. + if (_currentTransferJob && _currentTransferJob != _pendingTransfers.front()) { + _currentTransferJob.reset(); + } + + if (!_currentTransferJob) { + // Keeping hold of a strong pointer to the transfer job ensures that if the pending transfer queue is rebuilt, the transfer job + // doesn't leave scope, causing a crash in the buffering thread + _currentTransferJob = _pendingTransfers.front(); + + // Keeping hold of a strong pointer during the transfer ensures that the transfer thread cannot try to access a destroyed texture + _currentTransferTexture = currentTexture; + } + + // transfer jobs use asynchronous buffering of the texture data because it may involve disk IO, so we execute a try here to determine if the buffering + // is complete + if (_currentTransferJob->tryTransfer()) { + _pendingTransfers.pop(); + // Once a given job is finished, release the shared pointers keeping them alive + _currentTransferTexture.reset(); + _currentTransferJob.reset(); + result = true; + } +#else + if (_pendingTransfers.front()->tryTransfer()) { + _pendingTransfers.pop(); + result = true; + } +#endif + } + return result; +} + +#if THREADED_TEXTURE_BUFFERING +void GLVariableAllocationSupport::executeNextBuffer(const TexturePointer& currentTexture) { + if (_currentTransferJob && !_currentTransferJob->bufferingCompleted()) { + return; + } + + // If the transfer queue is empty, rebuild it + if (_pendingTransfers.empty()) { + populateTransferQueue(); + } + + if (!_pendingTransfers.empty()) { + if (!_currentTransferJob) { + _currentTransferJob = _pendingTransfers.front(); + _currentTransferTexture = currentTexture; + } + + _currentTransferJob->startBuffering(); + } +} +#endif + +void GLVariableAllocationSupport::incrementPopulatedSize(Size delta) const { + _populatedSize += delta; + // Keep the 2 code paths to be able to debug + if (_size < _populatedSize) { + Backend::textureResourcePopulatedGPUMemSize.update(0, delta); + } else { + Backend::textureResourcePopulatedGPUMemSize.update(0, delta); + } +} +void GLVariableAllocationSupport::decrementPopulatedSize(Size delta) const { + _populatedSize -= delta; + // Keep the 2 code paths to be able to debug + if (_size < _populatedSize) { + Backend::textureResourcePopulatedGPUMemSize.update(delta, 0); + } else { + Backend::textureResourcePopulatedGPUMemSize.update(delta, 0); + } +} \ No newline at end of file diff --git a/libraries/gpu-gles/src/gpu/gl/GLTexture.h b/libraries/gpu-gles/src/gpu/gl/GLTexture.h index 03353ae67d..ce27d02033 100644 --- a/libraries/gpu-gles/src/gpu/gl/GLTexture.h +++ b/libraries/gpu-gles/src/gpu/gl/GLTexture.h @@ -8,10 +8,15 @@ #ifndef hifi_gpu_gl_GLTexture_h #define hifi_gpu_gl_GLTexture_h +#include +#include + #include "GLShared.h" -#include "GLTextureTransfer.h" #include "GLBackend.h" #include "GLTexelFormat.h" +#include + +#define THREADED_TEXTURE_BUFFERING 1 namespace gpu { namespace gl { @@ -20,214 +25,187 @@ struct GLFilterMode { GLint magFilter; }; +class GLVariableAllocationSupport { + friend class GLBackend; + +public: + GLVariableAllocationSupport(); + virtual ~GLVariableAllocationSupport(); + + enum class MemoryPressureState { + Idle, + Transfer, + Oversubscribed, + Undersubscribed, + }; + + using QueuePair = std::pair; + struct QueuePairLess { + bool operator()(const QueuePair& a, const QueuePair& b) { + return a.second < b.second; + } + }; + using WorkQueue = std::priority_queue, QueuePairLess>; + + class TransferJob { + using VoidLambda = std::function; + using VoidLambdaQueue = std::queue; + const GLTexture& _parent; + Texture::PixelsPointer _mipData; + size_t _transferOffset { 0 }; + size_t _transferSize { 0 }; + + bool _bufferingRequired { true }; + VoidLambda _transferLambda; + VoidLambda _bufferingLambda; + +#if THREADED_TEXTURE_BUFFERING + // Indicates if a transfer from backing storage to interal storage has started + QFuture _bufferingStatus; + static QThreadPool* _bufferThreadPool; +#endif + + public: + TransferJob(const TransferJob& other) = delete; + TransferJob(const GLTexture& parent, std::function transferLambda); + TransferJob(const GLTexture& parent, uint16_t sourceMip, uint16_t targetMip, uint8_t face, uint32_t lines = 0, uint32_t lineOffset = 0); + ~TransferJob(); + bool tryTransfer(); + +#if THREADED_TEXTURE_BUFFERING + void startBuffering(); + bool bufferingRequired() const; + bool bufferingCompleted() const; + static void startBufferingThread(); +#endif + + private: + void transfer(); + }; + + using TransferJobPointer = std::shared_ptr; + using TransferQueue = std::queue; + static MemoryPressureState _memoryPressureState; + +public: + static void addMemoryManagedTexture(const TexturePointer& texturePointer); + +protected: + static size_t _frameTexturesCreated; + static std::atomic _memoryPressureStateStale; + static std::list _memoryManagedTextures; + static WorkQueue _transferQueue; + static WorkQueue _promoteQueue; + static WorkQueue _demoteQueue; +#if THREADED_TEXTURE_BUFFERING + static TexturePointer _currentTransferTexture; + static TransferJobPointer _currentTransferJob; +#endif + static const uvec3 INITIAL_MIP_TRANSFER_DIMENSIONS; + static const uvec3 MAX_TRANSFER_DIMENSIONS; + static const size_t MAX_TRANSFER_SIZE; + + + static void updateMemoryPressure(); + static void processWorkQueues(); + static void processWorkQueue(WorkQueue& workQueue); + static TexturePointer getNextWorkQueueItem(WorkQueue& workQueue); + static void addToWorkQueue(const TexturePointer& texture); + static WorkQueue& getActiveWorkQueue(); + + static void manageMemory(); + + //bool canPromoteNoAllocate() const { return _allocatedMip < _populatedMip; } + bool canPromote() const { return _allocatedMip > _minAllocatedMip; } + bool canDemote() const { return _allocatedMip < _maxAllocatedMip; } + bool hasPendingTransfers() const { return _populatedMip > _allocatedMip; } +#if THREADED_TEXTURE_BUFFERING + void executeNextBuffer(const TexturePointer& currentTexture); +#endif + bool executeNextTransfer(const TexturePointer& currentTexture); + virtual void populateTransferQueue() = 0; + virtual void promote() = 0; + virtual void demote() = 0; + + // THe amount of memory currently allocated + Size _size { 0 }; + + // The amount of memory currnently populated + void incrementPopulatedSize(Size delta) const; + void decrementPopulatedSize(Size delta) const; + mutable Size _populatedSize { 0 }; + + // The allocated mip level, relative to the number of mips in the gpu::Texture object + // The relationship between a given glMip to the original gpu::Texture mip is always + // glMip + _allocatedMip + uint16 _allocatedMip { 0 }; + // The populated mip level, relative to the number of mips in the gpu::Texture object + // This must always be >= the allocated mip + uint16 _populatedMip { 0 }; + // The highest (lowest resolution) mip that we will support, relative to the number + // of mips in the gpu::Texture object + uint16 _maxAllocatedMip { 0 }; + // The lowest (highest resolution) mip that we will support, relative to the number + // of mips in the gpu::Texture object + uint16 _minAllocatedMip { 0 }; + // Contains a series of lambdas that when executed will transfer data to the GPU, modify + // the _populatedMip and update the sampler in order to fully populate the allocated texture + // until _populatedMip == _allocatedMip + TransferQueue _pendingTransfers; +}; class GLTexture : public GLObject { + using Parent = GLObject; + friend class GLBackend; + friend class GLVariableAllocationSupport; public: static const uint16_t INVALID_MIP { (uint16_t)-1 }; static const uint8_t INVALID_FACE { (uint8_t)-1 }; - static void initTextureTransferHelper(); - static std::shared_ptr _textureTransferHelper; - - template - static GLTexture* sync(GLBackend& backend, const TexturePointer& texturePointer, bool needTransfer) { - const Texture& texture = *texturePointer; - - // Special case external textures - //CLIMAX_MERGE_START - //Z:/HiFi_Android/HiFi_GIT/libraries/gpu-gl-android/src/gpu/gl/../gles/../gl/GLTexture.h:37:32: error: no member named 'isExternal' in 'gpu::Texture::Usage' - // The only instance of this being used again. replace. - // if (texture.getUsage().isExternal()) { - // Texture::ExternalUpdates updates = texture.getUpdates(); - // if (!updates.empty()) { - // Texture::ExternalRecycler recycler = texture.getExternalRecycler(); - // Q_ASSERT(recycler); - // // Discard any superfluous updates - // while (updates.size() > 1) { - // const auto& update = updates.front(); - // // Superfluous updates will never have been read, but we want to ensure the previous - // // writes to them are complete before they're written again, so return them with the - // // same fences they arrived with. This can happen on any thread because no GL context - // // work is involved - // recycler(update.first, update.second); - // updates.pop_front(); - // } - - // // The last texture remaining is the one we'll use to create the GLTexture - // const auto& update = updates.front(); - // // Check for a fence, and if it exists, inject a wait into the command stream, then destroy the fence - // if (update.second) { - // GLsync fence = static_cast(update.second); - // glWaitSync(fence, 0, GL_TIMEOUT_IGNORED); - // glDeleteSync(fence); - // } - - // // Create the new texture object (replaces any previous texture object) - // new GLTextureType(backend.shared_from_this(), texture, update.first); - // } - - - // Return the texture object (if any) associated with the texture, without extensive logic - // (external textures are - //return Backend::getGPUObject(texture); - //} - //CLIMAX_MERGE_END - if (!texture.isDefined()) { - // NO texture definition yet so let's avoid thinking - return nullptr; - } - - // If the object hasn't been created, or the object definition is out of date, drop and re-create - GLTexture* object = Backend::getGPUObject(texture); - - // Create the texture if need be (force re-creation if the storage stamp changes - // for easier use of immutable storage) - if (!object || object->isInvalid()) { - // This automatically any previous texture - object = new GLTextureType(backend.shared_from_this(), texture, needTransfer); - if (!object->_transferrable) { - object->createTexture(); - object->_contentStamp = texture.getDataStamp(); - object->updateSize(); - object->postTransfer(); - } - } - - // Object maybe doens't neet to be tranasferred after creation - if (!object->_transferrable) { - return object; - } - - // If we just did a transfer, return the object after doing post-transfer work - if (GLSyncState::Transferred == object->getSyncState()) { - object->postTransfer(); - } - - if (object->isOutdated()) { - // Object might be outdated, if so, start the transfer - // (outdated objects that are already in transfer will have reported 'true' for ready() - _textureTransferHelper->transferTexture(texturePointer); - return nullptr; - } - - if (!object->isReady()) { - return nullptr; - } - - ((GLTexture*)object)->updateMips(); - - return object; - } - - template - static GLuint getId(GLBackend& backend, const TexturePointer& texture, bool shouldSync) { - if (!texture) { - return 0; - } - GLTexture* object { nullptr }; - if (shouldSync) { - object = sync(backend, texture, shouldSync); - } else { - object = Backend::getGPUObject(*texture); - } - - if (!object) { - return 0; - } - - if (!shouldSync) { - return object->_id; - } - - // Don't return textures that are in transfer state - if ((object->getSyncState() != GLSyncState::Idle) || - // Don't return transferrable textures that have never completed transfer - (!object->_transferrable || 0 != object->_transferCount)) { - return 0; - } - - return object->_id; - } - ~GLTexture(); - // Is this texture generated outside the GPU library? - const bool _external; const GLuint& _texture { _id }; const std::string _source; - const Stamp _storageStamp; const GLenum _target; - const GLenum _internalFormat; - const uint16 _maxMip; - uint16 _minMip; - const GLuint _virtualSize; // theoretical size as expected - Stamp _contentStamp { 0 }; - const bool _transferrable; - Size _transferCount { 0 }; - GLuint size() const { return _size; } - GLSyncState getSyncState() const { return _syncState; } + GLTexelFormat _texelFormat; - // Is the storage out of date relative to the gpu texture? - bool isInvalid() const; + static const std::vector& getFaceTargets(GLenum textureType); + static uint8_t getFaceCount(GLenum textureType); + static GLenum getGLTextureType(const Texture& texture); - // Is the content out of date relative to the gpu texture? - bool isOutdated() const; - - // Is the texture in a state where it can be rendered with no work? - bool isReady() const; - - // Execute any post-move operations that must occur only on the main thread - virtual void postTransfer(); - - uint16 usedMipLevels() const { return (_maxMip - _minMip) + 1; } - - static const size_t CUBE_NUM_FACES = 6; - static const GLenum CUBE_FACE_LAYOUT[6]; + static const uint8_t TEXTURE_2D_NUM_FACES = 1; + static const uint8_t TEXTURE_CUBE_NUM_FACES = 6; + static const GLenum CUBE_FACE_LAYOUT[TEXTURE_CUBE_NUM_FACES]; static const GLFilterMode FILTER_MODES[Sampler::NUM_FILTERS]; static const GLenum WRAP_MODES[Sampler::NUM_WRAP_MODES]; - // Return a floating point value indicating how much of the allowed - // texture memory we are currently consuming. A value of 0 indicates - // no texture memory usage, while a value of 1 indicates all available / allowed memory - // is consumed. A value above 1 indicates that there is a problem. - static float getMemoryPressure(); protected: - - static const std::vector& getFaceTargets(GLenum textureType); - - static GLenum getGLTextureType(const Texture& texture); - - - const GLuint _size { 0 }; // true size as reported by the gl api - std::atomic _syncState { GLSyncState::Idle }; - - GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id, bool transferrable); - GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id); - - void setSyncState(GLSyncState syncState) { _syncState = syncState; } - - void createTexture(); - - virtual void updateMips() {} - virtual void allocateStorage() const = 0; - virtual void updateSize() const = 0; - virtual void syncSampler() const = 0; + virtual Size size() const = 0; virtual void generateMips() const = 0; - virtual void withPreservedTexture(std::function f) const; + virtual void syncSampler() const = 0; -protected: - void setSize(GLuint size) const; + virtual Size copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum internalFormat, GLenum format, GLenum type, Size sourceSize, const void* sourcePointer) const = 0; + virtual Size copyMipFaceFromTexture(uint16_t sourceMip, uint16_t targetMip, uint8_t face) const final; + virtual void copyTextureMipsInGPUMem(GLuint srcId, GLuint destId, uint16_t srcMipOffset, uint16_t destMipOffset, uint16_t populatedMips) {} // Only relevant for Variable Allocation textures - virtual void startTransfer(); - // Returns true if this is the last block required to complete transfer - virtual bool continueTransfer() { return false; } - virtual void finishTransfer(); - -private: - friend class GLTextureTransferHelper; - friend class GLBackend; + GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id); }; +class GLExternalTexture : public GLTexture { + using Parent = GLTexture; + friend class GLBackend; +public: + ~GLExternalTexture(); +protected: + GLExternalTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id); + void generateMips() const override {} + void syncSampler() const override {} + Size copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum internalFormat, GLenum format, GLenum type, Size sourceSize, const void* sourcePointer) const override { return 0;} + + Size size() const override { return 0; } +}; + + } } #endif diff --git a/libraries/gpu-gles/src/gpu/gl/GLTextureTransfer.cpp b/libraries/gpu-gles/src/gpu/gl/GLTextureTransfer.cpp deleted file mode 100644 index cec46cb90d..0000000000 --- a/libraries/gpu-gles/src/gpu/gl/GLTextureTransfer.cpp +++ /dev/null @@ -1,207 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/04/03 -// Copyright 2013-2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#include "GLTextureTransfer.h" - -#include -#include - -#include "GLShared.h" -#include "GLTexture.h" - -#ifdef HAVE_NSIGHT -#include "nvToolsExt.h" -std::unordered_map _map; -#endif - - -#ifdef TEXTURE_TRANSFER_PBOS -#define TEXTURE_TRANSFER_BLOCK_SIZE (64 * 1024) -#define TEXTURE_TRANSFER_PBO_COUNT 128 -#endif - -using namespace gpu; -using namespace gpu::gl; - -GLTextureTransferHelper::GLTextureTransferHelper() { -#ifdef THREADED_TEXTURE_TRANSFER - setObjectName("TextureTransferThread"); - _context.create(); - initialize(true, QThread::LowPriority); - // Clean shutdown on UNIX, otherwise _canvas is freed early - connect(qApp, &QCoreApplication::aboutToQuit, [&] { terminate(); }); -#else - initialize(false, QThread::LowPriority); -#endif -} - -GLTextureTransferHelper::~GLTextureTransferHelper() { -#ifdef THREADED_TEXTURE_TRANSFER - if (isStillRunning()) { - terminate(); - } -#else - terminate(); -#endif -} - -void GLTextureTransferHelper::transferTexture(const gpu::TexturePointer& texturePointer) { - GLTexture* object = Backend::getGPUObject(*texturePointer); - - //CLIMAX_MERGE_START - //Backend::incrementTextureGPUTransferCount(); - object->setSyncState(GLSyncState::Pending); - Lock lock(_mutex); - _pendingTextures.push_back(texturePointer); -} - -void GLTextureTransferHelper::setup() { -#ifdef THREADED_TEXTURE_TRANSFER - _context.makeCurrent(); - -#ifdef TEXTURE_TRANSFER_FORCE_DRAW - // FIXME don't use opengl 4.5 DSA functionality without verifying it's present - glCreateRenderbuffers(1, &_drawRenderbuffer); - glNamedRenderbufferStorage(_drawRenderbuffer, GL_RGBA8, 128, 128); - glCreateFramebuffers(1, &_drawFramebuffer); - glNamedFramebufferRenderbuffer(_drawFramebuffer, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _drawRenderbuffer); - glCreateFramebuffers(1, &_readFramebuffer); -#endif - -#ifdef TEXTURE_TRANSFER_PBOS - std::array pbos; - glCreateBuffers(TEXTURE_TRANSFER_PBO_COUNT, &pbos[0]); - for (uint32_t i = 0; i < TEXTURE_TRANSFER_PBO_COUNT; ++i) { - TextureTransferBlock newBlock; - newBlock._pbo = pbos[i]; - glNamedBufferStorage(newBlock._pbo, TEXTURE_TRANSFER_BLOCK_SIZE, 0, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); - newBlock._mapped = glMapNamedBufferRange(newBlock._pbo, 0, TEXTURE_TRANSFER_BLOCK_SIZE, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); - _readyQueue.push(newBlock); - } -#endif -#endif -} - -void GLTextureTransferHelper::shutdown() { -#ifdef THREADED_TEXTURE_TRANSFER - _context.makeCurrent(); -#endif - -#ifdef TEXTURE_TRANSFER_FORCE_DRAW - glNamedFramebufferRenderbuffer(_drawFramebuffer, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, 0); - glDeleteFramebuffers(1, &_drawFramebuffer); - _drawFramebuffer = 0; - glDeleteFramebuffers(1, &_readFramebuffer); - _readFramebuffer = 0; - - glNamedFramebufferTexture(_readFramebuffer, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0); - glDeleteRenderbuffers(1, &_drawRenderbuffer); - _drawRenderbuffer = 0; -#endif -} - -void GLTextureTransferHelper::queueExecution(VoidLambda lambda) { - Lock lock(_mutex); - _pendingCommands.push_back(lambda); -} - -#define MAX_TRANSFERS_PER_PASS 2 - -bool GLTextureTransferHelper::process() { - // Take any new textures or commands off the queue - VoidLambdaList pendingCommands; - TextureList newTransferTextures; - { - Lock lock(_mutex); - newTransferTextures.swap(_pendingTextures); - pendingCommands.swap(_pendingCommands); - } - - if (!pendingCommands.empty()) { - for (auto command : pendingCommands) { - command(); - } - glFlush(); - } - - if (!newTransferTextures.empty()) { - for (auto& texturePointer : newTransferTextures) { -#ifdef HAVE_NSIGHT - _map[texturePointer] = nvtxRangeStart("TextureTansfer"); -#endif - GLTexture* object = Backend::getGPUObject(*texturePointer); - object->startTransfer(); - _transferringTextures.push_back(texturePointer); - _textureIterator = _transferringTextures.begin(); - } - _transferringTextures.sort([](const gpu::TexturePointer& a, const gpu::TexturePointer& b)->bool { - return a->getSize() < b->getSize(); - }); - } - - // No transfers in progress, sleep - if (_transferringTextures.empty()) { -#ifdef THREADED_TEXTURE_TRANSFER - QThread::usleep(1); -#endif - return true; - } - - static auto lastReport = usecTimestampNow(); - auto now = usecTimestampNow(); - auto lastReportInterval = now - lastReport; - if (lastReportInterval > USECS_PER_SECOND * 4) { - lastReport = now; - qDebug() << "Texture list " << _transferringTextures.size(); - } - - size_t transferCount = 0; - for (_textureIterator = _transferringTextures.begin(); _textureIterator != _transferringTextures.end();) { - if (++transferCount > MAX_TRANSFERS_PER_PASS) { - break; - } - auto texture = *_textureIterator; - GLTexture* gltexture = Backend::getGPUObject(*texture); - if (gltexture->continueTransfer()) { - ++_textureIterator; - continue; - } - - gltexture->finishTransfer(); - -#ifdef TEXTURE_TRANSFER_FORCE_DRAW - // FIXME force a draw on the texture transfer thread before passing the texture to the main thread for use -#endif - -#ifdef THREADED_TEXTURE_TRANSFER - clientWait(); -#endif - gltexture->_contentStamp = gltexture->_gpuObject.getDataStamp(); - gltexture->updateSize(); - gltexture->setSyncState(gpu::gl::GLSyncState::Transferred); - //CLIMAX_MERGE_START - //Backend::decrementTextureGPUTransferCount(); -#ifdef HAVE_NSIGHT - // Mark the texture as transferred - nvtxRangeEnd(_map[texture]); - _map.erase(texture); -#endif - _textureIterator = _transferringTextures.erase(_textureIterator); - } - -#ifdef THREADED_TEXTURE_TRANSFER - if (!_transferringTextures.empty()) { - // Don't saturate the GPU - clientWait(); - } else { - // Don't saturate the CPU - QThread::msleep(1); - } -#endif - - return true; -} diff --git a/libraries/gpu-gles/src/gpu/gl/GLTextureTransfer.h b/libraries/gpu-gles/src/gpu/gl/GLTextureTransfer.h deleted file mode 100644 index a23c282fd4..0000000000 --- a/libraries/gpu-gles/src/gpu/gl/GLTextureTransfer.h +++ /dev/null @@ -1,78 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/04/03 -// Copyright 2013-2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#ifndef hifi_gpu_gl_GLTextureTransfer_h -#define hifi_gpu_gl_GLTextureTransfer_h - -#include -#include - -#include - -#include - -#include "GLShared.h" - -#ifdef Q_OS_WIN -#define THREADED_TEXTURE_TRANSFER -#endif - -#ifdef THREADED_TEXTURE_TRANSFER -// FIXME when sparse textures are enabled, it's harder to force a draw on the transfer thread -// also, the current draw code is implicitly using OpenGL 4.5 functionality -//#define TEXTURE_TRANSFER_FORCE_DRAW -// FIXME PBO's increase the complexity and don't seem to work reliably -//#define TEXTURE_TRANSFER_PBOS -#endif - -namespace gpu { namespace gl { - -using TextureList = std::list; -using TextureListIterator = TextureList::iterator; - -class GLTextureTransferHelper : public GenericThread { -public: - using VoidLambda = std::function; - using VoidLambdaList = std::list; - using Pointer = std::shared_ptr; - GLTextureTransferHelper(); - ~GLTextureTransferHelper(); - void transferTexture(const gpu::TexturePointer& texturePointer); - void queueExecution(VoidLambda lambda); - - void setup() override; - void shutdown() override; - bool process() override; - -private: -#ifdef THREADED_TEXTURE_TRANSFER - ::gl::OffscreenContext _context; -#endif - -#ifdef TEXTURE_TRANSFER_FORCE_DRAW - // Framebuffers / renderbuffers for forcing access to the texture on the transfer thread - GLuint _drawRenderbuffer { 0 }; - GLuint _drawFramebuffer { 0 }; - GLuint _readFramebuffer { 0 }; -#endif - - // A mutex for protecting items access on the render and transfer threads - Mutex _mutex; - // Commands that have been submitted for execution on the texture transfer thread - VoidLambdaList _pendingCommands; - // Textures that have been submitted for transfer - TextureList _pendingTextures; - // Textures currently in the transfer process - // Only used on the transfer thread - TextureList _transferringTextures; - TextureListIterator _textureIterator; - -}; - -} } - -#endif \ No newline at end of file diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackend.h b/libraries/gpu-gles/src/gpu/gles/GLESBackend.h index ebecf29d44..19f8575802 100644 --- a/libraries/gpu-gles/src/gpu/gles/GLESBackend.h +++ b/libraries/gpu-gles/src/gpu/gles/GLESBackend.h @@ -27,6 +27,12 @@ class GLESBackend : public GLBackend { friend class Context; public: + static const GLint TRANSFORM_OBJECT_SLOT { 31 }; + static const GLint RESOURCE_TRANSFER_TEX_UNIT { 32 }; + static const GLint RESOURCE_TRANSFER_EXTRA_TEX_UNIT { 33 }; + static const GLint RESOURCE_BUFFER_TEXBUF_TEX_UNIT { 34 }; + static const GLint RESOURCE_BUFFER_SLOT0_TEX_UNIT { 35 }; + explicit GLESBackend(bool syncCache) : Parent(syncCache) {} GLESBackend() : Parent() {} virtual ~GLESBackend() { @@ -38,33 +44,95 @@ public: static const std::string GLES_VERSION; const std::string& getVersion() const override { return GLES_VERSION; } - class GLESTexture : public GLTexture { using Parent = GLTexture; - GLuint allocate(); - public: - GLESTexture(const std::weak_ptr& backend, const Texture& buffer, GLuint externalId); - GLESTexture(const std::weak_ptr& backend, const Texture& buffer, bool transferrable); - + friend class GLESBackend; + GLuint allocate(const Texture& texture); protected: - void transferMip(uint16_t mipLevel, uint8_t face) const; - void startTransfer() override; - void allocateStorage() const override; - void updateSize() const override; - void syncSampler() const override; + GLESTexture(const std::weak_ptr& backend, const Texture& buffer); void generateMips() const override; + Size copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum internalFormat, GLenum format, GLenum type, Size sourceSize, const void* sourcePointer) const override; + void syncSampler() const override; + + void withPreservedTexture(std::function f) const; }; + // + // Textures that have fixed allocation sizes and cannot be managed at runtime + // + + class GLESFixedAllocationTexture : public GLESTexture { + using Parent = GLESTexture; + friend class GLESBackend; + + public: + GLESFixedAllocationTexture(const std::weak_ptr& backend, const Texture& texture); + ~GLESFixedAllocationTexture(); + + protected: + Size size() const override { return _size; } + void allocateStorage() const; + void syncSampler() const override; + const Size _size { 0 }; + }; + + class GLESAttachmentTexture : public GLESFixedAllocationTexture { + using Parent = GLESFixedAllocationTexture; + friend class GLESBackend; + protected: + GLESAttachmentTexture(const std::weak_ptr& backend, const Texture& texture); + ~GLESAttachmentTexture(); + }; + + class GLESStrictResourceTexture : public GLESFixedAllocationTexture { + using Parent = GLESFixedAllocationTexture; + friend class GLESBackend; + protected: + GLESStrictResourceTexture(const std::weak_ptr& backend, const Texture& texture); + ~GLESStrictResourceTexture(); + }; + + class GLESVariableAllocationTexture : public GLESTexture, public GLVariableAllocationSupport { + using Parent = GLESTexture; + friend class GLESBackend; + using PromoteLambda = std::function; + + + protected: + GLESVariableAllocationTexture(const std::weak_ptr& backend, const Texture& texture); + ~GLESVariableAllocationTexture(); + + void allocateStorage(uint16 allocatedMip); + void syncSampler() const override; + void promote() override; + void demote() override; + void populateTransferQueue() override; + + Size copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum internalFormat, GLenum format, GLenum type, Size sourceSize, const void* sourcePointer) const override; + Size copyMipsFromTexture(); + + void copyTextureMipsInGPUMem(GLuint srcId, GLuint destId, uint16_t srcMipOffset, uint16_t destMipOffset, uint16_t populatedMips) override; + + Size size() const override { return _size; } + }; + + class GLESResourceTexture : public GLESVariableAllocationTexture { + using Parent = GLESVariableAllocationTexture; + friend class GLESBackend; + protected: + GLESResourceTexture(const std::weak_ptr& backend, const Texture& texture); + ~GLESResourceTexture(); + }; protected: GLuint getFramebufferID(const FramebufferPointer& framebuffer) override; GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) override; GLuint getBufferID(const Buffer& buffer) override; + GLuint getResourceBufferID(const Buffer& buffer); GLBuffer* syncGPUObject(const Buffer& buffer) override; - GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) override; - GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) override; + GLTexture* syncGPUObject(const TexturePointer& texture) override; GLuint getQueryID(const QueryPointer& query) override; GLQuery* syncGPUObject(const Query& query) override; @@ -78,14 +146,13 @@ protected: void do_multiDrawIndexedIndirect(const Batch& batch, size_t paramOffset) override; // Input Stage - void updateInput() override; void resetInputStage() override; + void updateInput() override; // Synchronize the state cache of this Backend with the actual real state of the GL Context void transferTransformState(const Batch& batch) const override; void initTransform() override; - void updateTransform(const Batch& batch); - void resetTransformStage(); + void updateTransform(const Batch& batch) override; // Resource Stage bool bindResourceBuffer(uint32_t slot, BufferPointer& buffer) override; @@ -93,6 +160,11 @@ protected: // Output stage void do_blit(const Batch& batch, size_t paramOffset) override; + + std::string getBackendShaderHeader() const override; + void makeProgramBindings(ShaderObject& shaderObject) override; + int makeResourceBufferSlots(GLuint glprogram, const Shader::BindingSet& slotBindings,Shader::SlotSet& resourceBuffers) override; + }; } } diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackendOutput.cpp b/libraries/gpu-gles/src/gpu/gles/GLESBackendOutput.cpp index 0b7c525fbb..dc4025247e 100644 --- a/libraries/gpu-gles/src/gpu/gles/GLESBackendOutput.cpp +++ b/libraries/gpu-gles/src/gpu/gles/GLESBackendOutput.cpp @@ -53,10 +53,12 @@ public: GL_COLOR_ATTACHMENT15 }; int unit = 0; + auto backend = _backend.lock(); for (auto& b : _gpuObject.getRenderBuffers()) { surface = b._texture; if (surface) { - gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer + Q_ASSERT(TextureUsageType::RENDERBUFFER == surface->getUsageType()); + gltexture = backend->syncGPUObject(surface); } else { gltexture = nullptr; } @@ -81,9 +83,11 @@ public: } if (_gpuObject.getDepthStamp() != _depthStamp) { + auto backend = _backend.lock(); auto surface = _gpuObject.getDepthStencilBuffer(); if (_gpuObject.hasDepthStencil() && surface) { - gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer + Q_ASSERT(TextureUsageType::RENDERBUFFER == surface->getUsageType()); + gltexture = backend->syncGPUObject(surface); } if (gltexture) { @@ -99,8 +103,8 @@ public: if (!_colorBuffers.empty()) { glDrawBuffers((GLsizei)_colorBuffers.size(), _colorBuffers.data()); } else { - static const std::vector NO_BUFFERS{ GL_NONE }; - glDrawBuffers((GLsizei)NO_BUFFERS.size(), NO_BUFFERS.data()); + GLenum DrawBuffers[1] = {GL_NONE}; + glDrawBuffers(1, DrawBuffers); } // Now check for completness @@ -111,7 +115,7 @@ public: glBindFramebuffer(GL_DRAW_FRAMEBUFFER, currentFBO); } - checkStatus(GL_DRAW_FRAMEBUFFER); + checkStatus(); } @@ -120,7 +124,7 @@ public: : Parent(backend, framebuffer, allocate()) { } }; -gl::GLFramebuffer* gpu::gles::GLESBackend::syncGPUObject(const Framebuffer& framebuffer) { +gl::GLFramebuffer* GLESBackend::syncGPUObject(const Framebuffer& framebuffer) { return GLESFramebuffer::sync(*this, framebuffer); } diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackendShader.cpp b/libraries/gpu-gles/src/gpu/gles/GLESBackendShader.cpp new file mode 100644 index 0000000000..01a87978c2 --- /dev/null +++ b/libraries/gpu-gles/src/gpu/gles/GLESBackendShader.cpp @@ -0,0 +1,113 @@ +// +// Created by Sam Gateau on 2017/04/13 +// Copyright 2013-2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "GLESBackend.h" +#include "../gl/GLShader.h" + +using namespace gpu; +using namespace gpu::gl; +using namespace gpu::gles; + +// GLSL version +std::string GLESBackend::getBackendShaderHeader() const { + return Parent::getBackendShaderHeader(); +} + +int GLESBackend::makeResourceBufferSlots(GLuint glprogram, const Shader::BindingSet& slotBindings,Shader::SlotSet& resourceBuffers) { + GLint ssboCount = 0; + GLint uniformsCount = 0; + + glGetProgramiv(glprogram, GL_ACTIVE_UNIFORMS, &uniformsCount); + + for (int i = 0; i < uniformsCount; i++) { + const GLint NAME_LENGTH = 256; + GLchar name[NAME_LENGTH]; + GLint length = 0; + GLint size = 0; + GLenum type = 0; + glGetActiveUniform(glprogram, i, NAME_LENGTH, &length, &size, &type, name); + GLint location = glGetUniformLocation(glprogram, name); + const GLint INVALID_UNIFORM_LOCATION = -1; + + // Try to make sense of the gltype + auto elementResource = getFormatFromGLUniform(type); + + // The uniform as a standard var type + if (location != INVALID_UNIFORM_LOCATION) { + + if (elementResource._resource == Resource::BUFFER) { + if (elementResource._element.getSemantic() == gpu::RESOURCE_BUFFER) { + // Let's make sure the name doesn't contains an array element + std::string sname(name); + auto foundBracket = sname.find_first_of('['); + if (foundBracket != std::string::npos) { + // std::string arrayname = sname.substr(0, foundBracket); + + if (sname[foundBracket + 1] == '0') { + sname = sname.substr(0, foundBracket); + } else { + // skip this uniform since it's not the first element of an array + continue; + } + } + + // For texture/Sampler, the location is the actual binding value + GLint binding = -1; + glGetUniformiv(glprogram, location, &binding); + + if (binding == GLESBackend::TRANSFORM_OBJECT_SLOT) { + continue; + } + + auto requestedBinding = slotBindings.find(std::string(sname)); + if (requestedBinding != slotBindings.end()) { + GLint requestedLoc = (*requestedBinding)._location + GLESBackend::RESOURCE_BUFFER_SLOT0_TEX_UNIT; + if (binding != requestedLoc) { + binding = requestedLoc; + } + } else { + binding += GLESBackend::RESOURCE_BUFFER_SLOT0_TEX_UNIT; + } + glProgramUniform1i(glprogram, location, binding); + + ssboCount++; + resourceBuffers.insert(Shader::Slot(name, binding, elementResource._element, elementResource._resource)); + } + } + } + } + + return ssboCount; +} + +void GLESBackend::makeProgramBindings(ShaderObject& shaderObject) { + if (!shaderObject.glprogram) { + return; + } + GLuint glprogram = shaderObject.glprogram; + GLint loc = -1; + + GLBackend::makeProgramBindings(shaderObject); + + // now assign the ubo binding, then DON't relink! + + //Check for gpu specific uniform slotBindings + loc = glGetUniformLocation(glprogram, "transformObjectBuffer"); + if (loc >= 0) { + glProgramUniform1i(glprogram, loc, GLESBackend::TRANSFORM_OBJECT_SLOT); + shaderObject.transformObjectSlot = GLESBackend::TRANSFORM_OBJECT_SLOT; + } + + loc = glGetUniformBlockIndex(glprogram, "transformCameraBuffer"); + if (loc >= 0) { + glUniformBlockBinding(glprogram, loc, gpu::TRANSFORM_CAMERA_SLOT); + shaderObject.transformCameraSlot = gpu::TRANSFORM_CAMERA_SLOT; + } + + (void)CHECK_GL_ERROR(); +} + diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackendTexture.cpp b/libraries/gpu-gles/src/gpu/gles/GLESBackendTexture.cpp index 31a98edd12..97d38bacce 100644 --- a/libraries/gpu-gles/src/gpu/gles/GLESBackendTexture.cpp +++ b/libraries/gpu-gles/src/gpu/gles/GLESBackendTexture.cpp @@ -12,41 +12,90 @@ #include #include -#include -// #include "../gl/GLTexelFormat.h" +#include "../gl/GLTexelFormat.h" using namespace gpu; using namespace gpu::gl; using namespace gpu::gles; -//using GL41TexelFormat = GLTexelFormat; +GLTexture* GLESBackend::syncGPUObject(const TexturePointer& texturePointer) { + if (!texturePointer) { + return nullptr; + } + + const Texture& texture = *texturePointer; + if (TextureUsageType::EXTERNAL == texture.getUsageType()) { + return Parent::syncGPUObject(texturePointer); + } + + if (!texture.isDefined()) { + // NO texture definition yet so let's avoid thinking + return nullptr; + } + + GLESTexture* object = Backend::getGPUObject(texture); + if (!object) { + switch (texture.getUsageType()) { + case TextureUsageType::RENDERBUFFER: + object = new GLESAttachmentTexture(shared_from_this(), texture); + break; + + case TextureUsageType::STRICT_RESOURCE: + qCDebug(gpugllogging) << "Strict texture " << texture.source().c_str(); + object = new GLESStrictResourceTexture(shared_from_this(), texture); + break; + + case TextureUsageType::RESOURCE: + qCDebug(gpugllogging) << "variable / Strict texture " << texture.source().c_str(); + object = new GLESResourceTexture(shared_from_this(), texture); + GLVariableAllocationSupport::addMemoryManagedTexture(texturePointer); + break; + + default: + Q_UNREACHABLE(); + } + } else { + if (texture.getUsageType() == TextureUsageType::RESOURCE) { + auto varTex = static_cast (object); + + if (varTex->_minAllocatedMip > 0) { + auto minAvailableMip = texture.minAvailableMipLevel(); + if (minAvailableMip < varTex->_minAllocatedMip) { + varTex->_minAllocatedMip = minAvailableMip; + GLESVariableAllocationTexture::_memoryPressureStateStale = true; + } + } + } + } + + return object; +} + using GLESTexture = GLESBackend::GLESTexture; -GLuint GLESTexture::allocate() { - //CLIMAX_MERGE_START - //Backend::incrementTextureGPUCount(); - //CLIMAX_MERGE_END +GLESTexture::GLESTexture(const std::weak_ptr& backend, const Texture& texture) + : GLTexture(backend, texture, allocate(texture)) { +} + +void GLESTexture::withPreservedTexture(std::function f) const { + glActiveTexture(GL_TEXTURE0 + GLESBackend::RESOURCE_TRANSFER_TEX_UNIT); + glBindTexture(_target, _texture); + (void)CHECK_GL_ERROR(); + + f(); + glBindTexture(_target, 0); + (void)CHECK_GL_ERROR(); +} + + +GLuint GLESTexture::allocate(const Texture& texture) { GLuint result; glGenTextures(1, &result); return result; } -GLuint GLESBackend::getTextureID(const TexturePointer& texture, bool transfer) { - return GLESTexture::getId(*this, texture, transfer); -} -GLTexture* GLESBackend::syncGPUObject(const TexturePointer& texture, bool transfer) { - return GLESTexture::sync(*this, texture, transfer); -} - -GLESTexture::GLESTexture(const std::weak_ptr& backend, const Texture& texture, GLuint externalId) - : GLTexture(backend, texture, externalId) { -} - -GLESTexture::GLESTexture(const std::weak_ptr& backend, const Texture& texture, bool transferrable) - : GLTexture(backend, texture, allocate(), transferrable) { -} void GLESTexture::generateMips() const { withPreservedTexture([&] { @@ -55,102 +104,62 @@ void GLESTexture::generateMips() const { (void)CHECK_GL_ERROR(); } -void GLESTexture::allocateStorage() const { - GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); - glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, 0); - (void)CHECK_GL_ERROR(); - glTexParameteri(_target, GL_TEXTURE_MAX_LEVEL, _maxMip - _minMip); - (void)CHECK_GL_ERROR(); -/* if (GLEW_VERSION_4_2 && !_gpuObject.getTexelFormat().isCompressed()) { - // Get the dimensions, accounting for the downgrade level - Vec3u dimensions = _gpuObject.evalMipDimensions(_minMip); - glTexStorage2D(_target, usedMipLevels(), texelFormat.internalFormat, dimensions.x, dimensions.y); - (void)CHECK_GL_ERROR(); - } else {*/ - for (uint16_t l = _minMip; l <= _maxMip; l++) { - // Get the mip level dimensions, accounting for the downgrade level - Vec3u dimensions = _gpuObject.evalMipDimensions(l); - for (GLenum target : getFaceTargets(_target)) { - glTexImage2D(target, l - _minMip, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format, texelFormat.type, NULL); - (void)CHECK_GL_ERROR(); - } - } - //} -} +Size GLESTexture::copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum internalFormat, GLenum format, GLenum type, Size sourceSize, const void* sourcePointer) const { + Size amountCopied = sourceSize; + if (GL_TEXTURE_2D == _target) { + qDebug() << "[UNIMPLEMENTED] GL_TEXTURE_2D internalFormat: " << internalFormat; + /*switch (internalFormat) { + case GL_COMPRESSED_SRGB_S3TC_DXT1_EXT: + case GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT: + case GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT: + case GL_COMPRESSED_RED_RGTC1: + case GL_COMPRESSED_RG_RGTC2: + case GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM: + case GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT: + glCompressedTexSubImage2D(_target, mip, 0, yOffset, size.x, size.y, internalFormat, + static_cast(sourceSize), sourcePointer); + break; + default:*/ + glTexSubImage2D(_target, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); + //break; + //} + } else if (GL_TEXTURE_CUBE_MAP == _target) { + auto target = GLTexture::CUBE_FACE_LAYOUT[face]; + qDebug() << "[UNIMPLEMENTED] GL_TEXTURE_CUBE_MAP internalFormat: " << internalFormat; -void GLESTexture::updateSize() const { - setSize(_virtualSize); - if (!_id) { - return; + /*switch (internalFormat) { + case GL_COMPRESSED_SRGB_S3TC_DXT1_EXT: + case GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT: + case GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT: + case GL_COMPRESSED_RED_RGTC1: + case GL_COMPRESSED_RG_RGTC2: + case GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM: + case GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT: + glCompressedTexSubImage2D(target, mip, 0, yOffset, size.x, size.y, internalFormat, + static_cast(sourceSize), sourcePointer); + break; + default:*/ + glTexSubImage2D(target, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); + // break; + //} + } else { + // TODO: implement for android + assert(false); + amountCopied = 0; } - - if (_gpuObject.getTexelFormat().isCompressed()) { - GLenum proxyType = GL_TEXTURE_2D; - GLuint numFaces = 1; - if (_gpuObject.getType() == gpu::Texture::TEX_CUBE) { - proxyType = CUBE_FACE_LAYOUT[0]; - numFaces = (GLuint)CUBE_NUM_FACES; - } - GLint gpuSize{ 0 }; - glGetTexLevelParameteriv(proxyType, 0, GL_TEXTURE_COMPRESSED, &gpuSize); - (void)CHECK_GL_ERROR(); - - if (gpuSize) { - for (GLuint level = _minMip; level < _maxMip; level++) { - GLint levelSize{ 0 }; - //glGetTexLevelParameteriv(proxyType, level, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &levelSize); - //qDebug() << "TODO: GLBackendTexture.cpp:updateSize GL_TEXTURE_COMPRESSED_IMAGE_SIZE"; - levelSize *= numFaces; - - if (levelSize <= 0) { - break; - } - gpuSize += levelSize; - } - (void)CHECK_GL_ERROR(); - setSize(gpuSize); - return; - } - } -} - -// Move content bits from the CPU to the GPU for a given mip / face -void GLESTexture::transferMip(uint16_t mipLevel, uint8_t face) const { - auto mip = _gpuObject.accessStoredMipFace(mipLevel, face); - GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), _gpuObject.getStoredMipFormat()); - //GLenum target = getFaceTargets()[face]; - GLenum target = _target == GL_TEXTURE_2D ? GL_TEXTURE_2D : CUBE_FACE_LAYOUT[face]; - auto size = _gpuObject.evalMipDimensions(mipLevel); - glTexSubImage2D(target, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); (void)CHECK_GL_ERROR(); + return amountCopied; } -void GLESTexture::startTransfer() { - PROFILE_RANGE(render_gpu_gl, __FUNCTION__); - Parent::startTransfer(); - - glBindTexture(_target, _id); - (void)CHECK_GL_ERROR(); - - // transfer pixels from each faces - uint8_t numFaces = (Texture::TEX_CUBE == _gpuObject.getType()) ? CUBE_NUM_FACES : 1; - for (uint8_t f = 0; f < numFaces; f++) { - for (uint16_t i = 0; i < Sampler::MAX_MIP_LEVEL; ++i) { - if (_gpuObject.isStoredMipFaceAvailable(i, f)) { - transferMip(i, f); - } - } - } -} - -void GLESBackend::GLESTexture::syncSampler() const { +void GLESTexture::syncSampler() const { const Sampler& sampler = _gpuObject.getSampler(); + const auto& fm = FILTER_MODES[sampler.getFilter()]; glTexParameteri(_target, GL_TEXTURE_MIN_FILTER, fm.minFilter); glTexParameteri(_target, GL_TEXTURE_MAG_FILTER, fm.magFilter); if (sampler.doComparison()) { - glTexParameteri(_target, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE); + glTexParameteri(_target, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE); // GL_COMPARE_R_TO_TEXTURE glTexParameteri(_target, GL_TEXTURE_COMPARE_FUNC, COMPARISON_TO_GL[sampler.getComparisonFunction()]); } else { glTexParameteri(_target, GL_TEXTURE_COMPARE_MODE, GL_NONE); @@ -161,16 +170,453 @@ void GLESBackend::GLESTexture::syncSampler() const { glTexParameteri(_target, GL_TEXTURE_WRAP_R, WRAP_MODES[sampler.getWrapModeW()]); glTexParameterfv(_target, GL_TEXTURE_BORDER_COLOR_EXT, (const float*)&sampler.getBorderColor()); - - glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, (uint16)sampler.getMipOffset()); + glTexParameterf(_target, GL_TEXTURE_MIN_LOD, (float)sampler.getMinMip()); glTexParameterf(_target, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); + (void)CHECK_GL_ERROR(); - //qDebug() << "[GPU-GL-GLBackend] syncSampler 12 " << _target << "," << sampler.getMaxAnisotropy(); //glTexParameterf(_target, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); - //(void)CHECK_GL_ERROR(); - //qDebug() << "[GPU-GL-GLBackend] syncSampler end"; +} + +using GLESFixedAllocationTexture = GLESBackend::GLESFixedAllocationTexture; + +GLESFixedAllocationTexture::GLESFixedAllocationTexture(const std::weak_ptr& backend, const Texture& texture) : GLESTexture(backend, texture), _size(texture.evalTotalSize()) { + withPreservedTexture([&] { + allocateStorage(); + syncSampler(); + }); +} + +GLESFixedAllocationTexture::~GLESFixedAllocationTexture() { +} + +void GLESFixedAllocationTexture::allocateStorage() const { + const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); + const auto numMips = _gpuObject.getNumMips(); + + // glTextureStorage2D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y); + for (GLint level = 0; level < numMips; level++) { + Vec3u dimensions = _gpuObject.evalMipDimensions(level); + for (GLenum target : getFaceTargets(_target)) { + glTexImage2D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format, texelFormat.type, nullptr); + } + } + + glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, 0); + glTexParameteri(_target, GL_TEXTURE_MAX_LEVEL, numMips - 1); + (void)CHECK_GL_ERROR(); +} + +void GLESFixedAllocationTexture::syncSampler() const { + Parent::syncSampler(); + const Sampler& sampler = _gpuObject.getSampler(); + auto baseMip = std::max(sampler.getMipOffset(), sampler.getMinMip()); + + glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, baseMip); + glTexParameterf(_target, GL_TEXTURE_MIN_LOD, (float)sampler.getMinMip()); + glTexParameterf(_target, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.0f : sampler.getMaxMip())); +} + +// Renderbuffer attachment textures +using GLESAttachmentTexture = GLESBackend::GLESAttachmentTexture; + +GLESAttachmentTexture::GLESAttachmentTexture(const std::weak_ptr& backend, const Texture& texture) : GLESFixedAllocationTexture(backend, texture) { + Backend::textureFramebufferCount.increment(); + Backend::textureFramebufferGPUMemSize.update(0, size()); +} + +GLESAttachmentTexture::~GLESAttachmentTexture() { + Backend::textureFramebufferCount.decrement(); + Backend::textureFramebufferGPUMemSize.update(size(), 0); +} + +// Strict resource textures +using GLESStrictResourceTexture = GLESBackend::GLESStrictResourceTexture; + +GLESStrictResourceTexture::GLESStrictResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GLESFixedAllocationTexture(backend, texture) { + Backend::textureResidentCount.increment(); + Backend::textureResidentGPUMemSize.update(0, size()); + + withPreservedTexture([&] { + + auto mipLevels = _gpuObject.getNumMips(); + for (uint16_t sourceMip = 0; sourceMip < mipLevels; sourceMip++) { + uint16_t targetMip = sourceMip; + size_t maxFace = GLTexture::getFaceCount(_target); + for (uint8_t face = 0; face < maxFace; face++) { + copyMipFaceFromTexture(sourceMip, targetMip, face); + } + } + }); + + if (texture.isAutogenerateMips()) { + generateMips(); + } +} + +GLESStrictResourceTexture::~GLESStrictResourceTexture() { + Backend::textureResidentCount.decrement(); + Backend::textureResidentGPUMemSize.update(size(), 0); +} + +using GLESVariableAllocationTexture = GLESBackend::GLESVariableAllocationTexture; + +GLESVariableAllocationTexture::GLESVariableAllocationTexture(const std::weak_ptr& backend, const Texture& texture) : + GLESTexture(backend, texture) +{ + Backend::textureResourceCount.increment(); + + auto mipLevels = texture.getNumMips(); + _allocatedMip = mipLevels; + _maxAllocatedMip = _populatedMip = mipLevels; + _minAllocatedMip = texture.minAvailableMipLevel(); + + uvec3 mipDimensions; + for (uint16_t mip = _minAllocatedMip; mip < mipLevels; ++mip) { + if (glm::all(glm::lessThanEqual(texture.evalMipDimensions(mip), INITIAL_MIP_TRANSFER_DIMENSIONS))) { + _maxAllocatedMip = _populatedMip = mip; + break; + } + } + + auto targetMip = _populatedMip - std::min(_populatedMip, 2); + uint16_t allocatedMip = std::max(_minAllocatedMip, targetMip); + + allocateStorage(allocatedMip); + _memoryPressureStateStale = true; + copyMipsFromTexture(); + + syncSampler(); +} + +GLESVariableAllocationTexture::~GLESVariableAllocationTexture() { + Backend::textureResourceCount.decrement(); + Backend::textureResourceGPUMemSize.update(_size, 0); + Backend::textureResourcePopulatedGPUMemSize.update(_populatedSize, 0); +} + +void GLESVariableAllocationTexture::allocateStorage(uint16 allocatedMip) { + _allocatedMip = allocatedMip; + + const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); + const auto dimensions = _gpuObject.evalMipDimensions(_allocatedMip); + const auto totalMips = _gpuObject.getNumMips(); + const auto mips = totalMips - _allocatedMip; + withPreservedTexture([&] { + // FIXME technically GL 4.2, but OSX includes the ARB_texture_storage extension + glTexStorage2D(_target, mips, texelFormat.internalFormat, dimensions.x, dimensions.y); CHECK_GL_ERROR(); + }); + auto mipLevels = _gpuObject.getNumMips(); + _size = 0; + for (uint16_t mip = _allocatedMip; mip < mipLevels; ++mip) { + _size += _gpuObject.evalMipSize(mip); + } + Backend::textureResourceGPUMemSize.update(0, _size); } +Size GLESVariableAllocationTexture::copyMipsFromTexture() { + auto mipLevels = _gpuObject.getNumMips(); + size_t maxFace = GLTexture::getFaceCount(_target); + Size amount = 0; + for (uint16_t sourceMip = _populatedMip; sourceMip < mipLevels; ++sourceMip) { + uint16_t targetMip = sourceMip - _allocatedMip; + for (uint8_t face = 0; face < maxFace; ++face) { + amount += copyMipFaceFromTexture(sourceMip, targetMip, face); + } + } + + + return amount; +} + +Size GLESVariableAllocationTexture::copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum internalFormat, GLenum format, GLenum type, Size sourceSize, const void* sourcePointer) const { + Size amountCopied = 0; + withPreservedTexture([&] { + amountCopied = Parent::copyMipFaceLinesFromTexture(mip, face, size, yOffset, internalFormat, format, type, sourceSize, sourcePointer); + }); + incrementPopulatedSize(amountCopied); + return amountCopied; +} + +void GLESVariableAllocationTexture::syncSampler() const { + withPreservedTexture([&] { + Parent::syncSampler(); + glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, _populatedMip - _allocatedMip); + }); +} + + +void copyUncompressedTexGPUMem(const gpu::Texture& texture, GLenum texTarget, GLuint srcId, GLuint destId, uint16_t numMips, uint16_t srcMipOffset, uint16_t destMipOffset, uint16_t populatedMips) { + // DestID must be bound to the GLESBackend::RESOURCE_TRANSFER_TEX_UNIT + + GLuint fbo { 0 }; + glGenFramebuffers(1, &fbo); + glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); + + uint16_t mips = numMips; + // copy pre-existing mips + for (uint16_t mip = populatedMips; mip < mips; ++mip) { + auto mipDimensions = texture.evalMipDimensions(mip); + uint16_t targetMip = mip - destMipOffset; + uint16_t sourceMip = mip - srcMipOffset; + for (GLenum target : GLTexture::getFaceTargets(texTarget)) { + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, target, srcId, sourceMip); + (void)CHECK_GL_ERROR(); + glCopyTexSubImage2D(target, targetMip, 0, 0, 0, 0, mipDimensions.x, mipDimensions.y); + (void)CHECK_GL_ERROR(); + } + } + + // destroy the transfer framebuffer + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &fbo); +} + +void copyCompressedTexGPUMem(const gpu::Texture& texture, GLenum texTarget, GLuint srcId, GLuint destId, uint16_t numMips, uint16_t srcMipOffset, uint16_t destMipOffset, uint16_t populatedMips) { + // DestID must be bound to the GLESBackend::RESOURCE_TRANSFER_TEX_UNIT + + struct MipDesc { + GLint _faceSize; + GLint _size; + GLint _offset; + GLint _width; + GLint _height; + }; + std::vector sourceMips(numMips); + + std::vector bytes; + + glActiveTexture(GL_TEXTURE0 + GLESBackend::RESOURCE_TRANSFER_EXTRA_TEX_UNIT); + glBindTexture(texTarget, srcId); + const auto& faceTargets = GLTexture::getFaceTargets(texTarget); + GLint internalFormat { 0 }; + + // Collect the mip description from the source texture + GLint bufferOffset { 0 }; + for (uint16_t mip = populatedMips; mip < numMips; ++mip) { + auto& sourceMip = sourceMips[mip]; + + uint16_t sourceLevel = mip - srcMipOffset; + + // Grab internal format once + if (internalFormat == 0) { + glGetTexLevelParameteriv(faceTargets[0], sourceLevel, GL_TEXTURE_INTERNAL_FORMAT, &internalFormat); + } + + // Collect the size of the first face, and then compute the total size offset needed for this mip level + auto mipDimensions = texture.evalMipDimensions(mip); + sourceMip._width = mipDimensions.x; + sourceMip._height = mipDimensions.y; +#ifdef DEBUG_COPY + glGetTexLevelParameteriv(faceTargets.front(), sourceLevel, GL_TEXTURE_WIDTH, &sourceMip._width); + glGetTexLevelParameteriv(faceTargets.front(), sourceLevel, GL_TEXTURE_HEIGHT, &sourceMip._height); +#endif + // TODO: retrieve the size of a compressed image + assert(false); + //glGetTexLevelParameteriv(faceTargets.front(), sourceLevel, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &sourceMip._faceSize); + sourceMip._size = (GLint)faceTargets.size() * sourceMip._faceSize; + sourceMip._offset = bufferOffset; + bufferOffset += sourceMip._size; + gpu::gl::checkGLError(); + } + (void)CHECK_GL_ERROR(); + + // Allocate the PBO to accomodate for all the mips to copy + GLuint pbo { 0 }; + glGenBuffers(1, &pbo); + glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo); + glBufferData(GL_PIXEL_PACK_BUFFER, bufferOffset, nullptr, GL_STATIC_COPY); + (void)CHECK_GL_ERROR(); + + // Transfer from source texture to pbo + for (uint16_t mip = populatedMips; mip < numMips; ++mip) { + auto& sourceMip = sourceMips[mip]; + + uint16_t sourceLevel = mip - srcMipOffset; + + for (GLint f = 0; f < (GLint)faceTargets.size(); f++) { + // TODO: implement for android + //glGetCompressedTexImage(faceTargets[f], sourceLevel, BUFFER_OFFSET(sourceMip._offset + f * sourceMip._faceSize)); + } + (void)CHECK_GL_ERROR(); + } + + // Now populate the new texture from the pbo + glBindTexture(texTarget, 0); + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo); + + glActiveTexture(GL_TEXTURE0 + GLESBackend::RESOURCE_TRANSFER_TEX_UNIT); + + // Transfer from pbo to new texture + for (uint16_t mip = populatedMips; mip < numMips; ++mip) { + auto& sourceMip = sourceMips[mip]; + + uint16_t destLevel = mip - destMipOffset; + + for (GLint f = 0; f < (GLint)faceTargets.size(); f++) { +#ifdef DEBUG_COPY + GLint destWidth, destHeight, destSize; + glGetTexLevelParameteriv(faceTargets.front(), destLevel, GL_TEXTURE_WIDTH, &destWidth); + glGetTexLevelParameteriv(faceTargets.front(), destLevel, GL_TEXTURE_HEIGHT, &destHeight); + glGetTexLevelParameteriv(faceTargets.front(), destLevel, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &destSize); +#endif + glCompressedTexSubImage2D(faceTargets[f], destLevel, 0, 0, sourceMip._width, sourceMip._height, internalFormat, + sourceMip._faceSize, BUFFER_OFFSET(sourceMip._offset + f * sourceMip._faceSize)); + gpu::gl::checkGLError(); + } + } + + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); + glDeleteBuffers(1, &pbo); +} + +void GLESVariableAllocationTexture::copyTextureMipsInGPUMem(GLuint srcId, GLuint destId, uint16_t srcMipOffset, uint16_t destMipOffset, uint16_t populatedMips) { + uint16_t numMips = _gpuObject.getNumMips(); + withPreservedTexture([&] { + qDebug() << "[TEXTURE] is compressed: " << _texelFormat.isCompressed(); + if (_texelFormat.isCompressed()) { + copyCompressedTexGPUMem(_gpuObject, _target, srcId, destId, numMips, srcMipOffset, destMipOffset, populatedMips); + } else { + copyUncompressedTexGPUMem(_gpuObject, _target, srcId, destId, numMips, srcMipOffset, destMipOffset, populatedMips); + } + }); +} + +void GLESVariableAllocationTexture::promote() { + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + Q_ASSERT(_allocatedMip > 0); + + uint16_t targetAllocatedMip = _allocatedMip - std::min(_allocatedMip, 2); + targetAllocatedMip = std::max(_minAllocatedMip, targetAllocatedMip); + + GLuint oldId = _id; + auto oldSize = _size; + uint16_t oldAllocatedMip = _allocatedMip; + + // create new texture + const_cast(_id) = allocate(_gpuObject); + + // allocate storage for new level + allocateStorage(targetAllocatedMip); + + // copy pre-existing mips + copyTextureMipsInGPUMem(oldId, _id, oldAllocatedMip, _allocatedMip, _populatedMip); + + // destroy the old texture + glDeleteTextures(1, &oldId); + + // Update sampler + syncSampler(); + + // update the memory usage + Backend::textureResourceGPUMemSize.update(oldSize, 0); + // no change to Backend::textureResourcePopulatedGPUMemSize + + populateTransferQueue(); +} + +void GLESVariableAllocationTexture::demote() { + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + Q_ASSERT(_allocatedMip < _maxAllocatedMip); + auto oldId = _id; + auto oldSize = _size; + auto oldPopulatedMip = _populatedMip; + + // allocate new texture + const_cast(_id) = allocate(_gpuObject); + uint16_t oldAllocatedMip = _allocatedMip; + allocateStorage(_allocatedMip + 1); + _populatedMip = std::max(_populatedMip, _allocatedMip); + + + // copy pre-existing mips + copyTextureMipsInGPUMem(oldId, _id, oldAllocatedMip, _allocatedMip, _populatedMip); + + // destroy the old texture + glDeleteTextures(1, &oldId); + + // Update sampler + syncSampler(); + + // update the memory usage + Backend::textureResourceGPUMemSize.update(oldSize, 0); + // Demoting unpopulate the memory delta + if (oldPopulatedMip != _populatedMip) { + auto numPopulatedDemoted = _populatedMip - oldPopulatedMip; + Size amountUnpopulated = 0; + for (int i = 0; i < numPopulatedDemoted; i++) { + amountUnpopulated += _gpuObject.evalMipSize(oldPopulatedMip + i); + } + decrementPopulatedSize(amountUnpopulated); + } + populateTransferQueue(); +} + + +void GLESVariableAllocationTexture::populateTransferQueue() { + PROFILE_RANGE(render_gpu_gl, __FUNCTION__); + if (_populatedMip <= _allocatedMip) { + return; + } + _pendingTransfers = TransferQueue(); + + const uint8_t maxFace = GLTexture::getFaceCount(_target); + uint16_t sourceMip = _populatedMip; + do { + --sourceMip; + auto targetMip = sourceMip - _allocatedMip; + auto mipDimensions = _gpuObject.evalMipDimensions(sourceMip); + for (uint8_t face = 0; face < maxFace; ++face) { + if (!_gpuObject.isStoredMipFaceAvailable(sourceMip, face)) { + continue; + } + + // If the mip is less than the max transfer size, then just do it in one transfer + if (glm::all(glm::lessThanEqual(mipDimensions, MAX_TRANSFER_DIMENSIONS))) { + // Can the mip be transferred in one go + _pendingTransfers.emplace(new TransferJob(*this, sourceMip, targetMip, face)); + continue; + } + + // break down the transfers into chunks so that no single transfer is + // consuming more than X bandwidth + // For compressed format, regions must be a multiple of the 4x4 tiles, so enforce 4 lines as the minimum block + auto mipSize = _gpuObject.getStoredMipFaceSize(sourceMip, face); + const auto lines = mipDimensions.y; + const uint32_t BLOCK_NUM_LINES { 4 }; + const auto numBlocks = (lines + (BLOCK_NUM_LINES - 1)) / BLOCK_NUM_LINES; + auto bytesPerBlock = mipSize / numBlocks; + Q_ASSERT(0 == (mipSize % lines)); + uint32_t linesPerTransfer = BLOCK_NUM_LINES * (uint32_t)(MAX_TRANSFER_SIZE / bytesPerBlock); + uint32_t lineOffset = 0; + while (lineOffset < lines) { + uint32_t linesToCopy = std::min(lines - lineOffset, linesPerTransfer); + _pendingTransfers.emplace(new TransferJob(*this, sourceMip, targetMip, face, linesToCopy, lineOffset)); + lineOffset += linesToCopy; + } + } + + // queue up the sampler and populated mip change for after the transfer has completed + _pendingTransfers.emplace(new TransferJob(*this, [=] { + _populatedMip = sourceMip; + syncSampler(); + })); + } while (sourceMip != _allocatedMip); +} + +// resource textures +using GLESResourceTexture = GLESBackend::GLESResourceTexture; + +GLESResourceTexture::GLESResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GLESVariableAllocationTexture(backend, texture) { + if (texture.isAutogenerateMips()) { + generateMips(); + } +} + +GLESResourceTexture::~GLESResourceTexture() { +} + + diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackendTransform.cpp b/libraries/gpu-gles/src/gpu/gles/GLESBackendTransform.cpp index 5050db6edd..7d33ca822d 100644 --- a/libraries/gpu-gles/src/gpu/gles/GLESBackendTransform.cpp +++ b/libraries/gpu-gles/src/gpu/gles/GLESBackendTransform.cpp @@ -38,9 +38,9 @@ void GLESBackend::transferTransformState(const Batch& batch) const { } if (!batch._objects.empty()) { - glBindBuffer(GL_SHADER_STORAGE_BUFFER, _transform._objectBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, batch._objects.size() * sizeof(Batch::TransformObject), batch._objects.data(), GL_STREAM_DRAW); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + glBindBuffer(GL_TEXTURE_BUFFER, _transform._objectBuffer); + glBufferData(GL_TEXTURE_BUFFER, batch._objects.size() * sizeof(Batch::TransformObject), batch._objects.data(), GL_DYNAMIC_DRAW); + glBindBuffer(GL_TEXTURE_BUFFER, 0); } if (!batch._namedData.empty()) { @@ -58,10 +58,44 @@ void GLESBackend::transferTransformState(const Batch& batch) const { glBindBuffer(GL_ARRAY_BUFFER, 0); } - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, TRANSFORM_OBJECT_SLOT, _transform._objectBuffer); + glActiveTexture(GL_TEXTURE0 + GLESBackend::TRANSFORM_OBJECT_SLOT); + glBindTexture(GL_TEXTURE_BUFFER, _transform._objectBufferTexture); + if (!batch._objects.empty()) { + glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, _transform._objectBuffer); + } CHECK_GL_ERROR(); // Make sure the current Camera offset is unknown before render Draw _transform._currentCameraOffset = INVALID_OFFSET; } + + +void GLESBackend::updateTransform(const Batch& batch) { + _transform.update(_commandIndex, _stereo); + + auto& drawCallInfoBuffer = batch.getDrawCallInfoBuffer(); + if (batch._currentNamedCall.empty()) { + auto& drawCallInfo = drawCallInfoBuffer[_currentDraw]; + if (_transform._enabledDrawcallInfoBuffer) { + glDisableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); // Make sure attrib array is disabled + _transform._enabledDrawcallInfoBuffer = false; + } + glVertexAttribI4i(gpu::Stream::DRAW_CALL_INFO, drawCallInfo.index, drawCallInfo.unused, 0, 0); + //glVertexAttribI2i(gpu::Stream::DRAW_CALL_INFO, drawCallInfo.index, drawCallInfo.unused); + } else { + if (!_transform._enabledDrawcallInfoBuffer) { + glEnableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); // Make sure attrib array is enabled +#ifdef GPU_STEREO_DRAWCALL_INSTANCED + glVertexAttribDivisor(gpu::Stream::DRAW_CALL_INFO, (isStereo() ? 2 : 1)); +#else + glVertexAttribDivisor(gpu::Stream::DRAW_CALL_INFO, 1); +#endif + _transform._enabledDrawcallInfoBuffer = true; + } + glBindBuffer(GL_ARRAY_BUFFER, _transform._drawCallInfoBuffer); + glVertexAttribIPointer(gpu::Stream::DRAW_CALL_INFO, 2, GL_UNSIGNED_SHORT, 0, _transform._drawCallInfoOffsets[batch._currentNamedCall]); + } + + (void)CHECK_GL_ERROR(); +} \ No newline at end of file From e41833987650434326c1938cae808406fa3ba2b8 Mon Sep 17 00:00:00 2001 From: Gabriel Calero Date: Fri, 29 Dec 2017 17:53:27 -0300 Subject: [PATCH 12/17] Fix uncompressed textures for android --- libraries/gpu-gles/src/gpu/gles/GLESBackendTexture.cpp | 1 - libraries/image/src/image/Image.cpp | 7 +++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackendTexture.cpp b/libraries/gpu-gles/src/gpu/gles/GLESBackendTexture.cpp index 97d38bacce..37134e5467 100644 --- a/libraries/gpu-gles/src/gpu/gles/GLESBackendTexture.cpp +++ b/libraries/gpu-gles/src/gpu/gles/GLESBackendTexture.cpp @@ -476,7 +476,6 @@ void copyCompressedTexGPUMem(const gpu::Texture& texture, GLenum texTarget, GLui void GLESVariableAllocationTexture::copyTextureMipsInGPUMem(GLuint srcId, GLuint destId, uint16_t srcMipOffset, uint16_t destMipOffset, uint16_t populatedMips) { uint16_t numMips = _gpuObject.getNumMips(); withPreservedTexture([&] { - qDebug() << "[TEXTURE] is compressed: " << _texelFormat.isCompressed(); if (_texelFormat.isCompressed()) { copyCompressedTexGPUMem(_gpuObject, _target, srcId, destId, numMips, srcMipOffset, destMipOffset, populatedMips); } else { diff --git a/libraries/image/src/image/Image.cpp b/libraries/image/src/image/Image.cpp index f78ed1a583..c42f56d329 100644 --- a/libraries/image/src/image/Image.cpp +++ b/libraries/image/src/image/Image.cpp @@ -689,6 +689,7 @@ void generateMips(gpu::Texture* texture, QImage&& image, const std::atomic } #else texture->setAutoGenerateMips(true); + texture->assignStoredMip(0, image.byteCount(), image.constBits()); #endif } @@ -727,9 +728,15 @@ gpu::TexturePointer TextureUsage::process2DTextureColorFromImage(QImage&& srcIma bool validAlpha = image.hasAlphaChannel(); bool alphaAsMask = false; +#ifndef ANDROID if (image.format() != QImage::Format_ARGB32) { image = image.convertToFormat(QImage::Format_ARGB32); } +#else + if (image.format() != QImage::Format_RGBA8888) { + image = image.convertToFormat(QImage::Format_RGBA8888); + } +#endif if (validAlpha) { processTextureAlpha(image, validAlpha, alphaAsMask); From 3d86c24b34a8c4e246fc0245068e7fb9d07916be Mon Sep 17 00:00:00 2001 From: Gabriel Calero Date: Wed, 3 Jan 2018 12:44:51 -0300 Subject: [PATCH 13/17] Fix uncompressed textures. Generate CPU mipmaps for android --- libraries/image/src/image/Image.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/libraries/image/src/image/Image.cpp b/libraries/image/src/image/Image.cpp index c42f56d329..172903a65b 100644 --- a/libraries/image/src/image/Image.cpp +++ b/libraries/image/src/image/Image.cpp @@ -680,6 +680,7 @@ void generateLDRMips(gpu::Texture* texture, QImage&& image, const std::atomic& abortProcessing = false, int face = -1) { #if CPU_MIPMAPS +#if !defined(Q_OS_ANDROID) PROFILE_RANGE(resource_parse, "generateMips"); if (image.format() == QIMAGE_HDR_FORMAT) { @@ -687,9 +688,18 @@ void generateMips(gpu::Texture* texture, QImage&& image, const std::atomic } else { generateLDRMips(texture, std::move(image), abortProcessing, face); } + +#else + //texture->setAutoGenerateMips(false); + texture->assignStoredMip(0, image.byteCount(), image.constBits()); + for (uint16 level = 1; level < texture->getNumMips(); ++level) { + QSize mipSize(texture->evalMipWidth(level), texture->evalMipHeight(level)); + QImage mipImage = image.scaled(mipSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + texture->assignStoredMip(level, mipImage.byteCount(), mipImage.constBits()); + } +#endif #else texture->setAutoGenerateMips(true); - texture->assignStoredMip(0, image.byteCount(), image.constBits()); #endif } @@ -776,6 +786,7 @@ gpu::TexturePointer TextureUsage::process2DTextureColorFromImage(QImage&& srcIma } theTexture->setUsage(usage.build()); theTexture->setStoredMipFormat(formatMip); + theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); generateMips(theTexture.get(), std::move(image), abortProcessing); } @@ -882,6 +893,7 @@ gpu::TexturePointer TextureUsage::process2DTextureNormalMapFromImage(QImage&& sr theTexture = gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR)); theTexture->setSource(srcImageName); theTexture->setStoredMipFormat(formatMip); + theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); generateMips(theTexture.get(), std::move(image), abortProcessing); } @@ -918,6 +930,7 @@ gpu::TexturePointer TextureUsage::process2DTextureGrayscaleFromImage(QImage&& sr theTexture = gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR)); theTexture->setSource(srcImageName); theTexture->setStoredMipFormat(formatMip); + theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); generateMips(theTexture.get(), std::move(image), abortProcessing); } From 32494a8e2425beec8c7fde400da91780c5249f41 Mon Sep 17 00:00:00 2001 From: Gabriel Calero Date: Wed, 3 Jan 2018 22:13:31 -0300 Subject: [PATCH 14/17] Set QMLEngine base path for android --- interface/CMakeLists.txt | 2 +- libraries/shared/src/PathUtils.cpp | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index cba8ce5479..23358092ca 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -12,7 +12,7 @@ function(JOIN VALUES GLUE OUTPUT) endfunction() -if (NOT DEV_BUILD) +if ANDROID OR (NOT DEV_BUILD) set(INTERFACE_QML_QRC ${CMAKE_CURRENT_BINARY_DIR}/qml.qrc) generate_qrc(OUTPUT ${INTERFACE_QML_QRC} PATH ${CMAKE_CURRENT_SOURCE_DIR}/resources GLOBS *.qml *.qss *.js *.html *.ttf *.gif *.svg *.png *.jpg) endif() diff --git a/libraries/shared/src/PathUtils.cpp b/libraries/shared/src/PathUtils.cpp index c6ff73492c..cf33cbed2b 100644 --- a/libraries/shared/src/PathUtils.cpp +++ b/libraries/shared/src/PathUtils.cpp @@ -53,7 +53,9 @@ const QString& PathUtils::projectRootPath() { #endif const QString& PathUtils::qmlBasePath() { -#ifdef DEV_BUILD +#ifdef Q_OS_ANDROID + static const QString staticResourcePath = QUrl::fromLocalFile(PathUtils::resourcesPath() + "qml/").toString(); +#elif defined (DEV_BUILD) static const QString staticResourcePath = QUrl::fromLocalFile(projectRootPath() + "/interface/resources/qml/").toString(); #else static const QString staticResourcePath = "qrc:///qml/"; From 8cac98545a17f6c1a813d34063a3df9326c62f10 Mon Sep 17 00:00:00 2001 From: Gabriel Calero Date: Thu, 4 Jan 2018 17:36:10 -0300 Subject: [PATCH 15/17] Fixes for Android --- android/app/src/main/AndroidManifest.xml | 2 -- android/build.gradle | 11 ------- interface/CMakeLists.txt | 6 ++-- interface/src/Application.cpp | 38 ++++++++++++++++++++---- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 3c7f59703e..efb7abbb93 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -63,8 +63,6 @@ - - diff --git a/android/build.gradle b/android/build.gradle index c53eac1ed0..d2e3a3a183 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -457,9 +457,7 @@ task cleanDependencies(type: Delete) { task generateAssetsFileList() { doLast { def assetsPath = "${appDir}/src/main/assets/" - //def assetsPath = "/Users/cduarte/dev/workspace-hifi/hifiparallel/build_android_hifiqt59/interface/apk/assets/" def addedByAndroidDeployQtName = "--Added-by-androiddeployqt--/" - //def addedByAndroidDeployQtName = "--Added-by-androiddeployqt--/" def addedByAndroidDeployQtPath = assetsPath + addedByAndroidDeployQtName @@ -468,7 +466,6 @@ task generateAssetsFileList() { throw new GradleScriptException("Failed to create directory " + addedByAndroidDeployQtPath, null); } def outputFilename = "/qt_cache_pregenerated_file_list" - //def outputFilename = "/qt_cache_pregenerated_file_list2" def outputFile = new File(addedByAndroidDeployQtPath + outputFilename); Map> directoryContents = new TreeMap<>(); @@ -483,10 +480,6 @@ task generateAssetsFileList() { fileName += "/" } - /*println ("full: [" + file.getAbsolutePath() + "]\n\t" - + "path: [" + pathName + "]\n\t" - + "name: [" + fileName + "]\n\t");*/ - if (!directoryContents.containsKey(pathName)) { directoryContents[pathName] = new ArrayList() } @@ -497,16 +490,12 @@ task generateAssetsFileList() { DataOutputStream fos = new DataOutputStream(new FileOutputStream(outputFile)); for (Map.Entry> e: directoryContents.entrySet()) { def entryList = e.getValue() - //stream << it.key() << entryList.size(); fos.writeInt(e.key.length()*2); // 2 bytes per char fos.writeChars(e.key); fos.writeInt(entryList.size()); - //println ("dir: " + e.key + " size: " + entryList.size()); for (String entry: entryList) { fos.writeInt(entry.length()*2); fos.writeChars(entry); - //println("\tentry: " + entry); - //stream << entry; } } fos.close(); diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 23358092ca..08d5778f18 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -12,7 +12,7 @@ function(JOIN VALUES GLUE OUTPUT) endfunction() -if ANDROID OR (NOT DEV_BUILD) +if (ANDROID OR NOT DEV_BUILD) set(INTERFACE_QML_QRC ${CMAKE_CURRENT_BINARY_DIR}/qml.qrc) generate_qrc(OUTPUT ${INTERFACE_QML_QRC} PATH ${CMAKE_CURRENT_SOURCE_DIR}/resources GLOBS *.qml *.qss *.js *.html *.ttf *.gif *.svg *.png *.jpg) endif() @@ -82,7 +82,7 @@ qt5_wrap_ui(QT_UI_HEADERS "${QT_UI_FILES}") # add them to the interface source files set(INTERFACE_SRCS ${INTERFACE_SRCS} "${QT_UI_HEADERS}" "${QT_RESOURCES}") -if (NOT DEV_BUILD) +if (ANDROID OR NOT DEV_BUILD) list(APPEND INTERFACE_SRCS ${INTERFACE_QML_QRC}) endif() @@ -267,6 +267,8 @@ include_directories("${PROJECT_SOURCE_DIR}/src") if (ANDROID) #set(ANDROID_PLATFORM_QT_LIBRARIES Qt5::WebView) + find_library(ANDROID_LOG_LIB log) + target_link_libraries(${TARGET_NAME} ${ANDROID_LOG_LIB}) else() set(NON_ANDROID_PLATFORM_QT_LIBRARIES Qt5::WebEngine) endif () diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e5ad9792f8..65005d0605 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -212,6 +212,7 @@ #include "webbrowser/WebBrowserSuggestionsEngine.h" #ifdef ANDROID #include +#include #endif // On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU // FIXME seems to be broken. @@ -556,7 +557,34 @@ void messageHandler(QtMsgType type, const QMessageLogContext& context, const QSt if (!logMessage.isEmpty()) { #ifdef Q_OS_WIN OutputDebugStringA(logMessage.toLocal8Bit().constData()); - OutputDebugStringA("\n"); + OutputDebugStringA("\n");i +#elif defined Q_OS_ANDROID + QString report=message; + if (context.file && !QString(context.file).isEmpty()) { + report+=" at "; + report+=QString(context.file); + report+=" : "; + report+=QString::number(context.line); + } + const char*const local=report.toLocal8Bit().constData(); + switch (type) { + case QtDebugMsg: + __android_log_write(ANDROID_LOG_DEBUG,"Interface",local); + break; + case QtInfoMsg: + __android_log_write(ANDROID_LOG_INFO,"Interface",local); + break; + case QtWarningMsg: + __android_log_write(ANDROID_LOG_WARN,"Interface",local); + break; + case QtCriticalMsg: + __android_log_write(ANDROID_LOG_ERROR,"Interface",local); + break; + case QtFatalMsg: + default: + __android_log_write(ANDROID_LOG_FATAL,"Interface",local); + abort(); + } #endif qApp->getLogger()->addMessage(qPrintable(logMessage + "\n")); } @@ -845,10 +873,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo #endif _logger = new FileLogger(this); -#ifndef Q_OS_ANDROID - // this prevents using logcat qInstallMessageHandler(messageHandler); -#endif QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "styles/Inconsolata.otf"); _window->setWindowTitle("High Fidelity Interface"); @@ -2344,8 +2369,11 @@ void Application::initializeUi() { offscreenUi->setProxyWindow(_window->windowHandle()); // OffscreenUi is a subclass of OffscreenQmlSurface specifically designed to // support the window management and scripting proxies for VR use +#ifndef Q_OS_ANDROID offscreenUi->createDesktop(QString("hifi/Desktop.qml")); - +#else + offscreenUi->createDesktop(QString("qrc:///qml/hifi/Desktop.qml")); +#endif // FIXME either expose so that dialogs can set this themselves or // do better detection in the offscreen UI of what has focus offscreenUi->setNavigationFocused(false); From 6d732dd8ba3c4c25226943b33bf5e051f48b7ba0 Mon Sep 17 00:00:00 2001 From: Gabriel Calero Date: Fri, 5 Jan 2018 19:45:02 -0300 Subject: [PATCH 16/17] Add missing Qt dependencies to gradle script --- android/app/build.gradle | 4 ---- android/build.gradle | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 343074047e..815a15e752 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,9 +1,5 @@ apply plugin: 'com.android.application' -ext.RELEASE_NUMBER = project.hasProperty('RELEASE_NUMBER') ? project.getProperty('RELEASE_NUMBER') : '0' -ext.RELEASE_TYPE = project.hasProperty('RELEASE_TYPE') ? project.getProperty('RELEASE_TYPE') : 'DEV' -ext.BUILD_BRANCH = project.hasProperty('BUILD_BRANCH') ? project.getProperty('BUILD_BRANCH') : '' - android { compileSdkVersion 26 defaultConfig { diff --git a/android/build.gradle b/android/build.gradle index d2e3a3a183..239d98f0bd 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -53,6 +53,8 @@ ext { 'Qt5WebSockets', 'Qt5Widgets', 'Qt5XmlPatterns', + 'Qt5Concurrent', + 'Qt5Svg', // Android specific 'Qt5AndroidExtras', 'Qt5WebView', @@ -502,6 +504,22 @@ task generateAssetsFileList() { } } +task copyInterfaceAssets() { + doLast { + def resourcesDir = new File("${appDir}/../../interface/resources") + def scriptsDir = new File("${appDir}/../../scripts") + def assetsDir = new File(appDir, 'src/main/assets') + copy { + from resourcesDir + into new File(assetsDir, "resources") + } + copy { + from scriptsDir + into new File(assetsDir, "scripts") + } + } +} + /* // FIXME derive the path from the gradle environment def toolchain = [ From 6050737e7c7c3b7ed42e62f948944473f9ecc3ed Mon Sep 17 00:00:00 2001 From: Gabriel Calero Date: Mon, 8 Jan 2018 17:36:08 -0300 Subject: [PATCH 17/17] Resolving comments in PR --- android/app/src/main/cpp/+android/simple.qml | 19 --- android/app/src/main/cpp/main.cpp | 156 ------------------ android/app/src/main/cpp/main.qrc | 6 - android/app/src/main/cpp/simple.qml | 10 -- android/build.gradle | 6 +- interface/CMakeLists.txt | 24 +-- interface/src/Application.cpp | 29 ++-- interface/src/main.cpp | 3 +- .../AssetMappingsScriptingInterface.cpp | 12 +- interface/src/scripting/LimitlessConnection.h | 2 +- interface/src/ui/Stats.cpp | 4 +- .../src/avatars-renderer/Avatar.cpp | 4 +- .../display-plugins/OpenGLDisplayPlugin.cpp | 8 +- libraries/gpu-gles/src/gpu/gl/GLBackend.cpp | 11 -- .../gpu-gles/src/gpu/gl/GLTexelFormat.cpp | 2 +- libraries/gpu/src/gpu/Framebuffer.h | 2 +- libraries/image/src/image/Image.cpp | 2 +- .../script-engine/src/ArrayBufferClass.h | 2 +- .../src/ConsoleScriptingInterface.cpp | 2 +- libraries/shared/src/PathUtils.cpp | 8 +- libraries/shared/src/shared/FileUtils.cpp | 6 +- libraries/ui/src/ui/OffscreenQmlSurface.cpp | 6 +- .../ui/src/ui/TabletScriptingInterface.cpp | 2 +- libraries/ui/src/ui/types/FileTypeProfile.cpp | 6 +- .../HFTabletWebEngineRequestInterceptor.h | 16 +- .../ui/src/ui/types/HFWebEngineProfile.cpp | 7 +- 26 files changed, 58 insertions(+), 297 deletions(-) delete mode 100644 android/app/src/main/cpp/+android/simple.qml delete mode 100644 android/app/src/main/cpp/main.cpp delete mode 100644 android/app/src/main/cpp/main.qrc delete mode 100644 android/app/src/main/cpp/simple.qml diff --git a/android/app/src/main/cpp/+android/simple.qml b/android/app/src/main/cpp/+android/simple.qml deleted file mode 100644 index fc722d7e2c..0000000000 --- a/android/app/src/main/cpp/+android/simple.qml +++ /dev/null @@ -1,19 +0,0 @@ -import QtQuick 2.7 -import QtWebView 1.1 - -Rectangle { - id: window - anchors.fill: parent - color: "red" - ColorAnimation on color { from: "blue"; to: "yellow"; duration: 1000; loops: Animation.Infinite } - - Text { - text: "Hello" - anchors.top: parent.top - } - WebView { - anchors.fill: parent - anchors.margins: 10 - url: "http://doc.qt.io/qt-5/qml-qtwebview-webview.html" - } -} diff --git a/android/app/src/main/cpp/main.cpp b/android/app/src/main/cpp/main.cpp deleted file mode 100644 index 27d43e34aa..0000000000 --- a/android/app/src/main/cpp/main.cpp +++ /dev/null @@ -1,156 +0,0 @@ -/* - The main function in this file overrides the one in interface library - #include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include - -Q_LOGGING_CATEGORY(gpugllogging, "hifi.gl") - -bool checkGLError(const char* name) { - GLenum error = glGetError(); - if (!error) { - return false; - } else { - switch (error) { - case GL_INVALID_ENUM: - qCDebug(gpugllogging) << "GLBackend::" << name << ": An unacceptable value is specified for an enumerated argument.The offending command is ignored and has no other side effect than to set the error flag."; - break; - case GL_INVALID_VALUE: - qCDebug(gpugllogging) << "GLBackend" << name << ": A numeric argument is out of range.The offending command is ignored and has no other side effect than to set the error flag"; - break; - case GL_INVALID_OPERATION: - qCDebug(gpugllogging) << "GLBackend" << name << ": The specified operation is not allowed in the current state.The offending command is ignored and has no other side effect than to set the error flag.."; - break; - case GL_INVALID_FRAMEBUFFER_OPERATION: - qCDebug(gpugllogging) << "GLBackend" << name << ": The framebuffer object is not complete.The offending command is ignored and has no other side effect than to set the error flag."; - break; - case GL_OUT_OF_MEMORY: - qCDebug(gpugllogging) << "GLBackend" << name << ": There is not enough memory left to execute the command.The state of the GL is undefined, except for the state of the error flags, after this error is recorded."; - break; - default: - qCDebug(gpugllogging) << "GLBackend" << name << ": Unknown error: " << error; - break; - } - return true; - } -} - -bool checkGLErrorDebug(const char* name) { - return checkGLError(name); -} - - -int QtMsgTypeToAndroidPriority(QtMsgType type) { - int priority = ANDROID_LOG_UNKNOWN; - switch (type) { - case QtDebugMsg: priority = ANDROID_LOG_DEBUG; break; - case QtWarningMsg: priority = ANDROID_LOG_WARN; break; - case QtCriticalMsg: priority = ANDROID_LOG_ERROR; break; - case QtFatalMsg: priority = ANDROID_LOG_FATAL; break; - case QtInfoMsg: priority = ANDROID_LOG_INFO; break; - default: break; - } - return priority; -} - -void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { - __android_log_write(QtMsgTypeToAndroidPriority(type), "Interface", message.toStdString().c_str()); -} - -void qt_gl_set_global_share_context(QOpenGLContext *context); - -int main(int argc, char* argv[]) -{ - qInstallMessageHandler(messageHandler); - QCoreApplication::setAttribute(Qt::AA_DisableHighDpiScaling); - QGuiApplication app(argc,argv); - app.setOrganizationName("QtProject"); - app.setOrganizationDomain("qt-project.org"); - app.setApplicationName(QFileInfo(app.applicationFilePath()).baseName()); - QtWebView::initialize(); - qputenv("QSG_RENDERER_DEBUG", (QStringList() << "render" << "build" << "change" << "upload" << "roots" << "dump").join(';').toUtf8()); - - OffscreenGLCanvas sharedCanvas; - if (!sharedCanvas.create()) { - qFatal("Unable to create primary offscreen context"); - } - qt_gl_set_global_share_context(sharedCanvas.getContext()); - auto globalContext = QOpenGLContext::globalShareContext(); - - GLWindow window; - window.create(); - window.setGeometry(qApp->primaryScreen()->availableGeometry()); - window.createContext(globalContext); - if (!window.makeCurrent()) { - qFatal("Unable to make primary window GL context current"); - } - - GLuint fbo = 0; - glGenFramebuffers(1, &fbo); - - static const ivec2 offscreenSize { 640, 480 }; - - OffscreenQmlSurface::setSharedContext(sharedCanvas.getContext()); - OffscreenQmlSurface* qmlSurface = new OffscreenQmlSurface(); - qmlSurface->create(); - qmlSurface->resize(fromGlm(offscreenSize)); - qmlSurface->load("qrc:///simple.qml"); - qmlSurface->resume(); - - auto discardLambda = qmlSurface->getDiscardLambda(); - - window.showFullScreen(); - QTimer timer; - timer.setInterval(10); - timer.setSingleShot(false); - OffscreenQmlSurface::TextureAndFence currentTextureAndFence; - timer.connect(&timer, &QTimer::timeout, &app, [&]{ - window.makeCurrent(); - glClearColor(0, 1, 0, 1); - glClear(GL_COLOR_BUFFER_BIT); - - OffscreenQmlSurface::TextureAndFence newTextureAndFence; - if (qmlSurface->fetchTexture(newTextureAndFence)) { - if (currentTextureAndFence.first) { - discardLambda(currentTextureAndFence.first, currentTextureAndFence.second); - } - currentTextureAndFence = newTextureAndFence; - } - checkGLErrorDebug(__FUNCTION__); - - if (currentTextureAndFence.second) { - glWaitSync((GLsync)currentTextureAndFence.second, 0, GL_TIMEOUT_IGNORED); - glDeleteSync((GLsync)currentTextureAndFence.second); - currentTextureAndFence.second = nullptr; - } - - if (currentTextureAndFence.first) { - glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); - glFramebufferTexture(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, currentTextureAndFence.first, 0); - glBlitFramebuffer(0, 0, offscreenSize.x, offscreenSize.y, 100, 100, offscreenSize.x + 100, offscreenSize.y + 100, GL_COLOR_BUFFER_BIT, GL_NEAREST); - glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); - } - - window.swapBuffers(); - window.doneCurrent(); - }); - timer.start(); - return app.exec(); -} -*/ \ No newline at end of file diff --git a/android/app/src/main/cpp/main.qrc b/android/app/src/main/cpp/main.qrc deleted file mode 100644 index 81cf8ef111..0000000000 --- a/android/app/src/main/cpp/main.qrc +++ /dev/null @@ -1,6 +0,0 @@ - - - simple.qml - +android/simple.qml - - diff --git a/android/app/src/main/cpp/simple.qml b/android/app/src/main/cpp/simple.qml deleted file mode 100644 index 38f3c80374..0000000000 --- a/android/app/src/main/cpp/simple.qml +++ /dev/null @@ -1,10 +0,0 @@ -import QtQuick 2.0 - -Rectangle { - id: window - width: 320 - height: 480 - focus: true - color: "red" - ColorAnimation on color { from: "red"; to: "yellow"; duration: 1000; loops: Animation.Infinite } -} diff --git a/android/build.gradle b/android/build.gradle index 239d98f0bd..b164ed5cca 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -40,6 +40,7 @@ ext { BUILD_BRANCH = project.hasProperty('BUILD_BRANCH') ? project.getProperty('BUILD_BRANCH') : '' EXEC_SUFFIX = Os.isFamily(Os.FAMILY_WINDOWS) ? '.exe' : '' QT5_DEPS = [ + 'Qt5Concurrent', 'Qt5Core', 'Qt5Gui', 'Qt5Multimedia', @@ -47,14 +48,15 @@ ext { 'Qt5OpenGL', 'Qt5Qml', 'Qt5Quick', + 'Qt5QuickControls2', + 'Qt5QuickTemplates2', 'Qt5Script', 'Qt5ScriptTools', + 'Qt5Svg', 'Qt5WebChannel', 'Qt5WebSockets', 'Qt5Widgets', 'Qt5XmlPatterns', - 'Qt5Concurrent', - 'Qt5Svg', // Android specific 'Qt5AndroidExtras', 'Qt5WebView', diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 08d5778f18..8d58d4a6e8 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -58,14 +58,6 @@ else () list(REMOVE_ITEM INTERFACE_SRCS ${SPEECHRECOGNIZER_CPP}) endif () -if (ANDROID) - set(PLATFORM_QT_COMPONENTS AndroidExtras) -# set(PLATFORM_QT_LIBRARIES Qt5::AndroidExtras) -else () - set(PLATFORM_QT_COMPONENTS WebEngine WebEngineWidgets) - set(PLATFORM_QT_LIBRARIES Qt5::WebEngine Qt5::WebEngineWidgets) -endif () - find_package( Qt5 COMPONENTS Gui Multimedia Network OpenGL Qml Quick Script Svg @@ -197,13 +189,6 @@ if (WIN32) set_property(TARGET ${TARGET_NAME} APPEND_STRING PROPERTY LINK_FLAGS_DEBUG "/OPT:NOREF /OPT:NOICF") endif() -if (NOT ANDROID) - set(NON_ANDROID_LIBRARIES gpu-gl) -else() - set(ANDROID_LIBRARIES gpu-gles) -endif () - - # link required hifi libraries link_hifi_libraries( shared octree ktx gpu gl procedural model render @@ -213,8 +198,6 @@ link_hifi_libraries( render-utils entities-renderer avatars-renderer ui auto-updater midi controllers plugins image trackers ui-plugins display-plugins input-plugins - ${ANDROID_LIBRARIES} - ${NON_ANDROID_LIBRARIES} ${PLATFORM_GL_BACKEND} ) @@ -266,19 +249,16 @@ endforeach() include_directories("${PROJECT_SOURCE_DIR}/src") if (ANDROID) - #set(ANDROID_PLATFORM_QT_LIBRARIES Qt5::WebView) find_library(ANDROID_LOG_LIB log) target_link_libraries(${TARGET_NAME} ${ANDROID_LOG_LIB}) -else() - set(NON_ANDROID_PLATFORM_QT_LIBRARIES Qt5::WebEngine) endif () target_link_libraries( ${TARGET_NAME} Qt5::Gui Qt5::Network Qt5::Multimedia Qt5::OpenGL Qt5::Qml Qt5::Quick Qt5::Script Qt5::Svg - Qt5::WebChannel ${NON_ANDROID_PLATFORM_QT_LIBRARIES} - ${ANDROID_PLATFORM_QT_LIBRARIES} + Qt5::WebChannel + ${PLATFORM_QT_LIBRARIES} ) if (UNIX AND NOT ANDROID) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 65005d0605..633cad14a7 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -210,7 +210,7 @@ #include "commerce/QmlCommerce.h" #include "webbrowser/WebBrowserSuggestionsEngine.h" -#ifdef ANDROID +#if defined(Q_OS_ANDROID) #include #include #endif @@ -235,7 +235,7 @@ extern "C" { } #endif -#ifdef ANDROID +#if defined(Q_OS_ANDROID) extern "C" { JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeOnCreate(JNIEnv* env, jobject obj, jobject instance, jobject asset_mgr) { @@ -557,16 +557,9 @@ void messageHandler(QtMsgType type, const QMessageLogContext& context, const QSt if (!logMessage.isEmpty()) { #ifdef Q_OS_WIN OutputDebugStringA(logMessage.toLocal8Bit().constData()); - OutputDebugStringA("\n");i + OutputDebugStringA("\n"); #elif defined Q_OS_ANDROID - QString report=message; - if (context.file && !QString(context.file).isEmpty()) { - report+=" at "; - report+=QString(context.file); - report+=" : "; - report+=QString::number(context.line); - } - const char*const local=report.toLocal8Bit().constData(); + const char * local=logMessage.toStdString().c_str(); switch (type) { case QtDebugMsg: __android_log_write(ANDROID_LOG_DEBUG,"Interface",local); @@ -2252,10 +2245,10 @@ void Application::initializeGL() { // Set up the render engine render::CullFunctor cullFunctor = LODManager::shouldRender; static const QString RENDER_FORWARD = "HIFI_RENDER_FORWARD"; -#ifndef Q_OS_ANDROID - bool isDeferred = !QProcessEnvironment::systemEnvironment().contains(RENDER_FORWARD); -#else +#ifdef Q_OS_ANDROID bool isDeferred = false; +#else + bool isDeferred = !QProcessEnvironment::systemEnvironment().contains(RENDER_FORWARD); #endif _renderEngine->addJob("UpdateScene"); #ifndef Q_OS_ANDROID @@ -2369,10 +2362,10 @@ void Application::initializeUi() { offscreenUi->setProxyWindow(_window->windowHandle()); // OffscreenUi is a subclass of OffscreenQmlSurface specifically designed to // support the window management and scripting proxies for VR use -#ifndef Q_OS_ANDROID - offscreenUi->createDesktop(QString("hifi/Desktop.qml")); +#ifdef Q_OS_ANDROID + offscreenUi->createDesktop(PathUtils::qmlBasePath() + "hifi/Desktop.qml"); #else - offscreenUi->createDesktop(QString("qrc:///qml/hifi/Desktop.qml")); + offscreenUi->createDesktop(QString("hifi/Desktop.qml")); #endif // FIXME either expose so that dialogs can set this themselves or // do better detection in the offscreen UI of what has focus @@ -5960,7 +5953,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, LocationScriptingInterface::locationSetter); -#ifndef ANDROID +#if !defined(Q_OS_ANDROID) scriptEngine->registerFunction("OverlayWebWindow", QmlWebWindowClass::constructor); #endif scriptEngine->registerFunction("OverlayWindow", QmlWindowClass::constructor); diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 3a21f5fadf..b8b3f4ab97 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -106,6 +106,7 @@ int main(int argc, const char* argv[]) { instanceMightBeRunning = false; } +#if defined(Q_OS_ANDROID) std::vector assetDirs = { "/resources", "/scripts", @@ -115,7 +116,7 @@ int main(int argc, const char* argv[]) { QString dir = *it; PathUtils::copyDirDeep("assets:" + dir, QUrl::fromLocalFile(dirInfo.canonicalPath() + dir).toLocalFile()); } - +#endif // this needs to be done here in main, as the mechanism for setting the // scripts directory appears not to work. See the bug report // https://highfidelity.fogbugz.com/f/cases/5759/Issues-changing-scripts-directory-in-ScriptsEngine diff --git a/interface/src/scripting/AssetMappingsScriptingInterface.cpp b/interface/src/scripting/AssetMappingsScriptingInterface.cpp index 5c27de84c9..499f1829b6 100644 --- a/interface/src/scripting/AssetMappingsScriptingInterface.cpp +++ b/interface/src/scripting/AssetMappingsScriptingInterface.cpp @@ -37,7 +37,7 @@ AssetMappingsScriptingInterface::AssetMappingsScriptingInterface() { void AssetMappingsScriptingInterface::setMapping(QString path, QString hash, QJSValue callback) { auto assetClient = DependencyManager::get(); auto request = assetClient->createSetMappingRequest(path, hash); -#ifndef ANDROID +#if !defined(Q_OS_ANDROID) // TODO: just to make android compile connect(request, &SetMappingRequest::finished, this, [this, callback](SetMappingRequest* request) mutable { if (callback.isCallable()) { @@ -55,7 +55,7 @@ void AssetMappingsScriptingInterface::setMapping(QString path, QString hash, QJS void AssetMappingsScriptingInterface::getMapping(QString path, QJSValue callback) { auto assetClient = DependencyManager::get(); auto request = assetClient->createGetMappingRequest(path); -#ifndef ANDROID +#if !defined(Q_OS_ANDROID) // TODO: just to make android compile connect(request, &GetMappingRequest::finished, this, [this, callback](GetMappingRequest* request) mutable { auto hash = request->getHash(); @@ -144,7 +144,7 @@ void AssetMappingsScriptingInterface::uploadFile(QString path, QString mapping, void AssetMappingsScriptingInterface::deleteMappings(QStringList paths, QJSValue callback) { auto assetClient = DependencyManager::get(); auto request = assetClient->createDeleteMappingsRequest(paths); -#ifndef ANDROID +#if !defined(Q_OS_ANDROID) // TODO: just to make android compile connect(request, &DeleteMappingsRequest::finished, this, [this, callback](DeleteMappingsRequest* request) mutable { if (callback.isCallable()) { @@ -162,7 +162,7 @@ void AssetMappingsScriptingInterface::deleteMappings(QStringList paths, QJSValue void AssetMappingsScriptingInterface::getAllMappings(QJSValue callback) { auto assetClient = DependencyManager::get(); auto request = assetClient->createGetAllMappingsRequest(); -#ifndef ANDROID +#if !defined(Q_OS_ANDROID) // TODO: just to make android compile connect(request, &GetAllMappingsRequest::finished, this, [this, callback](GetAllMappingsRequest* request) mutable { auto mappings = request->getMappings(); @@ -187,7 +187,7 @@ void AssetMappingsScriptingInterface::getAllMappings(QJSValue callback) { void AssetMappingsScriptingInterface::renameMapping(QString oldPath, QString newPath, QJSValue callback) { auto assetClient = DependencyManager::get(); auto request = assetClient->createRenameMappingRequest(oldPath, newPath); -#ifndef ANDROID +#if !defined(Q_OS_ANDROID) // TODO: just to make android compile connect(request, &RenameMappingRequest::finished, this, [this, callback](RenameMappingRequest* request) mutable { if (callback.isCallable()) { @@ -204,7 +204,7 @@ void AssetMappingsScriptingInterface::renameMapping(QString oldPath, QString new void AssetMappingsScriptingInterface::setBakingEnabled(QStringList paths, bool enabled, QJSValue callback) { auto assetClient = DependencyManager::get(); auto request = assetClient->createSetBakingEnabledRequest(paths, enabled); -#ifndef ANDROID +#if !defined(Q_OS_ANDROID) // TODO: just to make android compile connect(request, &SetBakingEnabledRequest::finished, this, [this, callback](SetBakingEnabledRequest* request) mutable { if (callback.isCallable()) { diff --git a/interface/src/scripting/LimitlessConnection.h b/interface/src/scripting/LimitlessConnection.h index cdb64a8197..6acf651c4e 100644 --- a/interface/src/scripting/LimitlessConnection.h +++ b/interface/src/scripting/LimitlessConnection.h @@ -15,7 +15,7 @@ #include #include #include -#ifdef ANDROID +#if defined(Q_OS_ANDROID) #include #endif class LimitlessConnection : public QObject { diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 2e15f11c3e..c157898e74 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -42,7 +42,7 @@ using namespace std; static Stats* INSTANCE{ nullptr }; -#ifndef ANDROID +#if !defined (Q_OS_ANDROID) QString getTextureMemoryPressureModeString(); #endif Stats* Stats::getInstance() { @@ -360,7 +360,7 @@ void Stats::updateStats(bool force) { STAT_UPDATE(gpuTextureResourceMemory, (int)BYTES_TO_MB(gpu::Context::getTextureResourceGPUMemSize())); STAT_UPDATE(gpuTextureResourcePopulatedMemory, (int)BYTES_TO_MB(gpu::Context::getTextureResourcePopulatedGPUMemSize())); STAT_UPDATE(gpuTextureExternalMemory, (int)BYTES_TO_MB(gpu::Context::getTextureExternalGPUMemSize())); -#ifndef ANDROID +#if !defined(Q_OS_ANDROID) STAT_UPDATE(gpuTextureMemoryPressureState, getTextureMemoryPressureModeString()); #endif STAT_UPDATE(gpuFreeMemory, (int)BYTES_TO_MB(gpu::Context::getFreeGPUMemSize())); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index c41372ee55..c532e7659f 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -117,7 +117,7 @@ Avatar::Avatar(QThread* thread) : } Avatar::~Avatar() { - auto treeRenderer = DependencyManager::get(); + auto treeRenderer = DependencyManager::get(); EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; if (entityTree) { entityTree->withWriteLock([&] { @@ -1287,7 +1287,7 @@ int Avatar::parseDataFromBuffer(const QByteArray& buffer) { const float MOVE_DISTANCE_THRESHOLD = 0.001f; _moving = glm::distance(oldPosition, getWorldPosition()) > MOVE_DISTANCE_THRESHOLD; if (_moving) { - addPhysicsFlags(Simulation::DIRTY_POSITION); + addPhysicsFlags(Simulation::DIRTY_POSITION); } if (_moving || _hasNewJointData) { locationChanged(); diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 90a4e75669..0facf5f2d0 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -891,12 +891,12 @@ void OpenGLDisplayPlugin::copyTextureToQuickFramebuffer(NetworkTexturePointer ne GLuint fbo[2] {0, 0}; // need mipmaps for blitting texture -#ifndef ANDROID +#if !defined(Q_OS_ANDROID) glGenerateTextureMipmap(sourceTexture); #endif // create 2 fbos (one for initial texture, second for scaled one) -#ifndef ANDROID +#if !defined(Q_OS_ANDROID) glCreateFramebuffers(2, fbo); #endif @@ -928,8 +928,8 @@ void OpenGLDisplayPlugin::copyTextureToQuickFramebuffer(NetworkTexturePointer ne } else { newY = (target->height() - newHeight) / 2; } -#ifndef ANDROID - glBlitNamedFramebuffer(fbo[0], fbo[1], 0, 0, texWidth, texHeight, newX, newY, newX + newWidth, newY + newHeight, GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT, GL_NEAREST); +#if !defined(Q_OS_ANDROID) + glBlitNamedFramebuffer(fbo[0], fbo[1], 0, 0, texWidth, texHeight, newX, newY, newX + newWidth, newY + newHeight, GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT, GL_NEAREST); #endif // don't delete the textures! diff --git a/libraries/gpu-gles/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gles/src/gpu/gl/GLBackend.cpp index 667b408801..7d00552663 100644 --- a/libraries/gpu-gles/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gles/src/gpu/gl/GLBackend.cpp @@ -147,17 +147,6 @@ void GLBackend::init() { qCDebug(gpugllogging) << "\tcard:" << gpu->getName(); qCDebug(gpugllogging) << "\tdriver:" << gpu->getDriver(); qCDebug(gpugllogging) << "\tdedicated memory:" << gpu->getMemory() << "MB"; - - /*glewExperimental = true; - GLenum err = glewInit(); - glGetError(); // clear the potential error from glewExperimental - if (GLEW_OK != err) { - // glewInit failed, something is seriously wrong. - qCDebug(gpugllogging, "Error: %s\n", glewGetErrorString(err)); - } - qCDebug(gpugllogging, "Status: Using GLEW %s\n", glewGetString(GLEW_VERSION)); - */ - #if THREADED_TEXTURE_BUFFERING // This has to happen on the main thread in order to give the thread // pool a reasonable parent object diff --git a/libraries/gpu-gles/src/gpu/gl/GLTexelFormat.cpp b/libraries/gpu-gles/src/gpu/gl/GLTexelFormat.cpp index a390079322..bff3998573 100644 --- a/libraries/gpu-gles/src/gpu/gl/GLTexelFormat.cpp +++ b/libraries/gpu-gles/src/gpu/gl/GLTexelFormat.cpp @@ -12,7 +12,7 @@ using namespace gpu; using namespace gpu::gl; bool GLTexelFormat::isCompressed() const { - return false; + return false; } GLenum GLTexelFormat::evalGLTexelFormatInternal(const gpu::Element& dstFormat) { diff --git a/libraries/gpu/src/gpu/Framebuffer.h b/libraries/gpu/src/gpu/Framebuffer.h index b3cf0fbba3..f470cc8aa9 100755 --- a/libraries/gpu/src/gpu/Framebuffer.h +++ b/libraries/gpu/src/gpu/Framebuffer.h @@ -134,7 +134,7 @@ public: float getAspectRatio() const { return getWidth() / (float) getHeight() ; } -#ifndef ANDROID +#if !defined(Q_OS_ANDROID) static const uint32 MAX_NUM_RENDER_BUFFERS = 8; #else static const uint32 MAX_NUM_RENDER_BUFFERS = 4; diff --git a/libraries/image/src/image/Image.cpp b/libraries/image/src/image/Image.cpp index 172903a65b..58c2b5f938 100644 --- a/libraries/image/src/image/Image.cpp +++ b/libraries/image/src/image/Image.cpp @@ -738,7 +738,7 @@ gpu::TexturePointer TextureUsage::process2DTextureColorFromImage(QImage&& srcIma bool validAlpha = image.hasAlphaChannel(); bool alphaAsMask = false; -#ifndef ANDROID +#if !defined(Q_OS_ANDROID) if (image.format() != QImage::Format_ARGB32) { image = image.convertToFormat(QImage::Format_ARGB32); } diff --git a/libraries/script-engine/src/ArrayBufferClass.h b/libraries/script-engine/src/ArrayBufferClass.h index d65693bece..166a21b773 100644 --- a/libraries/script-engine/src/ArrayBufferClass.h +++ b/libraries/script-engine/src/ArrayBufferClass.h @@ -19,7 +19,7 @@ #include #include #include -#include +#include class ScriptEngine; diff --git a/libraries/script-engine/src/ConsoleScriptingInterface.cpp b/libraries/script-engine/src/ConsoleScriptingInterface.cpp index 1e0ca2e42f..b4ef98938d 100644 --- a/libraries/script-engine/src/ConsoleScriptingInterface.cpp +++ b/libraries/script-engine/src/ConsoleScriptingInterface.cpp @@ -15,9 +15,9 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include #include "ConsoleScriptingInterface.h" #include "ScriptEngine.h" -#include #define INDENTATION 4 // 1 Tab - 4 spaces const QString LINE_SEPARATOR = "\n "; diff --git a/libraries/shared/src/PathUtils.cpp b/libraries/shared/src/PathUtils.cpp index cf33cbed2b..22e11464bd 100644 --- a/libraries/shared/src/PathUtils.cpp +++ b/libraries/shared/src/PathUtils.cpp @@ -54,7 +54,7 @@ const QString& PathUtils::projectRootPath() { const QString& PathUtils::qmlBasePath() { #ifdef Q_OS_ANDROID - static const QString staticResourcePath = QUrl::fromLocalFile(PathUtils::resourcesPath() + "qml/").toString(); + static const QString staticResourcePath = "qrc:///qml/"; #elif defined (DEV_BUILD) static const QString staticResourcePath = QUrl::fromLocalFile(projectRootPath() + "/interface/resources/qml/").toString(); #else @@ -76,10 +76,10 @@ QString PathUtils::getAppLocalDataPath() { } // otherwise return standard path -#ifndef Q_OS_ANDROID - return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/"; -#else +#ifdef Q_OS_ANDROID return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/"; +#else + return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/"; #endif } diff --git a/libraries/shared/src/shared/FileUtils.cpp b/libraries/shared/src/shared/FileUtils.cpp index d326bff1d4..7e1db8b4ca 100644 --- a/libraries/shared/src/shared/FileUtils.cpp +++ b/libraries/shared/src/shared/FileUtils.cpp @@ -84,10 +84,10 @@ void FileUtils::locateFile(QString filePath) { QString FileUtils::standardPath(QString subfolder) { // standard path // Mac: ~/Library/Application Support/Interface -#ifndef Q_OS_ANDROID - QString path = QStandardPaths::writableLocation(QStandardPaths::DataLocation); -#else +#ifdef Q_OS_ANDROID QString path = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); +#else + QString path = QStandardPaths::writableLocation(QStandardPaths::DataLocation); #endif if (!subfolder.startsWith("/")) { subfolder.prepend("/"); diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index aa64610930..19ffe96148 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -282,7 +282,7 @@ private: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f); -#ifndef ANDROID +#if !defined(Q_OS_ANDROID) glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -0.2f); #endif glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f); @@ -450,7 +450,7 @@ void initializeQmlEngine(QQmlEngine* engine, QQuickWindow* window) { if (!javaScriptToInject.isEmpty()) { rootContext->setContextProperty("eventBridgeJavaScriptToInject", QVariant(javaScriptToInject)); } -#ifndef ANDROID +#if !defined(Q_OS_ANDROID) rootContext->setContextProperty("FileTypeProfile", new FileTypeProfile(rootContext)); rootContext->setContextProperty("HFWebEngineProfile", new HFWebEngineProfile(rootContext)); #endif @@ -554,7 +554,7 @@ void OffscreenQmlSurface::render() { GLuint texture = offscreenTextures.getNextTexture(_size); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo); -#ifndef ANDROID +#if !defined(Q_OS_ANDROID) glFramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0); #endif glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index 6e86d75940..0e793fef21 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -268,7 +268,7 @@ static const char* WEB_VIEW_SOURCE_URL = "hifi/tablet/TabletWebView.qml"; static const char* VRMENU_SOURCE_URL = "hifi/tablet/TabletMenu.qml"; class TabletRootWindow : public QmlWindowClass { - virtual QString qmlSource() const override { return "qrc:///qml/hifi/tablet/WindowRoot.qml"; } + virtual QString qmlSource() const override { return "hifi/tablet/WindowRoot.qml"; } }; TabletProxy::TabletProxy(QObject* parent, const QString& name) : QObject(parent), _name(name) { diff --git a/libraries/ui/src/ui/types/FileTypeProfile.cpp b/libraries/ui/src/ui/types/FileTypeProfile.cpp index d31f09a981..d090ae6f5d 100644 --- a/libraries/ui/src/ui/types/FileTypeProfile.cpp +++ b/libraries/ui/src/ui/types/FileTypeProfile.cpp @@ -17,17 +17,13 @@ static const QString QML_WEB_ENGINE_STORAGE_NAME = "qmlWebEngine"; FileTypeProfile::FileTypeProfile(QObject* parent) -#ifndef ANDROID - : QQuickWebEngineProfile(parent) -#endif + : QQuickWebEngineProfile(parent) { static const QString WEB_ENGINE_USER_AGENT = "Chrome/48.0 (HighFidelityInterface)"; -#ifndef ANDROID setHttpUserAgent(WEB_ENGINE_USER_AGENT); auto requestInterceptor = new FileTypeRequestInterceptor(this); setRequestInterceptor(requestInterceptor); -#endif } #endif \ No newline at end of file diff --git a/libraries/ui/src/ui/types/HFTabletWebEngineRequestInterceptor.h b/libraries/ui/src/ui/types/HFTabletWebEngineRequestInterceptor.h index 5c5f30ce0b..8be2974782 100644 --- a/libraries/ui/src/ui/types/HFTabletWebEngineRequestInterceptor.h +++ b/libraries/ui/src/ui/types/HFTabletWebEngineRequestInterceptor.h @@ -11,26 +11,20 @@ #ifndef hifi_HFTabletWebEngineRequestInterceptor_h #define hifi_HFTabletWebEngineRequestInterceptor_h +#if !defined(Q_OS_ANDROID) #include -#ifndef ANDROID #include -#endif class HFTabletWebEngineRequestInterceptor -#ifndef ANDROID - : public QWebEngineUrlRequestInterceptor -#endif + : public QWebEngineUrlRequestInterceptor { public: HFTabletWebEngineRequestInterceptor(QObject* parent) -#ifndef ANDROID - : QWebEngineUrlRequestInterceptor(parent) -#endif + : QWebEngineUrlRequestInterceptor(parent) {}; -#ifndef ANDROID - virtual void interceptRequest(QWebEngineUrlRequestInfo& info) override; -#endif + virtual void interceptRequest(QWebEngineUrlRequestInfo& info) override; }; +#endif #endif // hifi_HFWebEngineRequestInterceptor_h diff --git a/libraries/ui/src/ui/types/HFWebEngineProfile.cpp b/libraries/ui/src/ui/types/HFWebEngineProfile.cpp index a443a7c160..8962a9d61d 100644 --- a/libraries/ui/src/ui/types/HFWebEngineProfile.cpp +++ b/libraries/ui/src/ui/types/HFWebEngineProfile.cpp @@ -18,15 +18,12 @@ static const QString QML_WEB_ENGINE_STORAGE_NAME = "qmlWebEngine"; HFWebEngineProfile::HFWebEngineProfile(QObject* parent) -#ifndef ANDROID - : QQuickWebEngineProfile(parent) -#endif + : QQuickWebEngineProfile(parent) { -#ifndef ANDROID setStorageName(QML_WEB_ENGINE_STORAGE_NAME); + setStorageName(QML_WEB_ENGINE_STORAGE_NAME); // we use the HFWebEngineRequestInterceptor to make sure that web requests are authenticated for the interface user auto requestInterceptor = new HFWebEngineRequestInterceptor(this); setRequestInterceptor(requestInterceptor); -#endif } #endif \ No newline at end of file