save code

This commit is contained in:
xsl
2026-04-26 00:12:56 +08:00
parent 50680ee854
commit c5ec7c7dc5
22 changed files with 1170 additions and 216 deletions
+15 -1
View File
@@ -60,7 +60,10 @@ void Application::initWindow()
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
window = glfwCreateWindow(480, 480, "Vulkan", nullptr, nullptr);
// Windows 端仅用于本机调试。改成接近手机的竖屏比例(540x960),
// 这样能直观验证「画布居中正方形 + 上下黑边 letterbox」的效果。
// Android 端走另一条分支,窗口尺寸由系统 surface 决定。
window = glfwCreateWindow(540, 960, "Vulkan", nullptr, nullptr);
#else
#endif
@@ -134,6 +137,17 @@ void Application::initVulkan()
}
}
// ⚠️ Android 路径不要调这个!这个函数销毁的是「第二阶段 init」期间创建的所有
// 资源(含 renderPass / graphicsPipeline / pipelineLayout),但**没有**重置
// _applicationInited / _sceondInited,也没有把销毁的句柄置 VK_NULL_HANDLE。
//
// 之前 main.cpp android_main 退出时调过它,结果用户在主界面切换镜头再进入
// MakeupActivity 时 onWindowInit 看到 _applicationInited=1 走 reinit 复用路径
// 引用悬空 renderPassvalidation layer 检测出 use-after-free 触发 SEGV。
//
// Android 上的窗口生命周期是 cleanupForWindowLost() + reinitForNewWindow()
// renderPass / pipeline 跨 NativeActivity 实例由 g_Application 单例持有。
// 这个函数目前只保留给 vulkan/main.cpp 那个独立 desktop 入口的进程退出路径。
void Application::cleanupSecondInit()
{
vkDestroySwapchainKHR(device, swapChain, nullptr);
+244 -40
View File
@@ -1,4 +1,4 @@
#include "FaceApp.h"
#include "FaceApp.h"
#include "hardcode_data.h"
#include "lodepng.h"
#include <sstream>
@@ -47,10 +47,13 @@ void FaceApp::Stop()
void ReceiveFacePoint(float* pos, int pointCount, int width, int height)
{
FaceApp* self = FaceApp::Get();
if (self != nullptr)
if (self == nullptr)
{
FaceApp::Get()->update_face_vertex_buffer(pos, pointCount);
FACE_DBG_LOG_THROTTLED("ReceiveFacePoint.noFaceApp",
"ReceiveFacePoint dropped: FaceApp::Get() == nullptr");
return;
}
self->update_face_vertex_buffer(pos, pointCount);
}
// 定义帧数据结构
@@ -60,10 +63,12 @@ struct FrameData {
int height;
int rowStride;
size_t dataSize;
int rotation; // CameraX 给的 rotationDegrees,用于 shader 端 UV 旋转
bool mirrorX; // 前置摄像头时为 trueshader 在 gl_Position.x 翻转一次
FrameData(uint8_t* d, int w, int h, int rs, size_t ds)
: data(nullptr), width(w), height(h), rowStride(rs), dataSize(ds) {
// 深拷贝数据
FrameData(uint8_t* d, int w, int h, int rs, size_t ds, int rot, bool mx)
: data(nullptr), width(w), height(h), rowStride(rs), dataSize(ds),
rotation(rot), mirrorX(mx) {
if (d && ds > 0) {
data = new uint8_t[dataSize];
memcpy(data, d, dataSize);
@@ -77,18 +82,16 @@ struct FrameData {
}
}
// 禁止拷贝构造和赋值(使用移动语义)
FrameData(const FrameData&) = delete;
FrameData& operator=(const FrameData&) = delete;
// 移动构造
FrameData(FrameData&& other) noexcept
: data(other.data), width(other.width), height(other.height),
rowStride(other.rowStride), dataSize(other.dataSize) {
rowStride(other.rowStride), dataSize(other.dataSize),
rotation(other.rotation), mirrorX(other.mirrorX) {
other.data = nullptr;
}
// 移动赋值
FrameData& operator=(FrameData&& other) noexcept {
if (this != &other) {
if (data) delete[] data;
@@ -97,6 +100,8 @@ struct FrameData {
height = other.height;
rowStride = other.rowStride;
dataSize = other.dataSize;
rotation = other.rotation;
mirrorX = other.mirrorX;
other.data = nullptr;
}
return *this;
@@ -108,7 +113,7 @@ std::queue<FrameData> frameQueue;
std::mutex queueMutex;
const int MAX_QUEUE_SIZE = 4;
void TextureLoadProcessWithVulkan(uint8_t* data, int width, int height, int rowStride, size_t dataSize)
void TextureLoadProcessWithVulkan(uint8_t* data, int width, int height, int rowStride, size_t dataSize, int rotation, bool mirrorX)
{
if (!FaceApp::Get()->isInited()) {
return;
@@ -116,24 +121,25 @@ void TextureLoadProcessWithVulkan(uint8_t* data, int width, int height, int rowS
std::lock_guard<std::mutex> lock(queueMutex);
// 添加新帧数据到队列
frameQueue.emplace(data, width, height, rowStride, dataSize);
frameQueue.emplace(data, width, height, rowStride, dataSize, rotation, mirrorX);
// 如果队列大小达到阈值,开始处理最旧的一帧
if (frameQueue.size() >= MAX_QUEUE_SIZE) {
Texture& tex_bg = FaceApp::Get()->tex_bg;
FrameData& oldestFrame = frameQueue.front();
FaceApp::Get()->processWithVulkan(
// 走 FaceApp::processCameraFrame:相比 base::processWithVulkan,它多做两件
// 事 —— ① 检测帧尺寸变化时 destroy/recreate tex_bg(不同手机或 CameraX
// 重建 session 后第一帧尺寸可能与之前不同);② 缓存 raw_aspect / rotation
// / mirrorX 到 m_cameraAspect / m_cameraRotation / m_mirrorX,由 render()
// 推到 push constant。
FaceApp::Get()->processCameraFrame(
oldestFrame.data,
oldestFrame.width,
oldestFrame.height,
oldestFrame.rowStride,
oldestFrame.dataSize,
tex_bg,
false,
FaceApp::Get()->commandPool,
"data_from_camera"
oldestFrame.rotation,
oldestFrame.mirrorX
);
frameQueue.pop();
@@ -298,15 +304,21 @@ void FaceApp::create_face_pipelines()
VkPipelineRasterizationStateCreateInfo rasterization_state{};
rasterization_state.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rasterization_state.polygonMode = VK_POLYGON_MODE_FILL;
// ★ 关键:face 必须 NONE。原因:
// 1. face mesh 是 2D 平面网格(texture.vert 让所有顶点 z=0.5),不存在
// 自遮挡,没必要 cull;
// 2. 前置摄像头镜子效果是在 vertex shader 里通过 NDC.x 翻转实现的
// `position.x *= (1.0 - 2.0 * pc.mirror_x)`)。X 翻转会把所有三角
// 形的卷绕方向从 CCW 变 CW。如果这里 cullMode=BACK_BIT + frontFace=
// CCW,前置时整张脸会被 cull,face 完全不可见——这正是历史踩坑点。
// 所以请勿改回 BACK_BIT。如果未来要再加 cull,必须配两套 pipeline(前置
// 用 CW、后置用 CCW),或在前置时改成 frontFace=CW,否则一定会复现这个 bug。
rasterization_state.cullMode = VK_CULL_MODE_NONE;
rasterization_state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
rasterization_state.flags = 0;
rasterization_state.depthClampEnable = VK_FALSE;
rasterization_state.lineWidth = 1.0f;
rasterization_state.cullMode = VK_CULL_MODE_BACK_BIT;
rasterization_state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
VkPipelineColorBlendAttachmentState colorBlendAttachment{};
@@ -333,13 +345,16 @@ void FaceApp::create_face_pipelines()
colorBlending.attachmentCount = 1;
colorBlending.pAttachments = &colorBlendAttachment;
// Note: Using reversed depth-buffer for increased precision, so Greater depth values are kept
// face 是 2D 平面网格(texture.vert 让顶点 z=0.5 一致),完全不需要 depth
// test/write。bg 管线本来也是关 depth 的;保持两边一致,让"先 bg 再 face"
// 的覆盖顺序由 draw 顺序而非 depth 决定,避免 reversed-depthGREATER+
// renderpass 没显式 clear depth attachment 时引发的"face fragment 全部
// 被 depth test fail"的隐性 bug。
VkPipelineDepthStencilStateCreateInfo depth_stencil_state = {};
depth_stencil_state.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
depth_stencil_state.depthTestEnable = VK_TRUE;
depth_stencil_state.depthWriteEnable = VK_TRUE;
depth_stencil_state.depthCompareOp = VK_COMPARE_OP_GREATER;
depth_stencil_state.depthTestEnable = VK_FALSE;
depth_stencil_state.depthWriteEnable = VK_FALSE;
depth_stencil_state.depthCompareOp = VK_COMPARE_OP_ALWAYS;
depth_stencil_state.front = depth_stencil_state.back;
depth_stencil_state.back.compareOp = VK_COMPARE_OP_ALWAYS;
@@ -617,6 +632,8 @@ void FaceApp::setup_descriptor_set()
void FaceApp::update_descriptor_set(vector<Texture>& texs, vector<VkDescriptorSet>& descriptSet)
{
FACE_DBG_LOG("update_descriptor_set: texs.size=%zu descriptSet.size=%zu",
texs.size(), descriptSet.size());
for (int i = 0; i < texs.size(); ++i)
{
if (texs[i].image == VK_NULL_HANDLE)
@@ -697,21 +714,84 @@ void FaceApp::render(VkCommandBuffer commandBuffer, long long frameTime)
//Application::render(commandBuffer);
// === 分辨率适配:每帧把"业务参数"缩放到当前屏幕物理像素 ===
// 业务层(MotionManager / InitArg)传进来的 radius / offset_x / offset_y 一直
// 都是按"480x480 设计画布"给的,比如 radius=240 表示在 480 屏上半径 240px
// (即 50% 短边)。现在屏幕变成任意分辨率后,要保持视觉一致:
// 实际像素 = 业务值 * (画布短边 / 480)
// 同时把 canvas/screen 几何写进 push constantshader 用它们:
// 1) 把"画布 NDC ∈ [-1,+1]" 缩到屏幕中央正方形(letterbox);
// 2) 把 fragment 里的圆心从写死的 (240,240) 改成屏幕真实中心。
//
// 注意 pushConstants 本身保留业务原始值不动(SetInitArg 设进来的),
// 这里只把缩放后的副本 pc 推给 GPU。下面 ux/uy 仍然写回 pushConstants
// 是因为它们和分辨率无关,是 motion 帧的纹理坐标。
updateCanvasMetrics();
PushConstants pc = pushConstants;
const float k = m_canvasSize / 480.0f;
pc.radius *= k;
pc.offset_x *= k;
pc.offset_y *= k;
pc.canvas_size = m_canvasSize;
pc.screen_w = m_screenW;
pc.screen_h = m_screenH;
// 相机帧元信息:由 processCameraFrame 在每帧上传纹理之前更新。第一帧之前
// (还没收到相机数据时)m_cameraAspect/m_cameraRotation 是构造函数设的
// 4:3 + 90° 默认值 —— 这两个值正是 CameraX RATIO_4_3 + 大部分 Android 后置
// 主摄竖屏的典型组合,所以即使首帧 bg 用默认值渲染也不会出现明显错位。
pc.camera_aspect = m_cameraAspect;
pc.camera_rotation = m_cameraRotation;
// mirror_x: 前置摄像头时 = 1.0,所有顶点 shader 在最后输出 gl_Position 前
// 把 NDC.x 翻转一次,达到镜子效果(用户右手 → 屏幕左侧)。bg 与 face 都
// 同步翻转,对齐保持。后置时 = 0.0,shader 公式 (1 - 2 * mirror_x) = 1 短路。
pc.mirror_x = m_mirrorX;
vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipelineLayout_bg, 0, 1, &m_descriptor_set_bg, 0, NULL);
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, m_graphicsPipeline_bg);
vkCmdPushConstants(commandBuffer, m_pipelineLayout_bg, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(pushConstants), &pushConstants);
// bg.frag 也读 push constantr/g/b/radius/screen_w/screen_h)所以 stage 必须
// 同时包含 fragment;老代码这里只写了 VERTEX_BIT,是个隐藏的 spec 违规。
vkCmdPushConstants(commandBuffer, m_pipelineLayout_bg, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(pc), &pc);
vkCmdDraw(commandBuffer, 6, 1, 0, 0);
// 诊断:把 face 渲染所有"前置门"状态打成一行节流日志。一旦 face 没出现,
// 直接看这条就能知道断在哪一关:
// _isChangeMostion=0 → 业务还没调 PlayMotionList / changeMotionList
// _curMotions=0 → motion list 是空的(getMotionByName 全 miss
// _curMotionIndex 越界 → 业务逻辑或 callback 状态机错了
// delta_last_update >= 2000 → mediapipe / passDataToNative 链路 2s 没动静
const long long now_ms = getCurrentTimeMillis();
const long long delta_lu = now_ms - (long long)last_update_time;
FACE_DBG_LOG_THROTTLED("render.face.gate",
"render.face gate _isChangeMostion=%d _curMotions=%zu"
" _curMotionIndex=%d _curFrameIndex=%d delta_lastUpdate=%lldms"
" _playMotion=%d",
(int)_isChangeMostion, _curMotions.size(),
(int)_curMotionIndex, (int)_curFrameIndex,
delta_lu, (int)_playMotion);
if (!_isChangeMostion)
{
return;
}
if (_curMotions.empty())
{
FACE_DBG_LOG_THROTTLED("render.face.emptyMotions",
"render.face dropped: _isChangeMostion=1 but _curMotions is empty");
return;
}
string curMotionName = _curMotions[_curMotionIndex].name;
int loadMotionIndex = motion_list_map[curMotionName];
// face draw 前关键参数节流日志,保留少量"任何时候挂了能立刻判断断点"的字段。
FACE_DBG_LOG_THROTTLED("render.face.preDraw",
"render.face preDraw motion='%s' loadMotionIdx=%d obj_idx=%zu mirror=%.1f",
curMotionName.c_str(), loadMotionIndex, obj_indices.size(), pc.mirror_x);
//if (cur_left)
//{
vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pipelineLayout, 0, 1, &m_descriptor_sets_left[loadMotionIndex], 0, NULL);
@@ -730,12 +810,14 @@ void FaceApp::render(VkCommandBuffer commandBuffer, long long frameTime)
float uy = _curMotions[_curMotionIndex].frames[_curFrameIndex].y;
pushConstants.ux = ux;
pushConstants.uy = uy;
pc.ux = ux;
pc.uy = uy;
}
}
//fsValues[1] = 0;
//fsValues[2] = 360;
vkCmdPushConstants(commandBuffer, m_pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT| VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(pushConstants), &pushConstants);
vkCmdPushConstants(commandBuffer, m_pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT| VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(pc), &pc);
//vkCmdPushConstants(commandBuffer, m_pipelineLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(fsValues), fsValues);
@@ -751,7 +833,16 @@ void FaceApp::render(VkCommandBuffer commandBuffer, long long frameTime)
#else
if (getCurrentTimeMillis() - last_update_time < 2000)
{
vkCmdDrawIndexed(commandBuffer, obj_indices.size(), 1, 0, 0, 0);
FACE_DBG_LOG_THROTTLED("render.face.draw",
"render.face vkCmdDrawIndexed indices=%zu motion=%s frame=%d",
obj_indices.size(), curMotionName.c_str(), (int)_curFrameIndex);
vkCmdDrawIndexed(commandBuffer, obj_indices.size(), 1, 0, 0, 0);
}
else
{
FACE_DBG_LOG_THROTTLED("render.face.heartbeatStale",
"render.face SKIP vkCmdDrawIndexed: delta_last_update=%lldms (>= 2000ms)",
(long long)(getCurrentTimeMillis() - last_update_time));
}
#endif
}
@@ -964,11 +1055,14 @@ void FaceApp::recreatePipelinesForSwapchain()
void FaceApp::update_uniform_buffers()
{
uint32_t width = 480;
uint32_t height = 480;
float zoom = 2;
// 注意:当前 vertex shader (texture.vert) 实际并没有使用 ubo.projection 把模型
// 投影到屏幕——它直接拿 inPos.xy*2-1 当 NDC 用。所以下面 aspect 取 1.0 只是
// 为了让 ubo 不再依赖原本写死的 480x480 输入分辨率;要真正影响屏幕显示比例的
// 是 push constant 里的 canvas_size / screen_w / screen_h,由 render() 写入。
const float aspect = 1.0f;
const float zoom = 2.0f;
// Vertex shader
ubo_vs.projection = glm::perspective(glm::radians(60.0f), static_cast<float>(width) / static_cast<float>(height), 0.001f, 256.0f);
ubo_vs.projection = glm::perspective(glm::radians(60.0f), aspect, 0.001f, 256.0f);
glm::mat4 view_matrix = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, zoom));
ubo_vs.model = view_matrix * glm::translate(glm::mat4(1.0f), camera_pos);
@@ -981,6 +1075,26 @@ void FaceApp::update_uniform_buffers()
memcpy(uniform_buffer_mapped, &ubo_vs, sizeof(ubo_vs));
}
void FaceApp::updateCanvasMetrics()
{
// 把"屏幕中央 min(w,h) 的正方形"作为设计画布。画布外的部分会落在
// shader 计算的 NDC ∈ [-1,+1] 之外,被 GPU 自动裁剪为黑色 letterbox。
//
// swapChainExtent 来自 Application::createSwapChain 里的
// surface_capabilities.currentExtent,即当前窗口/Surface 的真实分辨率。
// 任何 swapchain 重建(onWindowLost -> onWindowInit)都会刷新它,所以
// 这里每帧重算是廉价且自洽的。
m_screenW = (float)swapChainExtent.width;
m_screenH = (float)swapChainExtent.height;
m_canvasSize = (m_screenW < m_screenH) ? m_screenW : m_screenH;
if (m_canvasSize <= 0.0f) {
// swapchain 还没初始化 / 已销毁时给个安全值,避免 shader 除零。
m_canvasSize = 480.0f;
m_screenW = 480.0f;
m_screenH = 480.0f;
}
}
void FaceApp::createVertexBuffer()
{
VkDeviceSize vertexBufferSize = sizeof(TextureLoadingVertexStructure) * obj_vertices.size();
@@ -1029,17 +1143,85 @@ void FaceApp::uploadVertexData() {
void FaceApp::processCameraFrame(uint8_t* data, int width, int height,
int rowStride, size_t dataSize, int rotation,
bool mirrorX)
{
if (!isInited() || data == nullptr || width <= 0 || height <= 0) {
return;
}
// 1) rotation 收敛到 0/90/180/270。CameraX 文档保证只会给这四个值,
// 但理论上 ((rotation % 360) + 360) % 360 更鲁棒。
int rotNorm = ((rotation % 360) + 360) % 360;
if (rotNorm != 0 && rotNorm != 90 && rotNorm != 180 && rotNorm != 270) {
// 非法值时保守用 90(与原 hardcode 一致)。
rotNorm = 90;
}
// 2) 元信息更新(无锁单写者:仅相机分析线程写,render 线程读)。
// 放在 sizeChanged 判断前后都安全:render() 每帧重新拷到 push constant。
m_cameraAspect = (float)width / (float)height;
m_cameraRotation = (float)rotNorm;
m_mirrorX = mirrorX ? 1.0f : 0.0f;
// 3) 检查帧尺寸是否与现有 tex_bg 一致。
// 场景:CameraX session 重建(如 Activity onResume)后第一帧分辨率可能
// 不同;或者 setTargetResolution 在不同设备上落地为不同尺寸。
bool sizeChanged = (tex_bg.image != VK_NULL_HANDLE)
&& (tex_bg.width != width || tex_bg.height != height);
if (!sizeChanged) {
// 快路径(每帧):直接走 base 标准上传路径。第一次 image == VK_NULL_HANDLE
// base 会自动 createTexture(width, height, ...) 自适应分辨率;之后每帧
// image 已存在,直接走 staging buffer 拷贝。
processWithVulkan(data, width, height, rowStride, dataSize,
tex_bg, false, commandPool, "data_from_camera");
return;
}
// 慢路径(罕见,整个 session 通常 1 次):尺寸变化 → 重建 image + 刷新
// descriptor set。整个 ceremony 必须严格串行:
// ① createTextureMtx 与 drawFrame / 其它 processWithVulkan 互斥
// FaceApp::drawFrame 进 Application::drawFrame 之前会持此锁,所以
// 我们持锁期间渲染线程被阻塞,不会有新的 cmdbuf 绑定 m_descriptor_set_bg
// 并 submit)。
// ② vkDeviceWaitIdle 等待所有"已 submit 但未完成"的 cmdbuf 跑完,确保
// 销毁旧 image/view 时 GPU 已经不再采样它。
// ③ destroy → create → updateTexture → refresh_descriptor_set_bg
// 全部在锁内完成,期间 m_descriptor_set_bg 处于"指向无效 view"的中间
// 状态,但因为渲染线程被锁阻塞,这个中间状态对 GPU 不可见。
// 注意不能调 base::processWithVulkan,那会再次 lock createTextureMtx 死锁;
// 直接调 createTexture + updateTexture,这两个 base 方法不持 createTextureMtx。
#ifndef _WIN32
DebugLog::log("processCameraFrame: bg size changed %dx%d -> %dx%d, recreate texture",
tex_bg.width, tex_bg.height, width, height);
#endif
std::unique_lock<std::mutex> lk_tex(createTextureMtx);
{
std::lock_guard<std::mutex> lk_pool(poolQueueMtx);
vkDeviceWaitIdle(device);
}
destroyTexture(device, tex_bg);
createTexture(device, physicalDevice, width, height, tex_bg, false,
"data_from_camera", dataSize);
updateTexture(device, physicalDevice, commandPool, graphicsQueue,
data, width, height, rowStride, dataSize, tex_bg);
refresh_descriptor_set_bg();
}
void FaceApp::update_face_vertex_buffer(float* pos, int pointCount)
{
if(!isInited())
{
FACE_DBG_LOG_THROTTLED("update_face_vertex_buffer.notInited",
"update_face_vertex_buffer dropped: !isInited()");
return;
}
std::lock_guard<std::mutex> lock(mtx_point);
last_update_time = getCurrentTimeMillis();
for (int i = 0; i < obj_vertices.size(); ++i)
{
@@ -1251,8 +1433,12 @@ void FaceApp::setup_descriptor_set_layout_bg()
pipeline_layout_create_info.pSetLayouts = &m_descriptorSetLayout_bg;
// bg.frag 同样要读 push constantr/g/b/radius,以及分辨率适配重构后的
// screen_w/screen_h),所以 stage 必须包含 FRAGMENT_BIT。老代码这里只写了
// VERTEX_BIT,按 Vulkan spec 是非法的(验证层会 warning,部分驱动会读到
// 未定义内容),借这次重构修掉。
VkPushConstantRange pushConstantRanges[1];
pushConstantRanges[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
pushConstantRanges[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
pushConstantRanges[0].offset = 0;
pushConstantRanges[0].size = sizeof(PushConstants);
@@ -1273,7 +1459,18 @@ void FaceApp::setup_descriptor_set_bg()
VK_CHECK(vkAllocateDescriptorSets(device, &alloc_info, &m_descriptor_set_bg));
refresh_descriptor_set_bg();
}
void FaceApp::refresh_descriptor_set_bg()
{
if (m_descriptor_set_bg == VK_NULL_HANDLE) {
return;
}
if (tex_bg.view == VK_NULL_HANDLE || tex_bg.sampler == VK_NULL_HANDLE) {
// 还没 loadTexture / processCameraFrame 过,没法绑定。
return;
}
VkDescriptorImageInfo image_descriptor;
image_descriptor.imageView = tex_bg.view;
@@ -1288,9 +1485,7 @@ void FaceApp::setup_descriptor_set_bg()
write_descriptor_set.pImageInfo = &image_descriptor;
write_descriptor_set.descriptorCount = 1;
std::vector<VkWriteDescriptorSet> write_descriptor_sets = { write_descriptor_set };
vkUpdateDescriptorSets(device, static_cast<uint32_t>(write_descriptor_sets.size()), write_descriptor_sets.data(), 0, NULL);
vkUpdateDescriptorSets(device, 1, &write_descriptor_set, 0, NULL);
}
void FaceApp::destroyTexture(VkDevice device, Texture& texture) {
@@ -1604,6 +1799,7 @@ void FaceApp::loadMotionThread()
string name = m.name;
if (motion_list_map.find(name) != motion_list_map.end())
{
FACE_DBG_LOG("loadMotionThread: skip already-loaded motion='%s'", name.c_str());
continue;
}
m_texs_left.push_back(Texture());
@@ -1620,6 +1816,10 @@ void FaceApp::loadMotionThread()
#endif // _WIN32
motion_list_map[name] = m_texs_left.size() - 1;
_loadMotionMap[name] = m;
FACE_DBG_LOG("loadMotionThread: loaded motion='%s' idx=%zu tex_image=%p tex_view=%p tex_sampler=%p frames=%zu",
name.c_str(), m_texs_left.size() - 1,
(void*)newTex.image, (void*)newTex.view, (void*)newTex.sampler,
m.frames.size());
}
// for (int i = 0; i < _loadMotions.size(); ++i)
@@ -1680,7 +1880,11 @@ void FaceApp::changeMotionList(vector<string> motions, AnimationFinishedCallback
vector<Motion> motion_list;
for (auto ms : motions) {
Motion m = getMotionByName(ms);
motion_list.push_back(m);
auto it = motion_list_map.find(ms);
int idx = (it == motion_list_map.end()) ? -1 : (int)it->second;
FACE_DBG_LOG("changeMotionList: requested='%s' idx=%d frames=%zu (-1=NOT FOUND)",
ms.c_str(), idx, m.frames.size());
motion_list.push_back(m);
}
_curMotions = motion_list;
+86
View File
@@ -49,6 +49,15 @@ struct InitArg
};
// 推送给 shader 的常量。前 9 个 float 是「业务原始值」,单位以 480x480 设计画布
// 为基准(向后兼容):业务给 radius=240 / offset_x=200 / offset_y=-200 等都按这个画布
// 思考。后 3 个 float 是 SDK 在每次 render() 时根据当前 swapchain 尺寸计算并写入的
// 「画布几何」,shader 通过它把「画布坐标」映射到「屏幕坐标」并把圆形 mask 中心
// 锚定在屏幕中心。
//
// ★ 字段顺序和命名必须和 GLSL 中的 layout(push_constant) 块严格一致,否则会读到
// 错位数据。修改时四个 shader (bg.vert / bg.frag / texture.vert / texture.frag)
// 都要同步更新。
struct PushConstants {
float zoom = 0.5f;
float r = 0;
@@ -59,6 +68,32 @@ struct PushConstants {
float uy = 0;
float offset_x = 0;
float offset_y = 0;
// 由 SDK 在 FaceApp::render() 内每帧写入,业务无感。
// canvas_size = min(swapchain.w, swapchain.h),单位:物理像素
// screen_w / screen_h = swapchain 当前实际尺寸,单位:物理像素
// 设计画布始终居中铺在屏幕中央正方形区域;外围是 letterbox 黑边。
float canvas_size = 480.0f;
float screen_w = 480.0f;
float screen_h = 480.0f;
// 相机帧元信息,由 SDK 在收到第一帧 / 帧尺寸变化时更新。shader 用来:
// 1) 把相机帧(任意 raw aspect)正确 cover 到 1:1 画布;
// 2) 按 camera_rotation 旋转 UV,让画面在屏幕上正立——这是不同手机的
// sensor orientation 不同导致画面侧着 / 倒着的根因。
// camera_aspect = camera_raw_w / camera_raw_h(典型 4:3 = 1.333…)
// camera_rotation = CameraX 的 ImageInfo.getRotationDegrees()
// 表示「图像顺时针旋转多少度才能正立」,离散 0/90/180/270。
// 默认值 4/3 + 90° 是兼容老 hardcode 行为(大部分 Android 手机后置主摄竖屏)。
float camera_aspect = 4.0f / 3.0f;
float camera_rotation = 90.0f;
// 镜像标志:1.0 表示前置摄像头需要"镜子效果",所有顶点 shader 会把
// gl_Position.x 翻转一次(NDC 水平翻转)。配合 FaceLandmarkerHelper 在
// bitmap 阶段做的 postScale(-1,1)bg 与 face 同步翻转、互相对齐。
// 后置摄像头此值为 0.0,shader 短路无开销。
// 用 float 而不是 bool/int 是为了和 Vulkan push constant 4-byte 对齐 + GLSL
// 端 layout 简单一致;shader 里 `1.0 - 2.0 * mirror_x` 实现无分支翻转。
float mirror_x = 0.0f;
};
using Callback = std::function<void()>;
@@ -163,13 +198,64 @@ private:
//float myFloatValue = 1.5f;
// 「业务原始值」缓存:SetInitArg 把 InitArg 拷到这里,render() 每帧读出来再
// 缩放 + 注入屏幕几何。注意 ux/uy 由 motion 帧每帧写入,也保留在这里。
PushConstants pushConstants;
// ===== 屏幕分辨率适配 =====
// 原 SDK 把屏幕写死 480x480,所有业务参数都按 480 像素画布给。现在改成支持
// 任意分辨率:把"屏幕中央 min(w,h) 边长的正方形"作为设计画布,画布外是黑边。
// 业务参数语义不变,SDK 在 render() 里按 (canvas_size / 480) 缩放成屏幕物理
// 像素后再推给 shader。
// m_canvasSize = min(swapChainExtent.w, swapChainExtent.h)
// m_screenW / m_screenH = swapChainExtent.w / swapChainExtent.h
// updateCanvasMetrics() 由 render() 在每次 vkCmdPushConstants 之前调用。
// 因为 swapchain 重建会走 onWindowLost -> onWindowInit ->
// recreatePipelinesForSwapchain,而 render() 永远在 drawFrame 里被调,所以
// 这套数值天然会跟随 swapchain 变化。
float m_canvasSize = 480.0f;
float m_screenW = 480.0f;
float m_screenH = 480.0f;
void updateCanvasMetrics();
// ===== 相机帧元信息适配 =====
// 不同手机摄像头物理分辨率和 sensor orientation 都不一样,CameraX 给的图像
// 既不一定是精确 4:3,也不一定是「正立」朝向。SDK 在这里缓存最近一帧的
// (raw_aspect, rotation)render() 时塞进 push constant 让 shader 自己处理:
// - bg.vert 按 m_cameraRotation 旋转 UV,让画面在屏幕上方向正确
// - bg.vert 按 m_cameraAspect 自适应 cover 画布,避免拉扁/拉长
// 由 processCameraFrame() 根据每次相机帧实际 width / height / rotation 更新。
// 第一帧之前用安全默认值(4:3 + 90°,对应大部分 Android 后置主摄竖屏行为)。
float m_cameraAspect = 4.0f / 3.0f;
float m_cameraRotation = 90.0f;
// 镜像标志缓存:与 m_cameraAspect 同写者(相机分析线程),由 render() 拷到
// push constant 的 mirror_x 字段。前置=1.0,后置=0.0。
float m_mirrorX = 0.0f;
public:
// JNI 入口(main.cpp)调用,用来在每帧上传相机纹理之前同步元信息。
// 内部会:
// 1) 检测 width × height 与已有 tex_bg 是否一致,不一致就 destroy + recreate
// 2) 更新 m_cameraAspect / m_cameraRotation / m_mirrorX
// 3) 调用 base 的 processWithVulkan 走 staging buffer 上传。
// mirrorX 来自 Java 层 lensFacing == LENS_FACING_FRONT,表示是否需要镜子效果。
// 必须由唯一的相机分析线程调用(FaceActivity backgroundExecutor),
// 内部依赖 createTextureMtx 序列化与 drawFrame / cleanup 的并发。
void processCameraFrame(uint8_t* data, int width, int height,
int rowStride, size_t dataSize, int rotation,
bool mirrorX);
private:
void create_pipelines_bg();
void setup_descriptor_set_layout_bg();
void setup_descriptor_set_bg();
// 把 m_descriptor_set_bg 重新绑定到当前的 tex_bg.view / sampler。
// 当相机分辨率变化导致 tex_bg 被 destroy + recreate 后,新建的 image
// 有新的 VkImageView 句柄,旧的 descriptor set 里缓存的 handle 失效,
// 必须 vkUpdateDescriptorSets 让 descriptor 指向新 view。该函数不重新
// 分配 descriptor set,仅刷新绑定。
void refresh_descriptor_set_bg();
VkPipeline m_graphicsPipeline_bg = VK_NULL_HANDLE;
VkPipelineLayout m_pipelineLayout_bg = VK_NULL_HANDLE;
VkDescriptorSetLayout m_descriptorSetLayout_bg = VK_NULL_HANDLE;