1.1.1 音频设备的管理

虽然AudioFlinger实体已经成功创建并初始化,但到目前为止它还是一块静态的内存空间,没有涉及到具体的工作。

从职能分布上来讲,AudioPolicyService是策略的制定者,比如什么时候打开音频接口设备、某种Stream类型的音频对应什么设备等等。而AudioFlinger则是策略的执行者,例如具体如何与音频设备通信,如何维护现有系统中的音频设备,以及多个音频流的混音如何处理等等都得由它来完成。

目前Audio系统中支持的音频设备接口(Audio Interface)分为三大类,即:

/*frameworks/av/services/audioflinger/AudioFlinger.cpp*/

static const char * const audio_interfaces[] = {

AUDIO_HARDWARE_MODULE_ID_PRIMARY, //主音频设备,必须存在

AUDIO_HARDWARE_MODULE_ID_A2DP, //蓝牙A2DP音频

AUDIO_HARDWARE_MODULE_ID_USB, //USB音频,早期的版本不支持

};

每种音频设备接口由一个对应的so库提供支持。那么AudioFlinger怎么会知道当前设备中支持上述的哪些接口,每种接口又支持哪些具体的音频设备呢?这是AudioPolicyService的责任之一,即根据用户配置来指导AudioFlinger加载设备接口。

当AudioPolicyManagerBase(AudioPolicyService中持有的Policy管理者,后面小节有详细介绍)构造时,它会读取厂商关于音频设备的描述文件(audio_policy.conf),然后据此来打开以上三类音频接口(如果存在的话)。这一过程最终会调用loadHwModule@AudioFlinger,如下所示:

/*frameworks/av/services/audioflinger*/

audio_module_handle_t AudioFlinger::loadHwModule(const char *name)/*name就是前面audio_interfaces 数组

成员中的字符串*/

{

if (!settingsAllowed()) {

return 0;

}

Mutex::Autolock _l(mLock);

returnloadHwModule_l(name);

}

这个函数没有做实质性的工作,只是执行了加锁动作,然后接着调用下面的函数:

audio_module_handle_t AudioFlinger::loadHwModule_l(const char *name)

{

/*Step 1. 是否已经添加了这个interface?*/

for (size_t i = 0; i <mAudioHwDevs.size(); i++) {

if(strncmp(mAudioHwDevs.valueAt(i)->moduleName(), name, strlen(name)) == 0) {

ALOGW("loadHwModule() module %s already loaded", name);

returnmAudioHwDevs.keyAt(i);

}

}

/*Step 2. 加载audio interface*/

audio_hw_device_t *dev;

int rc =load_audio_interface(name, &dev);

/*Step 3. 初始化*/

mHardwareStatus = AUDIO_HW_INIT;

rc = dev->init_check(dev);

mHardwareStatus = AUDIO_HW_IDLE;

if ((mMasterVolumeSupportLvl !=MVS_NONE) && (NULL != dev->set_master_volume)) {

AutoMutex lock(mHardwareLock);

mHardwareStatus = AUDIO_HW_SET_MASTER_VOLUME;

dev->set_master_volume(dev, mMasterVolume);

mHardwareStatus = AUDIO_HW_IDLE;

}

/*Step 4. 添加到全局变量中*/

audio_module_handle_t  handle = nextUniqueId();

mAudioHwDevs.add(handle,new AudioHwDevice(name, dev));

return handle;

}

Step1@ loadHwModule_l. 首先查找mAudioHwDevs是否已经添加了变量name所指示的audio interface,如果是的话直接返回。第一次进入时mAudioHwDevs的size为0,所以还会继续往下执行。

Step2@ loadHwModule_l. 加载指定的audiointerface,比如“primary”、“a2dp”或者“usb”。函数load_audio_interface用来加载设备所需的库文件,然后打开设备并创建一个audio_hw_device_t实例。音频接口设备所对应的库文件名称是有一定格式的,比如a2dp的模块名可能是audio.a2dp.so或者audio.a2dp.default.so等等。查找路径主要有两个,即:

/** Base path of the hal modules */

#define HAL_LIBRARY_PATH1 "/system/lib/hw"

#define HAL_LIBRARY_PATH2 "/vendor/lib/hw"

当然,因为Android是完全开源的,各开发商可以根据自己的需要来进行相应的修改,比如下面是某手机设备的音频库截图:

图 13‑12 音频库实例

Step3@ loadHwModule_l,进行初始化操作。其中init_check是为了确定这个audio interface是否已经成功初始化,0是成功,其它值表示失败。接下来如果这个device支持主音量,我们还需要通过set_master_volume进行设置。在每次操作device前,都要先改变mHardwareStatus的状态值,操作结束后将其复原为AUDIO_HW_IDLE(根据源码中的注释,这样做是为了方便dump时正确输出内部状态,这里我们就不去深究了)。

Step4@ loadHwModule_l. 把加载后的设备添加入mAudioHwDevs键值对中,其中key的值是由nextUniqueId生成的,这样做保证了这个audiointerface拥有全局唯一的id号。

完成了audiointerface的模块加载只是万里长征的第一步。因为每一个interface包含的设备通常不止一个,Android系统目前支持的音频设备如下列表所示:

表格 13‑4 Android系统支持的音频设备(输出)

Device Name

Description

AUDIO_DEVICE_OUT_EARPIECE

听筒

AUDIO_DEVICE_OUT_SPEAKER

喇叭

AUDIO_DEVICE_OUT_WIRED_HEADSET

带话筒的耳机

AUDIO_DEVICE_OUT_WIRED_HEADPHONE

耳机

AUDIO_DEVICE_OUT_BLUETOOTH_SCO

SCO 蓝牙

AUDIO_DEVICE_OUT_BLUETOOTH_SCO_HEADSET

SCO 蓝牙耳机

AUDIO_DEVICE_OUT_BLUETOOTH_SCO_CARKIT

SCO 车载套件

AUDIO_DEVICE_OUT_BLUETOOTH_A2DP

A2DP 蓝牙

AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES

A2DP 蓝牙耳机

AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER

A2DP 蓝牙喇叭

AUDIO_DEVICE_OUT_AUX_DIGITAL

AUX IN

AUDIO_DEVICE_OUT_ANLG_DOCK_HEADSET

模拟dock headset

AUDIO_DEVICE_OUT_DGTL_DOCK_HEADSET

数字dock headset

AUDIO_DEVICE_OUT_USB_ACCESSORY

USB配件

AUDIO_DEVICE_OUT_USB_DEVICE

USB设备

AUDIO_DEVICE_OUT_DEFAULT

默认设备

AUDIO_DEVICE_OUT_ALL

上述每种设备只占int值一个bit位,这里是指上述设备的集合

AUDIO_DEVICE_OUT_ALL_A2DP

上述设备中与A2DP蓝牙相关的设备集合

AUDIO_DEVICE_OUT_ALL_SCO

上述设备中与SCO蓝牙相关的设备集合

AUDIO_DEVICE_OUT_ALL_USB

上述设备中与USB相关的设备集合

大家可能会有疑问:

Ø 这么多的输出设备,那么当我们回放音频流(录音也是类似的情况)时,该选择哪一种呢?

Ø 而且当前系统中audio interface也很可能不止一个,应该如何选择?

显然这些决策工作将由AudioPolicyService来完成,我们会在下一小节做详细阐述。这里先给大家分析下,AudioFlinger是如何打开一个Output通道的(一个audiointerface可能包含若干个output)。

打开音频输出通道(output)在AF中对应的接口是openOutput(),即:

audio_io_handle_t  AudioFlinger::openOutput(audio_module_handle_tmodule, audio_devices_t *pDevices,

uint32_t *pSamplingRate,audio_format_t *pFormat,

audio_channel_mask_t *pChannelMask,

uint32_t *pLatencyMs, audio_output_flags_t flags)

{

/*入参中的module是由前面的loadHwModule 获得的,它是一个audiointerface的id号,可以通过此id在mAudioHwDevs中查找到对应的AudioHwDevice对象*/

status_t status;

PlaybackThread *thread =NULL;

audio_stream_out_t  *outStream = NULL;

audio_hw_device_t*outHwDev;

/*Step 1. 查找相应的audio interface

outHwDev = findSuitableHwDev_l(module, *pDevices);

/*Step 2. 为设备打开一个输出流*/

mHardwareStatus =AUDIO_HW_OUTPUT_OPEN;

status = outHwDev->open_output_stream(outHwDev, id, *pDevices,(audio_output_flags_t)flags,

&config, &outStream);

mHardwareStatus =AUDIO_HW_IDLE;

if (status == NO_ERROR &&outStream != NULL) {

/*Step 3.生成AudioStreamOut*/

AudioStreamOut *output = newAudioStreamOut(outHwDev, outStream);

/*Step 4.创建PlaybackThread*/

if ((flags & AUDIO_OUTPUT_FLAG_DIRECT) ||(config.format != AUDIO_FORMAT_PCM_16_BIT) ||

(config.channel_mask != AUDIO_CHANNEL_OUT_STEREO)) {

thread = new DirectOutputThread(this, output, id, *pDevices);

} else {

thread = new MixerThread(this, output, id, *pDevices);

}

mPlaybackThreads.add(id,thread); //添加播放线程

/*Step 5.Primary output情况下的处理*/

if ((mPrimaryHardwareDev== NULL) &&flags & AUDIO_OUTPUT_FLAG_PRIMARY)) {

ALOGI("Usingmodule %d has the primary audio interface", module);

mPrimaryHardwareDev = outHwDev;

AutoMutexlock(mHardwareLock);

mHardwareStatus =AUDIO_HW_SET_MODE;

outHwDev->set_mode(outHwDev, mMode);

float initialVolume = 1.0;

mMasterVolumeSupportLvl = MVS_NONE;

mHardwareStatus = AUDIO_HW_GET_MASTER_VOLUME; //测试设备是否支持主音量获取

if ((NULL !=outHwDev->get_master_volume) &&

(NO_ERROR ==outHwDev->get_master_volume (outHwDev, &initialVolume))) {

mMasterVolumeSupportLvl = MVS_FULL;

} else {

mMasterVolumeSupportLvl = MVS_SETONLY;

initialVolume= 1.0;

}

mHardwareStatus = AUDIO_HW_SET_MASTER_VOLUME;//测试是否支持主音量设置

if ((NULL ==outHwDev->set_master_volume) ||

(NO_ERROR !=outHwDev->set_master_volume (outHwDev, initialVolume))) {

mMasterVolumeSupportLvl = MVS_NONE;

}

for (size_t i = 0; i <mAudioHwDevs.size(); i++) {

audio_hw_device_t *dev = mAudioHwDevs.valueAt(i)->hwDevice();

if ((dev !=mPrimaryHardwareDev) &&

(NULL !=dev->set_master_volume)) {

dev->set_master_volume(dev,initialVolume);

}

}

mHardwareStatus =AUDIO_HW_IDLE;

mMasterVolumeSW =(MVS_NONE == mMasterVolumeSupportLvl)? initialVolume: 1.0;

mMasterVolume   = initialVolume;

}

return id;

}

return 0;

}

上面这段代码中,颜色加深的部分是我们接下来分析的重点,主要还是围绕outHwDev这个变量所做的一系列操作,即:

·        查找合适的音频接口设备(findSuitableHwDev_l)

·        创建音频输出流(通过open_output_stream获得一个audio_stream_out_t)

·        利用AudioStreamOut来封装audio_stream_out_t与audio_hw_device_t

·        创建播放线程(PlaybackThread)

·        如果当前设备是主设备,则还需要进行相应的设置,包括模式、主音量等等

显然,outHwDev用于记录一个打开的音频接口设备,它的数据类型是audio_hw_device_t,是由HAL规定的一个音频接口设备所应具有的属性集合,如下所示:

struct audio_hw_device {

struct hw_device_t common;

int (*set_master_volume)(struct audio_hw_device *dev, float volume);

int (*set_mode)(struct audio_hw_device *dev, audio_mode_t mode);

int (*open_output_stream)(struct audio_hw_device *dev,

audio_io_handle_t handle,

audio_devices_t devices,

audio_output_flags_t flags,

struct audio_config *config,

struct audio_stream_out **stream_out);

…}

其中common代表了HAL层所有设备的共有属性;set_master_volume、set_mode、open_output_stream分别为我们设置audio interface的主音量、设置音频模式类型(比如AUDIO_MODE_RINGTONE、AUDIO_MODE_IN_CALL等等)、打开输出数据流提供了接口。

接下来我们分步来阐述。

Step1@ AudioFlinger::openOutput. 在openOutput中,设备outHwDev是通过查找当前系统来得到的,代码如下:

audio_hw_device_t*  AudioFlinger::findSuitableHwDev_l(audio_module_handle_tmodule, uint32_t devices)

{

if (module == 0) {

for (size_t i = 0; i< ARRAY_SIZE(audio_interfaces); i++) {

loadHwModule_l(audio_interfaces[i]);

}

} else {

AudioHwDevice*audioHwdevice = mAudioHwDevs.valueFor(module);

if (audioHwdevice !=NULL) {

returnaudioHwdevice->hwDevice();

}

}

// then try to find amodule supporting the requested device.

for (size_t i = 0; i <mAudioHwDevs.size(); i++) {

audio_hw_device_t *dev= mAudioHwDevs.valueAt(i)->hwDevice();

if((dev->get_supported_devices(dev) & devices) == devices)

return dev;

}

return NULL;

}

变量module值为0的情况,是为了兼容之前的Audio Policy而特别做的处理。当module等于0时,首先加载所有已知的音频接口设备,然后再根据devices来确定其中符合要求的。入参devices的值实际上来源于“表格 13‑4 Android系统支持的音频设备(输出)”所示的设备。可以看到,enum中每个设备类型都对应一个特定的比特位,因而上述代码段中可以通过“与运算”来找到匹配的设备。

当modules为非0值时,说明Audio Policy指定了具体的设备id号,这时就通过查找全局的mAudioHwDevs变量来确认是否存在符合要求的设备。

DefaultKeyedVector<audio_module_handle_t, AudioHwDevice*>  mAudioHwDevs;

变量mAudioHwDevs是一个Vector,以audio_module_handle_t为key,每一个handle值唯一确定了已经添加的音频设备。那么在什么时候添加设备呢?

一种情况就是前面看到的modules为0时,会load所有潜在设备,另一种情况就是AudioPolicyManagerBase在构造时会预加载所有audio_policy.conf中所描述的output。不管是哪一种情况,最终都会调用loadHwModuleàloadHwModule_l,这个函数我们开头就分析过了。

如果modules为非0,且从mAudioHwDevs中也找不到符合要求的设备,程序并不会就此终结——它会退而求其次,遍历数组中的所有元素寻找支持devices的任何一个audio interface。

Step2@ AudioFlinger::openOutput,调用open_output_stream打开一个audio_stream_out_t。如果直接讲解这个函数的作用,大家可能觉得很抽象,所以这里我们提供一个具体硬件方案上的实现。原生态代码中就包括了一些具体音频设备的实现,如samsung的tuna,其源码实现如下:

/*device/samsung/tuna/audio/Audio_hw.c*/

static int  adev_open_output_stream(…structaudio_stream_out **stream_out)

{

struct tuna_audio_device*ladev = (struct tuna_audio_device *)dev;

struct tuna_stream_out *out;

*stream_out = NULL;

out = (structtuna_stream_out *)calloc(1, sizeof(struct tuna_stream_out));

out->stream.common.set_parameters = out_set_parameters;…

*stream_out =&out->stream;

}

我们去掉了其中的大部分代码,只留下核心部分。可以看到,tuna_stream_out类型包含了audio_stream_out,后者就是最后要返回的结果。这种方式在HAL层实现中非常多见,读者应该要熟悉这样的写法。而对于audio_stream_out的操作,无非就是根据入参需要,为它的函数指针做初始化,比如set_parameters的实现就最终指向了out_set_parameters。接下来的实现就涉及linux驱动了,我们这里先不往下分析,后面音量调节小节还会再遇到这个函数。

Step3@ AudioFlinger::openOutput,生成AudioStreamOut对象。这个变量没什么特别的,它把audio_hw_device_t和audio_stream_out_t做为一个整体来封装。

Step4@ AudioFlinger::openOutput. 既然通道已经打开,那么由谁来往通道里放东西呢?这就是PlaybackThread。这里分两种不同的情况:

·        DirectOutput

如果不需要混音

·        Mixer

需要混音

这两种情况分别对应DirectOutputThread和MixerThread两种线程。我们以后者为例来分析下PlaybackThread的工作模式,也会后面小节打下基础。

图 13‑13 Playback各线程类关系

如上图所示,用于Playback的线程种类不少,它们的基类都是Thread。

AudioFlinger中用于记录Record和Playback线程的有两个全局变量,如下:

DefaultKeyedVector< audio_io_handle_t, sp<PlaybackThread>>  mPlaybackThreads;

DefaultKeyedVector< audio_io_handle_t, sp<RecordThread>>    mRecordThreads;

在openOutput中,加入mPlaybackThreads的是一个新建的线程类实例,比如MixerThread。它的构造函数如下:

AudioFlinger::MixerThread::MixerThread(…): PlaybackThread(audioFlinger,output, id, device, type),…

{   …

mAudioMixer = new AudioMixer(mNormalFrameCount, mSampleRate);

if (mChannelCount == 1) {

ALOGE("Invalidaudio hardware channel count");

}

mOutputSink = newAudioStreamOutSink(output->stream);

if (initFastMixer) {

} else {

mFastMixer = NULL;

}

}

首先生成一个AudioMixer对象,这是混音处理的关键,我们会在后面有详细介绍。然后检查声道数量,在Mixer情况下肯定不止一个声道。接着创建一个NBAIO(Non-blockingaudio I/O interface) sink(即AudioStreamOutSink),并进行negotiate。最后根据配置(initFastMixer)来判断是否使用fast mixer。

可以想象一下,一个放音线程的任务就是不断处理上层的数据请求,然后将其传递到下一层,最终写入硬件设备。但是在上面这个函数中,似乎并没有看到程序去启动一个新线程,也没有看到进入线程循环的地方,或者去调用其它可能引起线程创建的函数。那么究竟在什么情况下MixerThread才会真正进入线程循环呢?

不知大家有没有注意到之前mPlaybackThreads的定义,我们再次列出如下:

DefaultKeyedVector< audio_io_handle_t, sp<PlaybackThread> > mPlaybackThreads;

它实际上是由audio_io_handle_t和PlaybackThread强指针所组成的键值对。同时也可以判断出,PlaybackThread类的祖先中一定会有RefBase。具体来说,就是它的父类Thread继承自RefBase:

/*frameworks/native/include/utils/Thread.h*/

class Thread : virtual public RefBase

{…

根据强指针的特性,目标对象在第一次被引用时是会调用onFirstRef的,这点在前面小节分析AudioFlinger时我们也见过。这个函数实现如下:

void AudioFlinger::PlaybackThread::onFirstRef()

{

run(mName,ANDROID_PRIORITY_URGENT_AUDIO);

}

很简单,只是调用了run方法,从而启动一个新线程并间接调用threadLoop,不断地处理Mix业务。这样我们就明白了一个PlaybackThread是如何进入线程循环的了,至于循环中需要做些什么,留在下一小节做详细介绍。

Step5@ AudioFlinger::openOutput,到目前为止,我们已经成功的建立起一个音频通道,就等着AudioTrack往里丢数据了。不过假如当前的output是“primary”的,则还有一些额外的工作要做。程序接着会对此音频设备设置主音量,前提是mMasterVolumeSupportLvl不为MVS_NONE(表示既不支持主音量的设置和获取。另外MVS_SETONLY表示只支持设置不能获取,MVS_FULL表示同时支持设置和获取)。

“测试设备是否支持主音量设置/获取”部分的代码很简单,我们就不详细说明了。不过要注意的是,当确定了主音量后,需要主动为系统当前已经存在的音频设置主音量(也就是openOutput最后的for循环部分)。这和loadHwModule_l中的设置主音量并不矛盾,试想一下主音量在什么时候被设置是不确定的,因而一旦设定后就先将系统已有的设备先做主音量设置,而后加的设备则由loadHwModule_l来完成。

我们来整理下这个小节所阐述的内容。

·        当AudioPolicyManagerBase构造时,它会根据用户提供的audio_policy.conf来分析系统中有哪些audio interface(primary,a2dp以及usb),然后通过AudioFlinger::loadHwModule加载各audio interface对应的库文件,并依次打开其中的output(openOutput)和input(openInput)

·        我们详细分析了openOutput所做的工作,包括打开一个audio_stream_out_t通道,生成AudioStreamOut对象,以及新建PlaybackThread等等。此时“万事俱备,只欠东风”,只要AudioTrack不断和AudioFlinger传递数据,整个音频回放就开始了。当然,这其中还涉及很多状态的管理、路由切换、以及数据的跨进程交互等等,这些都是我们后面内容所要解决的。

 

Android音频系统之AudioFlinger(二)的更多相关文章

  1. Android音频系统之AudioFlinger(一)

    1.1 AudioFlinger 在上面的框架图中,我们可以看到AudioFlinger(下面简称AF)是整个音频系统的核心与难点.作为Android系统中的音频中枢,它同时也是一个系统服务,启到承上 ...

  2. Android音频系统之AudioFlinger(四)

    http://blog.csdn.net/xuesen_lin/article/details/8805096 1.1.1 AudioMixer 每一个MixerThread都有一个唯一对应的Audi ...

  3. Android音频系统之AudioFlinger(三)

    http://blog.csdn.net/xuesen_lin/article/details/8805091 1.1.1 PlaybackThread的循环主体 当一个PlaybackThread进 ...

  4. Android音频系统之音频框架

    1.1 音频框架 转载请注明,From LXS, http://blog.csdn.net/uiop78uiop78/article/details/8796492 Android的音频系统在很长一段 ...

  5. 转:ANDROID音频系统散记之四:4.0音频系统HAL初探

    昨天(2011-11-15)发布了Android4.0的源码,今天download下来,开始挺进4.0时代.简单看了一下,发现音频系统方面与2.3的有较多地方不同,下面逐一描述. 一.代码模块位置 1 ...

  6. Android音频系统之AudioPolicyService

    地址:http://blog.csdn.net/edmond999/article/details/18599327 1.1 AudioPolicy Service 在AudioFlinger小节,我 ...

  7. Android音频系统

    1 分析思路 Thread如何创建? AudioPolicyService是策略的制定者,AudioFlinger是策略的执行者, 所以: AudioPolicyService根据配置文件使唤Audi ...

  8. Android 音频的播放之二MediaPlayer

    MediaPlayer类可用于控制音频/视频文件或流的播放.关于怎样使用这个类的方法还能够阅读VideoView类的文档. 1.状态图 对播放音频/视频文件和流的控制是通过一个状态机来管理的. 下图显 ...

  9. Android sensor 系统框架 (二)

    连载上一篇http://www.cnblogs.com/hackfun/p/7327320.html (D) 如何加载访问.so库 在前一篇博客http://www.cnblogs.com/hackf ...

随机推荐

  1. angular跨域访问的问题

    CORS跨域资源共享 跨域资源共享(CORS )是一种网络浏览器的技术规范,它为Web服务器定义了一种方式,允许网页从不同的域访问其资源. Form responseHeaders = (Form) ...

  2. Postgres数据库在Linux中的I/O优化

    I/O 优化1 打开 noatime方法: 修改 /etc/fstab2 调整预读方法: 查看 sudo blockdev --getra /dev/sda 设置 sudo blockdev --se ...

  3. css float 布局

    .clearfix:after { content: ''; display: table; clear: both; } .clearfix {; }

  4. mysql 排序后获得某行的位置

    假设有test表,下图为表机构和数据,score表示积分.现在要查询积分排名为第几的id?? 查询语句 select id,score,(@rowno:=@rowno+1) as rowno from ...

  5. Shell脚本,自动化发布tomcat项目【转载】

    Shell脚本,自动化发布tomcat项目脚本. 1. vko2c_auto_build_by_scp.sh 文件内容: #---------------------start------------ ...

  6. 图片应该放在drawable-hdpi下不要放在drawable下

    图片应该放在drawable-hdpi下或者mipmap-hdpi 不要放在drawable下,要不然显示有些不同

  7. 后台数据download成excel的方法(controller/action)

    jsp页面端 <a href="/portal/server/importExec" title="Data Download"> <img ...

  8. MySQL整理碎片

    1 innodb引擎表 alter table TABLE_NAME engine='innodb'; 还有一种方法 optiize table TABLE_NAME; http://stackove ...

  9. 【sort】 基数排序

    下面这段问答摘自csdn: 把基数排序说成桶排序应该是没有太大问题的.总的说来,应该把这一类归为分配排序,由于分配排序的一些缺陷,主要是时间代价很差,改进成为桶式排序(bucket sort),而桶排 ...

  10. 学习笔记:GLSL Core Tutorial – Pipeline (OpenGL 3.2 – OpenGL 4.2)

    GLSL Core Tutorial – Pipeline (OpenGL 3.2 – OpenGL 4.2) GLSL 是一种管道,一种图形化的流水线 1.GLSL 的具体工作流程: 简化流程如下: ...