diff options
| author | raysan5 <[email protected]> | 2017-03-14 01:05:22 +0100 |
|---|---|---|
| committer | raysan5 <[email protected]> | 2017-03-14 01:05:22 +0100 |
| commit | 5d1f6616618d52f173a918f6a0378aeae1cb05ad (patch) | |
| tree | ee8661a89e4b9ef591b9394eef6cb65c19ae0740 /examples | |
| parent | 3813722f170066c3113d4a3ef6a21ec53af71034 (diff) | |
| download | raylib-5d1f6616618d52f173a918f6a0378aeae1cb05ad.tar.gz raylib-5d1f6616618d52f173a918f6a0378aeae1cb05ad.zip | |
Remove Oculus support from code
Moved to custom example, now raylib only supports simulated VR
rendering.
Oculus code was too device dependant... waiting for OpenXR.
Diffstat (limited to 'examples')
| -rw-r--r-- | examples/core_oculus_rift.c | 469 |
1 files changed, 461 insertions, 8 deletions
diff --git a/examples/core_oculus_rift.c b/examples/core_oculus_rift.c index eb628cd7..f1b0bd3b 100644 --- a/examples/core_oculus_rift.c +++ b/examples/core_oculus_rift.c @@ -3,7 +3,12 @@ * raylib [core] example - Oculus Rift CV1 * * Compile example using: -* gcc -o $(NAME_PART).exe $(FILE_NAME) -L. -L..\src\external\OculusSDK\LibOVR -lLibOVRRT32_1 -lraylib -lglfw3 -lopengl32 -lgdi32 -std=c99 +* gcc -o $(NAME_PART).exe $(FILE_NAME) -I..\src\external -I..\src\external\OculusSDK\LibOVR\Include / +* -L. -L..\src\external\OculusSDK\LibOVR -lLibOVRRT32_1 -lraylib -lglfw3 -lopengl32 -lgdi32 -std=c99 / +* -Wl,-allow-multiple-definition +* +* #define SUPPORT_OCULUS_RIFT_CV1 / RLGL_OCULUS_SUPPORT +* Enable Oculus Rift CV1 functionality * * This example has been created using raylib 1.5 (www.raylib.com) * raylib is licensed under an unmodified zlib/libpng license (View raylib.h for details) @@ -14,6 +19,95 @@ #include "raylib.h" +#include "glad.h" // Required for: OpenGL types and functions declarations +#include "raymath.h" // Required for: Vector3, Quaternion and Matrix functionality + +#include <string.h> // Required for: memset() +#include <stdlib.h> // Required for: exit() +#include <stdio.h> // required for: vfprintf() +#include <stdarg.h> // Required for: va_list, va_start(), vfprintf(), va_end() + +#define RLGL_OCULUS_SUPPORT // Enable Oculus Rift code +#if defined(RLGL_OCULUS_SUPPORT) + #include "OVR_CAPI_GL.h" // Oculus SDK for OpenGL +#endif + +//---------------------------------------------------------------------------------- +// Defines and Macros +//---------------------------------------------------------------------------------- +// ... + +//---------------------------------------------------------------------------------- +// Types and Structures Definition +//---------------------------------------------------------------------------------- + +// TraceLog message types +typedef enum { INFO = 0, ERROR, WARNING, DEBUG, OTHER } TraceLogType; + +#if defined(RLGL_OCULUS_SUPPORT) +// Oculus buffer type +typedef struct OculusBuffer { + ovrTextureSwapChain textureChain; + GLuint depthId; + GLuint fboId; + int width; + int height; +} OculusBuffer; + +// Oculus mirror texture type +typedef struct OculusMirror { + ovrMirrorTexture texture; + GLuint fboId; + int width; + int height; +} OculusMirror; + +// Oculus layer type +typedef struct OculusLayer { + ovrViewScaleDesc viewScaleDesc; + ovrLayerEyeFov eyeLayer; // layer 0 + //ovrLayerQuad quadLayer; // TODO: layer 1: '2D' quad for GUI + Matrix eyeProjections[2]; + int width; + int height; +} OculusLayer; +#endif + +//---------------------------------------------------------------------------------- +// Global Variables Definition +//---------------------------------------------------------------------------------- +#if defined(RLGL_OCULUS_SUPPORT) +// OVR device variables +static ovrSession session; // Oculus session (pointer to ovrHmdStruct) +static ovrHmdDesc hmdDesc; // Oculus device descriptor parameters +static ovrGraphicsLuid luid; // Oculus locally unique identifier for the program (64 bit) +static OculusLayer layer; // Oculus drawing layer (similar to photoshop) +static OculusBuffer buffer; // Oculus internal buffers (texture chain and fbo) +static OculusMirror mirror; // Oculus mirror texture and fbo +static unsigned int frameIndex = 0; // Oculus frames counter, used to discard frames from chain +#endif + +//---------------------------------------------------------------------------------- +// Module specific Functions Declaration +//---------------------------------------------------------------------------------- +#if defined(RLGL_OCULUS_SUPPORT) +static bool InitOculusDevice(void); // Initialize Oculus device (returns true if success) +static void CloseOculusDevice(void); // Close Oculus device +static void UpdateOculusTracking(Camera *camera); // Update Oculus head position-orientation tracking +static void BeginOculusDrawing(void); // Setup Oculus buffers for drawing +static void EndOculusDrawing(void); // Finish Oculus drawing and blit framebuffer to mirror + +static OculusBuffer LoadOculusBuffer(ovrSession session, int width, int height); // Load Oculus required buffers +static void UnloadOculusBuffer(ovrSession session, OculusBuffer buffer); // Unload texture required buffers +static OculusMirror LoadOculusMirror(ovrSession session, int width, int height); // Load Oculus mirror buffers +static void UnloadOculusMirror(ovrSession session, OculusMirror mirror); // Unload Oculus mirror buffers +static void BlitOculusMirror(ovrSession session, OculusMirror mirror); // Copy Oculus screen buffer to mirror texture +static OculusLayer InitOculusLayer(ovrSession session); // Init Oculus layer (similar to photoshop) +static Matrix FromOvrMatrix(ovrMatrix4f ovrM); // Convert from Oculus ovrMatrix4f struct to raymath Matrix struct +#endif + +static void TraceLog(int msgType, const char *text, ...); + int main() { // Initialization @@ -25,8 +119,9 @@ int main() InitWindow(screenWidth, screenHeight, "raylib [core] example - oculus rift"); - // NOTE: If device is not available, it fallbacks to default device (simulator) - InitVrDevice(HMD_OCULUS_RIFT_CV1); // Init VR device (Oculus Rift CV1) + bool vrDeviceReady = InitOculusDevice(); // Init VR device Oculus Rift CV1 + + if (!vrDeviceReady) InitVrSimulator(HMD_OCULUS_RIFT_CV1); // Init VR simulator if device fails // Define the camera to look into our 3d world Camera camera; @@ -47,10 +142,10 @@ int main() { // Update //---------------------------------------------------------------------------------- - if (IsVrSimulator()) UpdateCamera(&camera); // Update camera (simulator mode) - else if (IsVrDeviceReady()) UpdateVrTracking(&camera); // Update camera with device tracking data - - if (IsKeyPressed(KEY_SPACE)) ToggleVrMode(); // Toggle VR mode + if (!vrDeviceReady) UpdateCamera(&camera); // Update camera (simulator mode) + else UpdateOculusTracking(&camera); // Update camera with device tracking data + + if (IsKeyPressed(KEY_SPACE)) ToggleVrMode(); // Toggle VR mode //---------------------------------------------------------------------------------- // Draw @@ -58,6 +153,9 @@ int main() BeginDrawing(); ClearBackground(RAYWHITE); + + if (vrDeviceReady) BeginOculusDrawing(); + else BeginVrDrawing(); Begin3dMode(camera); @@ -67,6 +165,9 @@ int main() DrawGrid(40, 1.0f); End3dMode(); + + if (vrDeviceReady) EndOculusDrawing(); + else EndVrDrawing(); DrawFPS(10, 10); @@ -76,10 +177,362 @@ int main() // De-Initialization //-------------------------------------------------------------------------------------- - CloseVrDevice(); // Close VR device + if (vrDeviceReady) CloseOculusDevice(); + else CloseVrSimulator(); CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- return 0; } + +//---------------------------------------------------------------------------------- +// Module specific Functions Definition +//---------------------------------------------------------------------------------- + +#if defined(RLGL_OCULUS_SUPPORT) +// Set internal projection and modelview matrix depending on eyes tracking data +static void SetStereoView(int eye, Matrix matProjection, Matrix matModelView) +{ + Matrix eyeProjection = matProjection; + Matrix eyeModelView = matModelView; + + glViewport(layer.eyeLayer.Viewport[eye].Pos.x, layer.eyeLayer.Viewport[eye].Pos.y, + layer.eyeLayer.Viewport[eye].Size.w, layer.eyeLayer.Viewport[eye].Size.h); + + Quaternion eyeRenderPose = (Quaternion){ layer.eyeLayer.RenderPose[eye].Orientation.x, + layer.eyeLayer.RenderPose[eye].Orientation.y, + layer.eyeLayer.RenderPose[eye].Orientation.z, + layer.eyeLayer.RenderPose[eye].Orientation.w }; + QuaternionInvert(&eyeRenderPose); + Matrix eyeOrientation = QuaternionToMatrix(eyeRenderPose); + Matrix eyeTranslation = MatrixTranslate(-layer.eyeLayer.RenderPose[eye].Position.x, + -layer.eyeLayer.RenderPose[eye].Position.y, + -layer.eyeLayer.RenderPose[eye].Position.z); + + Matrix eyeView = MatrixMultiply(eyeTranslation, eyeOrientation); // Matrix containing eye-head movement + eyeModelView = MatrixMultiply(matModelView, eyeView); // Combine internal camera matrix (modelview) wih eye-head movement + + eyeProjection = layer.eyeProjections[eye]; +} + +// Initialize Oculus device (returns true if success) +static bool InitOculusDevice(void) +{ + bool oculusReady = false; + + ovrResult result = ovr_Initialize(NULL); + + if (OVR_FAILURE(result)) TraceLog(WARNING, "OVR: Could not initialize Oculus device"); + else + { + result = ovr_Create(&session, &luid); + if (OVR_FAILURE(result)) + { + TraceLog(WARNING, "OVR: Could not create Oculus session"); + ovr_Shutdown(); + } + else + { + hmdDesc = ovr_GetHmdDesc(session); + + TraceLog(INFO, "OVR: Product Name: %s", hmdDesc.ProductName); + TraceLog(INFO, "OVR: Manufacturer: %s", hmdDesc.Manufacturer); + TraceLog(INFO, "OVR: Product ID: %i", hmdDesc.ProductId); + TraceLog(INFO, "OVR: Product Type: %i", hmdDesc.Type); + //TraceLog(INFO, "OVR: Serial Number: %s", hmdDesc.SerialNumber); + TraceLog(INFO, "OVR: Resolution: %ix%i", hmdDesc.Resolution.w, hmdDesc.Resolution.h); + + // NOTE: Oculus mirror is set to defined screenWidth and screenHeight... + // ...ideally, it should be (hmdDesc.Resolution.w/2, hmdDesc.Resolution.h/2) + + // Initialize Oculus Buffers + layer = InitOculusLayer(session); + buffer = LoadOculusBuffer(session, layer.width, layer.height); + mirror = LoadOculusMirror(session, hmdDesc.Resolution.w/2, hmdDesc.Resolution.h/2); // NOTE: hardcoded... + layer.eyeLayer.ColorTexture[0] = buffer.textureChain; //SetOculusLayerTexture(eyeLayer, buffer.textureChain); + + // Recenter OVR tracking origin + ovr_RecenterTrackingOrigin(session); + + oculusReady = true; + } + } + + return oculusReady; +} + +// Close Oculus device (and unload buffers) +static void CloseOculusDevice(void) +{ + UnloadOculusMirror(session, mirror); // Unload Oculus mirror buffer + UnloadOculusBuffer(session, buffer); // Unload Oculus texture buffers + + ovr_Destroy(session); // Free Oculus session data + ovr_Shutdown(); // Close Oculus device connection +} + +// Update Oculus head position-orientation tracking +static void UpdateOculusTracking(Camera *camera) +{ + frameIndex++; + + ovrPosef eyePoses[2]; + ovr_GetEyePoses(session, frameIndex, ovrTrue, layer.viewScaleDesc.HmdToEyeOffset, eyePoses, &layer.eyeLayer.SensorSampleTime); + + layer.eyeLayer.RenderPose[0] = eyePoses[0]; + layer.eyeLayer.RenderPose[1] = eyePoses[1]; + + // TODO: Update external camera with eyePoses data (position, orientation) + // NOTE: We can simplify to simple camera if we consider IPD and HMD device configuration again later + // it will be useful for the user to draw, lets say, billboards oriented to camera + + // Get session status information + ovrSessionStatus sessionStatus; + ovr_GetSessionStatus(session, &sessionStatus); + + if (sessionStatus.ShouldQuit) TraceLog(WARNING, "OVR: Session should quit..."); + if (sessionStatus.ShouldRecenter) ovr_RecenterTrackingOrigin(session); + //if (sessionStatus.HmdPresent) // HMD is present. + //if (sessionStatus.DisplayLost) // HMD was unplugged or the display driver was manually disabled or encountered a TDR. + //if (sessionStatus.HmdMounted) // HMD is on the user's head. + //if (sessionStatus.IsVisible) // the game or experience has VR focus and is visible in the HMD. +} + +// Setup Oculus buffers for drawing +static void BeginOculusDrawing(void) +{ + GLuint currentTexId; + int currentIndex; + + ovr_GetTextureSwapChainCurrentIndex(session, buffer.textureChain, ¤tIndex); + ovr_GetTextureSwapChainBufferGL(session, buffer.textureChain, currentIndex, ¤tTexId); + + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, buffer.fboId); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, currentTexId, 0); + //glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, buffer.depthId, 0); // Already binded +} + +// Finish Oculus drawing and blit framebuffer to mirror +static void EndOculusDrawing(void) +{ + // Unbind current framebuffer (Oculus buffer) + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + + ovr_CommitTextureSwapChain(session, buffer.textureChain); + + ovrLayerHeader *layers = &layer.eyeLayer.Header; + ovr_SubmitFrame(session, frameIndex, &layer.viewScaleDesc, &layers, 1); + + // Blit mirror texture to back buffer + BlitOculusMirror(session, mirror); +} + +// Load Oculus required buffers: texture-swap-chain, fbo, texture-depth +static OculusBuffer LoadOculusBuffer(ovrSession session, int width, int height) +{ + OculusBuffer buffer; + buffer.width = width; + buffer.height = height; + + // Create OVR texture chain + ovrTextureSwapChainDesc desc = {}; + desc.Type = ovrTexture_2D; + desc.ArraySize = 1; + desc.Width = width; + desc.Height = height; + desc.MipLevels = 1; + desc.Format = OVR_FORMAT_R8G8B8A8_UNORM_SRGB; // Requires glEnable(GL_FRAMEBUFFER_SRGB); + desc.SampleCount = 1; + desc.StaticImage = ovrFalse; + + ovrResult result = ovr_CreateTextureSwapChainGL(session, &desc, &buffer.textureChain); + + if (!OVR_SUCCESS(result)) TraceLog(WARNING, "OVR: Failed to create swap textures buffer"); + + int textureCount = 0; + ovr_GetTextureSwapChainLength(session, buffer.textureChain, &textureCount); + + if (!OVR_SUCCESS(result) || !textureCount) TraceLog(WARNING, "OVR: Unable to count swap chain textures"); + + for (int i = 0; i < textureCount; ++i) + { + GLuint chainTexId; + ovr_GetTextureSwapChainBufferGL(session, buffer.textureChain, i, &chainTexId); + glBindTexture(GL_TEXTURE_2D, chainTexId); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_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); + } + + glBindTexture(GL_TEXTURE_2D, 0); + + /* + // Setup framebuffer object (using depth texture) + glGenFramebuffers(1, &buffer.fboId); + glGenTextures(1, &buffer.depthId); + glBindTexture(GL_TEXTURE_2D, buffer.depthId); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_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, GL_DEPTH_COMPONENT16, buffer.width, buffer.height, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL); + */ + + // Setup framebuffer object (using depth renderbuffer) + glGenFramebuffers(1, &buffer.fboId); + glGenRenderbuffers(1, &buffer.depthId); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, buffer.fboId); + glBindRenderbuffer(GL_RENDERBUFFER, buffer.depthId); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, buffer.width, buffer.height); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, buffer.depthId); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + + return buffer; +} + +// Unload texture required buffers +static void UnloadOculusBuffer(ovrSession session, OculusBuffer buffer) +{ + if (buffer.textureChain) + { + ovr_DestroyTextureSwapChain(session, buffer.textureChain); + buffer.textureChain = NULL; + } + + if (buffer.depthId != 0) glDeleteTextures(1, &buffer.depthId); + if (buffer.fboId != 0) glDeleteFramebuffers(1, &buffer.fboId); +} + +// Load Oculus mirror buffers +static OculusMirror LoadOculusMirror(ovrSession session, int width, int height) +{ + OculusMirror mirror; + mirror.width = width; + mirror.height = height; + + ovrMirrorTextureDesc mirrorDesc; + memset(&mirrorDesc, 0, sizeof(mirrorDesc)); + mirrorDesc.Format = OVR_FORMAT_R8G8B8A8_UNORM_SRGB; + mirrorDesc.Width = mirror.width; + mirrorDesc.Height = mirror.height; + + if (!OVR_SUCCESS(ovr_CreateMirrorTextureGL(session, &mirrorDesc, &mirror.texture))) TraceLog(WARNING, "Could not create mirror texture"); + + glGenFramebuffers(1, &mirror.fboId); + + return mirror; +} + +// Unload Oculus mirror buffers +static void UnloadOculusMirror(ovrSession session, OculusMirror mirror) +{ + if (mirror.fboId != 0) glDeleteFramebuffers(1, &mirror.fboId); + if (mirror.texture) ovr_DestroyMirrorTexture(session, mirror.texture); +} + +// Copy Oculus screen buffer to mirror texture +static void BlitOculusMirror(ovrSession session, OculusMirror mirror) +{ + GLuint mirrorTextureId; + + ovr_GetMirrorTextureBufferGL(session, mirror.texture, &mirrorTextureId); + + glBindFramebuffer(GL_READ_FRAMEBUFFER, mirror.fboId); + glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mirrorTextureId, 0); +#if defined(GRAPHICS_API_OPENGL_33) + // NOTE: glBlitFramebuffer() requires extension: GL_EXT_framebuffer_blit (not available in OpenGL ES 2.0) + glBlitFramebuffer(0, 0, mirror.width, mirror.height, 0, mirror.height, mirror.width, 0, GL_COLOR_BUFFER_BIT, GL_NEAREST); +#endif + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); +} + +// Init Oculus layer (similar to photoshop) +static OculusLayer InitOculusLayer(ovrSession session) +{ + OculusLayer layer = { 0 }; + + layer.viewScaleDesc.HmdSpaceToWorldScaleInMeters = 1.0f; + + memset(&layer.eyeLayer, 0, sizeof(ovrLayerEyeFov)); + layer.eyeLayer.Header.Type = ovrLayerType_EyeFov; + layer.eyeLayer.Header.Flags = ovrLayerFlag_TextureOriginAtBottomLeft; + + ovrEyeRenderDesc eyeRenderDescs[2]; + + for (int eye = 0; eye < 2; eye++) + { + eyeRenderDescs[eye] = ovr_GetRenderDesc(session, eye, hmdDesc.DefaultEyeFov[eye]); + ovrMatrix4f ovrPerspectiveProjection = ovrMatrix4f_Projection(eyeRenderDescs[eye].Fov, 0.01f, 10000.0f, ovrProjection_None); //ovrProjection_ClipRangeOpenGL); + layer.eyeProjections[eye] = FromOvrMatrix(ovrPerspectiveProjection); // NOTE: struct ovrMatrix4f { float M[4][4] } --> struct Matrix + + layer.viewScaleDesc.HmdToEyeOffset[eye] = eyeRenderDescs[eye].HmdToEyeOffset; + layer.eyeLayer.Fov[eye] = eyeRenderDescs[eye].Fov; + + ovrSizei eyeSize = ovr_GetFovTextureSize(session, eye, layer.eyeLayer.Fov[eye], 1.0f); + layer.eyeLayer.Viewport[eye].Size = eyeSize; + layer.eyeLayer.Viewport[eye].Pos.x = layer.width; + layer.eyeLayer.Viewport[eye].Pos.y = 0; + + layer.height = eyeSize.h; //std::max(renderTargetSize.y, (uint32_t)eyeSize.h); + layer.width += eyeSize.w; + } + + return layer; +} + +// Convert from Oculus ovrMatrix4f struct to raymath Matrix struct +static Matrix FromOvrMatrix(ovrMatrix4f ovrmat) +{ + Matrix rmat; + + rmat.m0 = ovrmat.M[0][0]; + rmat.m1 = ovrmat.M[1][0]; + rmat.m2 = ovrmat.M[2][0]; + rmat.m3 = ovrmat.M[3][0]; + rmat.m4 = ovrmat.M[0][1]; + rmat.m5 = ovrmat.M[1][1]; + rmat.m6 = ovrmat.M[2][1]; + rmat.m7 = ovrmat.M[3][1]; + rmat.m8 = ovrmat.M[0][2]; + rmat.m9 = ovrmat.M[1][2]; + rmat.m10 = ovrmat.M[2][2]; + rmat.m11 = ovrmat.M[3][2]; + rmat.m12 = ovrmat.M[0][3]; + rmat.m13 = ovrmat.M[1][3]; + rmat.m14 = ovrmat.M[2][3]; + rmat.m15 = ovrmat.M[3][3]; + + MatrixTranspose(&rmat); + + return rmat; +} +#endif + +// Output a trace log message +// NOTE: Expected msgType: (0)Info, (1)Error, (2)Warning +static void TraceLog(int msgType, const char *text, ...) +{ + va_list args; + va_start(args, text); + + switch (msgType) + { + case INFO: fprintf(stdout, "INFO: "); break; + case ERROR: fprintf(stdout, "ERROR: "); break; + case WARNING: fprintf(stdout, "WARNING: "); break; + case DEBUG: fprintf(stdout, "DEBUG: "); break; + default: break; + } + + vfprintf(stdout, text, args); + fprintf(stdout, "\n"); + + va_end(args); + + if (msgType == ERROR) exit(1); +} + |
