SDL渲染开发
- 1. SDL简介
- 2. 环境搭建
- 2.1 windows
- 2.2 Linux
- 3. SDL子系统
- 3.1 eg1 创建窗口
- 4:SDL显示
- 4.1 显示图片
- 4.2 绘制长方形显示
- 5. SDL事件
- 6. SDL多线程
- 6.1 接口演示
- 6.2 yuv显示
- 6.3 pcm播放
1. SDL简介
SDL(Simple DirectMedia Layer)是一个跨平台开发库,旨在通过 OpenGL 和 Direct3D 提供对音频、键盘、鼠标、游戏杆和图形硬件的低级访问。它被视频播放软件、模拟器和热门游戏(包括Valve的获奖产品和许多Humble Bundle游戏)所使用。SDL 正式支持 Windows、macOS、Linux、iOS 和 Android。其他平台的支持可以在源代码中找到。SDL 是用 C 编写的,可以与 C++ 直接配合使用,并且还有几种其他语言的绑定,包括 C# 和 Python。
SDL 2.0 在zlib 许可 下发布。此许可允许您在任何软件中自由使用 SDL。
2. 环境搭建
https://github.com/libsdl-org/SDL/releases
2.1 windows
- 下载SDL2-devel-2.30.3-VC.zip(版本自己选即可) (我使用qt+cmake所以下面这些没用到)
- 添加环境变量
- 打开VS创建项目,进入VC++目录设置包含目录和库目录
- 进入链接器的输入,添加附加依赖项(库目录下的库)
2.2 Linux
- 下载
SDL2-2.30.3.tar.gz
- 解压然后执行命令
./configure (--prefix=...可以选择放置的目录) make sudo make install
- 如果出现Could not initialize SDL - No available video device(Did you set the DISPLAY variable?)需要安装x11库文件
sudo apt-get install libx11-dev sudo apt-get install xorg-dev
3. SDL子系统
SDL将功能分成下列数个子系统(subsystem):
◼ SDL_INIT_TIMER:定时器
◼ SDL_INIT_AUDIO:音频
◼ SDL_INIT_VIDEO:视频
◼ SDL_INIT_JOYSTICK:摇杆
◼ SDL_INIT_HAPTIC:触摸屏
◼ SDL_INIT_GAMECONTROLLER:游戏控制器
◼ SDL_INIT_EVENTS:事件
◼ SDL_INIT_EVERYTHING:包含上述所有选项
3.1 eg1 创建窗口
直接看代码, 创建一个持续5s的窗口
- CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
project(01-sdl-basic LANGUAGES C)
# 下面两个宏变量代表的是一个地址,在这里,为了记忆,都写在这里了
include_directories(${CMAKE_SOURCE_DIR}/SDL2-2.30.3/include)
link_directories(${PROJECT_SOURCE_DIR}/SDL2-2.30.3/lib/x64)
add_executable(01-sdl-basic main.c)
target_link_libraries(01-sdl-basic
SDL2
)
- main.c
#include <stdio.h>
#include <SDL.h>
#undef main
int main()
{
SDL_Init(SDL_INIT_VIDEO); //初始化SDL为视频子系统
SDL_Window* win = SDL_CreateWindow("my sdl",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
480, 360,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE); //创建窗口
if (!win) {
SDL_Log("SDL_CreateWindow error:", SDL_GetError());
SDL_Quit();
return 1;
}
SDL_Delay(5000);
SDL_DestroyWindow(win);//销毁窗口
SDL_Quit(); //释放资源
return 0;
}
4:SDL显示
◼ SDL_Init():初始化SDL系统
◼ SDL_CreateWindow():创建窗口SDL_Window
◼ SDL_CreateRenderer():创建渲染器SDL_Renderer
◼ SDL_CreateTexture():创建纹理SDL_Texture
◼ SDL_UpdateTexture():设置纹理的数据
◼ SDL_RenderCopy():将纹理的数据拷贝给渲染器
◼ SDL_RenderPresent():显示
◼ SDL_Delay():工具函数,用于延时
◼ SDL_Quit():退出SDL系统
4.1 显示图片
#include <stdio.h>
#include <SDL.h>
#undef main //一定要写啊
int main()
{
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
SDL_Log("SDL_Init error", SDL_GetError());
return 1;
}
SDL_Window* win = SDL_CreateWindow("lena!",
SDL_WINDOWPOS_CENTERED, //居中
SDL_WINDOWPOS_CENTERED,
640, 480,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
if (win == NULL) {
SDL_Log("SDL_CreateWindow error", SDL_GetError());
SDL_Quit();
return 1;
}
// 创建一个将绘制到窗口的渲染器,-1 指定我们要加载任何一个
// 视频驱动程序支持我们传递的标志
// 标志: SDL_RENDERER_ACCELERATED:我们想使用硬件加速渲染
// SDL_RENDERER_PRESENTVSYNC:我们希望渲染器的当前功能(更新屏幕)是与显示器的刷新率同步
SDL_Renderer *ren = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (ren == NULL) {
SDL_Log("SDL_CreateRenderer error", SDL_GetError());
SDL_DestroyWindow(win);
SDL_Quit();
return 1;
}
const char* imagePath = "./LenaRGB.bmp";
SDL_Surface *bmp = SDL_LoadBMP(imagePath);
if (bmp == NULL){
SDL_DestroyRenderer(ren);
SDL_DestroyWindow(win);
SDL_Log("SDL_LoadBMP error", SDL_GetError());
SDL_Quit();
return 1;
}
//To use a hardware accelerated texture for rendering we can create one from
//the surface we loaded
SDL_Texture *tex = SDL_CreateTextureFromSurface(ren, bmp);
//We no longer need the surface
SDL_FreeSurface(bmp);
if (tex == NULL){
SDL_DestroyRenderer(ren);
SDL_DestroyWindow(win);
SDL_Log("SDL_CreateTextureFromSurface error", SDL_GetError());
SDL_Quit();
return 1;
}
//A sleepy rendering loop, wait for 3 seconds and render and present the screen each time
for (int i = 0; i < 3; ++i){
//First clear the renderer
SDL_RenderClear(ren);
//Draw the texture
SDL_RenderCopy(ren, tex, NULL, NULL);
//Update the screen
SDL_RenderPresent(ren);
//Take a quick break after all that hard work
SDL_Delay(1000);
}
//Clean up our objects and quit
SDL_DestroyTexture(tex);
SDL_DestroyRenderer(ren);
SDL_DestroyWindow(win);
SDL_Quit();
return 0;
}
4.2 绘制长方形显示
cmake跟之前一样,注意把sdl.dll放到可执行文件同级目录即可
这里我注释了纹理相关代码,这样也是可以的
SDL_Texture 与SDL_Renderer相似,也是一种缓冲区。只不过它存放的不是真正的像素数据,而是存放的图像的描述信息。当渲染纹理时,SDL以这些描述信息为数据,底层通过OpenGL、D3D 或 Metal操作GPU,最终绘制出与SDL_Renderer一样的图形,且效率更高(因为它是GPU硬件计算的)。
存储RGB和存储纹理的区别:比如一个从左到右由红色渐变到蓝色的矩形,用存储RGB的话就需要把矩形中每个点的具体颜色值存储下来;而纹理只是一些描述信息,比如记录了矩形的大小、起始颜色、终止颜色等信息,显卡可以通过这些信息推算出矩形块的详细信息。所以相对于存储RGB而已,存储纹理占用的内存要少的多。
#include <iostream>
#include <SDL.h>
using namespace std;
#undef main
int main()
{
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
SDL_Log("SDL_Init error");
return 1;
}
SDL_Window *win = SDL_CreateWindow("长方形",
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
640, 480,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
if (win == nullptr) {
SDL_Log("SDL_CreateWindow error");
SDL_Quit();
return 1;
}
SDL_Renderer* ren = SDL_CreateRenderer(win, -1, 0); //基于窗口创建渲染器
if (ren == nullptr) {
SDL_Log("SDL_CreateRenderer error");
SDL_DestroyWindow(win);
SDL_Quit();
return 1;
}
SDL_Texture* texture = SDL_CreateTexture(ren,
SDL_PIXELFORMAT_ARGB8888,
SDL_TEXTUREACCESS_TARGET,
640, 480);
if (texture == nullptr) {
SDL_DestroyRenderer(ren);
SDL_DestroyWindow(win);
SDL_Quit();
return 1;
}
SDL_Rect rect{};
rect.w = 50;
rect.h = 50;
int cnt = 0;
while (true) {
rect.x = rand() % 600;
rect.y = rand() % 400;
//SDL_SetRenderTarget(ren, texture); //设置渲染器为纹理目标
SDL_SetRenderDrawColor(ren, 0, 0, 0, 255); //设置黑色背景
SDL_RenderClear(ren); //清屏为我们设置的颜色
SDL_RenderDrawRect(ren, &rect); //绘制长方形
SDL_SetRenderDrawColor(ren, 255, 255, 255, 255); //白色长方形
SDL_RenderFillRect(ren, &rect);
// SDL_SetRenderTarget(ren, nullptr); //恢复默认, 渲染目标为窗口
// SDL_RenderCopy(ren, texture, nullptr, nullptr); //拷贝到cpu
SDL_RenderPresent(ren); //输出到窗口
SDL_Delay(300);
if (cnt++ > 30) {
break;
}
}
SDL_DestroyTexture(texture);
SDL_DestroyRenderer(ren);
SDL_DestroyWindow(win);
SDL_Quit();
return 0;
}
5. SDL事件
◼ 函数
• SDL_WaitEvent():等待一个事件
• SDL_PushEvent():发送一个事件
• SDL_PumpEvents():将硬件设备产生的事件放入事件队列,用于读取事件,在调用该函数之前,必须调 用SDL_PumpEvents搜集键盘等事件
• SDL_PeepEvents():从事件队列提取一个事件
◼ 数据结构
• SDL_Event:代表一个事件
看代码:显示鼠标的坐标,捕获键盘事件
注意,我这里试了case SDLK_q只能检测到大写字母,而case 'w’则忽略大小写了
#include <iostream>
#include <SDL.h>
using namespace std;
#define FF_SDL_EVENT SDL_USEREVENT+1
#undef main
int main()
{
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
SDL_Log("SDL_Init error");
return 1;
}
SDL_Window *win = SDL_CreateWindow("事件",
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
640, 480,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
if (win == nullptr) {
SDL_Log("SDL_CreateWindow error");
SDL_Quit();
return 1;
}
SDL_Renderer* ren = SDL_CreateRenderer(win, -1, 0); //基于窗口创建渲染器
if (ren == nullptr) {
SDL_Log("SDL_CreateRenderer error");
SDL_DestroyWindow(win);
SDL_Quit();
return 1;
}
SDL_SetRenderDrawColor(ren, 0, 0, 255, 255); //设置蓝色背景
SDL_RenderClear(ren); //清屏为我们设置的颜色
SDL_RenderPresent(ren); //输出到窗口
SDL_Event event;
bool exit = false;
while (!exit)
{
while(SDL_PollEvent(&event)) {
if (event.type == SDL_KEYUP) {
switch (event.key.keysym.sym)
{
case 'w':
printf("key w down=============\n");
SDL_Event event_w;
event_w.type = FF_SDL_EVENT;
SDL_PushEvent(&event_w);
break;
case SDLK_q:
printf("key q down=============\n");
SDL_Event event_q;
event_q.type = FF_SDL_EVENT;
SDL_PushEvent(&event_q);
break;
case SDLK_ESCAPE:
printf("=================");
SDL_Event event_esc;
event_esc.type = FF_SDL_EVENT;
SDL_PushEvent(&event_esc);
break;
default:
printf("key down 0x%x\n", event.key.keysym.sym);
break;
}
} else if (event.type == SDL_MOUSEMOTION) {
//printf("mouse movie (%d,%d)\n", event.button.x, event.button.y);
} else if (event.type == FF_SDL_EVENT) {
exit = true;
}
}
}
SDL_DestroyRenderer(ren);
SDL_DestroyWindow(win);
SDL_Quit();
return 0;
}
SDL_PollEvent和SDL_WaitEvent区别
除了SDL_PollEvent方法去取消息外,还有SDL_WaitEvent方法。顾名思义,该方法会阻塞当前调用的线程,直到取出一个消息为止。
SDL_PollEvent:
SDL_PollEvent函数是一个非阻塞函数,它会不断地检查 SDL 事件队列,直到有事件产生。如果有事件到达,它将将其从队列中取出并返回。如果没有事件,SDL_PollEvent会返回0。这种方式可以在事件到达时立即处理,而不需要等待。但是,如果程序需要等待某个特定事件,这种方法可能不够灵活。
SDL_WaitEvent:
SDL_WaitEvent函数是一个阻塞函数,它会等待 SDL 事件队列中的事件。直到有事件到达时,它才会返回。与SDL_PollEvent不同,SDL_WaitEvent会在没有事件时阻塞等待,直到事件到达或超时。这种方式在需要等待特定事件(例如用户输入)时非常有用。但是,如果事件处理程序在等待事件时需要执行其他任务,程序可能会变得不够高效。
6. SDL多线程
6.1 接口演示
◼ SDL线程创建:SDL_CreateThread
◼ SDL线程等待:SDL_WaitThead
◼ SDL互斥锁:SDL_CreateMutex/SDL_DestroyMutex
◼ SDL锁定互斥:SDL_LockMutex/SDL_UnlockMutex
◼ SDL条件变量(信号量):SDL_CreateCond/SDL_DestoryCond
◼ SDL条件变量(信号量)等待/通知:SDL_CondWait/SDL_CondSingal
#include <stdio.h>
#include <unistd.h>
#include <SDL.h>
SDL_mutex* s_lock = NULL;
SDL_cond* s_cond = NULL;
int thread_work(void *arg)
{
SDL_LockMutex(s_lock);
printf("<=============thread working sleep\n");
flushall();
sleep(10); //用来测试获取锁
printf("<=============thread working wait\n");
//释放锁,并等待signal
SDL_CondWait(s_cond, s_lock); //另一个线程(1)发送signal和(2)释放lock后,这个函数退出
printf("<===========thread_work receive signal, continue to do ~_~!!!\n");
printf("<===========thread_work end\n");
SDL_UnlockMutex(s_lock);
return 0;
}
#undef main
int main()
{
s_lock = SDL_CreateMutex();
s_cond = SDL_CreateCond();
SDL_Thread *t = SDL_CreateThread(thread_work, "hello world", NULL);
if (!t) {
SDL_Log("error: ", SDL_GetError());
return -1;
}
for (int i = 0; i < 2; ++i) {
sleep(2);
printf("main execute------>\n");
}
printf("main SDL_LockMutex(s_lock) before ====================>\n");
SDL_LockMutex(s_lock); // 获取锁,但是子线程还拿着锁
printf("main ready send signal====================>\n");
printf("main SDL_CondSignal(s_cond) before ====================>\n");
SDL_CondSignal(s_cond); // 发送信号,唤醒等待的线程
printf("main SDL_CondSignal(s_cond) after ====================>\n");
sleep(10);
SDL_UnlockMutex(s_lock);// 释放锁,让其他线程可以拿到锁
printf("main SDL_UnlockMutex(s_lock) after ====================>\n");
SDL_WaitThread(t, NULL);
SDL_DestroyMutex(s_lock);
SDL_DestroyCond(s_cond);
return 0;
}
6.2 yuv显示
#include <stdio.h>
#include <SDL.h>
#define REFRESH_EVENT (SDL_USEREVENT + 1) //请求刷新事件
#define QUIT_EVENT (SDL_USEREVENT + 2) //请求刷新事件
int thread_exit_flag = 0;
static int yuv_width = 320;
static int yuv_height = 240;
int refresh_video_timer(void* data)
{
while (!thread_exit_flag) {
SDL_Event event;
event.type = REFRESH_EVENT;
SDL_PushEvent(&event);
SDL_Delay(40);
}
thread_exit_flag = 0;
SDL_Event event;
event.type = QUIT_EVENT;
SDL_PushEvent(&event);
return 0;
}
#undef main
int main()
{
//初始化SDL
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
SDL_Log("SDL_Init error");
return -1;
}
// 创建窗口
SDL_Window* win = SDL_CreateWindow("yuv播放器", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 320, 240, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
if (!win) {
SDL_Log("SDL_CreateWindow error");
goto FLAG;
}
//创建渲染器
SDL_Renderer* ren = SDL_CreateRenderer(win, -1, 0);
if (!ren) {
SDL_Log("SDL_CreateRenderer error");
goto FLAG;
}
// 基于渲染器创建纹理
SDL_Texture* texture = SDL_CreateTexture(ren, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, 320, 240);
if (!texture) {
SDL_Log("SDL_CreateTexture error");
goto FLAG;
}
//分配空间
// 我们测试的文件是YUV420P格式
uint32_t y_frame_len = yuv_width * yuv_height;
uint32_t u_frame_len = yuv_width * yuv_height / 4;
uint32_t v_frame_len = yuv_width * yuv_height / 4;
uint32_t yuv_frame_len = y_frame_len + u_frame_len + v_frame_len;
uint8_t* video_buf = (uint8_t*)malloc(yuv_frame_len);
if (!video_buf) {
SDL_Log("分配空间失败");
goto FLAG;
}
//打开yuv文件
FILE* fd = fopen("./yuv420p_320x240.yuv", "rb");
if (!fd) {
goto FLAG; //C和C++的goto不太一样
}
// 创建刷新线程
SDL_Thread* t = SDL_CreateThread(refresh_video_timer, NULL, NULL);
SDL_Event event; // 事件
SDL_Rect rect; // 矩形
int win_width = yuv_width, win_height = yuv_height;
while (1) {
// 接收事件
SDL_WaitEvent(&event);
if (event.type == REFRESH_EVENT) {
size_t video_buff_len = fread(video_buf, 1, yuv_frame_len, fd);
if (video_buff_len <= 0) {
SDL_Log("从文件读取数据失败");
goto FLAG;
}
// 设置纹理的数据
SDL_UpdateTexture(texture, NULL, video_buf, yuv_width);
// 显示的区域
rect.x = 0;
rect.y = 0;
float w_ratio = win_width * 1.0 /yuv_width;
float h_ratio = win_height * 1.0 /yuv_height;
rect.w = 320 * w_ratio;
rect.h = 240 * h_ratio;
// 清除当前显示
SDL_RenderClear(ren);
// 将纹理的数据拷贝给渲染器
SDL_RenderCopy(ren, texture, NULL, &rect);
// 显示
SDL_RenderPresent(ren);
} else if (event.type == SDL_WINDOWEVENT) {
//If Resize
SDL_GetWindowSize(win, &win_width, &win_height);
printf("SDL_WINDOWEVENT win_width:%d, win_height:%d\n",win_width,
win_height );
} else if (event.type == SDL_QUIT) {
thread_exit_flag = 1;
} else if (event.type == QUIT_EVENT) {
break;
}
}
FLAG:
thread_exit_flag = 1; // 保证线程能够退出
// 释放资源
if(t)
SDL_WaitThread(t, NULL); // 等待线程退出
if(video_buf)
free(video_buf);
if(fd)
fclose(fd);
if(texture)
SDL_DestroyTexture(texture);
if(ren)
SDL_DestroyRenderer(ren);
if(win)
SDL_DestroyWindow(win);
SDL_Quit();
return 0;
}
6.3 pcm播放
打开音频设备
int SDLCALL SDL_OpenAudio(SDL_AudioSpec * desired,
SDL_AudioSpec * obtained);
// desired:期望的参数。
// obtained:实际音频设备的参数,一般情况下设置为NULL即可。
主要用到的结构体SDL_AudioSpec:
typedef struct SDL_AudioSpec {
int freq; // 音频采样率
SDL_AudioFormat format; // 音频数据格式
Uint8 channels; // 声道数: 1 单声道, 2 立体声
Uint8 silence; // 设置静音的值,因为声音采样是有符号的,所以0当然就是这个值
Uint16 samples; // 音频缓冲区中的采样个数,要求必须是2的n次
Uint16 padding; // 考虑到兼容性的一个参数
Uint32 size; // 音频缓冲区的大小,以字节为单位
SDL_AudioCallback callback; // 填充音频缓冲区的回调函数
void *userdata; // 用户自定义的数据
} SDL_AudioSpec;
SDL_AudioCallback
// userdata:SDL_AudioSpec结构中的用户自定义数据,一般情况下可以不用。
// stream:该指针指向需要填充的音频缓冲区。
// len:音频缓冲区的大小(以字节为单位)1024*2*2。
void (SDLCALL * SDL_AudioCallback) (void *userdata, Uint8 *stream, int len);
播放音频数据
// 当pause_on设置为0的时候即可开始播放音频数据。设置为1的时候,将会
播放静音的值。
void SDLCALL SDL_PauseAudio(int pause_on);
代码示例
#include <stdio.h>
#include <SDL.h>
// 每次读取2帧数据, 以1024个采样点一帧 2通道 16bit采样点为例
#define PCM_BUFFER_SIZE (1024*2*2*2)
// 目前读取的位置
static Uint8 *s_audio_pos = NULL;
// 缓存结束位置
static Uint8 *s_audio_end = NULL;
//音频设备回调函数
void handler_audio_pcm(void *udata, Uint8 *stream, int len)
{
SDL_memset(stream, 0, len);
if(s_audio_pos >= s_audio_end) // 数据读取完毕
{
return;
}
// 数据够了就读预设长度,数据不够就只读部分(不够的时候剩多少就读取多少)
int remain_buffer_len = s_audio_end - s_audio_pos;
len = (len < remain_buffer_len) ? len : remain_buffer_len;
// 拷贝数据到stream并调整音量
SDL_MixAudio(stream, s_audio_pos, len, SDL_MIX_MAXVOLUME/8);
printf("len = %d\n", len);
s_audio_pos += len; // 移动缓存指针
}
#undef main
int main()
{
if (SDL_Init(SDL_INIT_AUDIO)) {
SDL_Log("sdl init err");
return -1;
}
// 打开pcm
FILE* audio_fd = fopen("./44100_16bit_2ch.pcm", "rb");
if (!audio_fd) {
SDL_Log("fopen err");
goto FLAG;
}
uint8_t* audio_buf = (uint8_t*)malloc(PCM_BUFFER_SIZE);
// 音频参数设置
SDL_AudioSpec spec;
spec.freq = 44100;
spec.format = AUDIO_S16SYS;
spec.channels = 2;
spec.samples = 1024;
spec.callback = handler_audio_pcm;
spec.userdata = NULL;
//打开音频设备
if(SDL_OpenAudio(&spec, NULL))
{
fprintf(stderr, "Failed to open audio device, %s\n", SDL_GetError());
goto FLAG;
}
//play audio
SDL_PauseAudio(0);
int data_count = 0;
// 每次缓存的长度
size_t read_buffer_len = 0;
while(1)
{
// 从文件读取PCM数据
read_buffer_len = fread(audio_buf, 1, PCM_BUFFER_SIZE, audio_fd);
if(read_buffer_len == 0)
{
break;
}
data_count += read_buffer_len; // 统计读取的数据总字节数
printf("now playing %10d bytes data.\n",data_count);
s_audio_end = audio_buf + read_buffer_len; // 更新buffer的结束位置
s_audio_pos = audio_buf; // 更新buffer的起始位置
//the main thread wait for a moment
while(s_audio_pos < s_audio_end)
{
SDL_Delay(10); // 等待PCM数据消耗
}
}
printf("play PCM finish\n");
// 关闭音频设备
SDL_CloseAudio();
FLAG:
if (audio_buf) {
free(audio_buf);
}
if (audio_fd) {
fclose(audio_fd);
}
SDL_Quit();
return 0;
}