picture: move to libtrx

This commit is contained in:
Marcin Kurczewski 2024-08-23 17:18:41 +02:00
parent 9061c915c9
commit 84cc44df0a
No known key found for this signature in database
GPG key ID: CC65E6FD28CAE42A
11 changed files with 36 additions and 658 deletions

View file

@ -233,7 +233,6 @@ sources = [
'src/game/phase/phase_pause.c',
'src/game/phase/phase_picture.c',
'src/game/phase/phase_stats.c',
'src/game/picture.c',
'src/game/random.c',
'src/game/requester.c',
'src/game/room.c',
@ -273,7 +272,6 @@ sources = [
'src/specific/s_fmv.c',
'src/specific/s_input.c',
'src/specific/s_output.c',
'src/specific/s_picture.c',
'src/specific/s_shell.c',
resources,
]

View file

@ -6,7 +6,6 @@
#include "game/gamebuf.h"
#include "game/overlay.h"
#include "game/phase/phase.h"
#include "game/picture.h"
#include "game/random.h"
#include "game/viewport.h"
#include "global/const.h"
@ -18,6 +17,8 @@
#include "specific/s_output.h"
#include "specific/s_shell.h"
#include <libtrx/engine/image.h>
#include <libtrx/filesystem.h>
#include <libtrx/memory.h>
#include <libtrx/utils.h>
@ -59,7 +60,10 @@ static XYZ_32 m_LsVectorView = { 0 };
static int32_t m_LightningCount = 0;
static LIGHTNING m_LightningTable[MAX_LIGHTNINGS];
char *m_BackdropImagePath = NULL;
static char *m_BackdropImagePath = NULL;
static const char *m_ImageExtensions[] = {
".png", ".jpg", ".jpeg", ".pcx", NULL,
};
static void Output_DrawBlackOverlay(uint8_t alpha);
@ -950,18 +954,18 @@ void Output_LoadBackdropImage(const char *filename)
}
const char *old_path = m_BackdropImagePath;
m_BackdropImagePath = Memory_DupStr(filename);
m_BackdropImagePath = File_GuessExtension(filename, m_ImageExtensions);
Memory_FreePointer(&old_path);
PICTURE *orig_pic = Picture_CreateFromFile(m_BackdropImagePath);
if (orig_pic) {
PICTURE *scaled_pic = Picture_ScaleSmart(
orig_pic, Viewport_GetWidth(), Viewport_GetHeight());
if (scaled_pic) {
S_Output_DownloadBackdropSurface(scaled_pic);
Picture_Free(scaled_pic);
IMAGE *orig_img = Image_CreateFromFile(m_BackdropImagePath);
if (orig_img) {
IMAGE *scaled_img = Image_ScaleSmart(
orig_img, Viewport_GetWidth(), Viewport_GetHeight());
if (scaled_img) {
S_Output_DownloadBackdropSurface(scaled_img);
Image_Free(scaled_img);
}
Picture_Free(orig_pic);
Image_Free(orig_img);
}
}

View file

@ -1,84 +0,0 @@
#include "game/picture.h"
#include "specific/s_picture.h"
#include <libtrx/filesystem.h>
#include <libtrx/memory.h>
#include <assert.h>
static const char *m_Extensions[] = {
".png", ".jpg", ".jpeg", ".pcx", NULL,
};
PICTURE *Picture_Create(int width, int height)
{
PICTURE *picture = Memory_Alloc(sizeof(PICTURE));
picture->width = width;
picture->height = height;
picture->data = Memory_Alloc(width * height * sizeof(RGB_888));
return picture;
}
PICTURE *Picture_CreateFromFile(const char *path)
{
char *final_path = File_GuessExtension(path, m_Extensions);
PICTURE *picture = S_Picture_CreateFromFile(final_path);
Memory_FreePointer(&final_path);
return picture;
}
bool Picture_SaveToFile(const PICTURE *pic, const char *path)
{
assert(pic);
assert(path);
return S_Picture_SaveToFile(pic, path);
}
PICTURE *Picture_ScaleLetterbox(
const PICTURE *source_pic, size_t target_width, size_t target_height)
{
assert(source_pic);
return S_Picture_ScaleLetterbox(source_pic, target_width, target_height);
}
PICTURE *Picture_ScaleCrop(
const PICTURE *source_pic, size_t target_width, size_t target_height)
{
assert(source_pic);
return S_Picture_ScaleCrop(source_pic, target_width, target_height);
}
PICTURE *Picture_ScaleSmart(
const PICTURE *source_pic, size_t target_width, size_t target_height)
{
assert(source_pic);
const float source_ratio = source_pic->width / (float)source_pic->height;
const float target_ratio = target_width / (float)target_height;
// if the difference between aspect ratios is under 10%, just stretch it
const float ar_diff =
(source_ratio > target_ratio ? source_ratio / target_ratio
: target_ratio / source_ratio)
- 1.0f;
if (ar_diff <= 0.1f) {
return S_Picture_ScaleStretch(source_pic, target_width, target_height);
}
// if the viewport is too wide, center the image
if (source_ratio <= target_ratio) {
return S_Picture_ScaleLetterbox(
source_pic, target_width, target_height);
}
// if the image is too wide, crop the image
return S_Picture_ScaleCrop(source_pic, target_width, target_height);
}
void Picture_Free(PICTURE *picture)
{
if (picture) {
Memory_FreePointer(&picture->data);
}
Memory_FreePointer(&picture);
}

View file

@ -1,20 +0,0 @@
#pragma once
#include "global/types.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
PICTURE *Picture_Create(int width, int height);
PICTURE *Picture_CreateFromFile(const char *path);
void Picture_Free(PICTURE *picture);
bool Picture_SaveToFile(const PICTURE *pic, const char *path);
PICTURE *Picture_ScaleFit(
const PICTURE *source_pic, size_t target_width, size_t target_height);
PICTURE *Picture_ScaleCover(
const PICTURE *source_pic, size_t target_width, size_t target_height);
PICTURE *Picture_ScaleSmart(
const PICTURE *source_pic, size_t target_width, size_t target_height);

View file

@ -1,8 +1,8 @@
#include "gfx/screenshot.h"
#include "game/picture.h"
#include "global/types.h"
#include <libtrx/engine/image.h>
#include <libtrx/memory.h>
#include <assert.h>
@ -17,17 +17,17 @@ bool GFX_Screenshot_CaptureToFile(const char *path)
GFX_Screenshot_CaptureToBuffer(
NULL, &width, &height, 3, GL_RGB, GL_UNSIGNED_BYTE, true);
PICTURE *pic = Picture_Create(width, height);
assert(pic);
IMAGE *image = Image_Create(width, height);
assert(image);
GFX_Screenshot_CaptureToBuffer(
(uint8_t *)pic->data, &width, &height, 3, GL_RGB, GL_UNSIGNED_BYTE,
(uint8_t *)image->data, &width, &height, 3, GL_RGB, GL_UNSIGNED_BYTE,
true);
ret = Picture_SaveToFile(pic, path);
ret = Image_SaveToFile(image, path);
if (pic) {
Picture_Free(pic);
if (image) {
Image_Free(image);
}
return ret;
}

View file

@ -1985,12 +1985,6 @@ typedef struct SAMPLE_INFO {
int16_t flags;
} SAMPLE_INFO;
typedef struct PICTURE {
int32_t width;
int32_t height;
RGB_888 *data;
} PICTURE;
typedef union INPUT_STATE {
uint64_t any;
struct {

View file

@ -475,9 +475,9 @@ void S_Output_DrawBackdropSurface(void)
GFX_2D_Renderer_Render(m_Renderer2D);
}
void S_Output_DownloadBackdropSurface(const PICTURE *pic)
void S_Output_DownloadBackdropSurface(const IMAGE *const image)
{
if (!pic) {
if (!image) {
if (m_PictureSurface) {
bool result = GFX_2D_Surface_Clear(m_PictureSurface);
S_Output_CheckError(result);
@ -490,8 +490,8 @@ void S_Output_DownloadBackdropSurface(const PICTURE *pic)
// first, download the picture directly to a temporary surface
{
GFX_2D_SurfaceDesc surface_desc = {
.width = pic->width,
.height = pic->height,
.width = image->width,
.height = image->height,
};
picture_surface = GFX_2D_Surface_Create(&surface_desc);
}
@ -502,8 +502,8 @@ void S_Output_DownloadBackdropSurface(const PICTURE *pic)
S_Output_CheckError(result);
uint32_t *output_ptr = surface_desc.pixels;
RGB_888 *input_ptr = pic->data;
for (int i = 0; i < pic->width * pic->height; i++) {
IMAGE_PIXEL *input_ptr = image->data;
for (int i = 0; i < image->width * image->height; i++) {
uint8_t r = input_ptr->r;
uint8_t g = input_ptr->g;
uint8_t b = input_ptr->b;
@ -525,8 +525,8 @@ void S_Output_DownloadBackdropSurface(const PICTURE *pic)
int32_t target_width = m_SurfaceWidth;
int32_t target_height = m_SurfaceHeight;
int32_t source_width = pic->width;
int32_t source_height = pic->height;
int32_t source_width = image->width;
int32_t source_height = image->height;
// keep aspect ratio and fit inside, adding black bars on the sides
const float source_ratio = source_width / (float)source_height;
@ -541,8 +541,8 @@ void S_Output_DownloadBackdropSurface(const PICTURE *pic)
GFX_BlitterRect source_rect = {
.left = 0,
.top = 0,
.right = pic->width,
.bottom = pic->height,
.right = image->width,
.bottom = image->height,
};
GFX_BlitterRect target_rect = {
.left = (target_width - new_width) / 2,

View file

@ -1,8 +1,9 @@
#pragma once
#include "game/picture.h"
#include "global/types.h"
#include <libtrx/engine/image.h>
#include <stdbool.h>
#include <stdint.h>
@ -27,7 +28,7 @@ RGB_888 S_Output_GetPaletteColor(uint8_t idx);
void S_Output_DownloadTextures(int32_t pages);
void S_Output_SelectTexture(int tex_num);
void S_Output_DownloadBackdropSurface(const PICTURE *pic);
void S_Output_DownloadBackdropSurface(const IMAGE *image);
void S_Output_DrawBackdropSurface(void);
void S_Output_DrawFlatTriangle(

View file

@ -1,500 +0,0 @@
#include "specific/s_picture.h"
#include "game/picture.h"
#include <libtrx/filesystem.h>
#include <libtrx/log.h>
#include <libtrx/memory.h>
#include <assert.h>
#include <errno.h>
#include <libavcodec/avcodec.h>
#include <libavcodec/codec.h>
#include <libavcodec/codec_id.h>
#include <libavcodec/packet.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libavutil/error.h>
#include <libavutil/frame.h>
#include <libavutil/imgutils.h>
#include <libavutil/mem.h>
#include <libavutil/pixfmt.h>
#include <libavutil/rational.h>
#include <libswscale/swscale.h>
#include <stdint.h>
#include <string.h>
PICTURE *S_Picture_CreateFromFile(const char *path)
{
int error_code;
AVFormatContext *format_ctx = NULL;
const AVCodec *codec = NULL;
AVCodecContext *codec_ctx = NULL;
AVFrame *frame = NULL;
AVPacket *packet = NULL;
struct SwsContext *sws_ctx = NULL;
uint8_t *dst_data[4] = { 0 };
int dst_linesize[4] = { 0 };
PICTURE *target_pic = NULL;
char *full_path = File_GetFullPath(path);
error_code = avformat_open_input(&format_ctx, full_path, NULL, NULL);
Memory_FreePointer(&full_path);
if (error_code != 0) {
goto cleanup;
}
error_code = avformat_find_stream_info(format_ctx, NULL);
if (error_code < 0) {
goto cleanup;
}
AVStream *video_stream = NULL;
for (unsigned int i = 0; i < format_ctx->nb_streams; i++) {
AVStream *current_stream = format_ctx->streams[i];
if (current_stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream = current_stream;
break;
}
}
if (!video_stream) {
error_code = AVERROR_STREAM_NOT_FOUND;
goto cleanup;
}
codec = avcodec_find_decoder(video_stream->codecpar->codec_id);
if (!codec) {
error_code = AVERROR_DEMUXER_NOT_FOUND;
goto cleanup;
}
codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
error_code = AVERROR(ENOMEM);
goto cleanup;
}
error_code =
avcodec_parameters_to_context(codec_ctx, video_stream->codecpar);
if (error_code) {
goto cleanup;
}
error_code = avcodec_open2(codec_ctx, codec, NULL);
if (error_code < 0) {
goto cleanup;
}
packet = av_packet_alloc();
av_new_packet(packet, 0);
error_code = av_read_frame(format_ctx, packet);
if (error_code < 0) {
goto cleanup;
}
error_code = avcodec_send_packet(codec_ctx, packet);
if (error_code < 0) {
goto cleanup;
}
frame = av_frame_alloc();
if (!frame) {
error_code = AVERROR(ENOMEM);
goto cleanup;
}
error_code = avcodec_receive_frame(codec_ctx, frame);
if (error_code < 0) {
goto cleanup;
}
target_pic = Picture_Create(frame->width, frame->height);
sws_ctx = sws_getContext(
codec_ctx->width, codec_ctx->height, codec_ctx->pix_fmt,
target_pic->width, target_pic->height, AV_PIX_FMT_RGB24, SWS_BILINEAR,
NULL, NULL, NULL);
if (!sws_ctx) {
LOG_ERROR("Failed to get SWS context");
error_code = AVERROR_EXTERNAL;
goto cleanup;
}
error_code = av_image_alloc(
dst_data, dst_linesize, target_pic->width, target_pic->height,
AV_PIX_FMT_RGB24, 1);
if (error_code < 0) {
goto cleanup;
}
sws_scale(
sws_ctx, (const uint8_t *const *)frame->data, frame->linesize, 0,
frame->height, dst_data, dst_linesize);
av_image_copy_to_buffer(
(uint8_t *)target_pic->data,
target_pic->width * target_pic->height * sizeof(RGB_888),
(const uint8_t *const *)dst_data, dst_linesize, AV_PIX_FMT_RGB24,
target_pic->width, target_pic->height, 1);
error_code = 0;
goto success;
cleanup:
if (target_pic) {
Picture_Free(target_pic);
target_pic = NULL;
}
if (error_code) {
LOG_ERROR(
"Error while opening picture %s: %s", path, av_err2str(error_code));
}
success:
av_freep(&dst_data[0]);
if (sws_ctx) {
sws_freeContext(sws_ctx);
}
if (packet) {
av_packet_free(&packet);
}
if (frame) {
av_frame_free(&frame);
}
if (codec_ctx) {
avcodec_close(codec_ctx);
av_free(codec_ctx);
codec_ctx = NULL;
}
if (format_ctx) {
avformat_close_input(&format_ctx);
}
return target_pic;
}
bool S_Picture_SaveToFile(const PICTURE *pic, const char *path)
{
assert(pic);
assert(path);
bool ret = false;
int error_code = 0;
const AVCodec *codec = NULL;
AVCodecContext *codec_ctx = NULL;
AVFrame *frame = NULL;
AVPacket *packet = NULL;
struct SwsContext *sws_ctx = NULL;
MYFILE *fp = NULL;
enum AVPixelFormat source_pix_fmt = AV_PIX_FMT_RGB24;
enum AVPixelFormat target_pix_fmt;
enum AVCodecID codec_id;
if (strstr(path, ".jpg")) {
target_pix_fmt = AV_PIX_FMT_YUVJ420P;
codec_id = AV_CODEC_ID_MJPEG;
} else if (strstr(path, ".png")) {
target_pix_fmt = AV_PIX_FMT_RGB24;
codec_id = AV_CODEC_ID_PNG;
} else {
LOG_ERROR("Cannot determine picture format based on path '%s'", path);
goto cleanup;
}
fp = File_Open(path, FILE_OPEN_WRITE);
if (!fp) {
LOG_ERROR("Cannot create picture file: %s", path);
goto cleanup;
}
codec = avcodec_find_encoder(codec_id);
if (!codec) {
error_code = AVERROR_MUXER_NOT_FOUND;
goto cleanup;
}
codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
error_code = AVERROR(ENOMEM);
goto cleanup;
}
codec_ctx->bit_rate = 400000;
codec_ctx->width = pic->width;
codec_ctx->height = pic->height;
codec_ctx->time_base = (AVRational) { 1, 25 };
codec_ctx->pix_fmt = target_pix_fmt;
if (codec_id == AV_CODEC_ID_MJPEG) {
// 9 JPEG quality
codec_ctx->flags |= AV_CODEC_FLAG_QSCALE;
codec_ctx->global_quality = FF_QP2LAMBDA * 9;
}
error_code = avcodec_open2(codec_ctx, codec, NULL);
if (error_code < 0) {
goto cleanup;
}
frame = av_frame_alloc();
if (!frame) {
error_code = AVERROR(ENOMEM);
goto cleanup;
}
frame->format = codec_ctx->pix_fmt;
frame->width = codec_ctx->width;
frame->height = codec_ctx->height;
frame->pts = 0;
error_code = av_image_alloc(
frame->data, frame->linesize, codec_ctx->width, codec_ctx->height,
codec_ctx->pix_fmt, 32);
if (error_code < 0) {
goto cleanup;
}
packet = av_packet_alloc();
av_new_packet(packet, 0);
sws_ctx = sws_getContext(
pic->width, pic->height, source_pix_fmt, frame->width, frame->height,
target_pix_fmt, SWS_BILINEAR, NULL, NULL, NULL);
if (!sws_ctx) {
LOG_ERROR("Failed to get SWS context");
error_code = AVERROR_EXTERNAL;
goto cleanup;
}
uint8_t *src_planes[4];
int src_linesize[4];
av_image_fill_arrays(
src_planes, src_linesize, (const uint8_t *)pic->data, source_pix_fmt,
pic->width, pic->height, 1);
sws_scale(
sws_ctx, (const uint8_t *const *)src_planes, src_linesize, 0,
pic->height, frame->data, frame->linesize);
error_code = avcodec_send_frame(codec_ctx, frame);
if (error_code < 0) {
goto cleanup;
}
while (error_code >= 0) {
error_code = avcodec_receive_packet(codec_ctx, packet);
if (error_code == AVERROR(EAGAIN) || error_code == AVERROR_EOF) {
error_code = 0;
break;
}
if (error_code < 0) {
goto cleanup;
}
File_WriteData(fp, packet->data, packet->size);
av_packet_unref(packet);
}
cleanup:
if (error_code) {
LOG_ERROR(
"Error while saving picture %s: %s", path, av_err2str(error_code));
}
if (fp) {
File_Close(fp);
fp = NULL;
}
if (sws_ctx) {
sws_freeContext(sws_ctx);
}
if (packet) {
av_packet_free(&packet);
}
if (codec) {
avcodec_close(codec_ctx);
av_free(codec_ctx);
codec_ctx = NULL;
}
if (frame) {
av_freep(&frame->data[0]);
av_frame_free(&frame);
}
return ret;
}
PICTURE *S_Picture_ScaleLetterbox(
const PICTURE *source_pic, int target_width, int target_height)
{
assert(source_pic);
assert(source_pic->data);
PICTURE *target_pic = NULL;
int source_width = source_pic->width;
int source_height = source_pic->height;
const float source_ratio = source_width / (float)source_height;
const float target_ratio = target_width / (float)target_height;
{
int new_width = source_ratio < target_ratio
? target_height * source_ratio
: target_width;
int new_height = source_ratio < target_ratio
? target_height
: target_width / source_ratio;
target_width = new_width;
target_height = new_height;
}
struct SwsContext *sws_ctx = sws_getContext(
source_width, source_height, AV_PIX_FMT_RGB24, target_width,
target_height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);
if (!sws_ctx) {
LOG_ERROR("Failed to get SWS context");
goto cleanup;
}
target_pic = Picture_Create(target_width, target_height);
uint8_t *src_planes[4];
uint8_t *dst_planes[4];
int src_linesize[4];
int dst_linesize[4];
av_image_fill_arrays(
src_planes, src_linesize, (const uint8_t *)source_pic->data,
AV_PIX_FMT_RGB24, source_width, source_height, 1);
av_image_fill_arrays(
dst_planes, dst_linesize, (const uint8_t *)target_pic->data,
AV_PIX_FMT_RGB24, target_pic->width, target_pic->height, 1);
sws_scale(
sws_ctx, (const uint8_t *const *)src_planes, src_linesize, 0,
source_height, (uint8_t *const *)dst_planes, dst_linesize);
cleanup:
if (sws_ctx) {
sws_freeContext(sws_ctx);
}
return target_pic;
}
PICTURE *S_Picture_ScaleCrop(
const PICTURE *source_pic, int target_width, int target_height)
{
assert(source_pic);
assert(source_pic->data);
PICTURE *target_pic = NULL;
int source_width = source_pic->width;
int source_height = source_pic->height;
const float source_ratio = source_width / (float)source_height;
const float target_ratio = target_width / (float)target_height;
int crop_width = source_ratio < target_ratio ? source_width
: source_height * target_ratio;
int crop_height = source_ratio < target_ratio ? source_width / target_ratio
: source_height;
struct SwsContext *sws_ctx = sws_getContext(
crop_width, crop_height, AV_PIX_FMT_RGB24, target_width, target_height,
AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);
if (!sws_ctx) {
LOG_ERROR("Failed to get SWS context");
goto cleanup;
}
target_pic = Picture_Create(target_width, target_height);
uint8_t *src_planes[4];
uint8_t *dst_planes[4];
int src_linesize[4];
int dst_linesize[4];
av_image_fill_arrays(
src_planes, src_linesize, (const uint8_t *)source_pic->data,
AV_PIX_FMT_RGB24, source_width, source_height, 1);
src_planes[0] += ((source_height - crop_height) / 2) * src_linesize[0];
src_planes[0] += ((source_width - crop_width) / 2) * sizeof(RGB_888);
av_image_fill_arrays(
dst_planes, dst_linesize, (const uint8_t *)target_pic->data,
AV_PIX_FMT_RGB24, target_pic->width, target_pic->height, 1);
sws_scale(
sws_ctx, (const uint8_t *const *)src_planes, src_linesize, 0,
crop_height, (uint8_t *const *)dst_planes, dst_linesize);
cleanup:
if (sws_ctx) {
sws_freeContext(sws_ctx);
}
return target_pic;
}
PICTURE *S_Picture_ScaleStretch(
const PICTURE *source_pic, int target_width, int target_height)
{
assert(source_pic);
assert(source_pic->data);
PICTURE *target_pic = NULL;
int source_width = source_pic->width;
int source_height = source_pic->height;
struct SwsContext *sws_ctx = sws_getContext(
source_width, source_height, AV_PIX_FMT_RGB24, target_width,
target_height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);
if (!sws_ctx) {
LOG_ERROR("Failed to get SWS context");
goto cleanup;
}
target_pic = Picture_Create(target_width, target_height);
uint8_t *src_planes[4];
uint8_t *dst_planes[4];
int src_linesize[4];
int dst_linesize[4];
av_image_fill_arrays(
src_planes, src_linesize, (const uint8_t *)source_pic->data,
AV_PIX_FMT_RGB24, source_width, source_height, 1);
av_image_fill_arrays(
dst_planes, dst_linesize, (const uint8_t *)target_pic->data,
AV_PIX_FMT_RGB24, target_pic->width, target_pic->height, 1);
sws_scale(
sws_ctx, (const uint8_t *const *)src_planes, src_linesize, 0,
source_height, (uint8_t *const *)dst_planes, dst_linesize);
cleanup:
if (sws_ctx) {
sws_freeContext(sws_ctx);
}
return target_pic;
}

View file

@ -1,15 +0,0 @@
#pragma once
#include "global/types.h"
#include <stdbool.h>
PICTURE *S_Picture_CreateFromFile(const char *path);
bool S_Picture_SaveToFile(const PICTURE *pic, const char *path);
PICTURE *S_Picture_ScaleLetterbox(
const PICTURE *source_pic, int target_width, int target_height);
PICTURE *S_Picture_ScaleCrop(
const PICTURE *source_pic, int target_width, int target_height);
PICTURE *S_Picture_ScaleStretch(
const PICTURE *source_pic, int target_width, int target_height);

@ -1 +1 @@
Subproject commit 444fd607a13d25abf1b11a488226c58af866b277
Subproject commit ad9728191e112a4760671f76b69594756f58bd96