Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1102)

Side by Side Diff: ppapi/examples/video_decode/video_decode.cc

Issue 270213004: Implement Pepper PPB_VideoDecoder interface. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Rebase, update to PPAPI message map macros. Created 6 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 // Copyright (c) 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include <stdio.h>
6 #include <string.h>
7
8 #include <iostream>
9 #include <queue>
10 #include <sstream>
11
12 #include "ppapi/c/pp_errors.h"
13 #include "ppapi/c/ppb_console.h"
14 #include "ppapi/c/ppb_opengles2.h"
15 #include "ppapi/cpp/graphics_3d.h"
16 #include "ppapi/cpp/graphics_3d_client.h"
17 #include "ppapi/cpp/input_event.h"
18 #include "ppapi/cpp/instance.h"
19 #include "ppapi/cpp/module.h"
20 #include "ppapi/cpp/rect.h"
21 #include "ppapi/cpp/var.h"
22 #include "ppapi/cpp/video_decoder.h"
23 #include "ppapi/examples/video_decode/testdata.h"
24 #include "ppapi/lib/gl/include/GLES2/gl2.h"
25 #include "ppapi/lib/gl/include/GLES2/gl2ext.h"
26 #include "ppapi/utility/completion_callback_factory.h"
27
28 // Use assert as a poor-man's CHECK, even in non-debug mode.
29 // Since <assert.h> redefines assert on every inclusion (it doesn't use
30 // include-guards), make sure this is the last file #include'd in this file.
31 #undef NDEBUG
32 #include <assert.h>
33
34 // Assert |context_| isn't holding any GL Errors. Done as a macro instead of a
35 // function to preserve line number information in the failure message.
36 #define assertNoGLError() assert(!gles2_if_->GetError(context_->pp_resource()));
37
38 namespace {
39
40 struct Shader {
41 Shader() : program(0), texcoord_scale_location(0) {}
42 ~Shader() {}
43
44 GLuint program;
45 GLint texcoord_scale_location;
46 };
47
48 class Decoder;
49 class MyInstance;
50
51 class MyInstance : public pp::Instance, public pp::Graphics3DClient {
52 public:
53 MyInstance(PP_Instance instance, pp::Module* module);
54 virtual ~MyInstance();
55
56 // pp::Instance implementation.
57 virtual void DidChangeView(const pp::Rect& position,
58 const pp::Rect& clip_ignored);
59
60 // pp::Graphics3DClient implementation.
61 virtual void Graphics3DContextLost() {
62 // TODO(vrk/fischman): Properly reset after a lost graphics context. In
63 // particular need to delete context_ and re-create textures.
64 // Probably have to recreate the decoder from scratch, because old textures
65 // can still be outstanding in the decoder!
66 assert(false && "Unexpectedly lost graphics context");
67 }
68
69 void PaintPicture(Decoder* decoder, const PP_VideoPicture& picture);
70
71 private:
72 // Log an error to the developer console and stderr by creating a temporary
73 // object of this type and streaming to it. Example usage:
74 // LogError(this).s() << "Hello world: " << 42;
75 class LogError {
76 public:
77 LogError(MyInstance* instance) : instance_(instance) {}
78 ~LogError() {
79 const std::string& msg = stream_.str();
80 instance_->console_if_->Log(
81 instance_->pp_instance(), PP_LOGLEVEL_ERROR, pp::Var(msg).pp_var());
82 std::cerr << msg << std::endl;
83 }
84 // Impl note: it would have been nicer to have LogError derive from
85 // std::ostringstream so that it can be streamed to directly, but lookup
86 // rules turn streamed string literals to hex pointers on output.
87 std::ostringstream& s() { return stream_; }
88
89 private:
90 MyInstance* instance_;
91 std::ostringstream stream_;
92 };
93
94 void InitializeDecoders();
95
96 // GL-related functions.
97 void InitGL();
98 void CreateGLObjects();
99 void Create2DProgramOnce();
100 void CreateRectangleARBProgramOnce();
101 Shader CreateProgram(const char* vertex_shader, const char* fragment_shader);
102 void CreateShader(GLuint program, GLenum type, const char* source, int size);
103 void PaintFinished(int32_t result, Decoder* decoder, PP_VideoPicture picture);
104
105 pp::Size plugin_size_;
106 bool is_painting_;
107 // When decode outpaces render, we queue up decoded pictures for later
108 // painting. Elements are <decoder,picture>.
109 typedef std::queue<std::pair<Decoder*, PP_VideoPicture> > PictureQueue;
110 PictureQueue pictures_pending_paint_;
111
112 int num_frames_rendered_;
113 PP_TimeTicks first_frame_delivered_ticks_;
114 PP_TimeTicks last_swap_request_ticks_;
115 PP_TimeTicks swap_ticks_;
116 pp::CompletionCallbackFactory<MyInstance> callback_factory_;
117
118 // Unowned pointers.
119 const PPB_Console* console_if_;
120 const PPB_Core* core_if_;
121 const PPB_OpenGLES2* gles2_if_;
122
123 // Owned data.
124 pp::Graphics3D* context_;
125 typedef std::vector<Decoder*> DecoderList;
126 DecoderList video_decoders_;
127
128 // Shader program to draw GL_TEXTURE_2D target.
129 Shader shader_2d_;
130 // Shader program to draw GL_TEXTURE_RECTANGLE_ARB target.
131 Shader shader_rectangle_arb_;
132 };
133
134 class Decoder {
135 public:
136 Decoder(MyInstance* instance, int id, const pp::Graphics3D& graphics_3d);
137 ~Decoder();
138
139 int id() const { return id_; }
140 bool ready() const { return !flushing_ && !resetting_; }
141
142 void Seek(int frame);
143 void RecyclePicture(const PP_VideoPicture& picture);
144
145 private:
146 void InitializeDone(int32_t result);
147 void Start(int frame);
148 void DecodeNextFrames();
149 void DecodeDone(int32_t result);
150 void PictureReady(int32_t result, PP_VideoPicture picture);
151 void FlushDone(int32_t result);
152 void ResetDone(int32_t result);
153
154 MyInstance* instance_;
155 int id_;
156
157 pp::VideoDecoder* decoder_;
158 pp::CompletionCallbackFactory<Decoder> callback_factory_;
159
160 size_t encoded_data_next_pos_to_decode_;
161 int next_picture_id_;
162 int seek_frame_;
163 bool flushing_;
164 bool resetting_;
165 };
166
167 // Returns true if the current position is at the start of a NAL unit.
168 static bool LookingAtNAL(const unsigned char* encoded, size_t pos) {
169 return pos + 3 < kDataLen && encoded[pos] == 0 && encoded[pos + 1] == 0 &&
170 encoded[pos + 2] == 0 && encoded[pos + 3] == 1;
171 }
172
173 // Find the start and end of the next frame.
174 static void GetNextFrame(size_t* start_pos, size_t* end_pos) {
175 // H264 frames start with 0, 0, 0, 1.
176 assert(LookingAtNAL(kData, *start_pos));
177 *end_pos = *start_pos;
178 *end_pos += 4;
179 while (*end_pos + 3 < kDataLen && !LookingAtNAL(kData, *end_pos)) {
180 ++*end_pos;
181 }
182 if (*end_pos + 3 >= kDataLen) {
183 *end_pos = kDataLen;
184 }
185 }
186
187 Decoder::Decoder(MyInstance* instance,
188 int id,
189 const pp::Graphics3D& graphics_3d)
190 : instance_(instance),
191 id_(id),
192 decoder_(new pp::VideoDecoder(instance)),
yzshen1 2014/05/14 18:08:23 It seems this is leaked.
bbudge 2014/05/14 19:35:04 Good catch. Deleted in dtor.
193 callback_factory_(this),
194 encoded_data_next_pos_to_decode_(0),
195 next_picture_id_(0),
196 seek_frame_(0),
197 flushing_(false),
198 resetting_(false) {
199 assert(!decoder_->is_null());
200 const PP_VideoProfile profile = PP_VIDEOPROFILE_H264MAIN;
201 decoder_->Initialize(graphics_3d,
202 profile,
203 PP_TRUE /* allow_software_fallback */,
204 callback_factory_.NewCallback(&Decoder::Start));
yzshen1 2014/05/14 18:08:23 InitializeDone?
bbudge 2014/05/14 19:35:04 Done.
205 }
206
207 Decoder::~Decoder() {
208 }
209
210 void Decoder::InitializeDone(int32_t result) {
yzshen1 2014/05/14 18:08:23 Currently this method is not used.
bbudge 2014/05/14 19:35:04 Changed callback to call this.
211 assert(decoder_);
212 assert(result == PP_OK);
213 assert(ready());
214 Start(0);
215 }
216
217 void Decoder::Start(int frame) {
218 assert(decoder_);
219
220 // Skip to |frame|.
221 size_t start_pos = 0;
222 size_t end_pos = 0;
223 for (int i = frame; i > 0; i--)
224 GetNextFrame(&start_pos, &end_pos);
225 encoded_data_next_pos_to_decode_ = end_pos;
226
227 DecodeNextFrames();
228 // Register callback to get the first picture. We call GetPicture
229 // again in PictureReady to keep getting pictures.
230 decoder_->GetPicture(
231 callback_factory_.NewCallbackWithOutput(&Decoder::PictureReady));
232 }
233
234 void Decoder::Seek(int frame) {
235 assert(decoder_);
236 seek_frame_ = frame;
237 resetting_ = true;
238 decoder_->Reset(callback_factory_.NewCallback(&Decoder::ResetDone));
239 }
240
241 void Decoder::RecyclePicture(const PP_VideoPicture& picture) {
242 assert(decoder_);
243 decoder_->RecyclePicture(picture);
244 }
245
246 void Decoder::DecodeNextFrames() {
247 assert(decoder_);
248 while (encoded_data_next_pos_to_decode_ <= kDataLen) {
249 // If we've just reached the end of the bitstream, flush and wait.
250 if (!flushing_ && encoded_data_next_pos_to_decode_ == kDataLen) {
251 flushing_ = true;
252 decoder_->Flush(callback_factory_.NewCallback(&Decoder::FlushDone));
253 return;
254 }
255
256 // Find the start of the next NALU.
257 size_t start_pos = encoded_data_next_pos_to_decode_;
258 size_t end_pos;
259 GetNextFrame(&start_pos, &end_pos);
260 encoded_data_next_pos_to_decode_ = end_pos;
261 // Decode the NALU.
262 uint32_t size = static_cast<uint32_t>(end_pos - start_pos);
263 int32_t result =
264 decoder_->Decode(next_picture_id_++,
265 size,
266 kData + start_pos,
267 callback_factory_.NewCallback(&Decoder::DecodeDone));
268 if (result != PP_OK) {
269 assert(result == PP_OK_COMPLETIONPENDING);
270 break;
271 }
272 }
273 }
274
275 void Decoder::DecodeDone(int32_t result) {
276 assert(decoder_);
277 assert(result == PP_OK || result == PP_ERROR_ABORTED);
278 if (result == PP_OK && ready())
279 DecodeNextFrames();
280 }
281
282 void Decoder::PictureReady(int32_t result, PP_VideoPicture picture) {
283 assert(decoder_);
284 assert(result == PP_OK || result == PP_ERROR_ABORTED);
285 if (result == PP_OK) {
286 decoder_->GetPicture(
287 callback_factory_.NewCallbackWithOutput(&Decoder::PictureReady));
288 instance_->PaintPicture(this, picture);
289 }
290 }
291
292 void Decoder::FlushDone(int32_t result) {
293 assert(decoder_);
294 assert(result == PP_OK || result == PP_ERROR_ABORTED);
295 flushing_ = false;
296 }
297
298 void Decoder::ResetDone(int32_t result) {
299 assert(decoder_);
300 assert(result == PP_OK);
301 resetting_ = false;
302 }
303
304 MyInstance::MyInstance(PP_Instance instance, pp::Module* module)
305 : pp::Instance(instance),
306 pp::Graphics3DClient(this),
307 is_painting_(false),
308 num_frames_rendered_(0),
309 first_frame_delivered_ticks_(-1),
yzshen1 2014/05/14 18:08:23 please also init last_swap_request_ticks_
bbudge 2014/05/14 19:35:04 Done.
310 swap_ticks_(0),
311 callback_factory_(this),
312 context_(NULL) {
313 assert((console_if_ = static_cast<const PPB_Console*>(
314 module->GetBrowserInterface(PPB_CONSOLE_INTERFACE))));
315 assert((core_if_ = static_cast<const PPB_Core*>(
316 module->GetBrowserInterface(PPB_CORE_INTERFACE))));
317 assert((gles2_if_ = static_cast<const PPB_OpenGLES2*>(
318 module->GetBrowserInterface(PPB_OPENGLES2_INTERFACE))));
319 RequestInputEvents(PP_INPUTEVENT_CLASS_MOUSE);
320 }
321
322 MyInstance::~MyInstance() {
323 if (!context_)
324 return;
325
326 PP_Resource graphics_3d = context_->pp_resource();
327 if (shader_2d_.program)
328 gles2_if_->DeleteProgram(graphics_3d, shader_2d_.program);
329 if (shader_rectangle_arb_.program)
330 gles2_if_->DeleteProgram(graphics_3d, shader_rectangle_arb_.program);
331
332 for (DecoderList::iterator it = video_decoders_.begin();
333 it != video_decoders_.end();
334 ++it)
335 delete *it;
336
337 delete context_;
338 }
339
340 void MyInstance::DidChangeView(const pp::Rect& position,
341 const pp::Rect& clip_ignored) {
342 if (position.width() == 0 || position.height() == 0)
343 return;
344 if (plugin_size_.width()) {
345 assert(position.size() == plugin_size_);
346 return;
347 }
348 plugin_size_ = position.size();
349
350 // Initialize graphics.
351 InitGL();
352 InitializeDecoders();
353 }
354
355 void MyInstance::InitializeDecoders() {
356 assert(video_decoders_.empty());
357 // Create two decoders with ids 0 and 1.
358 video_decoders_.push_back(new Decoder(this, 0, *context_));
359 video_decoders_.push_back(new Decoder(this, 1, *context_));
360 }
361
362 void MyInstance::PaintPicture(Decoder* decoder,
363 const PP_VideoPicture& picture) {
364 if (first_frame_delivered_ticks_ == -1)
365 assert((first_frame_delivered_ticks_ = core_if_->GetTimeTicks()) != -1);
366 if (is_painting_) {
367 pictures_pending_paint_.push(std::make_pair(decoder, picture));
368 return;
369 }
370
371 assert(!is_painting_);
372 is_painting_ = true;
373 int x = 0;
374 int y = 0;
375 int half_width = plugin_size_.width() / 2;
376 int half_height = plugin_size_.height() / 2;
377 if (decoder->id() != 0) {
378 x = half_width;
379 y = half_height;
380 }
381
382 PP_Resource graphics_3d = context_->pp_resource();
383 if (picture.texture_target == GL_TEXTURE_2D) {
384 Create2DProgramOnce();
385 gles2_if_->UseProgram(graphics_3d, shader_2d_.program);
386 gles2_if_->Uniform2f(
387 graphics_3d, shader_2d_.texcoord_scale_location, 1.0, 1.0);
388 } else {
389 assert(picture.texture_target == GL_TEXTURE_RECTANGLE_ARB);
390 CreateRectangleARBProgramOnce();
391 gles2_if_->UseProgram(graphics_3d, shader_rectangle_arb_.program);
392 gles2_if_->Uniform2f(graphics_3d,
393 shader_rectangle_arb_.texcoord_scale_location,
394 picture.texture_size.width,
395 picture.texture_size.height);
396 }
397
398 gles2_if_->Viewport(graphics_3d, x, y, half_width, half_height);
399 gles2_if_->ActiveTexture(graphics_3d, GL_TEXTURE0);
400 gles2_if_->BindTexture(
401 graphics_3d, picture.texture_target, picture.texture_id);
402 gles2_if_->DrawArrays(graphics_3d, GL_TRIANGLE_STRIP, 0, 4);
403
404 gles2_if_->UseProgram(graphics_3d, 0);
405
406 last_swap_request_ticks_ = core_if_->GetTimeTicks();
407 assert(PP_OK_COMPLETIONPENDING ==
408 context_->SwapBuffers(callback_factory_.NewCallback(
409 &MyInstance::PaintFinished, decoder, picture)));
410 }
411
412 void MyInstance::PaintFinished(int32_t result,
413 Decoder* decoder,
414 PP_VideoPicture picture) {
415 assert(result == PP_OK);
416 swap_ticks_ += core_if_->GetTimeTicks() - last_swap_request_ticks_;
417 is_painting_ = false;
418 ++num_frames_rendered_;
419 if (num_frames_rendered_ % 50 == 0) {
420 double elapsed = core_if_->GetTimeTicks() - first_frame_delivered_ticks_;
421 double fps = (elapsed > 0) ? num_frames_rendered_ / elapsed : 1000;
422 double ms_per_swap = (swap_ticks_ * 1e3) / num_frames_rendered_;
423 LogError(this).s() << "Rendered frames: " << num_frames_rendered_
424 << ", fps: " << fps
425 << ", with average ms/swap of: " << ms_per_swap;
426 }
427 decoder->RecyclePicture(picture);
428 // Keep painting as long as we have pictures.
429 if (!pictures_pending_paint_.empty()) {
430 std::pair<Decoder*, PP_VideoPicture> pending =
431 pictures_pending_paint_.front();
432 pictures_pending_paint_.pop();
433 PaintPicture(pending.first, pending.second);
434 }
435 }
436
437 void MyInstance::InitGL() {
438 assert(plugin_size_.width() && plugin_size_.height());
439 is_painting_ = false;
440
441 assert(!context_);
442 int32_t context_attributes[] = {
443 PP_GRAPHICS3DATTRIB_ALPHA_SIZE, 8,
444 PP_GRAPHICS3DATTRIB_BLUE_SIZE, 8,
445 PP_GRAPHICS3DATTRIB_GREEN_SIZE, 8,
446 PP_GRAPHICS3DATTRIB_RED_SIZE, 8,
447 PP_GRAPHICS3DATTRIB_DEPTH_SIZE, 0,
448 PP_GRAPHICS3DATTRIB_STENCIL_SIZE, 0,
449 PP_GRAPHICS3DATTRIB_SAMPLES, 0,
450 PP_GRAPHICS3DATTRIB_SAMPLE_BUFFERS, 0,
451 PP_GRAPHICS3DATTRIB_WIDTH, plugin_size_.width(),
452 PP_GRAPHICS3DATTRIB_HEIGHT, plugin_size_.height(),
453 PP_GRAPHICS3DATTRIB_NONE,
454 };
455 context_ = new pp::Graphics3D(this, context_attributes);
456 assert(!context_->is_null());
457 assert(BindGraphics(*context_));
458
459 // Clear color bit.
460 gles2_if_->ClearColor(context_->pp_resource(), 1, 0, 0, 1);
461 gles2_if_->Clear(context_->pp_resource(), GL_COLOR_BUFFER_BIT);
462
463 assertNoGLError();
464
465 CreateGLObjects();
466 }
467
468 void MyInstance::CreateGLObjects() {
469 // Assign vertex positions and texture coordinates to buffers for use in
470 // shader program.
471 static const float kVertices[] = {
472 -1, 1, -1, -1, 1, 1, 1, -1, // Position coordinates.
473 0, 1, 0, 0, 1, 1, 1, 0, // Texture coordinates.
474 };
475
476 GLuint buffer;
477 gles2_if_->GenBuffers(context_->pp_resource(), 1, &buffer);
478 gles2_if_->BindBuffer(context_->pp_resource(), GL_ARRAY_BUFFER, buffer);
479
480 gles2_if_->BufferData(context_->pp_resource(),
481 GL_ARRAY_BUFFER,
482 sizeof(kVertices),
483 kVertices,
484 GL_STATIC_DRAW);
485 assertNoGLError();
486 }
487
488 static const char kVertexShader[] =
489 "varying vec2 v_texCoord; \n"
490 "attribute vec4 a_position; \n"
491 "attribute vec2 a_texCoord; \n"
492 "uniform vec2 v_scale; \n"
493 "void main() \n"
494 "{ \n"
495 " v_texCoord = v_scale * a_texCoord; \n"
496 " gl_Position = a_position; \n"
497 "}";
498
499 void MyInstance::Create2DProgramOnce() {
500 if (shader_2d_.program)
501 return;
502 static const char kFragmentShader2D[] =
503 "precision mediump float; \n"
504 "varying vec2 v_texCoord; \n"
505 "uniform sampler2D s_texture; \n"
506 "void main() \n"
507 "{"
508 " gl_FragColor = texture2D(s_texture, v_texCoord); \n"
509 "}";
510 shader_2d_ = CreateProgram(kVertexShader, kFragmentShader2D);
511 assertNoGLError();
512 }
513
514 void MyInstance::CreateRectangleARBProgramOnce() {
515 if (shader_rectangle_arb_.program)
516 return;
517 static const char kFragmentShaderRectangle[] =
518 "#extension GL_ARB_texture_rectangle : require\n"
519 "precision mediump float; \n"
520 "varying vec2 v_texCoord; \n"
521 "uniform sampler2DRect s_texture; \n"
522 "void main() \n"
523 "{"
524 " gl_FragColor = texture2DRect(s_texture, v_texCoord).rgba; \n"
525 "}";
526 shader_rectangle_arb_ =
527 CreateProgram(kVertexShader, kFragmentShaderRectangle);
528 }
529
530 Shader MyInstance::CreateProgram(const char* vertex_shader,
531 const char* fragment_shader) {
532 Shader shader;
533
534 // Create shader program.
535 shader.program = gles2_if_->CreateProgram(context_->pp_resource());
536 CreateShader(
537 shader.program, GL_VERTEX_SHADER, vertex_shader, strlen(vertex_shader));
538 CreateShader(shader.program,
539 GL_FRAGMENT_SHADER,
540 fragment_shader,
541 strlen(fragment_shader));
542 gles2_if_->LinkProgram(context_->pp_resource(), shader.program);
543 gles2_if_->UseProgram(context_->pp_resource(), shader.program);
544 gles2_if_->Uniform1i(
545 context_->pp_resource(),
546 gles2_if_->GetUniformLocation(
547 context_->pp_resource(), shader.program, "s_texture"),
548 0);
549 assertNoGLError();
550
551 shader.texcoord_scale_location = gles2_if_->GetUniformLocation(
552 context_->pp_resource(), shader.program, "v_scale");
553
554 GLint pos_location = gles2_if_->GetAttribLocation(
555 context_->pp_resource(), shader.program, "a_position");
556 GLint tc_location = gles2_if_->GetAttribLocation(
557 context_->pp_resource(), shader.program, "a_texCoord");
558 assertNoGLError();
559
560 gles2_if_->EnableVertexAttribArray(context_->pp_resource(), pos_location);
561 gles2_if_->VertexAttribPointer(
562 context_->pp_resource(), pos_location, 2, GL_FLOAT, GL_FALSE, 0, 0);
563 gles2_if_->EnableVertexAttribArray(context_->pp_resource(), tc_location);
564 gles2_if_->VertexAttribPointer(
565 context_->pp_resource(),
566 tc_location,
567 2,
568 GL_FLOAT,
569 GL_FALSE,
570 0,
571 static_cast<float*>(0) + 8); // Skip position coordinates.
572
573 gles2_if_->UseProgram(context_->pp_resource(), 0);
574 assertNoGLError();
575 return shader;
576 }
577
578 void MyInstance::CreateShader(GLuint program,
579 GLenum type,
580 const char* source,
581 int size) {
582 GLuint shader = gles2_if_->CreateShader(context_->pp_resource(), type);
583 gles2_if_->ShaderSource(context_->pp_resource(), shader, 1, &source, &size);
584 gles2_if_->CompileShader(context_->pp_resource(), shader);
585 gles2_if_->AttachShader(context_->pp_resource(), program, shader);
586 gles2_if_->DeleteShader(context_->pp_resource(), shader);
587 }
588
589 // This object is the global object representing this plugin library as long
590 // as it is loaded.
591 class MyModule : public pp::Module {
592 public:
593 MyModule() : pp::Module() {}
594 virtual ~MyModule() {}
595
596 virtual pp::Instance* CreateInstance(PP_Instance instance) {
597 return new MyInstance(instance, this);
598 }
599 };
600
601 } // anonymous namespace
602
603 namespace pp {
604 // Factory function for your specialization of the Module object.
605 Module* CreateModule() {
606 return new MyModule();
607 }
608 } // namespace pp
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698