File frame_dumper.hpp
File List > openxr_interface > frame_dumper.hpp
Go to the documentation of this file
#pragma once
// Quest 3 Storage Permissions:
// - For app-specific storage (recommended): No permissions needed
// - Path: /storage/emulated/0/Android/data//files/
//
// To retrieve files via ADB:
// adb pull /storage/emulated/0/Android/data/com.yourapp/files/frame_dumps/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
namespace ILLIXR {
class frame_dumper {
public:
// Initialize the frame dumper
explicit frame_dumper(const std::string& base_path, int max_dumps = 200)
: base_path_(base_path)
, max_dumps_(max_dumps)
, dump_count_(0) {
// Create dump directory
dump_dir_ = base_path_ + "/frame_dumps";
mkdir(dump_dir_.c_str(), 0755);
spdlog::get("illixr")->info("frame_dumper: Initialized, output dir: {}", dump_dir_);
}
// Dump raw NV12 data to a file
void dump_nv12(const uint8_t* data, int width, int height, int eye, uint64_t frame_num) {
if (dump_count_ >= max_dumps_)
return;
std::string filename = dump_dir_ + "/frame_" + std::to_string(frame_num) + "_eye" + std::to_string(eye) + "_" +
std::to_string(width) + "x" + std::to_string(height) + ".nv12";
size_t size = width * height * 3 / 2; // NV12 size
std::ofstream file(filename, std::ios::binary);
if (file.is_open()) {
file.write(reinterpret_cast<const char*>(data), size);
file.close();
spdlog::get("illixr")->info("frame_dumper: Wrote NV12 {}", filename);
dump_count_++;
} else {
spdlog::get("illixr")->error("frame_dumper: Failed to open {}", filename);
}
}
// Dump raw NV12 data and also convert to PPM for easy viewing
void dump_nv12_as_ppm(const uint8_t* data, int width, int height, int eye, uint64_t frame_num) {
if (dump_count_ >= max_dumps_)
return;
// First dump raw NV12
dump_nv12(data, width, height, eye, frame_num);
// Convert to RGB and save as PPM (simple format that's easy to view)
std::vector<uint8_t> rgb(width * height * 3);
nv12_to_rgb(data, rgb.data(), width, height);
std::string filename = dump_dir_ + "/frame_" + std::to_string(frame_num) + "_eye" + std::to_string(eye) + ".ppm";
std::ofstream file(filename, std::ios::binary);
if (file.is_open()) {
// PPM header
file << "P6\n" << width << " " << height << "\n255\n";
file.write(reinterpret_cast<const char*>(rgb.data()), rgb.size());
file.close();
spdlog::get("illixr")->info("frame_dumper: Wrote PPM {}", filename);
}
}
// Dump a GL texture to disk (reads back from GPU)
void dump_gl_texture(GLuint texture_id, int width, int height, int eye, uint64_t frame_num, bool is_external_oes = false) {
if (dump_count_ >= max_dumps_)
return;
if (is_external_oes) {
// External OES textures can't be read back directly
// We need to render to an FBO and read from that
dump_external_oes_texture(texture_id, width, height, eye, frame_num);
} else {
dump_regular_texture(texture_id, width, height, eye, frame_num);
}
}
// Dump the current framebuffer contents
void dump_framebuffer(int width, int height, int eye, uint64_t frame_num) {
if (dump_count_ >= max_dumps_)
return;
std::vector<uint8_t> pixels(width * height * 4); // RGBA
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
GLenum err = glGetError();
if (err != GL_NO_ERROR) {
spdlog::get("illixr")->error("frame_dumper: glReadPixels failed: 0x{:X}", err);
return;
}
// Convert RGBA to RGB for PPM
std::vector<uint8_t> rgb(width * height * 3);
for (int i = 0; i < width * height; i++) {
rgb[i * 3 + 0] = pixels[i * 4 + 0];
rgb[i * 3 + 1] = pixels[i * 4 + 1];
rgb[i * 3 + 2] = pixels[i * 4 + 2];
}
std::string filename = dump_dir_ + "/fb_" + std::to_string(frame_num) + "_eye" + std::to_string(eye) + ".ppm";
std::ofstream file(filename, std::ios::binary);
if (file.is_open()) {
file << "P6\n" << width << " " << height << "\n255\n";
file.write(reinterpret_cast<const char*>(rgb.data()), rgb.size());
file.close();
spdlog::get("illixr")->info("frame_dumper: Wrote framebuffer {}", filename);
dump_count_++;
}
}
// Log texture info without dumping
void log_texture_info(GLuint texture_id, const char* name) {
GLint current_tex = 0;
glGetIntegerv(GL_TEXTURE_BINDING_2D, ¤t_tex);
glBindTexture(GL_TEXTURE_2D, texture_id);
GLint width = 0, height = 0, internal_format = 0;
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width);
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &height);
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &internal_format);
spdlog::get("illixr")->info("Texture '{}' (ID {}): {}x{}, format=0x{:X}", name, texture_id, width, height,
internal_format);
glBindTexture(GL_TEXTURE_2D, current_tex);
}
// Get number of dumps made
int get_dump_count() const {
return dump_count_;
}
// Reset dump counter
void reset_count() {
dump_count_ = 0;
}
// Get the dump directory path
const std::string& get_dump_dir() const {
return dump_dir_;
}
private:
void nv12_to_rgb(const uint8_t* nv12, uint8_t* rgb, int width, int height) {
const uint8_t* y_plane = nv12;
const uint8_t* uv_plane = nv12 + width * height;
for (int j = 0; j < height; j++) {
for (int i = 0; i < width; i++) {
int y_idx = j * width + i;
int uv_idx = (j / 2) * width + (i & ~1); // UV is half resolution, interleaved
int Y = y_plane[y_idx];
int U = uv_plane[uv_idx];
int V = uv_plane[uv_idx + 1];
// BT.601 conversion (commonly used)
int C = Y - 16;
int D = U - 128;
int E = V - 128;
int R = (298 * C + 409 * E + 128) >> 8;
int G = (298 * C - 100 * D - 208 * E + 128) >> 8;
int B = (298 * C + 516 * D + 128) >> 8;
// Clamp
R = R < 0 ? 0 : (R > 255 ? 255 : R);
G = G < 0 ? 0 : (G > 255 ? 255 : G);
B = B < 0 ? 0 : (B > 255 ? 255 : B);
int rgb_idx = (j * width + i) * 3;
rgb[rgb_idx + 0] = static_cast<uint8_t>(R);
rgb[rgb_idx + 1] = static_cast<uint8_t>(G);
rgb[rgb_idx + 2] = static_cast<uint8_t>(B);
}
}
}
void dump_regular_texture(GLuint texture_id, int width, int height, int eye, uint64_t frame_num) {
// Create FBO to read from texture
GLuint fbo;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture_id, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) {
dump_framebuffer(width, height, eye, frame_num);
} else {
spdlog::get("illixr")->error("frame_dumper: Framebuffer incomplete for texture read");
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &fbo);
}
void dump_external_oes_texture(GLuint texture_id, int width, int height, int eye, uint64_t frame_num) {
// For external OES textures, we need to render to an intermediate texture
// This requires setting up a simple shader - for now just log that we can't dump directly
spdlog::get("illixr")->warn("frame_dumper: External OES texture {} - use dump_framebuffer() after rendering instead",
texture_id);
// Alternative: dump framebuffer after the texture has been rendered
// The caller should use dump_framebuffer() after render_eye() instead
}
std::string base_path_;
std::string dump_dir_;
int max_dumps_;
int dump_count_;
};
// Get the app's external files directory (no permissions needed)
inline std::string get_app_files_dir(android_app* app) {
if (!app || !app->activity) {
return "";
}
// Get JNI env
JNIEnv* env = nullptr;
app->activity->vm->AttachCurrentThread(&env, nullptr);
if (!env) {
return "";
}
// Get activity class
jclass activity_class = env->GetObjectClass(app->activity->clazz);
// Get getExternalFilesDir method
jmethodID get_external_files_dir =
env->GetMethodID(activity_class, "getExternalFilesDir", "(Ljava/lang/String;)Ljava/io/File;");
// Call getExternalFilesDir(null)
jobject file_obj = env->CallObjectMethod(app->activity->clazz, get_external_files_dir, nullptr);
if (!file_obj) {
env->DeleteLocalRef(activity_class);
return "";
}
// Get File.getAbsolutePath()
jclass file_class = env->GetObjectClass(file_obj);
jmethodID get_path = env->GetMethodID(file_class, "getAbsolutePath", "()Ljava/lang/String;");
jstring path_str = (jstring) env->CallObjectMethod(file_obj, get_path);
const char* path_chars = env->GetStringUTFChars(path_str, nullptr);
std::string result(path_chars);
env->ReleaseStringUTFChars(path_str, path_chars);
env->DeleteLocalRef(path_str);
env->DeleteLocalRef(file_class);
env->DeleteLocalRef(file_obj);
env->DeleteLocalRef(activity_class);
return result;
}
} // namespace ILLIXR