/* * Libretro 3DEngine * Copyright (C) 2013-2014 - Hans-Kristian Arntzen * Copyright (C) 2013-2014 - Daniel De Matteis * Copyright (C) 2013-2014 - Michael Lelli * * Libretro 3DEngine is free software: you can redistribute it and/or modify it under the terms * of the GNU General Public License as published by the Free Software Found- * ation, either version 3 of the License, or (at your option) any later version. * * Libretro 3DEngine is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with Libretro 3DEngine. * If not, see . */ #include "libretro.h" #include #include #include #include #include #include #include #include #include "rpng.h" #include "program.h" #include "gl.hpp" #ifdef HAVE_VULKAN #include "engine/vk_renderer.hpp" #endif #define FPS 60.0 retro_position_t previous_location; retro_position_t current_location; bool renderer_dead_state = true; bool sensor_initialized = false; bool sensor_enable = false; bool location_camera_control_enable = false; static bool location_enable = false; bool camera_enable = false; struct retro_hw_render_callback hw_render; static struct retro_camera_callback camera_cb; retro_log_printf_t log_cb; struct retro_location_callback location_cb; retro_video_refresh_t video_cb; static retro_audio_sample_t audio_cb; static retro_audio_sample_batch_t audio_batch_cb; retro_environment_t environ_cb; retro_input_poll_t input_poll_cb; retro_input_state_t input_state_cb; static const engine_program_t *engine_program_cb; struct retro_sensor_interface sensor_cb; enum retro_hw_context_type active_hw_context = RETRO_HW_CONTEXT_NONE; static bool display_position; #define BASE_WIDTH 320 #define BASE_HEIGHT 240 #ifdef HAVE_OPENGLES #define MAX_WIDTH 1024 #define MAX_HEIGHT 1024 #else #define MAX_WIDTH 2048 #define MAX_HEIGHT 2048 #endif unsigned engine_width = BASE_WIDTH; unsigned engine_height = BASE_HEIGHT; GLuint tex; GLuint g_texture_target = GL_TEXTURE_2D; bool vr_active = false; unsigned vr_eye_width = 0; unsigned vr_eye_height = 0; bool vr_user_enable = true; bool renderer_is_vulkan(void) { return active_hw_context == RETRO_HW_CONTEXT_VULKAN; } void retro_init(void) { struct retro_log_callback log; if (environ_cb(RETRO_ENVIRONMENT_GET_LOG_INTERFACE, &log)) log_cb = log.log; else log_cb = NULL; environ_cb(RETRO_ENVIRONMENT_GET_SENSOR_INTERFACE, &sensor_cb); } void retro_deinit(void) { } unsigned retro_api_version(void) { return RETRO_API_VERSION; } void retro_set_controller_port_device(unsigned port, unsigned device) { (void)port; (void)device; } void retro_get_system_info(struct retro_system_info *info) { memset(info, 0, sizeof(*info)); info->library_name = "Libretro 3DEngine"; #ifndef GIT_VERSION #define GIT_VERSION "" #endif info->library_version = "v1" GIT_VERSION; info->need_fullpath = false; info->valid_extensions = "png|jpg|mtl|obj"; } void retro_get_system_av_info(struct retro_system_av_info *info) { info->timing.fps = FPS; info->timing.sample_rate = 30000.0; info->geometry.base_width = BASE_WIDTH; info->geometry.base_height = BASE_HEIGHT; info->geometry.max_width = MAX_WIDTH; info->geometry.max_height = MAX_HEIGHT; if (vr_active) { info->geometry.base_width = info->geometry.max_width = vr_eye_width * 2; info->geometry.base_height = info->geometry.max_height = vr_eye_height; info->geometry.aspect_ratio = (float)(vr_eye_width * 2) / vr_eye_height; } } void retro_set_environment(retro_environment_t cb) { environ_cb = cb; struct retro_variable variables[] = { { "3dengine-resolution", "Internal resolution; 320x240|360x480|480x272|512x384|512x512|640x240|640x448|640x480|720x576|800x600|960x720|1024x768" #ifndef HAVE_OPENGLES "|1024x1024|1280x720|1280x960|1600x1200|1920x1080|1920x1440|1920x1600|2048x1152|2048x1536|2048x2048|320x240" #endif }, { "3dengine-cube-size", "Cube size; 0|1|2|4|8|16|32|64|128" }, { "3dengine-cube-stride", "Cube stride; 2.0|3.0|4.0|5.0|6.0|7.0|8.0" }, { "3dengine-camera-enable", "Camera enable; disabled|enabled" }, { "3dengine-camera-type", "Camera FB Type; texture|raw framebuffer" }, { "3dengine-sensor-enable", "Sensor enable; disabled|enabled" }, { "3dengine-location-enable", "Location enable; disabled|enabled" }, { "3dengine-location-camera-control-enable", "Location camera control; disabled|enabled" }, { "3dengine-modelviewer-discard-hack", "Discard hack enable; disabled|enabled" }, { "3dengine-location-display-position", "Location position OSD; disabled|enabled" }, { "3dengine-modelviewer-vr-enable", "VR enable in modelviewer; enabled|disabled" }, { NULL, NULL }, }; environ_cb(RETRO_ENVIRONMENT_SET_VARIABLES, variables); } void retro_set_audio_sample(retro_audio_sample_t cb) { audio_cb = cb; } void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb) { audio_batch_cb = cb; } void retro_set_input_poll(retro_input_poll_t cb) { input_poll_cb = cb; } void retro_set_input_state(retro_input_state_t cb) { input_state_cb = cb; } void retro_set_video_refresh(retro_video_refresh_t cb) { video_cb = cb; } static void update_variables(void) { struct retro_variable var; var.key = "3dengine-resolution"; var.value = NULL; if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { char *pch; char str[100]; snprintf(str, sizeof(str), "%s", var.value); pch = strtok(str, "x"); if (pch) engine_width = strtoul(pch, NULL, 0); pch = strtok(NULL, "x"); if (pch) engine_height = strtoul(pch, NULL, 0); if (log_cb) log_cb(RETRO_LOG_INFO, "Got size: %u x %u.\n", engine_width, engine_height); } var.key = "3dengine-location-display-position"; var.value = NULL; if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { if (!strcmp(var.value, "disabled")) display_position = false; else if (!strcmp(var.value, "enabled")) display_position = true; } var.key = "3dengine-location-camera-control-enable"; var.value = NULL; if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { if (!strcmp(var.value, "disabled")) location_camera_control_enable = false; else if (!strcmp(var.value, "enabled")) location_camera_control_enable = true; } if (engine_program_cb && engine_program_cb->update_variables) engine_program_cb->update_variables(environ_cb); } void retro_run(void) { bool updated = false; if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated) && updated) update_variables(); if (location_enable && location_cb.get_position) { double lat, lon, hacc, vacc; if (location_cb.get_position(&lat, &lon, &hacc, &vacc)) { previous_location = current_location; current_location.latitude = lat; current_location.longitude = lon; current_location.horizontal_accuracy = hacc; current_location.vertical_accuracy = vacc; if (display_position) { struct retro_message msg; char msg_local[512]; snprintf(msg_local, sizeof(msg_local), "LAT %f LON %f HACC %f VACC %f", current_location.latitude, current_location.longitude, current_location.horizontal_accuracy, current_location.vertical_accuracy); msg.msg = msg_local; msg.frames = 180; environ_cb(RETRO_ENVIRONMENT_SET_MESSAGE, (void*)&msg); } } } if (engine_program_cb && engine_program_cb->run) engine_program_cb->run(); } static void camera_gl_callback(unsigned texture_id, unsigned texture_target, const float *affine) { g_texture_target = texture_target; // TODO: support GL_TEXTURE_RECTANGLE and others? if (texture_target == GL_TEXTURE_2D) tex = texture_id; #ifdef ANDROID else if (texture_target == GL_TEXTURE_EXTERNAL_OES) tex = texture_id; #endif } static inline bool gl_query_extension(const char *ext) { #if !defined(ANDROID) && !defined(EMSCRIPTEN) // This code crashes right now on Android 4.4 (but not 4.0 to 4.3), so comment it out for now const char *str = (const char*)glGetString(GL_EXTENSIONS); bool ret = str && strstr(str, ext); return ret; #else return false; #endif } static bool support_unpack_row_length; static uint8_t *convert_buffer; #ifndef GL_BGRA_EXT #define GL_BGRA_EXT 0x80E1 #endif #ifndef GL_UNPACK_ROW_LENGTH #define GL_UNPACK_ROW_LENGTH 0x0CF2 #endif #ifdef HAVE_OPENGLES #define INTERNAL_FORMAT GL_BGRA_EXT #define TEX_TYPE GL_BGRA_EXT #define TEX_FORMAT GL_UNSIGNED_BYTE #else #define INTERNAL_FORMAT GL_RGBA #define TEX_TYPE GL_BGRA #define TEX_FORMAT GL_UNSIGNED_INT_8_8_8_8_REV #endif static void camera_raw_fb_callback(const uint32_t *buffer, unsigned width, unsigned height, size_t pitch) { unsigned base_size = 4; unsigned h; if (!tex) { glGenTextures(1, &tex); glBindTexture(GL_TEXTURE_2D, tex); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D(GL_TEXTURE_2D, 0, INTERNAL_FORMAT, width, height, 0, TEX_TYPE, TEX_FORMAT, NULL); if (!support_unpack_row_length) convert_buffer = new uint8_t[width * height * 4]; } else glBindTexture(GL_TEXTURE_2D, tex); if (support_unpack_row_length) { glPixelStorei(GL_UNPACK_ROW_LENGTH, pitch / base_size); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, TEX_TYPE, TEX_FORMAT, buffer); glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); } else { // No GL_UNPACK_ROW_LENGTH ;( unsigned pitch_width = pitch / base_size; if (width == pitch_width) // Happy path :D { glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, TEX_TYPE, TEX_FORMAT, buffer); } else // Slower path. { const unsigned line_bytes = width * base_size; uint8_t *dst = (uint8_t*)convert_buffer; // This buffer is preallocated for this purpose. const uint8_t *src = (const uint8_t*)buffer; for (h = 0; h < height; h++, src += pitch, dst += line_bytes) memcpy(dst, src, line_bytes); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, TEX_TYPE, TEX_FORMAT, convert_buffer); } } glBindTexture(GL_TEXTURE_2D, 0); } static void camera_initialized(void) { if (camera_enable && camera_cb.start) camera_cb.start(); } static void context_reset(void) { if (engine_program_cb && engine_program_cb->context_reset) engine_program_cb->context_reset(); } /* Only registered for Vulkan contexts (see try_hw_context()). */ static void context_destroy(void) { if (engine_program_cb && engine_program_cb->context_destroy) engine_program_cb->context_destroy(); } char retro_path_info[1024]; static void location_initialized(void) { if (location_enable && location_cb.start) location_cb.start(); } static void location_deinitialized(void) { if (location_enable && location_cb.stop) location_cb.stop(); } // VAR static struct vr_hold vr_hold_state; static bool vr_status_unsupported; static bool vr_refused; /* not retried until HMD or the option drops */ static bool vr_send_map(unsigned ew, unsigned eh, unsigned *rec_w, unsigned *rec_h) { struct retro_video_views views; struct retro_video_view v[2]; memset(&views, 0, sizeof(views)); memset(v, 0, sizeof(v)); v[0].width = ew; v[0].height = eh; v[0].eye = RETRO_VIDEO_VIEW_EYE_LEFT; v[1].x = ew; v[1].width = ew; v[1].height = eh; v[1].eye = RETRO_VIDEO_VIEW_EYE_RIGHT; views.views = v; views.num_views = 2; views.flags = RETRO_VIDEO_VIEWS_FLAG_TRACKED; views.reference_space = RETRO_VR_REFERENCE_SPACE_LOCAL; /* eye height == player_pos.y */ if (!environ_cb(RETRO_ENVIRONMENT_SET_VIDEO_VIEWS, &views)) return false; *rec_w = views.recommended_view_width; *rec_h = views.recommended_view_height; return true; } /* Only a sent map returns the recommended eye size, so the last size is * sent first and corrected. */ static bool vr_request_session(void) { unsigned ew = vr_eye_width ? vr_eye_width : 1024; unsigned eh = vr_eye_height ? vr_eye_height : 1024; unsigned rw = 0, rh = 0; if (!vr_send_map(ew, eh, &rw, &rh)) { if (log_cb) log_cb(RETRO_LOG_INFO, "VR session request failed.\n"); return false; } if (rw && rh && (rw != ew || rh != eh)) { ew = rw; eh = rh; if (!vr_send_map(ew, eh, &rw, &rh)) return false; } vr_eye_width = ew; vr_eye_height = eh; if (log_cb) log_cb(RETRO_LOG_INFO, "VR session request success. vr_eye_width: %d vr_eye_height: %d.\n", vr_eye_width, vr_eye_height); return true; } static void vr_clear_map(void) { struct retro_video_views views; memset(&views, 0, sizeof(views)); environ_cb(RETRO_ENVIRONMENT_SET_VIDEO_VIEWS, &views); } static void vr_send_av_info(void) { struct retro_system_av_info av; memset(&av, 0, sizeof(av)); retro_get_system_av_info(&av); environ_cb(RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO, &av); } /* True when a resize sent the map and the AV info again. */ static bool vr_poll_frame(struct retro_vr_frame_state *fs) { struct retro_vr_frame_state tmp = *fs; if (!environ_cb(RETRO_ENVIRONMENT_GET_VR_FRAME_STATE, &tmp)) return false; /* keep last pose */ *fs = tmp; if (!(tmp.flags & RETRO_VR_FRAME_TARGET_RESIZED) || !vr_request_session()) return false; vr_send_av_info(); return true; } /* Once per frame, before input. A core learns about a headset only from * HMD; *dupe asks for the last frame again while a lost one is waited for. */ bool vr_update(struct retro_vr_frame_state *fs, bool capable, bool *dupe) { unsigned status = 0; bool hmd; *dupe = false; /* Asked once on a frontend without the call: it logs every refusal. */ if ( !vr_status_unsupported && !environ_cb(RETRO_ENVIRONMENT_GET_VIDEO_VIEWS_STATUS, &status)) { vr_status_unsupported = true; status = 0; } hmd = (status & RETRO_VIDEO_VIEWS_STATUS_HMD) != 0; if (!hmd || !vr_user_enable) vr_refused = false; switch (vr_hold_update(&vr_hold_state, vr_user_enable && capable && !vr_refused, hmd, (unsigned)(FPS * VR_HOLD_SECONDS))) { case VR_HOLD_ENTER: if (!vr_request_session()) { vr_clear_map(); /* a first map may have been accepted */ vr_hold_state.active = false; vr_refused = true; return false; } vr_active = true; /* Pose first: the AV info reinits video, which restarts the session. */ if (!vr_poll_frame(fs)) vr_send_av_info(); return true; case VR_HOLD_TRACK: break; case VR_HOLD_WAIT: *dupe = true; return true; case VR_HOLD_LEAVE: vr_active = false; vr_clear_map(); vr_send_av_info(); return false; default: return false; } /* The map every frame: resending covers state loads and run-ahead. A * new size written back is a resize. */ if (!vr_poll_frame(fs)) { unsigned rw = 0, rh = 0; if ( vr_send_map(vr_eye_width, vr_eye_height, &rw, &rh) && rw && rh && (rw != vr_eye_width || rh != vr_eye_height) && vr_request_session()) vr_send_av_info(); } return true; } // END VR static bool try_hw_context(enum retro_hw_context_type type) { memset(&hw_render, 0, sizeof(hw_render)); hw_render.context_type = type; hw_render.context_reset = context_reset; if (type == RETRO_HW_CONTEXT_VULKAN) { #ifdef HAVE_VULKAN hw_render.context_destroy = context_destroy; if (!environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER, &hw_render)) return false; if (!VKR::set_negotiation_interface(environ_cb) && log_cb) log_cb(RETRO_LOG_WARN, "Vulkan context negotiation not available, using frontend defaults.\n"); return true; #else return false; #endif } hw_render.depth = true; if (!camera_enable) hw_render.bottom_left_origin = true; return environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER, &hw_render); } static bool init_hw_render(bool allow_vulkan) { unsigned i, n = 0; enum retro_hw_context_type order[2]; enum retro_hw_context_type preferred = RETRO_HW_CONTEXT_NONE; bool have_vulkan = false; #ifdef HAVE_OPENGLES const enum retro_hw_context_type gl_type = RETRO_HW_CONTEXT_OPENGLES2; #else const enum retro_hw_context_type gl_type = RETRO_HW_CONTEXT_OPENGL; #endif #ifdef HAVE_VULKAN have_vulkan = allow_vulkan; #else (void)allow_vulkan; #endif environ_cb(RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER, &preferred); if (have_vulkan && preferred == RETRO_HW_CONTEXT_VULKAN) { order[n++] = RETRO_HW_CONTEXT_VULKAN; order[n++] = gl_type; } else { order[n++] = gl_type; if (have_vulkan) order[n++] = RETRO_HW_CONTEXT_VULKAN; } for (i = 0; i < n; i++) { if (try_hw_context(order[i])) { active_hw_context = order[i]; if (log_cb) log_cb(RETRO_LOG_INFO, "Using %s renderer (frontend preference: %d).\n", order[i] == RETRO_HW_CONTEXT_VULKAN ? "Vulkan" : "OpenGL/GLES", (int)preferred); return true; } } active_hw_context = RETRO_HW_CONTEXT_NONE; return false; } bool retro_load_game(const struct retro_game_info *info) { retro_variable var; bool allow_vulkan = false; if (!info) return false; strcpy(retro_path_info, info->path); if (strstr(info->path, ".obj") || strstr(info->path, ".mtl")) { engine_program_cb = &engine_program_modelviewer; allow_vulkan = true; /* the mesh viewer/scene walker have a Vulkan back-end */ } else engine_program_cb = &engine_program_instancingviewer; /* GL only: camera textures, instanced cubes */ update_variables(); var.key = "3dengine-sensor-enable"; var.value = NULL; if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { if (!strcmp(var.value, "enabled")) { if (sensor_initialized) return true; if (log_cb) log_cb(RETRO_LOG_INFO, "Sensor interface found, enabling...\n"); if (sensor_cb.set_sensor_state) { sensor_cb.set_sensor_state(0, RETRO_SENSOR_ACCELEROMETER_ENABLE, FPS); sensor_enable = true; } sensor_initialized = true; } if (!strcmp(var.value, "disabled")) { if (sensor_initialized) return true; if (log_cb) log_cb(RETRO_LOG_INFO, "Sensor interface found, disabling...\n"); if (sensor_cb.set_sensor_state) { sensor_cb.set_sensor_state(0, RETRO_SENSOR_ACCELEROMETER_DISABLE, FPS); sensor_enable = false; } } } var.key = "3dengine-location-enable"; var.value = NULL; if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { if (!strcmp(var.value, "enabled")) { location_enable = true; location_cb.initialized = location_initialized; location_cb.deinitialized = location_deinitialized; environ_cb(RETRO_ENVIRONMENT_GET_LOCATION_INTERFACE, &location_cb); } } memset(&camera_cb, 0, sizeof(camera_cb)); enum retro_pixel_format fmt = RETRO_PIXEL_FORMAT_XRGB8888; if (!environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &fmt)) { if (log_cb) log_cb(RETRO_LOG_ERROR, "XRGB8888 is not supported.\n"); return false; } var.key = "3dengine-camera-enable"; var.value = NULL; if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { if (!strcmp(var.value, "enabled")) { camera_enable = true; var.key = "3dengine-camera-type"; var.value = NULL; if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { if (!strcmp(var.value, "texture")) { camera_cb.caps = (1 << RETRO_CAMERA_BUFFER_OPENGL_TEXTURE); camera_cb.frame_opengl_texture = camera_gl_callback; } else { camera_cb.caps = (1 << RETRO_CAMERA_BUFFER_RAW_FRAMEBUFFER); camera_cb.frame_raw_framebuffer = camera_raw_fb_callback; } } camera_cb.initialized = camera_initialized; if (camera_enable && !environ_cb(RETRO_ENVIRONMENT_GET_CAMERA_INTERFACE, &camera_cb)) { if (log_cb) log_cb(RETRO_LOG_ERROR, "camera is not supported.\n"); return false; } } } if (!init_hw_render(allow_vulkan)) return false; /* GL-only checks (they query a GL context / the camera GL paths). */ if (!renderer_is_vulkan()) { #ifdef HAVE_OPENGLES if (camera_enable && camera_cb.caps & (1 << RETRO_CAMERA_BUFFER_RAW_FRAMEBUFFER) && !gl_query_extension("BGRA8888")) { if (log_cb) log_cb(RETRO_LOG_ERROR, "no BGRA8888 support for raw framebuffer, exiting...\n"); return false; } support_unpack_row_length = gl_query_extension("GL_EXT_unpack_subimage"); #else support_unpack_row_length = true; #endif } if (log_cb) log_cb(RETRO_LOG_INFO, "Loaded game!\n"); if (engine_program_cb && engine_program_cb->load_game) engine_program_cb->load_game(info); vr_active = false; memset(&vr_hold_state, 0, sizeof(vr_hold_state)); vr_status_unsupported = false; vr_refused = false; return true; } void retro_unload_game(void) { renderer_dead_state = true; if (convert_buffer) delete[] convert_buffer; convert_buffer = NULL; if (vr_hold_state.active) vr_clear_map(); memset(&vr_hold_state, 0, sizeof(vr_hold_state)); vr_active = false; /* Vulkan resources are released in context_destroy(), while the device * is still alive; nothing to do for them here. */ } unsigned retro_get_region(void) { return RETRO_REGION_NTSC; } bool retro_load_game_special(unsigned type, const struct retro_game_info *info, size_t num) { (void)type; (void)info; (void)num; return false; } size_t retro_serialize_size(void) { return 0; } bool retro_serialize(void *data, size_t size) { (void)data; (void)size; return false; } bool retro_unserialize(const void *data, size_t size) { (void)data; (void)size; return false; } void *retro_get_memory_data(unsigned id) { (void)id; return NULL; } size_t retro_get_memory_size(unsigned id) { (void)id; return 0; } void retro_reset(void) { /* A GL context can simply be re-populated; a Vulkan one must not be * re-initialised behind the frontend's back. */ if (renderer_is_vulkan()) return; if (engine_program_cb && engine_program_cb->context_reset) engine_program_cb->context_reset(); } void retro_cheat_reset(void) {} void retro_cheat_set(unsigned index, bool enabled, const char *code) { (void)index; (void)enabled; (void)code; }