Photone Ray 2021-09-01T16:19:52+00:00 Cairo 和 Skia 的raster绘制 2021-01-31T00:00:00+00:00 Keyring http://www.photoneray.com/cairo-skia-raster Cairo 和 Skia 算是目前 2D 开源绘图库的代表。他们现在的架构都是前端API收集绘图指令,根据配置调用不同的后端 做真正的绘制。在硬件加速的大环境下,我这几天好奇他们内部软光栅是怎么实现的,遂下载源码跟了一下,跟的过程中顺便学到了2D绘图的抽象表达。

两者对 draw 的抽象差不多,分成 绘制对象(drawline/lineto )、绘制行为(fill/strke)。这些也是直接提供给用户的 API,比较直观。而在内部,对于绘制对象,都转成了 path 来表达(cairo-path / SkPath)。然后在需要的时候根据不同的backend,将path转义为该后端接受的 图元。

在内存中,都有个绘制容器(cairo-surface / SkCanvas)的东西来承载用户提交的绘图指令,然后交给不同的backend处理。比如,针对软光栅后端,cairo这边是 cairo-image-surface,skia这边叫 SkBitmapDevice

在软光栅后端里,对于真正的光栅化实现,cairo其实自己没有处理,而是交给了 pixman 这个像素处理的库。在 cairo-image-surface 里可以看到很多直接使用pixman的函数。而在 pixman 里,也有抽象一些 box,edge,rectangle的概念,但最重要的就是 pixman-edge,这里面介绍了怎么把线条变成像素(光栅化)。

skia对于软光栅化是在 SkDraw 里实现的,统一 drawpath,然后 path 转 SkEdge,然后使用 SkScan-Path 的 walk_edges,遍历所有edge,使用扫描线算法,将edge离散出来的 point 集合,通过 SkBlitter 里面不同的 blit 函数最终转成像素值。

以上就是大致思路,记录一下,方便以后有需要查阅时能快速回忆起来。

PS: cairo是用纯C写的,为了一些抽象和泛型,函数指针用的飞起,看的脑壳疼。相较而言,skia就好太多了。

PPS:我还是不确定他们到底用了 breseham 算法没有。

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

]]>
Incredibuild 加速编译 NDK 2020-03-13T00:00:00+00:00 Keyring http://www.photoneray.com/incredibuild_ndk 直接使用 Android Studio NDK来编译C++实在太慢了。想想用VS搭载 incredibuild 多快乐。同样都是编译CPP,何不用来加速NDK的编译呢。

首先,你需要已经安装好了incredibuild,并且在VS上曾经尝试成功使用过。

找到你的NDK_ROOT指向的路径。使用AS SDK Manager下载的一般在 SDK 目录下(比如AS 3.6在sdk下专门有个 ndk 目录。手动下载的就自己找。下了很多个的就环境变量里指定好。(其实现在的 Android Studio 已经很完善了,开箱即用,工具齐全,就是界面有时候还是有点卡)

打开 NDK 路径下的 build/ndk-build.cmd。修改里面的内容。实质就是用 XGConsole(incredibuild) 使用一个 Profile.xml 配置来跑原先的编译脚本。

"%PREBUILT_PATH%\bin\make.exe" -f "%NDK_ROOT%\build\core\build-local.mk" SHELL=cmd %***

修改为

XGConsole /COMMAND="%PREBUILT_PATH%\bin\make.exe -f %NDK_ROOT%\build\core\build-local.mk SHELL=cmd %*" /PROFILE=%NDK_ROOT%\Profile.xml

然后在 NDK 目录下,新建一个 Profile.xml。其实文件叫啥名,放哪里都可以,只要和上面命令里 /PROFILE= 的路径能对应上就行。Profile.xml里的内容如下,里面最重要的是 clang 那两行。毕竟NDK编译用的这个。

<?xml version="1.0" encoding="UTF-8" standalone="no" ?>    
<Profile FormatVersion="1">    
    <Tools>    
        <Tool Filename="make" AllowIntercept="true" />    
        <Tool Filename="cl" AllowRemote="true" />    
        <Tool Filename="link" AllowRemote="true" />    
        <Tool Filename="gcc" AllowRemote="true" />    
        <Tool Filename="clang++" AllowRemote="true" />    
        <Tool Filename="clang" AllowRemote="true" />    
        <Tool Filename="gcc-3" AllowRemote="true" />    
        <Tool Filename="arm-linux-androideabi-c++" AllowRemote="true" />  
        <Tool Filename="arm-linux-androideabi-cpp" AllowRemote="true" />  
        <Tool Filename="arm-linux-androideabi-g++" AllowRemote="true" />  
        <Tool Filename="arm-linux-androideabi-gcc" AllowRemote="true" />    
    </Tools>    
</Profile>

最后,我们在 AS 工程app里面的 build.gradle 里的 ndkBuild 参数里,-j 200。 200是你想的最大任务数。如果build里面没使用 task 这种方式,可以在 externalNativeBuild 加。

externalNativeBuild {
    ndkBuild {
            ……

            arguments '-j 200'  // 200指最大的任务数

            ……
        }
}

开始启动编译吧。

]]>
C 语言开发 Direct2D 2019-03-27T00:00:00+00:00 Keyring http://www.photoneray.com/ImageViewer-Direct2d-c 准备利用巨硬吹嘘了很久的 Direct2D 在Windows下做绘图。开发环境自然是 Windows 10 + Visual Studio 2017 + C语言。

通览了一遍MSDN,不得不说,巨硬的文档越写越好了。下载示例 Demo,编译运行一气呵成,成功的垫脚石已经搭好。撸起袖子,先写个图片浏览器。

很快,第一个坑来了。Direcr2D 相关的示例与文档,全是C++,但我要用C语言开发(为啥非要用C呢,我乐意,我瞎折腾)。好在 D2D 本质算是COM组件,肯定是能对C做兼容的。在 d2d1.h 里翻了半天,找到了 D2D_USE_C_DEFINITIONS 宏,细细翻阅,喜忧参半。喜得是确实有做C接口兼容,忧的是巨硬貌似没做完。

在 Google 上换着关键字搜索良久,找到一篇靠谱的回答。原来巨硬在新的 Windows SDK 里删掉了 C 接口(我就记得以前有啊)。

The 14393 version of the Windows SDK did remove the C definitions, but you could target your project to use the 10586 version of the SDK if needed. You can install it through the Visual Studio 2017 installer.

As for whether it is a bug or not, I would imagine not. Using C to program for DirectX related things isn’t that popular, and since the C related definitions take up a huge amount of space in the headers, I would imagine that they chose to do this to cut down on work.

Well, anyway, there isn’t much difference in the Direct2D headers, between 10586 and 14393. There was ID2D1SvgGlyphStyle, ID2D1Device4, ID2D1DeviceContext4 and ID2D1Factory5. So unless you want/need to use these then just use an older version. If you want to use these, then the only thing I can suggest, after complaining to Microsoft, is to look at the previous versions of the headers and write your own C style definitions. It is still COM so they will still have to be C compatible even if they only provide the C++ definitions.

所以咯,要用C语言写 Direct2D,要么用老版的 SDK(10586,VS2017 Installer 里可以选),要么祈祷哪天巨硬更新又支持了,要么就自己导(比如mingw32就维护了一份)。

我还是老老实实用回旧版SDK吧。


图片浏览器基本结构三步走:读取、解码、显示。利用Win32 + D2D + WIC 很快就完成了。

在实现 显示的图像大小实时跟着窗口大小调整*时遇到个小坑。按常理,这种逻辑只需要监听 WM_SIZE 消息即可。可最终发现窗口变大会触发重绘,但缩小(按住鼠标拖动边框)不松开鼠标的情况下不会重绘。这应该是与Windows的窗口无效区域有关,简言之,这种情况下系统不会发送 WM_PAINT 消息,也就不会触发重绘。

最后在 WM_SIZE 的处理最后手动加上 RedrawWindow 或者 InvalidateRect 强制重绘。


]]>
UE4 项目升级报错 2018-07-23T00:00:00+00:00 Keyring http://www.photoneray.com/ue4-project_upgrade 惊闻UE4已经到了4.20的版本了,心血来潮想着把自己的玩具项目(v4.16)升级一下。本以为只是右键项目工程文件,选择 Switch Unreal Engine Version 即可完事,结果还是报了错。

一番重试、重启、重装等骚操作后,老老实实的Google到了答案,原来是原工程里 build.cs 文件的 SetupBinaries 函数在新版不再支持了。参考此链接,修改代码,重新生成,编译运行一气呵成。

对照修改工程下三个 build.cs 文件:

  • YourProject\Source\YourProject\YourProject.Build.cs
  • YourProject\Source\YourProject.Target.cs
  • YourProject\Source\YourProjectEditor.Target.cs

]]>
VAO 与 VBO 的前世今生 2017-07-21T00:00:00+00:00 Keyring http://www.photoneray.com/opengl-vao-vbo 在现代OpenGL(3.0+)的体系里,VAO和VBO已经是个很基本的概念了,是学习GL必须要理解的一个点。昨天,组内的同学在学习Learn OpenGL的时候,就被这两个概念给拦住了。当然,具体遇到的问题倒不是理解障碍,实质是不清楚这几个概念的本质

我想了一下,空讲概念确实太虚,尤其是OpenGL这种带有历史尘埃的玩意。GL是一个工业上的标准,历史悠久,那么在设计上肯定是推陈出新,每一个新推出的特性概念都是为了解决实际使用中的问题,VAO,VBO也不例外。


数据传输与优化

OpenGL作为图形API,制定的是绘图标准,采用的是CS模式。它将自己看作Server端,接收Client端传过来的数据,然后开启流水线,按需绘制出最终结果。所以,我们遇到的第一个阶段就是数据传输

现在假设我们在client端(简单理解成CPU端)内存里定义了三个顶点数据,如何传输至GPU呢?如何高效大量地传输呢?如何高效大量灵活地传输呢?下述几种技术的出现本质就是为了解决这个问题。

    GLfloat vertices[] = {
        0.0f, 0.0f,
        1.0f, 0.0f, 
        0.0f, 1.0f
    }

glVertex*

最简单的传输就是一个个传过去,在glBegin、glEnd(已废弃)之间通过 glVertex*逐个传输,每一次调用都会和GPU通讯一次。这种方式概念清晰,做法简洁粗暴,而缺点也明显,每一次绘制,所有顶点数据依次传输,效率瓶颈明显。

    // 每一次绘制都需要传输三次
    glBegin(GL_TRIANGLES);
        glVertex(0.0f, 0.0f);
        glVertex(1.0f, 0.0f);
        glVertex(0.0f, 1.0f);
    glEnd();

Display List

使用glVertex的方式传输数据,数据量膨胀,那么传输效率会迅速降低。早期图形需求简单,每一次绘制传输的数据,多数情况下是完全相同的。那能不能让每一个数据只传一次呢?

Display List(显示列表)应运而生。

在glNewList、glEndList(已废弃)之间,将顶点传输过程包裹了起来,意味着它收集好顶点,统一传输给GPU,并保存在GPU上,这样在重复绘制的时候可以直接从GPU端取数据,不再重新传输,对传输效率的提升是极大的。

显示列表的局限性也很明显:没法在绘制时修改顶点数据,如果要修改顶点数据,只有在CPU端修改再重新传输一份。极端情况下,如果场景顶点数据每帧需要变化,显示列表就完全退化成了 glVertex 模式。

    // 只在初始化的时候传输三次
    GLuint listName = glGenLists (1);
    glNewList (listName, GL_COMPILE);
        glBegin (GL_TRIANGLES);
            glVertex2f (0.0, 0.0);
            glVertex2f (1.0, 0.0);
            glVertex2f (0.0, 1.0);
        glEnd ();
    glEndList ();

    ...

    // 绘制(不传输数据)
    glCallList(listName);     

Vertex Array

针对灵活多变的顶点变化需求,VA(顶点数组)加入到了规范里。它每一次绘制,将收集的顶点通过一次API调用传输给GPU,俗称打包数据传输。

VA与上述显示列表区别在于,它收集的顶点保存在CPU端,每次绘制都需要重新传一次数据,所以绘制速度上面慢于显示列表。注意:顶点数组是GL内置的,开发者只能选择启用与否。

    // 每次绘制都将 vertices 传输一次
    GLfloat vertices[] = {
        0.0f, 0.0f,
        1.0f, 0.0f, 
        0.0f, 1.0f
    }
    glEnableClientState(GL_VERTEX_ARRAY);
    glVertexPointer(2,GL_FLOAT,0,vertices);
    glDrawArray(GL_TRIANGLES, 0, 3);   

VBO (Vertex Buffer Object)

VBO出现之前,做OpenGL优化,提高顶点绘制效率的办法一般就两种:

  • 显示列表:把常规的绘制代码放置一个显示列表中(通常在初始化阶段完成,顶点数据还是需要一个个传输的),渲染时直接使用这个显示列表。优化点:减少数据传输次数
  • 顶点数组:把顶点以及顶点属性数据打包成单个数组,渲染时直接传输该数组。优化点:减少了函数调用次数(弃用glVertex)

VBO的目标就是鱼与熊掌兼得,想将显示列表的特性(绘制时不传输数据,快)和顶点数组的特性(数据打包传输,修改灵活)结合起来。

当然最终效果差强人意,效率介于两者之间,拥有良好的数据修改弹性。在渲染阶段,我们可以把该帧到达流水线的顶点数据映射回client端修改(vertex mapping),然后再提交回流水线(vertex unmapping),意味着顶点数据只在VBO里有一份;或者可以用 glBufferData(全部数据)\glBufferSubData(部分数据) 提交更改了的顶点数据,意味着顶点数据在client端和VBO里都有一份。

VBO本质上是一块服务端buffer(缓存),对应着client端的某份数据,在数据传输给VBO之后,client端的数据是可以删除的。系统会根据用户设置的 targetusage 来决定VBO最适合的存放位置(系统内存/AGP/显存)。当然,GL规范是一回事,显卡厂商的驱动实现又是另一回事了

在初始化阶段,VBO是不知道它所存储的是什么数据,而是在渲染阶段(精确说是 glVertexAttribPointer 函数)才确定数据作用类型(顶点位置、float类型、从偏移量0处开始采集数据、2个float算一个采集步长等等)。到真正绘制(glDrawArray/glDrawElement)的时候才从VBO里读取需要的数据进入渲染流水线。

    // 初始化
    GLfloat vertices[] = {
        0.0f, 0.0f,
        1.0f, 0.0f, 
        0.0f, 1.0f
    }

    GLuint vbo;
    glGenBuffer(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STREAM_DRAW);

    ...

    // 绘制
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2, (void*)0);
    glDrawArray(GL_TRIANGLES, 0, 3);

    ...


VAO (Vertex Array Object)

重看一遍上面的渲染阶段代码,如果我有两份不同的绘制代码,那就需要频繁的重复 glBindBuffer()-glEnableVertexAttribArray()-glVertexAttribPointer-glDrawArray()一套流程,那么本着偷懒的原则,优化方案来了——把这些绘制需要的信息状态在初始化的时候就完整记录下来,真正绘制时只需简单切换一下状态记录。

这就是 VAO 诞生的理由。

VAO 全称 Vertex Array Object,翻译过来叫顶点数组对象,但和Vertex Array(顶点数组)毫无联系!

VAO不是 buffer-object,所以不作数据存储;与顶点的绘制息息相关,即是说与VBO强相关。如上,VAO本质上是state-object(状态对象),记录的是一次绘制所需要的信息,包括数据在哪,数据格式之类的信息。如果抽象成数据结构,VAO 的数据结构如下:

    struct VertexAttribute  
    {  
        bool bIsEnabled = GL_FALSE;  
        int iSize = 4; //This is the number of elements in this attribute, 1-4.  
        unsigned int iStride = 0;  
        VertexAttribType eType = GL_FLOAT;  
        bool bIsNormalized = GL_FALSE;  
        bool bIsIntegral = GL_FALSE;  
        void * pBufferObjectOffset = 0;  
        BufferObject * pBufferObj = 0;  
    };  
    
    struct VertexArrayObject  
    {  
        BufferObject *pElementArrayBufferObject = NULL;  
        VertexAttribute attributes[GL_MAX_VERTEX_ATTRIB];  
    }  

从这个数据结构可以看出,VAO里面存了一个EBO的指针以及一个顶点属性数组,意味着上述一串操作的状态可以完全存储于VAO里面,而真正的数据依然在VBO里面。下面举一个示例代码:

    // 初始化
    unsigned int VAO;
    glGenVertexArrays(1, &VAO);  
    glBindVertexArray(VAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0); 

    ...

    // 绘制
    glBindVertexArray(VAO);
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
    glBindVertexArray(0);

对比不使用VAO的代码可以发现,我们把原先放在绘制阶段的 glEnableVertexAttribArray()-glVertexAttribPointer()移动到了初始化里面,而在真正绘制的时候,只是简单的绑定了一个VAO(glBindVertexArray(VAO))就开始绘制了。这样的话,如果要绘制另一个内容,只需绑定另一个VAO就可以了。

所以,你应该看出来,VAO是用来简化绘制代码的。


后记

通过追本溯源,我们可以发现,现代GL里常用的VAO/VBO实质是为了解决传输效率而做的优化手段。VBO是为了均衡数据的传输效率与灵活修改性;VAO的本质是储存绘制状态,简化绘制代码。

回到最初,组内的同学在看到下方Learn OpenGL的示例代码时,提出了一个问题:

    // set up vertex data (and buffer(s)) and configure vertex attributes
    // ------------------------------------------------------------------
    float vertices[] = {
         0.5f,  0.5f, 0.0f,  // top right
         0.5f, -0.5f, 0.0f,  // bottom right
        -0.5f, -0.5f, 0.0f,  // bottom left
        -0.5f,  0.5f, 0.0f   // top left 
    };
    unsigned int indices[] = {  // note that we start from 0!
        0, 1, 3,  // first Triangle
        1, 2, 3   // second Triangle
    };
    unsigned int VBO, VAO, EBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);
    // bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
    glBindVertexArray(VAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    // note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
    glBindBuffer(GL_ARRAY_BUFFER, 0); 

    // remember: do NOT unbind the EBO while a VAO is active as the bound element buffer object IS stored in the VAO; keep the EBO bound.
    //glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

    // You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other
    // VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
    glBindVertexArray(0); 

为什么在VAO里面可以解绑VBO,却不能解绑EBO呢?

    // note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
    glBindBuffer(GL_ARRAY_BUFFER, 0); 

    // remember: do NOT unbind the EBO while a VAO is active as the bound element buffer object IS stored in the VAO; keep the EBO bound.
    //glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

作者用注释解释了原因,懂则懂之,不懂请结合前述VAO的数据结构,相信你能豁然开朗。


参考

]]>
Bot Framework 的 Node.js 实践 2017-05-12T00:00:00+00:00 Keyring http://www.photoneray.com/bot-framework-nodejs-key-concepts Bot Framework 是微软2016年提出的智能机器人平台,当然,这个机器人是没有硬件机身的。简单的说,他提供了一系列的工具与服务来简化智能AI的搭建与开发,诸如语言理解知识扩展语音转换网络搜索图像视频识别等等服务,所有这些被统称为 认知服务

借助该平台,我们可以忽视AI算法,快速搭建一个类似微软小娜小冰这样的智能AI。Bot Framework 当前提供四种开发方式:.NET SDKNode.js SDKAzure Bot ServiceREST。这里我们介绍 Node.js 的开发方式与其核心概念。

准备工作

工欲善其事,必先利其器。准备好开发环境是第一步。

  • 安装 Node.js
  • 为你的机器人创建一个文件夹
  • 打开命令行,进入该文件夹
  • 运行 npm 命令 npm init

成功之后,文件夹下会生成一个package.json文件,里面包含了部分模块信息。

创建 Bot

  1. 在文件夹下新建app.js文件。
  2. 填入下面的代码:

var restify = require('restify');
var builder = require('botbuilder');

// Setup Restify Server
var server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, function () {
   console.log('%s listening to %s', server.name, server.url); 
});

// Create chat connector for communicating with the Bot Framework Service
var connector = new builder.ChatConnector({
    appId: process.env.MICROSOFT_APP_ID,
    appPassword: process.env.MICROSOFT_APP_PASSWORD
});

// Listen for messages from users 
server.post('/api/messages', connector.listen());

// Receive messages from the user and respond by echoing each message back (prefixed with 'You said:')
var bot = new builder.UniversalBot(connector, function (session) {
    session.send("You said: %s", session.message.text);
});

  1. 保存。准备运行测试。

测试 Bot

  1. 下载并安装 Bot Framework Emulator
  2. 启动模拟器,然后在代码文件夹下运行node app.js启动Bot。
  3. 使用指定的Microsoft App IDMicrosoft App Password(注册Bot时会提供,本地测试可使用默认值)连接模拟器与你的Bot。
  4. 可以和你的Bot进行交流了。

More

现在,bot运行在本地。你可以申请空间,让他运行在服务器上,还可以直接利用微软的Azure运行于云上。当前的bot只是简单的重复你提交的文字,你可以好好残月文档,为他加上更智能的能力。 详情参阅官方文档

ps: 接入微信公众号就可以做很多有趣的事情。

]]>
阿拉伯语数字·坑 2017-01-20T00:00:00+00:00 Keyring http://www.photoneray.com/string-format-number-locale 年前游戏做阿语的版本移植,本以为和其他语言版本一样,简单的做好翻译与UI适配即可迅速发布。结果将测试包发给沙特的测试人员后,反馈无法登录游戏。

无法登陆的bug在前面坐其他语言版本的时候也经常遇到,其原因不外乎后端代码未同步、配置出错、链接的URL已被废弃等等。迅速将上述可能的原因检查一遍之后,在本地常规测试通过,然后将新包发送给沙特的测试人员。由于时差关系,反馈要等到第二天。

然而,问题已然存在!

这时,后端日志发现,根本没有该测试人员的登录请求信息。也即是说,请求没有抵达后端


BUG还原

由于本地测试完全OK,在北京的合作伙伴也能测试通过,使得所有的分析都需要远在沙特的测试人员提供。尴尬的是,这位测试实际上只是一名普通玩家,不是专业测试人员,所以无法获取专业的反馈。再者,因为时差关系,无法即时通讯。因此,我们只有先猜测原因,然后引导沙特玩家验证。同时,我们让运营寻找更多的玩家帮忙测试。几经来回,收集到很多信息,事后来看,有些信息完全就是错误的干扰信息。

获取到的玩家基本信息:沙特阿拉伯、三星S6、Android 6.0、wifi数据网络都畅通、VPN畅通

游戏包信息:targetversion 23、各语言版本仅仅部分配置不一致、内网环境测试OK、外网环境测试OK

其他信息:

  • 阿语地区,约旦沙特不能登录,伊拉克、埃及、土耳其可以登录
  • 沙特约旦新政策,赌博类游戏全部下架
  • 游戏老版本在沙特可以登录正常游戏(新版与老版差异太大)
  • 在加拿大的阿语玩家可以登录
  • 沙特玩家另一部android 4.3的手机可以登录
  • 沙特玩家使用另一个越南语言版本的包,也不能登录。

排查

针对网络请求未到达,我们分解成三个步骤:1)请求未发出,2)请求被墙,未到nginx,3)请求到了nginx,但被nginx丢弃。

由于其他语言版本已经上线稳定运行,而沙特刚好禁赌,我们怀疑是否链接被墙,于是联系运维检查海外的服务器,检查nginx的相关配置,检查是否有玩家的请求信息。但运维提供的信息非常有限,都没法明确回答“是否接收到玩家的请求?”这个问题。后来玩家说,以前的版本可以登录,我们排除上述2的可能。根据nginx不会丢弃请求来排除3的可能。

最终问题回到客户端本身。到底是什么原因造成请求发不出去呢?为什么同一个架构下,只有沙特玩家使用android6.0手机登录不进去我们的游戏呢?

首先我们想到的是android6.0的特殊性,6.0的android升级了权限管理、废弃了老式的httpclient。这两个change与我们的bug看起来都有联系。

针对权限系统,我们降低了targetversion,降低了sdk版本。没用

针对httpclient,我们使用的是okhttp啊。排除

然后只剩下 玩家在沙特 这个条件了。于是我们在与网路请求有关的每一个步骤,都输出文件日志。最后让玩家将该文件发回来(教会玩家找到日志文件也是个苦力活)。


原来如此

第二天收到发回来的日志文件时,文件名上的阿语让我愣了一下,我记得日志的文件名是用的当地日期时间生成的,为什么会有阿拉伯语呢?难道。。。

打开日志文件,一路跟随,在请求的时候发现,请求URL为空!

幸好打印的日志信息足够多,前后对比,终于发现问题所在。

我们的游戏和新近大多数手游一样,采用的是lua + c++引擎 + 部分原生代码。在网络请求这一块,直接使用的原生实现,也就是android端用的okhttp,没有在引擎层用C++封装也没有JNI。所以请求的数据信息全部是lua逻辑层传过来的,包括请求地址,请求数据。数据在lua与原生(android的java,ios的oc)之间是通过字典(key-value)传递的。两方使用同一种规则构建一个key,一方存一方取。

对于网络http请求,每一个请求都有唯一的数字ID,双方约定的key格式是http_request_%d,lua这边用string.format,java用String.format。问题就出在格式化这个数字ID上,lua不会自动本地化,无论在哪个地区,传递的数字ID都是 1、2、3这种,相应存下的key就是 http_request_1,而在java端用format取的时候,如果没有指定locale,则会根据系统语言自动决定,在我们已发行的语言版本里,数字都是默认使用1、2、3这种所谓的阿拉伯数字。而真正的阿拉伯语言里,使用的数字是١、٢、٣(注意阿语是从右往左的,1对应١)。于是,java端format之后出来的key变成了http_request_١,自然是找不到对应的value,当然也就请求不出去了。下面给一张阿语数字的对照表


填坑

既然知晓了原因,解决办法也是多样。我们简单的在java里用了String.format数字的地方,指定locale为us,统一格式不受系统语言影响。

事后反思这个bug的跟踪过程,我们总结了不少。在代码层面、远程调试、各部门协作等方面都有思考与待优化的空间。

我本人也陷入了经验不足与思维盲区,比如最开始我就想到是否是系统语言的原因,尝试过将测试机刷成沙特地区的系统(未成功),却没想过直接将系统语言直接切换成阿语,这样就能本地重现bug,想起来真是尴尬。

再有就是针对线上的远程调试,我们考虑在一些关键的基础设施代码里写点本地日志,然后通过后端控制开启(与玩家协商)与否来决定是否日志上传。这样,以后发生类似线上bug,可以在取得玩家同意后,将玩家的本地日志上传至服务器(当然对于这个直接连不上服务器的bug没用)。这样可以避免玩家直接描述里附带的部分干扰信息(比如玩家说android4.0可以登录,让我们查了半天的6.0 changelist)。

还有就是部门之间的协同上面,效率真的太低了。大家时间都很宝贵的,如果交流这么困难,谁还愿意浪费时间与口水和你合作!

]]>
UE4 Timer 2016-09-21T00:00:00+00:00 Keyring http://www.photoneray.com/ue4-timer-delay Timer这个东西在游戏开发里太常用了,大到游戏世界的驱动,小到物体的状态变迁,均有timer的身影。游戏世界虽说是个虚拟世界,但总归是需要时间维度的。在UE4中,Timer的基本作用就是在固定间隔内重复或单次执行某些操作。具体到应用,常见的就是Delay、固定频率刷新某个操作。

UE4中所有的Timer都是由全局类FTimerManager管理。在AActor之外,可以指定任意类型的委托。FTimerManager提供一些函数用来操作Timer,同时这些函数也能用于Timer的委托中,比如可以在一个Timer的委托里新建(删除)另一个Timer。

AActor::GetWorldTimerManager()用来获取当前世界的TimerManager实例。然后通过这个实例调用函数就可以控制Timer了。

一般来讲,你可能还需要一个FTimerHandle来指定具体的Timer。

  • SetTimer

为Timer指定回调函数、间隔时间,是否循环等参数,并启动该Timer,使其开始计时。该函数也可用来重设已有的Timer,此时,计时也将重新开始。

在Blueprint中常见的Delay节点在C++就需要用这个函数指定一个一次性的Timer。

  • ClearTimer

销毁清理指定的Timer,使其不再计时,当然也不会再回调。将SetTimer的间隔时间参数设为<0.f有同样的效果。

  • PauseTimer

暂停指定的Timer,此时Timer停止计时但会保存已经过的时间和剩余时间,直到恢复计时。

  • UnPauseTimer

激活已暂停的Timer。

  • IsTimerActive

获取指定Timer的当前状态(运行/暂停)

  • GetTimerRate

获得指定Timer的当前频率(就是时间间隔参数)。 频率不支持直接修改,但可以在Timer的回调里重用TimerHandle重新SetTimer。 函数返回 -1 说明该TimerHandle非法。

  • GetTimerElapsed

获得指定Timer的当前间隔内已经计时的时长

  • GetTimerRemaining

获得指定Timer当前间隔内的剩余时长

已经计时的时长 + 剩余时长 = 间隔时间

常用的接口差不多就这些,更多更详细的可以参阅API文档。比如各种SetTimer的重载,更多Timer的状态信息。

]]>
Lua 表小测试 2016-08-11T00:00:00+00:00 Keyring http://www.photoneray.com/lua-table

local x = {1, [1] = 1.1}
local y = {[1] = 1, 1.1}
print(x[1], y[1])   
--[[ 
 1, 1.1
--]]


local x = {[1] = 1.1, nil,1,nil,2,nil,3,[6] =6, [8] = 8}
for k,v in pairs(x) do
	print(k,v)
end
--[[
 2, 1
 4, 2
 6, 3
 8, 8
--]]

local x = {[1] = 1.1, nil,1,nil,2,nil,3,[6] =6, [8] = 8}
for i=1,#x do
	print(i,x[i])
end
--[[
 1, nil
 2, 1
 3, nil
 4, 2
 5, nil
 6, 3
--]]

local x = {[1] = 1.1, nil,1,nil,2,nil,3,[6] =6, [8] = 8}
for i,v in ipairs(x) do
	print(i,v)
end
--[[

--]]

local x = {[1] = 1.1, nil,1,nil,2,nil,3,[6] =6, 7, [8] = 8}
for k,v in pairs(x) do
	print(k,v)
end
--[[
 2, 1
 4, 2
 6, 3
 7, 7
 8, 8
--]]

local x = {[1] = 1.1, nil,1,nil,2,nil,3,[6] =6, 7, [8] = 8}
for i=1,#x do
	print(i,x[i])
end
--[[
 1, nil
 2, 1
 3, nil
 4, 2
 5, nil
 6, 3
 7, 7
 8, 8
--]]

]]>
Blur 2016-06-03T00:00:00+00:00 Keyring http://www.photoneray.com/Blur Box Blur

Gaussian Blur

O(n^2)

Discrete Sample

  // discrete sample gaussian blur vs
  attribute vec4 a_position;
  attribute vec2 a_texcoord;

  uniform vec2 blurSize;
  
  varying vec2 vblurtexcoord[9];
  
  void main()
  {
    gl_Position = a_position;
    
    vec2 offset1 = 1.0 * blurSize;
    vec2 offset2 = 2.0 * blurSize;
    vec2 offset3 = 3.0 * blurSize;
    vec2 offset4 = 4.0 * blurSize;
    
    vblurtexcoord[0] = a_texcoord - offset4;
    vblurtexcoord[1] = a_texcoord - offset3;
    vblurtexcoord[2] = a_texcoord - offset2;
    vblurtexcoord[3] = a_texcoord - offset1;
    vblurtexcoord[4] = a_texcoord;
    vblurtexcoord[5] = a_texcoord + offset1;
    vblurtexcoord[6] = a_texcoord + offset2;
    vblurtexcoord[7] = a_texcoord + offset3;
    vblurtexcoord[8] = a_texcoord + offset4;
  }


  // discrete sample gaussian blur fs
  
  precision mediump float;
  uniform sampler2D  inputTexture;
  
  varying vec2 vblurtexcoord[9];
  
  void main()
  {
    lowp vec3 sample = texture2D(inputTexture, vblurtexcoord[0]).rgb * 0.05;
    sample += texture2D(inputTexture, vblurtexcoord[1]).rgb * 0.09;
    sample += texture2D(inputTexture, vblurtexcoord[2]).rgb * 0.12;
    sample += texture2D(inputTexture, vblurtexcoord[3]).rgb * 0.15;
    sample += texture2D(inputTexture, vblurtexcoord[4]).rgb * 0.18;
    sample += texture2D(inputTexture, vblurtexcoord[5]).rgb * 0.15;
    sample += texture2D(inputTexture, vblurtexcoord[6]).rgb * 0.12;
    sample += texture2D(inputTexture, vblurtexcoord[7]).rgb * 0.09;
    sample += texture2D(inputTexture, vblurtexcoord[8]).rgb * 0.05;
    
    gl_FragColor = vec4(sample, 1.0);
  }

Linear Sample

  // linear sample gaussian blur vs
  attribute vec4 a_position;
  attribute vec2 a_texcoord;

  uniform vec2 blurSize;
  
  varying vec2 vblurtexcoord[5];
  
  void main()
  {
    gl_Position = a_position;
    vec2 firstOffset = 1.3846153846 * blurSize;
    vec2 secondOffset = 3.2307692308 * blurSize;
    
    vblurtexcoord[0] = a_texcoord - secondOffset;
    vblurtexcoord[1] = a_texcoord - firstOffset;
    vblurtexcoord[2] = a_texcoord;
    vblurtexcoord[3] = a_texcoord + firstOffset;
    vblurtexcoord[4] = a_texcoord + secondOffset;
  }


  // linear sample gaussian blur fs
  
  precision mediump float;
  uniform sampler2D  inputTexture;
  
  varying vec2 vblurtexcoord[5];
  
  void main()
  {
    lowp vec3 sample = texture2D(inputTexture, vblurtexcoord[0]).rgb * 0.0702702703;
    sample += texture2D(inputTexture, vblurtexcoord[1]).rgb * 0.3162162162;
    sample += texture2D(inputTexture, vblurtexcoord[2]).rgb * 0.2270270270;
    sample += texture2D(inputTexture, vblurtexcoord[3]).rgb * 0.3162162162;
    sample += texture2D(inputTexture, vblurtexcoord[4]).rgb * 0.0702702703;
    
    gl_FragColor = vec4(sample, 1.0);
  }

Stack Blur

Reference

]]>